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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gHYZ4c9Twex73v4ZasmMJ
290 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|