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:
@@ -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
|
||||||
|
|||||||
@@ -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,167 +499,50 @@ 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">
|
{avatarError ? (
|
||||||
<span className="text-sm font-medium">Avatar</span>
|
<AlertBanner variant="error">{avatarError}</AlertBanner>
|
||||||
{avatarError ? (
|
) : null}
|
||||||
<AlertBanner variant="error">{avatarError}</AlertBanner>
|
<div className="flex items-center gap-3">
|
||||||
) : null}
|
<label
|
||||||
<div className="flex items-center gap-3">
|
aria-disabled={avatarUploading}
|
||||||
<label
|
className={[
|
||||||
aria-disabled={avatarUploading}
|
"inline-flex w-fit items-center gap-2 rounded-md border bg-background px-3 py-1.5 text-sm",
|
||||||
className={[
|
avatarUploading
|
||||||
"inline-flex w-fit items-center gap-2 rounded-md border bg-background px-3 py-1.5 text-sm",
|
? "cursor-not-allowed opacity-60"
|
||||||
avatarUploading
|
: "cursor-pointer hover:bg-accent hover:text-accent-foreground",
|
||||||
? "cursor-not-allowed opacity-60"
|
].join(" ")}
|
||||||
: "cursor-pointer hover:bg-accent hover:text-accent-foreground",
|
>
|
||||||
].join(" ")}
|
<input
|
||||||
>
|
data-action="profile-avatar-upload"
|
||||||
<input
|
type="file"
|
||||||
data-action="profile-avatar-upload"
|
accept="image/*"
|
||||||
type="file"
|
className="sr-only"
|
||||||
accept="image/*"
|
disabled={avatarUploading}
|
||||||
className="sr-only"
|
onChange={(e) => onPickAvatar(e.target.files?.[0] ?? null)}
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
{avatarUploading ? "Uploading…" : "Upload avatar"}
|
||||||
<Field
|
</label>
|
||||||
label="Default agent"
|
{prefs.avatarUrl && !avatarUploading && (
|
||||||
hint="Used as the active persona on first load."
|
<Button
|
||||||
>
|
data-action="profile-avatar-remove"
|
||||||
<DropdownMenu>
|
variant="ghost"
|
||||||
<DropdownMenuTrigger
|
size="sm"
|
||||||
data-action="profile-default-agent"
|
onClick={() => onPickAvatar(null)}
|
||||||
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"
|
className="text-muted-foreground"
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<Trash2 className="size-3.5" /> Remove
|
||||||
{defaultAgent ? (
|
</Button>
|
||||||
<>
|
|
||||||
<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>
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
PNG, JPG, GIF, or WebP. Max 8MB.
|
||||||
|
</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
Reference in New Issue
Block a user