evOo-local/apps/web/src/pages/SettingsOverlay.tsx
cynarAI 3291a32d37
Show About you inside General settings (#165)
The profile had its own settings tab although it is a few rows of personal
details. It now sits as one card under Account in General, hides itself
while empty, and the separate tab is gone.


Claude-Session: https://claude.ai/code/session_017gHYZ4c9Twex73v4ZasmMJ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 18:36:54 +02:00

290 lines
10 KiB
TypeScript

import { useLingui } from "@lingui/react/macro";
import type { SpaceMemoryConfig } from "@rakazo/contracts";
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import {
Bell,
Brain,
Clapperboard,
CloudDownload,
Cpu,
Gauge,
LayoutGrid,
Monitor,
Settings,
ShieldCheck,
Volume2,
XIcon,
} from "lucide-react";
import { type ComponentType, useEffect, useRef, useState } from "react";
import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
import { MediaSettings } from "../components/MediaSettings";
import { NotificationSettingsPanel } from "../components/NotificationSettingsPanel";
import {
ComputerSettingsPanel,
GeneralSettingsPanels,
UpdatesSettingsPanel,
UsageSettingsPanel,
} from "./AccountSettingsOverlay";
import { MemorySettingsOverlay } from "./MemorySettingsOverlay";
import { ModelSettingsOverlay } from "./ModelSettingsOverlay";
import { PluginsOverlay } from "./PluginsOverlay";
import { PrivacySettingsPanel } from "./PrivacySettingsOverlay";
import { VoiceSettingsOverlay } from "./VoiceSettingsOverlay";
export type SettingsSection =
| "general"
| "integrations"
| "models"
| "memory"
| "voice"
| "media"
| "notifications"
| "usage"
| "computer"
| "updates"
| "privacy";
type NavItem = {
id: SettingsSection;
label: string;
icon: ComponentType<{ className?: string; strokeWidth?: number }>;
};
export function SettingsOverlay({
email,
name,
usage,
initialSection = "general",
isDeploymentOwner = false,
sandboxProvider,
messagingEnabled = false,
onOpenMessaging,
memoryConfig,
onMemoryConfigChange,
onClose,
onVoiceStatusMaybeChanged,
activeBotId,
onOpenMcp,
}: {
email?: string | null;
name: string;
usage?: { runs: number; inputTokens: number; outputTokens: number } | null;
initialSection?: SettingsSection;
isDeploymentOwner?: boolean;
sandboxProvider?: string | null;
messagingEnabled?: boolean;
onOpenMessaging?: () => void;
memoryConfig: SpaceMemoryConfig | null | undefined;
onMemoryConfigChange: (config: SpaceMemoryConfig | null) => void;
onClose: () => void;
onVoiceStatusMaybeChanged?: () => void | Promise<void>;
activeBotId?: string;
onOpenMcp?: () => void;
}) {
const { t } = useLingui();
const panelRef = useRef<HTMLDivElement>(null);
const usageRef = useRef<HTMLDivElement>(null);
const [section, setSection] = useState<SettingsSection>(initialSection);
const [memoryBusy, setMemoryBusy] = useState(false);
const [voiceBusy, setVoiceBusy] = useState(false);
const showComputer = isDeploymentOwner && computersAreUnavailable(sandboxProvider);
const panelBusy = memoryBusy || voiceBusy;
useEffect(() => {
setSection(initialSection);
}, [initialSection]);
useEffect(() => {
if (section === "usage") {
usageRef.current?.focus();
}
}, [section]);
const navItems: NavItem[] = [
{ id: "general", label: t`General`, icon: Settings },
{ id: "integrations", label: t`Integrations`, icon: LayoutGrid },
{ id: "models", label: t`Models`, icon: Cpu },
{ id: "memory", label: t`Memory`, icon: Brain },
{ id: "voice", label: t`Voice`, icon: Volume2 },
...(isDeploymentOwner ? [{ id: "media" as const, label: t`Media`, icon: Clapperboard }] : []),
{ id: "notifications", label: t`Notifications`, icon: Bell },
{ id: "usage", label: t`Usage`, icon: Gauge },
...(showComputer ? [{ id: "computer" as const, label: t`Computer`, icon: Monitor }] : []),
{ id: "updates", label: t`Updates`, icon: CloudDownload },
{ id: "privacy", label: t`Privacy & Data`, icon: ShieldCheck },
];
const sectionTitle =
navItems.find((item) => item.id === section)?.label ??
(section === "general" ? t`General` : t`Settings`);
const closeLabel =
section === "models"
? t`Close model settings`
: section === "memory"
? t`Close memory settings`
: section === "voice"
? t`Close voice settings`
: section === "integrations"
? t`Close integrations settings`
: t`Close user settings`;
async function refreshVoiceStatus() {
await onVoiceStatusMaybeChanged?.();
}
function leaveSettings(next: () => void) {
if (panelBusy) return;
void refreshVoiceStatus().finally(next);
}
function requestClose() {
leaveSettings(onClose);
}
const widePane = section === "models" || section === "voice" || section === "integrations";
return (
<Dialog
open
onOpenChange={(open, details) => {
if (open) return;
if (panelBusy) {
details.cancel();
return;
}
requestClose();
}}
>
<DialogContent
ref={panelRef}
data-testid="user-settings"
data-settings-section={section}
showCloseButton={false}
initialFocus={() =>
section === "usage" ? (usageRef.current ?? panelRef.current) : panelRef.current
}
className={`flex max-h-[calc(100%-2rem)] flex-col gap-0 overflow-hidden rounded-2xl p-0 sm:max-h-[calc(100%-5rem)] ${
widePane
? "h-[min(760px,calc(100%-2rem))] w-[min(1080px,calc(100%-2rem))] sm:max-w-[1080px]"
: "h-[min(720px,calc(100%-2rem))] w-[min(920px,calc(100%-2rem))] sm:max-w-[920px]"
}`}
>
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
<nav
data-testid="settings-nav"
aria-label={t`Settings`}
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[200px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
>
{navItems.map((item) => {
const Icon = item.icon;
const active = item.id === section;
return (
<button
key={item.id}
type="button"
data-testid={`settings-nav-${item.id}`}
aria-current={active ? "page" : undefined}
disabled={panelBusy}
onClick={() => setSection(item.id)}
className={`flex shrink-0 items-center gap-2.5 rounded-lg px-2.5 py-2 text-start text-ui-body transition-colors disabled:pointer-events-none disabled:opacity-50 ${
active
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
}`}
>
<Icon className="size-4 shrink-0" strokeWidth={1.75} />
<span className="whitespace-nowrap">{item.label}</span>
</button>
);
})}
</nav>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex items-start justify-between gap-4 px-6 pt-6 sm:px-8 sm:pt-7">
<DialogTitle className="text-2xl font-medium text-foreground">
{sectionTitle}
</DialogTitle>
<DialogClose
aria-label={closeLabel}
disabled={panelBusy}
render={<Button variant="ghost" size="icon-sm" />}
>
<XIcon />
</DialogClose>
</div>
<div
className={`min-h-0 flex-1 ${
section === "models" ||
section === "voice" ||
section === "memory" ||
section === "integrations"
? "flex flex-col overflow-hidden"
: "rk-scroll overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8"
}`}
>
{section === "general" ? (
<GeneralSettingsPanels
email={email}
name={name}
messagingEnabled={messagingEnabled}
onOpenMessaging={
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
}
isDeploymentOwner={isDeploymentOwner}
/>
) : null}
{section === "integrations" ? (
<PluginsOverlay
embedded
activeBotId={activeBotId}
onClose={requestClose}
onOpenMcp={onOpenMcp ? () => leaveSettings(onOpenMcp) : undefined}
/>
) : null}
{section === "media" && isDeploymentOwner ? <MediaSettings /> : null}
{section === "notifications" ? <NotificationSettingsPanel /> : null}
{section === "usage" ? (
<UsageSettingsPanel usage={usage} panelRef={usageRef} />
) : null}
{section === "computer" && showComputer ? <ComputerSettingsPanel /> : null}
{section === "updates" ? (
<UpdatesSettingsPanel isDeploymentOwner={isDeploymentOwner} />
) : null}
{section === "models" ? (
<ModelSettingsOverlay embedded onClose={requestClose} />
) : null}
{section === "memory" ? (
<MemorySettingsOverlay
embedded
onClose={requestClose}
config={memoryConfig}
onConfigChange={onMemoryConfigChange}
onBusyChange={setMemoryBusy}
/>
) : null}
{section === "voice" ? (
<VoiceSettingsOverlay
embedded
onClose={requestClose}
onBusyChange={setVoiceBusy}
activeBotId={activeBotId}
/>
) : null}
{section === "privacy" ? (
<PrivacySettingsPanel
email={email ?? undefined}
isDeploymentOwner={isDeploymentOwner}
sandboxProvider={sandboxProvider}
onAccountDeleted={onClose}
/>
) : null}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}