diff --git a/apps/web/src/ui/app/AppShell.tsx b/apps/web/src/ui/app/AppShell.tsx index 439c09e..d2216d8 100644 --- a/apps/web/src/ui/app/AppShell.tsx +++ b/apps/web/src/ui/app/AppShell.tsx @@ -14,6 +14,7 @@ import type { RpcResult, ServerSettings, Screen, + UserProfile, WorkspaceSection, } from "@mosaiciq/contracts/rpc"; import { rpc } from "../../rpcClient"; @@ -140,6 +141,7 @@ export function App() { const [llmLoading, setLlmLoading] = useState(false); const [selectedLlmModel, setSelectedLlmModel] = useState(""); const [keybindingsDraft, setKeybindingsDraft] = useState>({}); + const [profile, setProfile] = useState>({}); const [workspaceSection, setWorkspaceSection] = useState(null); const [workspaceSources, setWorkspaceSources] = useState([]); const [workspaceLoading, setWorkspaceLoading] = useState(false); @@ -294,6 +296,7 @@ export function App() { if (settings.theme) setTheme(settings.theme); if (settings.density) setDensity(settings.density); setKeybindingsDraft(settings.keybindings ?? {}); + setProfile(settings.profile ?? {}); } const server = await rpc.call("settings.get", { scope: "server" }); if (server.ok) { @@ -557,6 +560,7 @@ export function App() { onScreenChange={setActiveScreen} onSettingsOpen={() => setSettingsOpen(true)} searchOpen={searchOpen} + profile={profile} searchQuery={searchQuery} onSearchChange={(q) => { setSearchOpen(true); @@ -876,6 +880,12 @@ export function App() { setProfileOpen(false)} + profile={profile} + onSave={async (newProfile) => { + setProfile(newProfile); + const result = await rpc.call("settings.update", { scope: "client", changes: { profile: newProfile } }); + if (!result.ok) addToast({ type: "error", title: "Profile save failed", desc: result.error.message }); + }} /> ; + profile?: Partial; }) { return (
@@ -1120,8 +1131,16 @@ function Topbar(props: {
); @@ -3301,11 +3320,34 @@ function SettingsOverlay({ function ProfileOverlay({ open, onClose, + profile, + onSave, }: { open: boolean; onClose: () => void; + profile: Partial; + onSave: (profile: Partial) => Promise; }) { + const [draft, setDraft] = useState>(profile); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setDraft(profile); + }, [profile]); + if (!open) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + await onSave(draft); + onClose(); + } finally { + setSaving(false); + } + }; + return (

- JD Profile + Profile

- - -

Profile editing is local-only until a profile backend is added.

+
+ + setDraft({ ...draft, name: e.target.value })} + placeholder="Your name" + /> + + + setDraft({ ...draft, role: e.target.value })} + placeholder="Portfolio Manager, Analyst, etc." + /> + + + setDraft({ ...draft, email: e.target.value || undefined })} + placeholder="you@example.com" + /> + + + setDraft({ ...draft, phone: e.target.value || undefined })} + placeholder="+1 (555) 123-4567" + /> + +
+ + +
+
); diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 2ed0db0..0bfb04f 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -187,6 +187,8 @@ export type ClientSettings = z.infer; +export type UserProfile = z.infer; + export type RpcRequestMap = { "portfolio.get": undefined; "portfolio.addHolding": { ticker: string }; diff --git a/packages/contracts/src/rpcSchemas.ts b/packages/contracts/src/rpcSchemas.ts index ac9a045..3bc68d4 100644 --- a/packages/contracts/src/rpcSchemas.ts +++ b/packages/contracts/src/rpcSchemas.ts @@ -28,12 +28,20 @@ const tickerString = z.string().trim().min(1).max(16); const nonNegativeIndex = z.number().int().min(0); const unknownRecord = z.record(z.unknown()); +export const UserProfileSchema = z.object({ + name: z.string().min(1).default(""), + role: z.string().min(1).default(""), + email: z.string().email().optional(), + phone: z.string().optional(), +}); + export const ClientSettingsSchema = z.object({ theme: z.enum(["light", "dark", "system"]), density: z.enum(["comfortable", "compact", "dense"]), sidebarWidth: z.number().int().min(160).max(520), navCollapsed: z.record(z.boolean()), keybindings: z.record(z.string()), + profile: UserProfileSchema.partial().default({}), }); export const ServerSettingsSchema = z.object({ diff --git a/packages/shared/src/db/queries.ts b/packages/shared/src/db/queries.ts index 62c903e..d966cf1 100644 --- a/packages/shared/src/db/queries.ts +++ b/packages/shared/src/db/queries.ts @@ -25,7 +25,7 @@ import type { ClientSettings, ServerSettings, } from "@mosaiciq/contracts/rpc"; -import { ClientSettingsSchema, ServerSettingsSchema } from "@mosaiciq/contracts/rpcSchemas"; +import { ClientSettingsSchema, ServerSettingsSchema, UserProfileSchema } from "@mosaiciq/contracts/rpcSchemas"; export function parseJsonWithSchema( value: string, @@ -982,6 +982,7 @@ const DEFAULT_CLIENT_SETTINGS: ClientSettings = { "navigation.agents": "Cmd+4", "navigation.home": "Cmd+0", }, + profile: {}, }; export function getClientSettings(db: Db): ClientSettings { @@ -999,6 +1000,9 @@ export function getClientSettings(db: Db): ClientSettings { } else if (row.key === "navCollapsed") { const navCollapsed = z.record(z.boolean()).safeParse(parsed); if (navCollapsed.success) settings.navCollapsed = { ...settings.navCollapsed, ...navCollapsed.data }; + } else if (row.key === "profile") { + const profile = UserProfileSchema.partial().safeParse(parsed); + if (profile.success) settings.profile = { ...settings.profile, ...profile.data }; } else { const candidate = { ...settings, [row.key]: parsed }; const validated = ClientSettingsSchema.safeParse(candidate); @@ -1041,6 +1045,9 @@ export function updateClientSettings(db: Db, settings: Partial): if (settings.keybindings !== undefined) { updateClientSetting(db, "keybindings", settings.keybindings); } + if (settings.profile !== undefined) { + updateClientSetting(db, "profile", settings.profile); + } } export function getServerSettings(db: Db): ServerSettings {