profile: drop unused title/signature/defaultAgentId fields

These were aspirational placeholders — stored to localStorage but never
read by anything. Removed from the form, types, and persistence layer.
Local profile is now just the avatar URL mirror, which the appbar reads
before the server profile fetch resolves on mount.

Preferences card renamed to "Avatar" since that's all that's left.
Re-add server-backed if/when something actually consumes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-05 09:54:24 +10:00
parent f6a92118da
commit ffe3fc0473
2 changed files with 46 additions and 198 deletions

View File

@@ -1,23 +1,16 @@
// Local user preferences — title, signature, default agent, plus a cache // Local mirror of the resolved avatar URL, so the appbar can render the
// mirror of the resolved avatar URL. Persisted in localStorage; reactive // avatar before the profile fetch resolves on next mount. The real
// across tabs. Identity (name, email) is owned by the arcadia session // profile (name, email, bio, phone, location, timezone, avatar) is
// (~/lib/session.ts); the public profile (bio, phone, location, timezone) // server-backed — see ~/lib/arcadia/profiles.ts.
// is server-backed via /api/v1/profile (~/lib/arcadia/profiles.ts).
import { useEffect, useSyncExternalStore } from "react" import { useEffect, useSyncExternalStore } from "react"
export type Profile = { export type Profile = {
title: string
signature: string
avatarUrl: string avatarUrl: string
defaultAgentId: string
} }
export const DEFAULT_PROFILE: Profile = { export const DEFAULT_PROFILE: Profile = {
title: "",
signature: "",
avatarUrl: "", avatarUrl: "",
defaultAgentId: "",
} }
const STORAGE_KEY = "crema.profile" const STORAGE_KEY = "crema.profile"
@@ -30,20 +23,10 @@ function readFromStorage(): Profile {
if (!raw) return DEFAULT_PROFILE if (!raw) return DEFAULT_PROFILE
const parsed = JSON.parse(raw) as Partial<Profile> const parsed = JSON.parse(raw) as Partial<Profile>
return { return {
title:
typeof parsed.title === "string" ? parsed.title : DEFAULT_PROFILE.title,
signature:
typeof parsed.signature === "string"
? parsed.signature
: DEFAULT_PROFILE.signature,
avatarUrl: avatarUrl:
typeof parsed.avatarUrl === "string" typeof parsed.avatarUrl === "string"
? parsed.avatarUrl ? parsed.avatarUrl
: DEFAULT_PROFILE.avatarUrl, : DEFAULT_PROFILE.avatarUrl,
defaultAgentId:
typeof parsed.defaultAgentId === "string"
? parsed.defaultAgentId
: DEFAULT_PROFILE.defaultAgentId,
} }
} catch { } catch {
return DEFAULT_PROFILE return DEFAULT_PROFILE

View File

@@ -15,19 +15,11 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "~/components/ui/card" } from "~/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import { Input } from "~/components/ui/input" import { Input } from "~/components/ui/input"
import { Textarea } from "~/components/ui/textarea" import { Textarea } from "~/components/ui/textarea"
import { useAgents } from "~/lib/agents"
import { pageTitle } from "~/lib/page-meta" import { pageTitle } from "~/lib/page-meta"
import { import {
profileInitials, profileInitials,
resetProfile,
saveProfile, saveProfile,
useProfile, useProfile,
type Profile, type Profile,
@@ -54,15 +46,14 @@ export default function ProfileRoute() {
const session = useSession() const session = useSession()
const arcadia = useArcadiaClient() const arcadia = useArcadiaClient()
const profile = useProfile() const profile = useProfile()
const agents = useAgents()
// Local preferences (avatar, title, bio, signature, default agent). // Mirror of the resolved avatar URL — kept in localStorage so the
// <AvatarImage> in the appbar can render before the profile fetch
// resolves on next mount.
const [prefs, setPrefs] = useState<Profile>(profile) const [prefs, setPrefs] = useState<Profile>(profile)
const [prefsSavedAt, setPrefsSavedAt] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
setPrefs(profile) setPrefs(profile)
}, [profile]) }, [profile])
const prefsDirty = JSON.stringify(prefs) !== JSON.stringify(profile)
// Arcadia account. // Arcadia account.
const [account, setAccount] = useState<User | null>(null) const [account, setAccount] = useState<User | null>(null)
@@ -260,20 +251,11 @@ export default function ProfileRoute() {
} }
} }
// Save the local-prefs mirror (separate from `savePrefs`, which only // Mirror the avatar URL into localStorage so it survives reloads.
// runs on the explicit "Save" button — avatar changes auto-persist
// because the server already accepted them).
const savePrefsLocal = (next: Profile) => { const savePrefsLocal = (next: Profile) => {
saveProfile(next) saveProfile(next)
} }
const savePrefs = () => {
saveProfile(prefs)
setPrefsSavedAt(Date.now())
}
const defaultAgent = agents.find((a) => a.id === prefs.defaultAgentId) ?? null
return ( return (
<AppShell> <AppShell>
<Card> <Card>
@@ -517,15 +499,12 @@ export default function ProfileRoute() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Preferences</CardTitle> <CardTitle>Avatar</CardTitle>
<CardDescription> <CardDescription>
Avatar uploads land in your tenant's storage backend; the rest Uploads land in your tenant's storage backend.
(title, signature, default persona) stays local to this browser.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-6"> <CardContent className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Avatar</span>
{avatarError ? ( {avatarError ? (
<AlertBanner variant="error">{avatarError}</AlertBanner> <AlertBanner variant="error">{avatarError}</AlertBanner>
) : null} ) : null}
@@ -562,122 +541,8 @@ export default function ProfileRoute() {
)} )}
</div> </div>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
PNG, JPG, or SVG. Stored locally as a data URL. PNG, JPG, GIF, or WebP. Max 8MB.
</span> </span>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Field label="Title" hint="Your role at work.">
<Input
data-action="profile-title"
value={prefs.title}
onChange={(e) =>
setPrefs((d) => ({ ...d, title: e.target.value }))
}
placeholder="e.g. Platform admin"
/>
</Field>
<Field
label="Default agent"
hint="Used as the active persona on first load."
>
<DropdownMenu>
<DropdownMenuTrigger
data-action="profile-default-agent"
className="inline-flex h-9 items-center justify-between gap-2 rounded-md border bg-background px-3 text-sm hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="truncate">
{defaultAgent ? (
<>
<span className="font-medium">{defaultAgent.name}</span>
<span className="text-muted-foreground">
{" "}
{defaultAgent.role}
</span>
</>
) : (
"Use first available"
)}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
<DropdownMenuItem
onClick={() =>
setPrefs((d) => ({ ...d, defaultAgentId: "" }))
}
data-state={!prefs.defaultAgentId ? "checked" : undefined}
>
First available
</DropdownMenuItem>
{agents.map((a) => (
<DropdownMenuItem
key={a.id}
onClick={() =>
setPrefs((d) => ({ ...d, defaultAgentId: a.id }))
}
data-state={
prefs.defaultAgentId === a.id ? "checked" : undefined
}
className="flex flex-col items-start"
>
<span className="font-medium">{a.name}</span>
<span className="text-xs text-muted-foreground">
{a.role}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</Field>
</div>
<Field
label="Signature"
hint="Appended automatically when you ask the assistant to draft an email or note."
>
<Textarea
data-action="profile-signature"
value={prefs.signature}
onChange={(e) =>
setPrefs((d) => ({ ...d, signature: e.target.value }))
}
rows={3}
placeholder={`Cheers,\n${account?.full_name || "Your name"}`}
/>
</Field>
<div className="flex flex-wrap items-center gap-2">
<Button
data-action="profile-prefs-save"
onClick={savePrefs}
disabled={!prefsDirty}
>
Save preferences
</Button>
<Button
data-action="profile-prefs-revert"
variant="ghost"
onClick={() => setPrefs(profile)}
disabled={!prefsDirty}
>
Revert
</Button>
<Button
data-action="profile-prefs-reset"
variant="ghost"
onClick={() => {
resetProfile()
setPrefsSavedAt(Date.now())
}}
>
Reset to defaults
</Button>
{prefsSavedAt && !prefsDirty && (
<span className="inline-flex items-center gap-1 text-sm text-emerald-700 dark:text-emerald-400">
<Check className="size-4" /> Saved.
</span>
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
</AppShell> </AppShell>