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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user