From 188e91920acb88f9d06818d72629cd2a89f3093d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 4 Mar 2026 21:36:58 +0000 Subject: [PATCH] add /settings/ai route with role-aware model management Two-tier model policy: system policy (admin, allowedModels) controls member access, per-user hiddenModels controls personal visibility. All model selectors now use useUserVisibleModels. Moved providers and model visibility out of system settings, AI models out of profile. Co-Authored-By: Claude Opus 4.6 --- src/apps/officer-web/App.tsx | 21 +- .../Dashboard/Layout/Header/UserMenu.tsx | 14 +- .../Screens/Dashboard/Settings/AISettings.tsx | 519 ++++++++++++++++++ .../Settings/ProfileSettings/AIModels.tsx | 4 +- .../Settings/ProfileSettings/TaskDefaults.tsx | 4 +- .../Settings/ProfileSettings/index.tsx | 36 +- .../Dashboard/Settings/SystemSettings.tsx | 386 +------------ .../Screens/Dashboard/Settings/index.tsx | 1 + .../Chat/EmbeddableChat/useEmbeddableChat.ts | 4 +- .../components/TaskRunnerModal.tsx | 4 +- src/workspaces/state/src/index.ts | 2 +- src/workspaces/state/src/useModels.ts | 23 +- src/workspaces/state/src/useSettings.ts | 1 + 13 files changed, 606 insertions(+), 413 deletions(-) create mode 100644 src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 191f9e21..93fd02d7 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -39,9 +39,19 @@ export function App() { } /> } /> - : } /> - : } /> - : } /> + } /> + : } + /> + : } + /> + : } + /> } /> } /> } /> @@ -66,7 +76,10 @@ export function App() { } /> } /> } /> - : } /> + : } + /> } /> } /> diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Header/UserMenu.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Header/UserMenu.tsx index 944599b3..cddfaaed 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Header/UserMenu.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Header/UserMenu.tsx @@ -1,7 +1,7 @@ import { Link } from 'react-router'; import * as Dropdown from '@/components/ui/dropdown-menu'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { User, Users, LogOut, Settings, Package, Puzzle, Rocket, Sun, Moon } from 'lucide-react'; +import { User, Users, LogOut, Settings, Package, Puzzle, Rocket, Sun, Moon, Bot } from 'lucide-react'; import { useAuth } from 'hooks/useAuth'; import { useTranslation } from '@/lib/i18n'; import { useColorMode } from '@/components/ui/ThemeProvider'; @@ -59,6 +59,12 @@ export function UserMenu() { )} + + + + AI + + @@ -81,11 +87,7 @@ export function UserMenu() { )} - {colorMode === 'dark' ? ( - - ) : ( - - )} + {colorMode === 'dark' ? : } {colorMode === 'dark' ? 'Light Mode' : 'Dark Mode'} diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx new file mode 100644 index 00000000..2ad5c33e --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Settings/AISettings.tsx @@ -0,0 +1,519 @@ +import { useState, useMemo, useCallback, type DragEvent } from 'react'; +import { toast } from 'sonner'; +import { Bot, Terminal, Eye, Sparkles, X, Plus, RefreshCw } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useClient } from 'hooks/useClient'; +import type { LayoutNode, PanelComponents } from 'officerdev'; +import { WorkspaceLayout } from 'officerdev'; +import { useAuth } from 'hooks/useAuth'; + +import { createSettingsPanelComponents, type SettingsSectionGroup } from './SettingsPanel'; +import { useSettings } from 'state/useSettings'; +import type { UserSettings } from 'state/useSettings'; +import { usePiModels, useVisiblePiModels, modelKey, getProviderDisplayName, type ModelOption } from 'state/useModels'; +import { useAccessPolicy } from 'state/useAccessPolicy'; +import { useUserState } from 'state/useUserState'; +import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection'; +import { AIModels } from './ProfileSettings/AIModels'; + +const PROVIDER_DISPLAY: Record = { + anthropic: 'Anthropic', + openai: 'OpenAI', + opencode: 'OpenCode Zen', + zai: 'ZAI', + google: 'Google', + groq: 'Groq', + mistral: 'Mistral', + xai: 'xAI', + openrouter: 'OpenRouter', + huggingface: 'Hugging Face', + 'github-copilot': 'GitHub Copilot', + minimax: 'MiniMax', + bedrock: 'Amazon Bedrock', + 'google-vertex': 'Google Vertex AI', + 'azure-openai': 'Azure OpenAI', +}; + +function displayProviderName(provider: string): string { + return PROVIDER_DISPLAY[provider] ?? getProviderDisplayName(provider); +} + +// --- Shared drag-and-drop components --- + +type ModelPillProps = { + label: string; + modelKey: string; + enabled: boolean; + onAction: (key: string) => void; + onDragStart: (ev: DragEvent, key: string) => void; +}; + +const ModelPill = ({ label, modelKey, enabled, onAction, onDragStart }: ModelPillProps) => ( + onDragStart(ev, modelKey)} + className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium cursor-grab active:cursor-grabbing select-none border border-duck-dark/15 dark:border-foreground/15 bg-background/60 text-duck-dark/80 dark:text-foreground/80 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors" + > + {label} + + +); + +type DropZoneProps = { + label: string; + children: React.ReactNode; + onDrop: (key: string) => void; +}; + +const DropZone = ({ label, children, onDrop }: DropZoneProps) => { + const [over, setOver] = useState(false); + + const handleDragOver = (ev: DragEvent) => { + ev.preventDefault(); + setOver(true); + }; + + const handleDrop = (ev: DragEvent) => { + ev.preventDefault(); + setOver(false); + const key = ev.dataTransfer.getData('text/plain'); + if (key) onDrop(key); + }; + + return ( +
+ + {label} + +
setOver(false)} + onDrop={handleDrop} + className={`min-h-[48px] p-2 rounded-lg border border-dashed transition-colors flex flex-wrap gap-1.5 ${over ? 'border-duck-teal bg-duck-teal/5' : 'border-duck-dark/15 dark:border-foreground/15'}`} + > + {children} +
+
+ ); +}; + +// --- My Models Section (per-user hidden models) --- + +function MyModelsSection() { + const { user } = useAuth(); + const { settings, saveSettings } = useSettings(); + const allModels = usePiModels(); + const policyModels = useVisiblePiModels(); + const isAdmin = user?.role !== 'Member'; + const visibleModels = isAdmin ? allModels : policyModels; + const [activeProvider, setActiveProvider] = useUserState('my-models-provider', ''); + + const hiddenModels = settings.chat.hiddenModels ?? []; + + const providerGroups = useMemo(() => { + const groups: Record = {}; + for (const m of visibleModels) { + const provider = m.provider ?? 'Other'; + if (!groups[provider]) groups[provider] = []; + groups[provider]!.push({ id: m.id, name: m.name }); + } + return Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([provider, models]) => ({ provider, models: models.sort((a, b) => a.name.localeCompare(b.name)) })); + }, [visibleModels]); + + const providers = useMemo(() => providerGroups.map((g) => g.provider), [providerGroups]); + const selected = providers.includes(activeProvider) ? activeProvider : (providers[0] ?? ''); + const currentGroup = providerGroups.find((g) => g.provider === selected); + + const { shown, hidden } = useMemo(() => { + if (!currentGroup) return { shown: [], hidden: [] }; + const hiddenSet = new Set(hiddenModels); + const s: { id: string; name: string; key: string }[] = []; + const h: { id: string; name: string; key: string }[] = []; + for (const m of currentGroup.models) { + const key = `${currentGroup.provider}:${m.id}`; + if (hiddenSet.has(key)) { + h.push({ ...m, key }); + } else { + s.push({ ...m, key }); + } + } + return { shown: s, hidden: h }; + }, [currentGroup, hiddenModels]); + + const saveHidden = useCallback( + async (newHidden: string[]) => { + const updated: UserSettings = { + ...settings, + chat: { ...settings.chat, hiddenModels: newHidden }, + }; + await saveSettings(updated); + }, + [settings, saveSettings], + ); + + const hideModel = useCallback( + async (key: string) => { + if (hiddenModels.includes(key)) return; + await saveHidden([...hiddenModels, key]); + }, + [hiddenModels, saveHidden], + ); + + const showModel = useCallback( + async (key: string) => { + await saveHidden(hiddenModels.filter((k) => k !== key)); + }, + [hiddenModels, saveHidden], + ); + + const onDragStart = useCallback((ev: DragEvent, key: string) => { + ev.dataTransfer.setData('text/plain', key); + ev.dataTransfer.effectAllowed = 'move'; + }, []); + + if (providerGroups.length === 0) { + return ( +

