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

@@ -15,19 +15,11 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import { Input } from "~/components/ui/input"
import { Textarea } from "~/components/ui/textarea"
import { useAgents } from "~/lib/agents"
import { pageTitle } from "~/lib/page-meta"
import {
profileInitials,
resetProfile,
saveProfile,
useProfile,
type Profile,
@@ -54,15 +46,14 @@ export default function ProfileRoute() {
const session = useSession()
const arcadia = useArcadiaClient()
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 [prefsSavedAt, setPrefsSavedAt] = useState<number | null>(null)
useEffect(() => {
setPrefs(profile)
}, [profile])
const prefsDirty = JSON.stringify(prefs) !== JSON.stringify(profile)
// Arcadia account.
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
// runs on the explicit "Save" button — avatar changes auto-persist
// because the server already accepted them).
// Mirror the avatar URL into localStorage so it survives reloads.
const savePrefsLocal = (next: Profile) => {
saveProfile(next)
}
const savePrefs = () => {
saveProfile(prefs)
setPrefsSavedAt(Date.now())
}
const defaultAgent = agents.find((a) => a.id === prefs.defaultAgentId) ?? null
return (
<AppShell>
<Card>
@@ -517,167 +499,50 @@ export default function ProfileRoute() {
<Card>
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardTitle>Avatar</CardTitle>
<CardDescription>
Avatar uploads land in your tenant's storage backend; the rest
(title, signature, default persona) stays local to this browser.
Uploads land in your tenant's storage backend.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Avatar</span>
{avatarError ? (
<AlertBanner variant="error">{avatarError}</AlertBanner>
) : null}
<div className="flex items-center gap-3">
<label
aria-disabled={avatarUploading}
className={[
"inline-flex w-fit items-center gap-2 rounded-md border bg-background px-3 py-1.5 text-sm",
avatarUploading
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent hover:text-accent-foreground",
].join(" ")}
>
<input
data-action="profile-avatar-upload"
type="file"
accept="image/*"
className="sr-only"
disabled={avatarUploading}
onChange={(e) => onPickAvatar(e.target.files?.[0] ?? null)}
/>
{avatarUploading ? "Uploading…" : "Upload avatar"}
</label>
{prefs.avatarUrl && !avatarUploading && (
<Button
data-action="profile-avatar-remove"
variant="ghost"
size="sm"
onClick={() => onPickAvatar(null)}
className="text-muted-foreground"
>
<Trash2 className="size-3.5" /> Remove
</Button>
)}
</div>
<span className="text-xs text-muted-foreground">
PNG, JPG, or SVG. Stored locally as a data URL.
</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"
<CardContent className="flex flex-col gap-2">
{avatarError ? (
<AlertBanner variant="error">{avatarError}</AlertBanner>
) : null}
<div className="flex items-center gap-3">
<label
aria-disabled={avatarUploading}
className={[
"inline-flex w-fit items-center gap-2 rounded-md border bg-background px-3 py-1.5 text-sm",
avatarUploading
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent hover:text-accent-foreground",
].join(" ")}
>
<input
data-action="profile-avatar-upload"
type="file"
accept="image/*"
className="sr-only"
disabled={avatarUploading}
onChange={(e) => onPickAvatar(e.target.files?.[0] ?? null)}
/>
</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>
{avatarUploading ? "Uploading…" : "Upload avatar"}
</label>
{prefs.avatarUrl && !avatarUploading && (
<Button
data-action="profile-avatar-remove"
variant="ghost"
size="sm"
onClick={() => onPickAvatar(null)}
className="text-muted-foreground"
>
<Trash2 className="size-3.5" /> Remove
</Button>
)}
</div>
<span className="text-xs text-muted-foreground">
PNG, JPG, GIF, or WebP. Max 8MB.
</span>
</CardContent>
</Card>
</AppShell>