import { Trans, useLingui } from "@lingui/react/macro";
import type { UserFactCategory, UserFactSource, UserProfileState } from "@rakazo/contracts";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
} from "@rakazo/ui-web";
import { PencilIcon, XIcon } from "lucide-react";
import type { ReactNode } from "react";
import { useEffect, useId, useState } from "react";
import { getActiveUiLocale } from "../lib/i18n";
import { rpc } from "../lib/rpc";
import type { ProfileField } from "../lib/user-profile-view";
import {
CLEARED_PROFILE_FIELD,
displayLanguageTag,
displayRegionTag,
factSpaceNames,
filledProfileFields,
groupUserFacts,
userFactEdit,
} from "../lib/user-profile-view";
import { UserFactEditor } from "./UserFactEditor";
function Row({
value,
detail,
actions,
testId,
status,
}: {
value: string;
detail: string;
actions: (describedBy: string) => ReactNode;
testId: string;
status?: string;
}) {
const valueId = useId();
return (
{actions(valueId)}
);
}
/** Everything the user's bots know about the user, with edit, consent and forget controls. */
export function UserProfileSettings() {
const { t } = useLingui();
const [state, setState] = useState(null);
const [spaces, setSpaces] = useState>([]);
const [editingId, setEditingId] = useState(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [forgetOpen, setForgetOpen] = useState(false);
useEffect(() => {
let cancelled = false;
// Only names a fact's spaces; without them the profile still works.
rpc.spaces
.list()
.then((navigation) => {
if (!cancelled) setSpaces(navigation.spaces.map(({ id, name }) => ({ id, name })));
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
rpc.profile
.get()
.then((next) => {
if (!cancelled) setState(next);
})
.catch((err: unknown) => {
if (!cancelled) setError(err instanceof Error ? err.message : t`Couldn't load`);
});
return () => {
cancelled = true;
};
}, [t]);
async function apply(change: () => Promise): Promise {
if (busy) return false;
setBusy(true);
setError(null);
try {
setState(await change());
return true;
} catch (err) {
setError(err instanceof Error ? err.message : t`Couldn't save`);
return false;
} finally {
setBusy(false);
}
}
const categoryLabels: Record = {
identity: t`Identity`,
work: t`Work`,
goals: t`Goals`,
people: t`People`,
health: t`Health`,
values: t`Values`,
habits: t`Habits`,
preferences: t`Preferences`,
tools: t`Tools`,
};
const sourceLabels: Record = {
onboarding: t`Onboarding`,
user: t`You`,
conversation: t`Chat`,
consolidation: t`Chat history`,
connector: t`Connected app`,
};
const fieldLabels: Record = {
timeZone: t`Time zone`,
language: t`Language`,
locale: t`Region`,
communicationStyle: t`Communication style`,
summary: t`Summary`,
};
const fields = state ? filledProfileFields(state.profile) : [];
const groups = state ? groupUserFacts(state.facts) : [];
const uiLocale = getActiveUiLocale();
const deleteButton = (describedBy: string, onClick: () => void) => (
);
if (!error && (!state || (fields.length === 0 && groups.length === 0))) return null;
return (
About you
{error ? (
{error}
) : null}
{fields.length > 0 ? (
{fields.map(({ field, value }) => (
deleteButton(
describedBy,
() => void apply(() => rpc.profile.update(CLEARED_PROFILE_FIELD[field])),
)
}
/>
))}
) : null}
{groups.map((group) => (
{categoryLabels[group.category]}
{group.facts.map((fact) =>
fact.id === editingId ? (
setEditingId(null)}
onSave={(draft) => {
const edit = userFactEdit(fact, draft);
if (!edit) {
setEditingId(null);
return;
}
void apply(() => rpc.profile.update({ editFacts: [edit] })).then((saved) => {
if (saved) setEditingId(null);
});
}}
/>
) : (
(
<>
{fact.status === "pending" ? (
) : null}
{deleteButton(
describedBy,
() => void apply(() => rpc.profile.forget({ factIds: [fact.id] })),
)}
>
)}
/>
),
)}
))}
{fields.length > 0 || groups.length > 0 ? (
) : null}
Forget everything about you?
Cancel
{
setForgetOpen(false);
void apply(() => rpc.profile.forget({ all: true }));
}}
>
Forget
);
}