+ No models available. Configure API keys in Providers. +

+ ); + } + + return ( +
+
+ {providers.map((p) => ( + + ))} +
+ + + {shown.length === 0 && ( + Drag models here to show + )} + {shown.map((m) => ( + + ))} + + + + {hidden.length === 0 && ( + All models visible + )} + {hidden.map((m) => ( + + ))} + +
+ ); +} + +// --- Member Models Section (system-wide policy, admin only) --- + +function MemberModelsSection() { + const client = useClient(); + const queryClient = useQueryClient(); + const { policy, savePolicy } = useAccessPolicy(); + const piModels = usePiModels(); + const [activeProvider, setActiveProvider] = useUserState('member-models-provider', ''); + const [refreshing, setRefreshing] = useState(false); + + const refreshModels = async () => { + setRefreshing(true); + try { + await client.post('/server-settings/pi-mono/local-providers/refresh'); + queryClient.invalidateQueries({ queryKey: ['PI_MODELS'] }); + toast.success('Models refreshed'); + } catch { + toast.error('Failed to refresh models'); + } finally { + setRefreshing(false); + } + }; + + const enabledModels = policy.allowedModels; + + const providerGroups = useMemo(() => { + const groups: Record = {}; + for (const m of piModels) { + const provider = m.provider ?? 'Other'; + if (!groups[provider]) groups[provider] = []; + groups[provider]!.push({ id: m.id, name: m.name }); + } + return Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([provider, models]) => ({ provider, models: models.sort((a, b) => a.name.localeCompare(b.name)) })); + }, [piModels]); + + const providers = useMemo(() => providerGroups.map((g) => g.provider), [providerGroups]); + const selected = providers.includes(activeProvider) ? activeProvider : (providers[0] ?? ''); + const currentGroup = providerGroups.find((g) => g.provider === selected); + + const { enabled, disabled } = useMemo(() => { + if (!currentGroup) return { enabled: [], disabled: [] }; + const allEnabled = enabledModels.length === 0; + const enabledProviderSet = new Set(enabledModels.map((key) => key.split(':')[0])); + const isNewProvider = !allEnabled && !enabledProviderSet.has(currentGroup.provider); + const en: { id: string; name: string; key: string }[] = []; + const dis: { id: string; name: string; key: string }[] = []; + for (const m of currentGroup.models) { + const key = `${currentGroup.provider}:${m.id}`; + if (allEnabled || isNewProvider || enabledModels.includes(key)) { + en.push({ ...m, key }); + } else { + dis.push({ ...m, key }); + } + } + return { enabled: en, disabled: dis }; + }, [currentGroup, enabledModels]); + + const enableModel = useCallback( + async (key: string) => { + if (enabledModels.includes(key)) return; + await savePolicy({ allowedModels: [...enabledModels, key] }); + }, + [enabledModels, savePolicy], + ); + + const disableModel = useCallback( + async (key: string) => { + let base = enabledModels; + if (base.length === 0) { + base = piModels.map((m) => modelKey(m)); + } else { + const provider = key.split(':')[0]!; + const knownProviders = new Set(base.map((k) => k.split(':')[0])); + if (!knownProviders.has(provider)) { + const newProviderKeys = piModels.filter((m) => m.provider === provider).map((m) => modelKey(m)); + base = [...base, ...newProviderKeys]; + } + } + await savePolicy({ allowedModels: base.filter((id) => id !== key) }); + }, + [enabledModels, piModels, savePolicy], + ); + + const onDragStart = useCallback((ev: DragEvent, key: string) => { + ev.dataTransfer.setData('text/plain', key); + ev.dataTransfer.effectAllowed = 'move'; + }, []); + + if (providerGroups.length === 0) { + return ( +

