diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx index 26ae7f4..3886996 100644 --- a/app/routes/settings.tsx +++ b/app/routes/settings.tsx @@ -1,12 +1,15 @@ -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" +import { Link } from "react-router" import { - Cpu, + Bot, Palette, User as UserIcon, Info, Users, + Building2, Plus, Trash2, + ExternalLink, } from "lucide-react" import { listModels } from "@crema/llm-ui" import { @@ -21,6 +24,16 @@ import { useArcadiaClient } from "@crema/arcadia-core-client" import { probeProxy, type LLMProxyProvider } from "~/lib/arcadia/llm-proxy" import { LlmConfigurationsPanel } from "~/components/settings/llm-configurations-panel" import { AppShell } from "~/components/layout/app-shell" +import { DataState } from "~/components/data-state" +import { ThemeToggle } from "~/components/layout/theme-toggle" +import { BackgroundPicker } from "~/components/layout/background-picker" +import { FontSizePicker } from "~/components/layout/font-size-picker" +import { SurfacePicker } from "~/components/layout/surface-picker" +import { BrandingTab } from "~/components/tenant-detail/branding-tab" +import { LocalizationTab } from "~/components/tenant-detail/localization-tab" +import { getTenant, type Tenant } from "~/lib/arcadia/tenants" +import { useCapabilities } from "~/lib/capabilities" +import { useSession } from "~/lib/session" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" import { Textarea } from "~/components/ui/textarea" @@ -46,20 +59,42 @@ export const meta = () => pageTitle("Settings") const SECTION_KEY = "crema.settings.section" -type SectionId = "llm" | "agents" | "appearance" | "account" | "about" +type SectionId = + | "assistant" + | "personas" + | "tenant" + | "appearance" + | "account" + | "about" -const sections: { +type SectionDef = { id: SectionId label: string icon: React.ComponentType<{ className?: string }> description: string -}[] = [ - { id: "llm", label: "LLM", icon: Cpu, description: "Model endpoint & budgets" }, + /** When set, the section only shows if the session holds this capability. */ + requiresCapability?: string +} + +const sections: SectionDef[] = [ { - id: "agents", - label: "Agents", + id: "assistant", + label: "Assistant", + icon: Bot, + description: "Model, endpoint & budgets", + }, + { + id: "personas", + label: "Personas", icon: Users, - description: "Personas, roles, sub-prompts", + description: "Assistant personas (this browser)", + }, + { + id: "tenant", + label: "Tenant", + icon: Building2, + description: "Branding & localization", + requiresCapability: "platform.tenants", }, { id: "appearance", @@ -67,7 +102,7 @@ const sections: { icon: Palette, description: "Theme, font size, surface, background", }, - { id: "account", label: "Account", icon: UserIcon, description: "Profile & preferences" }, + { id: "account", label: "Account", icon: UserIcon, description: "Your profile" }, { id: "about", label: "About", icon: Info, description: "Version & credits" }, ] @@ -159,13 +194,23 @@ export default function SettingsRoute() { } } + const caps = useCapabilities() + const visibleSections = sections.filter( + (s) => !s.requiresCapability || caps.has(s.requiresCapability as never), + ) + const [section, setSection] = useState(() => { - if (typeof window === "undefined") return "llm" + if (typeof window === "undefined") return "assistant" const stored = localStorage.getItem(SECTION_KEY) return sections.some((s) => s.id === stored) ? (stored as SectionId) - : "llm" + : "assistant" }) + // If the stored section isn't available to this user (e.g. Tenant for a + // non-platform admin), fall back to Assistant. + useEffect(() => { + if (!visibleSections.some((s) => s.id === section)) setSection("assistant") + }, [visibleSections, section]) useEffect(() => { if (typeof window !== "undefined") localStorage.setItem(SECTION_KEY, section) @@ -178,7 +223,7 @@ export default function SettingsRoute() { aria-label="Settings sections" className="flex flex-row flex-wrap gap-1 md:flex-col md:flex-nowrap md:gap-0.5" > - {sections.map((s) => { + {visibleSections.map((s) => { const Icon = s.icon const active = section === s.id return ( @@ -213,7 +258,7 @@ export default function SettingsRoute() {
- {section === "llm" && ( + {section === "assistant" && (
@@ -245,39 +290,37 @@ export default function SettingsRoute() {
)} - {section === "agents" && } + {section === "personas" && } + + {section === "tenant" && } {section === "appearance" && ( Appearance - Theme, font size, surface tint, and background atmosphere are - in the appbar — the toggles up top write to localStorage and - persist across sessions. + Theme, density, and background for this browser. These are + also in the top-right toolbar; they persist locally. - - Use the icons in the appbar (top right) to change theme, font - size, surface tint, and background. + + + + + + + + + + + + + )} - {section === "account" && ( - - - Account - - Identity and profile preferences. - - - - Wire ~/lib/identity.ts to a - real session to populate this panel. - - - )} + {section === "account" && } {section === "about" && ( @@ -286,11 +329,8 @@ export default function SettingsRoute() { App version and credits. +

Arcadia Admin — operator console for arcadia-core.

Built on the Crema design system.

-

- Hybrid traditional + AI-first scaffold with a virtual cursor - and command bus for assistant-driven UI control. -

)} @@ -318,6 +358,148 @@ function Field({ ) } +function AppearanceRow({ + label, + hint, + children, +}: { + label: string + hint: string + children: React.ReactNode +}) { + return ( +
+
+
{label}
+
{hint}
+
+ {children} +
+ ) +} + +function AccountSection() { + const session = useSession() + return ( + + + Account + Your identity on this deployment. + + +
+ + + + +
+
+ +
+
+
+ ) +} + +function ReadOnlyFact({ + label, + value, + mono, +}: { + label: string + value: string + mono?: boolean +}) { + return ( +
+
{label}
+
{value}
+
+ ) +} + +/** + * Tenant settings — reuses the Phase-4 tenant-detail tab components, scoped to + * the operator's active tenant. The branding/localization endpoints are + * platform-gated, so this section is only offered to platform admins (see the + * `requiresCapability` on the section); if the load 403s anyway, DataState + * surfaces it cleanly. + */ +function TenantSettingsSection() { + const arcadia = useArcadiaClient() + const session = useSession() + const tenantId = session?.tenantId + + const [tenant, setTenant] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const reload = useCallback(async () => { + if (!tenantId) { + setLoading(false) + return + } + setError(null) + setLoading(true) + try { + setTenant(await getTenant(arcadia, tenantId)) + } catch (err) { + setError(err) + } finally { + setLoading(false) + } + }, [arcadia, tenantId]) + + useEffect(() => { + reload() + }, [reload]) + + return ( +
+

+ Branding and localization for your active tenant + {session?.tenantSlug ? ( + <> + {" "} + ({session.tenantSlug}) + + ) : null} + . To manage a different tenant, open it from{" "} + + Tenants + + . +

+ + No active tenant on this session. +
+ } + > + {tenant ? ( +
+ + +
+ ) : null} + +
+ ) +} + function AgentsPanel() { const agents = useAgents() const [activeId, setActiveId] = useState(() => loadActiveAgentId()) @@ -354,12 +536,12 @@ function AgentsPanel() { return ( - Agents + Personas - Personas with their own sub-system prompts. Switch the active one in - the chat status bar — the assistant inherits its skills, tone, and - scope. Lets you keep contexts focused: a coder agent doesn't carry - writing-task context; a writer doesn't carry codebase context. + Assistant personas with their own sub-system prompts. Switch the + active one in the chat status bar — the assistant inherits its skills, + tone, and scope. Stored in this browser only, not synced to your + account. diff --git a/arcadia-admin-ux-spec.md b/arcadia-admin-ux-spec.md new file mode 100644 index 0000000..9615d62 --- /dev/null +++ b/arcadia-admin-ux-spec.md @@ -0,0 +1,15 @@ + +## Phase 6 — as built (2026-07-14) — Settings restructure + +`arcadia-admin` (branch `feat/admin-ux-overhaul`). Settings was "assistant config mislabeled as Settings, plus a dev-scaffold Account panel." Restructured honestly: + +- **Assistant** (was "LLM") — model/endpoint/budgets + advanced transport + reset. Honestly named. +- **Personas** (was "Agents") — the persona editor, now labelled "Stored in this browser only, not synced to your account" (they're localStorage). +- **Tenant** (new, `platform.tenants`-gated) — **reuses the Phase-4 BrandingTab + LocalizationTab** scoped to the operator's active tenant (loaded via getTenant(session.tenantId)). This is the component-reuse the phase order was designed for; verified end-to-end (branding save → "Branding updated"). Gated to platform admins because the underlying endpoints are platform-only (my Phase-1 change); a non-holder simply doesn't see the section. +- **Appearance** — replaced the "use the icons in the appbar" fluff with the real inline pickers (Theme, Font size, Surface tint, Background — the same components the appbar mounts). +- **Account** — replaced the dev scaffold ("Wire ~/lib/identity.ts…" shown verbatim to users) with real read-only identity (name/email/active tenant/roles from the session) + an "Edit profile" link to /profile. +- **About** — trimmed to honest one-liners. + +Sections filter by capability; the persisted section falls back to Assistant if unavailable to the user. + +Verified: honest section labels, no dev scaffold text, real Account identity, real Appearance controls, "this browser" persona label, and the Tenant section reusing Phase-4 tabs with a working save. Typecheck 36→36 (zero added), 24-route sweep clean.