+ No models available. Configure API keys in Providers. +

+ ); + } + + return ( +
+
+ {providers.map((p) => ( + + ))} + +
+ + + {enabled.length === 0 && ( + Drag models here to enable + )} + {enabled.map((m) => ( + + ))} + + + + {disabled.length === 0 && ( + All models enabled + )} + {disabled.map((m) => ( + + ))} + +
+ ); +} + +// --- Chat Defaults Section (commented out for future use) --- + +// function ChatDefaultsSection() { +// const { settings, saveSettings } = useSettings(); +// const piModels = useVisiblePiModels(); +// const [isSaving, setIsSaving] = useState(false); +// const [model, setModel] = useState(settings.chat.defaultModel); +// const [systemPrompt, setSystemPrompt] = useState(settings.chat.systemPrompt); +// const [temperature, setTemperature] = useState(settings.chat.temperature); +// const [defaultPwd, setDefaultPwd] = useState(settings.chat.defaultPwd); +// // ... full implementation for future use +// } + +// --- Build groups based on role --- + +function useAISettingsGroups(): SettingsSectionGroup[] { + const { user } = useAuth(); + const isAdmin = user?.role !== 'Member'; + + return useMemo(() => { + const modelsGroup: SettingsSectionGroup = { + label: 'Models', + icon: Eye, + sections: [ + { + key: 'my-models', + icon: Eye, + title: 'My Models', + description: 'Show or hide models for yourself', + content: , + }, + ...(isAdmin + ? [ + { + key: 'member-models', + icon: Eye, + title: 'Member Models', + description: 'Enable or disable models for members', + content: , + }, + ] + : []), + ], + }; + + const defaultsGroup: SettingsSectionGroup = { + label: 'Defaults', + icon: Sparkles, + sections: [ + { + key: 'default-models', + icon: Sparkles, + title: 'Default Models', + description: 'Default models for chat, projects, and tasks', + content: , + }, + ], + }; + + if (isAdmin) { + const providersGroup: SettingsSectionGroup = { + label: 'Providers', + icon: Terminal, + sections: [ + { + key: 'ai-harnesses', + icon: Terminal, + title: 'Providers', + description: 'Remote and local AI providers', + content: , + }, + ], + }; + return [providersGroup, modelsGroup, defaultsGroup]; + } + + return [modelsGroup, defaultsGroup]; + }, [isAdmin]); +} + +const layout: LayoutNode = { + type: 'group', + id: 'ai-root', + direction: 'horizontal', + children: [ + { node: { type: 'panel', id: 'ai-left', appType: null }, size: 20 }, + { node: { type: 'panel', id: 'ai-right', appType: null }, size: 80 }, + ], +}; + +export const AISettings = () => { + const groups = useAISettingsGroups(); + + const { Sidebar, Content } = useMemo( + () => + createSettingsPanelComponents({ + globalKey: 'AI_SETTINGS_SELECTED', + sidebarIcon: Bot, + sidebarLabel: 'AI', + groups, + }), + [groups], + ); + + const panelComponents: PanelComponents = useMemo( + () => ({ + 'ai-left': Sidebar, + 'ai-right': Content, + }), + [Sidebar, Content], + ); + + return ( +
+ {}} components={panelComponents} /> +
+ ); +}; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/AIModels.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/AIModels.tsx index 29654dac..b6641df4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/AIModels.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/AIModels.tsx @@ -12,7 +12,7 @@ import { SelectValue, } from '@/components/ui/select'; import { useSettings } from 'state/useSettings'; -import { useEnabledPiModels, getProviderDisplayName, type ModelOption } from 'state/useModels'; +import { useUserVisibleModels, getProviderDisplayName, type ModelOption } from 'state/useModels'; function buildGroups(models: ModelOption[]) { const groups: Record = {}; @@ -30,7 +30,7 @@ const NONE = '__none__'; export const AIModels = () => { const { settings, saveSettings } = useSettings(); - const piModels = useEnabledPiModels(); + const piModels = useUserVisibleModels(); const [isSaving, setIsSaving] = useState(false); const [chatModel, setChatModel] = useState(settings.chat.defaultModel); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/TaskDefaults.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/TaskDefaults.tsx index 414b6180..bd1443fb 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/TaskDefaults.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/TaskDefaults.tsx @@ -12,11 +12,11 @@ import { SelectValue, } from '@/components/ui/select'; import { useSettings } from 'state/useSettings'; -import { useVisiblePiModels, type ModelOption } from 'state/useModels'; +import { useUserVisibleModels, type ModelOption } from 'state/useModels'; export const TaskDefaults = () => { const { settings, saveSettings } = useSettings(); - const piModels = useVisiblePiModels(); + const piModels = useUserVisibleModels(); const [isSaving, setIsSaving] = useState(false); const [model, setModel] = useState(settings.tasks.defaultModel); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx index 7d1102bd..1036f3f1 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/index.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { User, Lock, Globe, Bot, LayoutGrid, Volume2 } from 'lucide-react'; +import { User, Lock, Globe, LayoutGrid, Volume2 } from 'lucide-react'; import type { LayoutNode, PanelComponents } from 'officerdev'; import { WorkspaceLayout } from 'officerdev'; @@ -7,7 +7,6 @@ import { createSettingsPanelComponents, type SettingsSection } from '../Settings import { UserData } from './UserData'; import { ChangePassword } from './ChangePassword'; import { Languages } from './Languages'; -import { AIModels } from './AIModels'; import { DockSettings } from './DockSettings'; import { VoicePreference } from './VoicePreference'; @@ -15,11 +14,34 @@ const GLOBAL_KEY = 'PROFILE_SETTINGS_SELECTED'; const sections: SettingsSection[] = [ { key: 'profile', icon: User, title: 'Profile', description: 'Avatar, name, and email', content: }, - { key: 'change-password', icon: Lock, title: 'Change Password', description: 'Update your password', content: }, - { key: 'ai-models', icon: Bot, title: 'AI Models', description: 'Default models for chat, projects, and tasks', content: }, - { key: 'languages', icon: Globe, title: 'Languages', description: 'Spoken, default, and translation', content: }, - { key: 'voice', icon: Volume2, title: 'Voice', description: 'Text-to-speech voice preference', content: }, - { key: 'dock', icon: LayoutGrid, title: 'Dock', description: 'Choose and reorder dock items', content: }, + { + key: 'change-password', + icon: Lock, + title: 'Change Password', + description: 'Update your password', + content: , + }, + { + key: 'languages', + icon: Globe, + title: 'Languages', + description: 'Spoken, default, and translation', + content: , + }, + { + key: 'voice', + icon: Volume2, + title: 'Voice', + description: 'Text-to-speech voice preference', + content: , + }, + { + key: 'dock', + icon: LayoutGrid, + title: 'Dock', + description: 'Choose and reorder dock items', + content: , + }, ]; const { Sidebar, Content } = createSettingsPanelComponents({ diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx index 6d3c7b95..d506ca09 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx @@ -1,79 +1,23 @@ -import { useState, useEffect, useMemo, useCallback, type DragEvent } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; -import { Terminal, Eye, Bot, Settings, X, Plus, Mail, Volume2, Mic, ScanText, RefreshCw } from 'lucide-react'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { Slider } from '@/components/ui/slider'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Bot, Settings, Mail, Volume2, Mic, ScanText, X } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; -import { useClient } from 'hooks/useClient'; import type { LayoutNode, PanelComponents } from 'officerdev'; import { WorkspaceLayout, TerminalView } from 'officerdev'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { createSettingsPanelComponents, type SettingsSectionGroup } from './SettingsPanel'; -import { useSettings } from 'state/useSettings'; -import { useUserState } from 'state/useUserState'; -import { usePiModels, useVisiblePiModels, modelKey, getProviderDisplayName, type ModelOption } from 'state/useModels'; -import { useAccessPolicy } from 'state/useAccessPolicy'; -import type { UserSettings } from 'state/useSettings'; -import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection'; import { RUN_COMMAND_CHANNEL, type RunCommandState } from './ServerSettings/run-command-channel'; import { SMTPSection } from './ServerSettings/SMTPSection'; import { TTSSection } from './ServerSettings/TTSSection'; import { STTSection } from './ServerSettings/STTSection'; import { OCRSection } from './ServerSettings/OCRSection'; -const PROVIDER_DISPLAY: Record = { - anthropic: 'Anthropic', - openai: 'OpenAI', - opencode: 'OpenCode Zen', - zai: 'ZAI', - google: 'Google', - groq: 'Groq', - mistral: 'Mistral', - xai: 'xAI', - openrouter: 'OpenRouter', - huggingface: 'Hugging Face', - 'github-copilot': 'GitHub Copilot', - minimax: 'MiniMax', - bedrock: 'Amazon Bedrock', - 'google-vertex': 'Google Vertex AI', - 'azure-openai': 'Azure OpenAI', -}; - -function displayProviderName(provider: string): string { - return PROVIDER_DISPLAY[provider] ?? getProviderDisplayName(provider); -} - const groups: SettingsSectionGroup[] = [ { label: 'AI', icon: Bot, sections: [ - { - key: 'ai-harnesses', - icon: Terminal, - title: 'Providers', - description: 'Remote and local AI providers', - content: , - }, - { - key: 'model-visibility', - icon: Eye, - title: 'Models', - description: 'Enable or disable models', - content: , - }, - { - key: 'chat-defaults', - icon: Terminal, - title: 'Chat Defaults', - description: 'Model, prompt, and temperature', - content: , - }, { key: 'tts', icon: Volume2, @@ -211,329 +155,3 @@ export const SystemSettings = () => { ); }; - -// --- AI sections --- - -function ChatDefaultsSection() { - const { settings, saveSettings } = useSettings(); - const piModels = useVisiblePiModels(); - const [isSaving, setIsSaving] = useState(false); - - const [model, setModel] = useState(settings.chat.defaultModel); - const [systemPrompt, setSystemPrompt] = useState(settings.chat.systemPrompt); - const [temperature, setTemperature] = useState(settings.chat.temperature); - const [defaultPwd, setDefaultPwd] = useState(settings.chat.defaultPwd); - - useEffect(() => { - setModel(settings.chat.defaultModel); - setSystemPrompt(settings.chat.systemPrompt); - setTemperature(settings.chat.temperature); - setDefaultPwd(settings.chat.defaultPwd); - }, [settings]); - - const handleSave = async () => { - if (isSaving) return; - setIsSaving(true); - try { - const updated: UserSettings = { - ...settings, - chat: { ...settings.chat, defaultModel: model, systemPrompt, temperature, defaultPwd }, - }; - await saveSettings(updated); - toast.success('Chat defaults saved'); - } catch { - toast.error('Failed to save settings'); - } finally { - setIsSaving(false); - } - }; - - return ( -
- - -