diff --git a/webui/src/App.tsx b/webui/src/App.tsx index f8d279f51..8fc89e055 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -57,7 +57,6 @@ import { } from "@/lib/bootstrap"; import { displayTitle, sortSessions } from "@/lib/chat-groups"; import { deriveTitle } from "@/lib/format"; -import type { ModelSetupIntent } from "@/lib/model-setup"; import { NanobotClient } from "@/lib/nanobot-client"; import { ClientProvider, useClient } from "@/providers/ClientProvider"; import type { @@ -1054,8 +1053,6 @@ function Shell({ const [temporaryChatEnabled, setTemporaryChatEnabled] = useState(false); const [settingsInitialSection, setSettingsInitialSection] = useState(initialRouteRef.current.settingsSection); - const [modelSetupIntent, setModelSetupIntent] = useState(null); - const [chatFocusRequest, setChatFocusRequest] = useState(0); const [hostSidebarOpen, setHostSidebarOpen] = useState(readSidebarOpen); const [hostSidebarPreviewOpen, setHostSidebarPreviewOpen] = useState(false); @@ -1984,12 +1981,8 @@ function Shell({ [onSelectChat], ); - const onOpenSettings = useCallback(( - section: SettingsSectionKey = "overview", - setupIntent: ModelSetupIntent | null = null, - ) => { + const onOpenSettings = useCallback((section: SettingsSectionKey = "overview") => { setSessionSearchOpen(false); - setModelSetupIntent(setupIntent); navigate({ view: "settings", activeKey, settingsSection: section }); setMobileSidebarOpen(false); }, [activeKey, navigate]); @@ -1998,8 +1991,8 @@ function Shell({ void loadSettingsView(); }, []); - const onOpenModelSettings = useCallback((intent?: ModelSetupIntent) => { - onOpenSettings("models", intent ?? null); + const onOpenModelSettings = useCallback(() => { + onOpenSettings("models"); }, [onOpenSettings]); const onOpenApps = useCallback(() => { @@ -2022,7 +2015,6 @@ function Shell({ const onSettingsSectionChange = useCallback( (section: SettingsSectionKey) => { - setModelSetupIntent(null); navigate({ view: shellViewForSettingsSection(section), activeKey, @@ -2033,9 +2025,7 @@ function Shell({ ); const onBackToChat = useCallback(() => { - const restoreComposerFocus = modelSetupIntent !== null; setMobileSidebarOpen(false); - setModelSetupIntent(null); const nextKey = (() => { if (!activeKey) return null; if (topicSessions.some((session) => session.key === activeKey)) return activeKey; @@ -2046,10 +2036,7 @@ function Shell({ activeKey: nextKey, settingsSection: "overview", }); - if (restoreComposerFocus) { - setChatFocusRequest((value) => value + 1); - } - }, [activeKey, modelSetupIntent, navigate, topicSessions]); + }, [activeKey, navigate, topicSessions]); const onRestart = useCallback(() => { const chatId = activeSession?.chatId ?? client.defaultChatId; @@ -2805,7 +2792,6 @@ function Shell({ onWorkspaceScopeChange={applyWorkspaceScope} settingsSnapshot={settingsSnapshot} onOpenModelSettings={onOpenModelSettings} - focusComposerRequest={chatFocusRequest} skills={skills} /> ); @@ -2864,7 +2850,6 @@ function Shell({ }} settingsSnapshot={settingsSnapshot} onOpenModelSettings={onOpenModelSettings} - focusComposerRequest={context.active ? chatFocusRequest : 0} skills={skills} /> ); @@ -2878,7 +2863,6 @@ function Shell({ theme={theme} initialSection={settingsInitialSection} initialSettings={settingsSnapshot} - modelSetupIntent={modelSetupIntent} showSidebar={view === "settings"} onToggleTheme={toggle} onBackToChat={onBackToChat} diff --git a/webui/src/components/settings/SettingsPage.tsx b/webui/src/components/settings/SettingsPage.tsx index a17b1783c..ed76dcd2c 100644 --- a/webui/src/components/settings/SettingsPage.tsx +++ b/webui/src/components/settings/SettingsPage.tsx @@ -31,12 +31,10 @@ import { ChannelsSettings } from "@/components/settings/system/ChannelsSettings" import { RuntimeSettings } from "@/components/settings/system/RuntimeSettings"; import type { SettingsController } from "@/components/settings/useSettingsController"; import type { SkillSummary } from "@/lib/types"; -import type { ModelSetupIntent } from "@/lib/model-setup"; import { cn } from "@/lib/utils"; interface SettingsPageProps { controller: SettingsController; - modelSetupIntent: ModelSetupIntent | null; theme: "light" | "dark"; showSidebar: boolean; onToggleTheme: () => void; @@ -49,7 +47,6 @@ interface SettingsPageProps { export function SettingsPage({ controller, - modelSetupIntent, theme, showSidebar, onToggleTheme, @@ -286,7 +283,6 @@ export function SettingsPage({ providerSaving={providerSaving} showBrandLogos={localPrefs.brandLogos} remoteBrowserAccess={remoteBrowserAccess} - setupIntent={modelSetupIntent} onToggleProvider={handleToggleProvider} onToggleProviderKey={toggleProviderKeyVisibility} onToggleProviderKeyEditing={toggleProviderKeyEditing} diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index e4b2d09f8..07f162b0f 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -1,7 +1,6 @@ import { SettingsPage } from "@/components/settings/SettingsPage"; import type { SettingsSectionKey } from "@/components/settings/contracts"; import { useSettingsController } from "@/components/settings/useSettingsController"; -import type { ModelSetupIntent } from "@/lib/model-setup"; import type { SettingsPayload, SkillSummary } from "@/lib/types"; export type { SettingsSectionKey } from "@/components/settings/contracts"; @@ -10,7 +9,6 @@ interface SettingsViewProps { theme: "light" | "dark"; initialSection?: SettingsSectionKey; initialSettings?: SettingsPayload | null; - modelSetupIntent?: ModelSetupIntent | null; showSidebar?: boolean; onToggleTheme: () => void; onBackToChat: () => void; @@ -29,7 +27,6 @@ export function SettingsView({ theme, initialSection = "overview", initialSettings = null, - modelSetupIntent = null, showSidebar = true, onToggleTheme, onBackToChat, @@ -56,7 +53,6 @@ export function SettingsView({ return ( { value: "responses", label: "Responses" }, ]; -const LOCAL_PROVIDER_ORDER = new Map( +const LOCAL_UNCONFIGURED_PROVIDER_ORDER = new Map( ["vllm", "ollama", "lm_studio", "atomic_chat", "ovms"].map((name, index) => [ name, index, ]), ); -const MODEL_SETUP_TITLE_KEYS: Record = { - account: "thread.composer.modelSetup.account.title", - apiKey: "thread.composer.modelSetup.apiKey.title", - local: "thread.composer.modelSetup.local.title", -}; export function ProviderOAuthLoginDialog({ flow, @@ -664,7 +655,6 @@ export function ProvidersSettings({ providerSaving, showBrandLogos, remoteBrowserAccess, - setupIntent, onToggleProvider, onToggleProviderKey, onToggleProviderKeyEditing, @@ -688,7 +678,6 @@ export function ProvidersSettings({ providerSaving: string | null; showBrandLogos: boolean; remoteBrowserAccess: boolean; - setupIntent?: ModelSetupIntent | null; onToggleProvider: (provider: string) => void; onToggleProviderKey: (provider: string) => void; onToggleProviderKeyEditing: (provider: string) => void; @@ -708,57 +697,23 @@ export function ProvidersSettings({ const [customProviderDraft, setCustomProviderDraft] = useState( emptyCustomProviderDraft, ); - const sectionRef = useRef(null); - const [setupPickerOpen, setSetupPickerOpen] = useState(false); const configuredProviders = settings.providers.filter((provider) => provider.configured); const unconfiguredProviders = useMemo( () => - orderProviderPickerOptions( + orderUnconfiguredProviders( settings.providers.filter( (provider) => !provider.configured && provider.name !== "custom", ), ), [settings.providers], ); - const providerPickerOptions = useMemo( - () => setupIntent - ? orderProviderPickerOptions( - settings.providers.filter( - (provider) => - provider.name !== "custom" - && providerMatchesModelSetupIntent(provider, setupIntent), - ), - ) - : unconfiguredProviders, - [settings.providers, setupIntent, unconfiguredProviders], - ); const selectedUnconfiguredProvider = unconfiguredProviders.find((provider) => provider.name === expandedProvider) ?? null; const customProviderSaving = providerSaving === CUSTOM_PROVIDER_CREATION_KEY; - useEffect(() => { - if (!setupIntent) { - setSetupPickerOpen(false); - return; - } - const frame = window.requestAnimationFrame(() => { - sectionRef.current?.scrollIntoView?.({ block: "start" }); - setSetupPickerOpen(true); - }); - return () => window.cancelAnimationFrame(frame); - }, [setupIntent]); const toggleProvider = (providerName: string) => { setCreatingCustomProvider(false); onToggleProvider(providerName); }; - const chooseProvider = (providerName: string) => { - setCreatingCustomProvider(false); - if (expandedProvider !== providerName) onToggleProvider(providerName); - window.requestAnimationFrame(() => { - document.getElementById(`settings-provider-${providerName}`)?.scrollIntoView?.({ - block: "start", - }); - }); - }; const beginCustomProviderCreation = () => { if (expandedProvider) onToggleProvider(expandedProvider); setCustomProviderDraft(emptyCustomProviderDraft()); @@ -821,11 +776,7 @@ export function ProvidersSettings({ ? (nanobotFeatures?.features ?? []).find((feature) => feature.name === supportName) : null; return ( -
+
) : null} -
+
{tx("settings.providers.title", "Model providers")} @@ -1282,12 +1233,7 @@ export function ProvidersSettings({ : null} {customProviderForm} {!expandedProvider && !creatingCustomProvider ? ( - + - ); - })} -
- - - ); -} diff --git a/webui/src/components/thread/ThreadComposer.tsx b/webui/src/components/thread/ThreadComposer.tsx index 29766b01a..3bd2b9ebd 100644 --- a/webui/src/components/thread/ThreadComposer.tsx +++ b/webui/src/components/thread/ThreadComposer.tsx @@ -70,7 +70,6 @@ import { ModelPresetBadge, type ModelPresetOption, } from "@/components/thread/ModelPresetBadge"; -import { ModelSetupDialog } from "@/components/thread/ModelSetupDialog"; import { ACCEPT_ATTR, MAX_ATTACHMENTS_PER_MESSAGE, @@ -114,7 +113,6 @@ import { } from "@/lib/session-drag"; import { formatQuotedUserMessage } from "@/lib/user-message-quote"; import { formatCompactTokenCount } from "@/lib/format"; -import type { ModelSetupAvailability, ModelSetupIntent } from "@/lib/model-setup"; import { cn } from "@/lib/utils"; const VOICE_SHORTCUT_CODE = "KeyD"; @@ -300,9 +298,8 @@ interface ThreadComposerProps { modelProvider?: string | null; modelProviderLabel?: string | null; modelNeedsSetup?: boolean; - modelSetupAvailability?: ModelSetupAvailability; fallbackModelName?: string | null; - onModelBadgeClick?: (intent?: ModelSetupIntent) => void; + onModelBadgeClick?: () => void; onManageModels?: () => void; contextUsage?: ComposerContextUsage | null; variant?: "thread" | "hero"; @@ -1000,7 +997,6 @@ export function ThreadComposer({ modelProvider = null, modelProviderLabel = null, modelNeedsSetup = false, - modelSetupAvailability = { account: false, apiKey: false, local: false }, fallbackModelName = null, onModelBadgeClick, onManageModels, @@ -1040,7 +1036,6 @@ export function ThreadComposer({ } | null>(null); const [inlineError, setInlineError] = useState(null); const [sendPending, setSendPending] = useState(false); - const [modelSetupOpen, setModelSetupOpen] = useState(false); const interactionDisabled = !!disabled || sendPending; const [voiceErrorFading, setVoiceErrorFading] = useState(false); const [slashMenuDismissed, setSlashMenuDismissed] = useState(false); @@ -2013,7 +2008,7 @@ export function ThreadComposer({ const submit = useCallback(() => { if (modelNeedsSetup) { - setModelSetupOpen(true); + onModelBadgeClick?.(); return; } if (!canSend) return; @@ -2121,6 +2116,7 @@ export function ThreadComposer({ isStreaming, maxTextBytes, modelNeedsSetup, + onModelBadgeClick, onSend, onStop, onQuotedContextChange, @@ -2131,15 +2127,6 @@ export function ThreadComposer({ value, ]); - const openModelSetup = useCallback(() => { - setModelSetupOpen(true); - }, []); - - const continueModelSetup = useCallback((intent: ModelSetupIntent) => { - setModelSetupOpen(false); - onModelBadgeClick?.(intent); - }, [onModelBadgeClick]); - const onKeyDown = (e: ReactKeyboardEvent) => { if (showCliAppMenu) { if (e.key === "ArrowDown") { @@ -2561,7 +2548,7 @@ export function ThreadComposer({ needsSetup={modelNeedsSetup} fallbackModelName={fallbackModelName} isHero={isHero} - onClick={modelNeedsSetup ? openModelSetup : undefined} + onClick={modelNeedsSetup ? onModelBadgeClick : undefined} /> ) : null} {!voiceRecorder.isRecording ? : null} @@ -2620,10 +2607,10 @@ export function ThreadComposer({ showStopButton ? t("thread.composer.stop") : modelNeedsSetup - ? t("thread.composer.openModelSetup", { defaultValue: "Open AI setup" }) + ? t("thread.composer.configureModel", { defaultValue: "Configure model" }) : t("thread.composer.send") } - onClick={showStopButton ? handleStop : modelNeedsSetup ? openModelSetup : undefined} + onClick={showStopButton ? handleStop : modelNeedsSetup ? onModelBadgeClick : undefined} className={cn( "thread-composer-action touch-target rounded-full transition-transform", showStopButton @@ -2669,13 +2656,6 @@ export function ThreadComposer({ ) : null} - textareaRef.current?.focus()} - onSelect={continueModelSetup} - /> ); } diff --git a/webui/src/components/thread/ThreadShell.tsx b/webui/src/components/thread/ThreadShell.tsx index 2cd47b880..8935284ad 100644 --- a/webui/src/components/thread/ThreadShell.tsx +++ b/webui/src/components/thread/ThreadShell.tsx @@ -39,10 +39,6 @@ import { } from "@/lib/mcp-preset-events"; import type { CanonicalRunSnapshot, StreamError } from "@/lib/nanobot-client"; import { inferProviderFromModelName, providerDisplayLabel } from "@/lib/provider-brand"; -import { - modelSetupAvailability, - type ModelSetupIntent, -} from "@/lib/model-setup"; import type { ChatSummary, SettingsPayload, @@ -358,7 +354,6 @@ interface ThreadShellProps { composerPortalTarget?: HTMLElement | null; composerActive?: boolean; composerInputAriaLabel?: string; - focusComposerRequest?: number; emptyComposerVariant?: "hero" | "thread"; workspaceScope?: WorkspaceScopePayload | null; workspaceDefaultScope?: WorkspaceScopePayload | null; @@ -367,7 +362,7 @@ interface ThreadShellProps { workspaceError?: string | null; onWorkspaceScopeChange?: (scope: WorkspaceScopePayload) => void; settingsSnapshot?: SettingsPayload | null; - onOpenModelSettings?: (intent?: ModelSetupIntent) => void; + onOpenModelSettings?: () => void; skills?: SkillSummary[]; } @@ -659,7 +654,6 @@ export function ThreadShell({ composerPortalTarget, composerActive = true, composerInputAriaLabel, - focusComposerRequest = 0, emptyComposerVariant = "hero", workspaceScope = null, workspaceDefaultScope = null, @@ -969,10 +963,6 @@ export function ThreadShell({ const modelBadgeLabel = modelBadge.needsSetup ? t("thread.composer.chooseAI", { defaultValue: "Choose your AI" }) : modelBadge.label; - const setupAvailability = useMemo( - () => modelSetupAvailability(settings?.providers), - [settings?.providers], - ); useEffect(() => { if (showHeroComposer && !wasShowingHeroComposerRef.current) { setHeroGreetingKey(randomHeroGreetingKey()); @@ -1527,7 +1517,6 @@ export function ThreadShell({ modelProvider={modelBadge.provider} modelProviderLabel={modelBadge.providerLabel} modelNeedsSetup={modelBadge.needsSetup} - modelSetupAvailability={setupAvailability} fallbackModelName={fallbackModelName} onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined} onManageModels={onOpenModelSettings} @@ -1555,7 +1544,7 @@ export function ThreadShell({ transcriptionProvider={settingsSnapshot?.transcription?.provider} ingressLimits={ingressLimits} quotedContext={quotedContext} - focusRequest={composerFocusSignal + focusComposerRequest} + focusRequest={composerFocusSignal} onQuotedContextChange={setQuotedContext} /> ) : ( @@ -1577,7 +1566,6 @@ export function ThreadShell({ modelProvider={modelBadge.provider} modelProviderLabel={modelBadge.providerLabel} modelNeedsSetup={modelBadge.needsSetup} - modelSetupAvailability={setupAvailability} fallbackModelName={fallbackModelName} onModelBadgeClick={modelBadge.needsSetup ? onOpenModelSettings : undefined} onManageModels={onOpenModelSettings} @@ -1603,7 +1591,6 @@ export function ThreadShell({ onWorkspaceScopeChange={onWorkspaceScopeChange} transcriptionProvider={settingsSnapshot?.transcription?.provider} ingressLimits={ingressLimits} - focusRequest={composerFocusSignal + focusComposerRequest} /> )} diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index bb5a1f2b2..467cea4dc 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -1193,25 +1193,9 @@ "stop": "Stop response", "quotedContext": "Quoted context", "removeQuotedContext": "Remove quoted context", - "openModelSetup": "Open AI setup", + "modelNotConfigured": "Model not configured", + "configureModel": "Configure model", "chooseAI": "Choose your AI", - "modelSetup": { - "title": "Choose your AI", - "description": "Pick a starting point. You can change models at any time.", - "ready": "Ready", - "account": { - "title": "Connect an account", - "description": "Use a supported AI subscription." - }, - "apiKey": { - "title": "Use an API key", - "description": "Bring a key from your preferred provider." - }, - "local": { - "title": "Run locally", - "description": "Connect Ollama, LM Studio, or vLLM." - } - }, "switchModel": "Switch model for this chat", "manageModels": "Manage models", "context": { diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 19955fcd0..55cb97bc0 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -1180,25 +1180,9 @@ "stop": "Detener respuesta", "quotedContext": "Contexto citado", "removeQuotedContext": "Quitar contexto citado", - "openModelSetup": "Abrir configuración de IA", + "modelNotConfigured": "Modelo no configurado", + "configureModel": "Configurar modelo", "chooseAI": "Elige tu IA", - "modelSetup": { - "title": "Elige tu IA", - "description": "Elige cómo empezar. Puedes cambiar de modelo en cualquier momento.", - "ready": "Listo", - "account": { - "title": "Conectar una cuenta", - "description": "Usa una suscripción de IA compatible." - }, - "apiKey": { - "title": "Usar una clave API", - "description": "Usa una clave de tu proveedor preferido." - }, - "local": { - "title": "Ejecutar localmente", - "description": "Conecta Ollama, LM Studio o vLLM." - } - }, "switchModel": "Cambiar el modelo de este chat", "manageModels": "Gestionar modelos", "context": { diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 7266d7f4e..e2816b3c5 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -1179,25 +1179,9 @@ "stop": "Arrêter la réponse", "quotedContext": "Contexte cité", "removeQuotedContext": "Supprimer le contexte cité", - "openModelSetup": "Ouvrir la configuration de l’IA", + "modelNotConfigured": "Modèle non configuré", + "configureModel": "Configurer le modèle", "chooseAI": "Choisissez votre IA", - "modelSetup": { - "title": "Choisissez votre IA", - "description": "Choisissez un point de départ. Vous pourrez changer de modèle à tout moment.", - "ready": "Prêt", - "account": { - "title": "Connecter un compte", - "description": "Utilisez un abonnement IA compatible." - }, - "apiKey": { - "title": "Utiliser une clé API", - "description": "Utilisez la clé du fournisseur de votre choix." - }, - "local": { - "title": "Exécuter localement", - "description": "Connectez Ollama, LM Studio ou vLLM." - } - }, "switchModel": "Changer le modèle de cette conversation", "manageModels": "Gérer les modèles", "context": { diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index fd0287f33..c74e94bdb 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -1179,25 +1179,9 @@ "stop": "Hentikan respons", "quotedContext": "Konteks kutipan", "removeQuotedContext": "Hapus konteks kutipan", - "openModelSetup": "Buka penyiapan AI", + "modelNotConfigured": "Model belum dikonfigurasi", + "configureModel": "Konfigurasi model", "chooseAI": "Pilih AI Anda", - "modelSetup": { - "title": "Pilih AI Anda", - "description": "Pilih cara memulai. Anda dapat mengganti model kapan saja.", - "ready": "Siap", - "account": { - "title": "Hubungkan akun", - "description": "Gunakan langganan AI yang didukung." - }, - "apiKey": { - "title": "Gunakan kunci API", - "description": "Gunakan kunci dari penyedia pilihan Anda." - }, - "local": { - "title": "Jalankan secara lokal", - "description": "Hubungkan Ollama, LM Studio, atau vLLM." - } - }, "switchModel": "Ganti model untuk percakapan ini", "manageModels": "Kelola model", "context": { diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index 639825c1d..bee12f23f 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -1179,25 +1179,9 @@ "stop": "応答を停止", "quotedContext": "引用したコンテキスト", "removeQuotedContext": "引用したコンテキストを削除", - "openModelSetup": "AI 設定を開く", + "modelNotConfigured": "モデルが未設定です", + "configureModel": "モデルを設定", "chooseAI": "AI を選択", - "modelSetup": { - "title": "AI を選択", - "description": "開始方法を選んでください。モデルはいつでも変更できます。", - "ready": "準備完了", - "account": { - "title": "アカウントを接続", - "description": "対応する AI サブスクリプションを使用します。" - }, - "apiKey": { - "title": "API キーを使用", - "description": "お好みのプロバイダーのキーを使用します。" - }, - "local": { - "title": "ローカルで実行", - "description": "Ollama、LM Studio、vLLM に接続します。" - } - }, "switchModel": "この会話で使うモデルを切り替える", "manageModels": "モデルを管理", "context": { diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index 657af8824..a2d276b56 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -1179,25 +1179,9 @@ "stop": "응답 중지", "quotedContext": "인용한 문맥", "removeQuotedContext": "인용한 문맥 제거", - "openModelSetup": "AI 설정 열기", + "modelNotConfigured": "모델이 설정되지 않음", + "configureModel": "모델 설정", "chooseAI": "AI 선택", - "modelSetup": { - "title": "AI 선택", - "description": "시작 방법을 선택하세요. 모델은 언제든 변경할 수 있습니다.", - "ready": "준비됨", - "account": { - "title": "계정 연결", - "description": "지원되는 AI 구독을 사용합니다." - }, - "apiKey": { - "title": "API 키 사용", - "description": "선호하는 제공업체의 키를 사용합니다." - }, - "local": { - "title": "로컬에서 실행", - "description": "Ollama, LM Studio 또는 vLLM에 연결합니다." - } - }, "switchModel": "이 대화에서 사용할 모델 전환", "manageModels": "모델 관리", "context": { diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index 9b43cf55f..63f9b63c1 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -1193,25 +1193,9 @@ "stop": "Parar resposta", "quotedContext": "Contexto citado", "removeQuotedContext": "Remover contexto citado", - "openModelSetup": "Abrir configuração de IA", + "modelNotConfigured": "Modelo não configurado", + "configureModel": "Configurar modelo", "chooseAI": "Escolha sua IA", - "modelSetup": { - "title": "Escolha sua IA", - "description": "Escolha como começar. Você pode trocar de modelo a qualquer momento.", - "ready": "Pronto", - "account": { - "title": "Conectar uma conta", - "description": "Use uma assinatura de IA compatível." - }, - "apiKey": { - "title": "Usar uma chave de API", - "description": "Use uma chave do seu provedor preferido." - }, - "local": { - "title": "Executar localmente", - "description": "Conecte o Ollama, LM Studio ou vLLM." - } - }, "switchModel": "Alternar o modelo desta conversa", "manageModels": "Gerenciar modelos", "context": { diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index 6189922a7..97ece64f2 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -1179,25 +1179,9 @@ "stop": "Dừng phản hồi", "quotedContext": "Ngữ cảnh được trích dẫn", "removeQuotedContext": "Xóa ngữ cảnh được trích dẫn", - "openModelSetup": "Mở thiết lập AI", + "modelNotConfigured": "Chưa cấu hình mô hình", + "configureModel": "Cấu hình mô hình", "chooseAI": "Chọn AI của bạn", - "modelSetup": { - "title": "Chọn AI của bạn", - "description": "Chọn cách bắt đầu. Bạn có thể đổi mô hình bất cứ lúc nào.", - "ready": "Sẵn sàng", - "account": { - "title": "Kết nối tài khoản", - "description": "Dùng gói đăng ký AI được hỗ trợ." - }, - "apiKey": { - "title": "Dùng khóa API", - "description": "Dùng khóa từ nhà cung cấp bạn chọn." - }, - "local": { - "title": "Chạy cục bộ", - "description": "Kết nối Ollama, LM Studio hoặc vLLM." - } - }, "switchModel": "Chuyển mô hình cho cuộc trò chuyện này", "manageModels": "Quản lý mô hình", "context": { diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index b44c0c0c0..1a935d1df 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -1192,25 +1192,9 @@ "stop": "停止响应", "quotedContext": "引用内容", "removeQuotedContext": "移除引用内容", - "openModelSetup": "打开 AI 设置", + "modelNotConfigured": "模型未配置", + "configureModel": "配置模型", "chooseAI": "选择你的 AI", - "modelSetup": { - "title": "选择你的 AI", - "description": "选择一种开始方式,之后可随时更换模型。", - "ready": "已就绪", - "account": { - "title": "连接账户", - "description": "使用支持的 AI 订阅。" - }, - "apiKey": { - "title": "使用 API 密钥", - "description": "使用你偏好的服务商密钥。" - }, - "local": { - "title": "在本地运行", - "description": "连接 Ollama、LM Studio 或 vLLM。" - } - }, "switchModel": "切换本次对话所用模型", "manageModels": "管理模型预设", "context": { diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index 3cf9001d4..4b0a01b52 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -1179,25 +1179,9 @@ "stop": "停止回覆", "quotedContext": "引用內容", "removeQuotedContext": "移除引用內容", - "openModelSetup": "開啟 AI 設定", + "modelNotConfigured": "尚未設定模型", + "configureModel": "設定模型", "chooseAI": "選擇你的 AI", - "modelSetup": { - "title": "選擇你的 AI", - "description": "選擇一種開始方式,之後可隨時更換模型。", - "ready": "已就緒", - "account": { - "title": "連結帳戶", - "description": "使用支援的 AI 訂閱。" - }, - "apiKey": { - "title": "使用 API 金鑰", - "description": "使用你偏好的服務商金鑰。" - }, - "local": { - "title": "在本機執行", - "description": "連結 Ollama、LM Studio 或 vLLM。" - } - }, "switchModel": "切換此對話使用的模型", "manageModels": "管理模型預設", "context": { diff --git a/webui/src/lib/model-setup.ts b/webui/src/lib/model-setup.ts deleted file mode 100644 index 08c0bd852..000000000 --- a/webui/src/lib/model-setup.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { SettingsPayload } from "@/lib/types"; - -export type ModelSetupIntent = "account" | "apiKey" | "local"; - -export type ModelSetupAvailability = Record; - -type Provider = SettingsPayload["providers"][number]; - -const LOCAL_MODEL_PROVIDERS = new Set([ - "atomic_chat", - "lm_studio", - "ollama", - "ovms", - "vllm", -]); - -function isLocalModelProvider(provider: Provider): boolean { - if (LOCAL_MODEL_PROVIDERS.has(provider.name)) return true; - const apiBase = provider.api_base?.trim().toLowerCase() ?? ""; - return ( - apiBase.includes("localhost") - || apiBase.includes("127.0.0.1") - || apiBase.includes("[::1]") - ); -} - -export function modelSetupIntentForProvider(provider: Provider): ModelSetupIntent { - if (provider.auth_type === "oauth") return "account"; - return isLocalModelProvider(provider) ? "local" : "apiKey"; -} - -export function providerMatchesModelSetupIntent( - provider: Provider, - intent: ModelSetupIntent, -): boolean { - return modelSetupIntentForProvider(provider) === intent; -} - -export function modelSetupAvailability( - providers: SettingsPayload["providers"] | null | undefined, -): ModelSetupAvailability { - const configured = providers?.filter((provider) => provider.configured) ?? []; - return { - account: configured.some((provider) => modelSetupIntentForProvider(provider) === "account"), - apiKey: configured.some((provider) => modelSetupIntentForProvider(provider) === "apiKey"), - local: configured.some((provider) => modelSetupIntentForProvider(provider) === "local"), - }; -} diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index 8d9abb196..1dfad5ca7 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -481,7 +481,7 @@ describe("App layout", () => { expect(screen.getByRole("heading", { level: 1, name: "Settings" })).toBeInTheDocument(); }); - it("carries a first-run setup path into settings and restores composer focus", async () => { + it("opens the full Models settings directly from the first-run prompt", async () => { const user = userEvent.setup(); const base = baseSettingsPayload(); mockFetchRoutes({ @@ -496,26 +496,6 @@ describe("App layout", () => { }, model_presets: [], model_call_order: [], - providers: [ - { - name: "openai_codex", - label: "OpenAI Codex", - configured: false, - auth_type: "oauth", - }, - { - name: "deepseek", - label: "DeepSeek", - configured: false, - auth_type: "api_key", - }, - { - name: "ollama", - label: "Ollama", - configured: false, - api_base: "http://127.0.0.1:11434", - }, - ], }, }); @@ -523,23 +503,14 @@ describe("App layout", () => { await waitFor(() => expect(connectSpy).toHaveBeenCalled()); await user.click(await screen.findByRole("button", { name: "Choose your AI" })); - await user.click(await screen.findByRole("button", { name: "Run locally" })); expect( await screen.findByRole("navigation", { name: "Settings sections" }), ).toBeInTheDocument(); - const providerMenu = await screen.findByRole("menu"); - expect(within(providerMenu).getByRole("menuitem", { name: "Ollama" })) + expect(screen.getByText("Model providers")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Add your own model provider" })) .toBeInTheDocument(); - expect(within(providerMenu).queryByRole("menuitem", { name: "OpenAI Codex" })) - .not.toBeInTheDocument(); - expect(within(providerMenu).queryByRole("menuitem", { name: "DeepSeek" })) - .not.toBeInTheDocument(); - - const composer = screen.getByRole("textbox", { name: "Message input" }); - expect(composer).not.toHaveFocus(); - await user.click(screen.getByRole("button", { name: "Back to chat" })); - await waitFor(() => expect(composer).toHaveFocus()); + expect(screen.queryByRole("dialog", { name: "Choose your AI" })).not.toBeInTheDocument(); }); it("places Automations after Skills in the main sidebar", async () => { diff --git a/webui/src/tests/settings-providers.test.tsx b/webui/src/tests/settings-providers.test.tsx index b5b2af8d4..4519ca739 100644 --- a/webui/src/tests/settings-providers.test.tsx +++ b/webui/src/tests/settings-providers.test.tsx @@ -14,48 +14,6 @@ async function chooseProviderToConfigure(label: string) { describe("Settings providers", () => { installSettingsViewTestHooks(); - it.each([ - ["account", "OpenAI Codex", ["DeepSeek", "Ollama", "Custom provider"]], - ["apiKey", "DeepSeek", ["OpenAI Codex", "Ollama"]], - ["local", "Ollama", ["OpenAI Codex", "DeepSeek", "Custom provider"]], - ] as const)("opens the %s first-run provider path", async (intent, expected, excluded) => { - const base = settingsPayload(); - const payload: SettingsPayload = { - ...base, - providers: [ - { - name: "openai_codex", - label: "OpenAI Codex", - configured: false, - auth_type: "oauth", - }, - { - name: "deepseek", - label: "DeepSeek", - configured: false, - auth_type: "api_key", - }, - { - name: "ollama", - label: "Ollama", - configured: false, - api_base: "http://127.0.0.1:11434", - }, - ], - }; - - renderSettingsView({ - initialSection: "models", - initialSettings: payload, - modelSetupIntent: intent, - }); - - const menu = await screen.findByRole("menu"); - expect(within(menu).getByRole("menuitem", { name: expected })).toBeInTheDocument(); - for (const label of excluded) { - expect(within(menu).queryByRole("menuitem", { name: label })).not.toBeInTheDocument(); - } - }); it("signs in to the xAI Grok provider", async () => { const base = settingsPayload(); diff --git a/webui/src/tests/settings-test-utils.tsx b/webui/src/tests/settings-test-utils.tsx index 4808e31d1..6dec851f9 100644 --- a/webui/src/tests/settings-test-utils.tsx +++ b/webui/src/tests/settings-test-utils.tsx @@ -3,7 +3,6 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, vi } from "vitest"; import { SettingsView } from "@/components/settings/SettingsView"; import { ClientProvider } from "@/providers/ClientProvider"; -import type { ModelSetupIntent } from "@/lib/model-setup"; import type { SettingsPayload } from "@/lib/types"; export const requestMutationMock = vi.fn(); @@ -138,7 +137,6 @@ export function renderSettingsView( | "browser" | "runtime"; initialSettings?: SettingsPayload; - modelSetupIntent?: ModelSetupIntent; showSidebar?: boolean; onBackToChat?: () => void; onSettingsChange?: (payload: SettingsPayload) => void; @@ -151,7 +149,6 @@ export function renderSettingsView( theme="light" initialSection={options.initialSection ?? "apps"} initialSettings={options.initialSettings} - modelSetupIntent={options.modelSetupIntent} showSidebar={options.showSidebar} onToggleTheme={() => {}} onBackToChat={options.onBackToChat ?? (() => {})} diff --git a/webui/src/tests/thread-shell.test.tsx b/webui/src/tests/thread-shell.test.tsx index cca9cb52a..e11099461 100644 --- a/webui/src/tests/thread-shell.test.tsx +++ b/webui/src/tests/thread-shell.test.tsx @@ -835,7 +835,7 @@ describe("ThreadShell", () => { expect(screen.getByText("Default")).toBeInTheDocument(); }); - it("opens first-run model setup without clearing the draft", async () => { + it("opens model settings directly without clearing the draft", async () => { const client = makeClient(); const settings = modelSettings("openai-codex/gpt-5.1-codex", "openai_codex"); settings.agent.has_api_key = false; @@ -882,55 +882,26 @@ describe("ThreadShell", () => { expect(screen.getByTestId("composer-model-setup-label")).toHaveTextContent("Choose your AI"); expect(badge).not.toHaveClass("border-amber-500/35"); expect(screen.queryByTestId("composer-model-logo-openai_codex")).not.toBeInTheDocument(); - fireEvent.click(badge); - expect(await screen.findByRole("dialog", { name: "Choose your AI" })).toBeInTheDocument(); - expect(screen.getAllByText("Ready")).toHaveLength(3); - expect(onOpenModelSettings).not.toHaveBeenCalled(); - - fireEvent.click(screen.getByRole("button", { name: "Close" })); const input = screen.getByRole("textbox", { name: "Message input" }); - await waitFor(() => expect(input).toHaveFocus()); fireEvent.change(input, { target: { value: "hello" }, }); + fireEvent.click(badge); + + expect(screen.queryByRole("dialog", { name: "Choose your AI" })).not.toBeInTheDocument(); + expect(onOpenModelSettings).toHaveBeenCalledTimes(1); + expect(input).toHaveValue("hello"); + expect(client.sendMessage).not.toHaveBeenCalled(); + + onOpenModelSettings.mockClear(); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); - expect(await screen.findByRole("dialog", { name: "Choose your AI" })).toBeInTheDocument(); + expect(onOpenModelSettings).toHaveBeenCalledTimes(1); expect(input).toHaveValue("hello"); - fireEvent.click(screen.getByRole("button", { name: "Use an API key" })); - - expect(onOpenModelSettings).toHaveBeenCalledWith("apiKey"); - expect(input).toHaveValue("hello"); - expect(input).not.toHaveFocus(); expect(client.sendMessage).not.toHaveBeenCalled(); }); - it("focuses the composer when returning from model setup", async () => { - const client = makeClient(); - const settings = modelSettings("openai-codex/gpt-5.1-codex", "openai_codex"); - settings.agent.has_api_key = false; - const view = (focusComposerRequest: number) => wrap( - client, - {}} - settingsSnapshot={settings} - focusComposerRequest={focusComposerRequest} - />, - "openai-codex/gpt-5.1-codex", - ); - const { rerender } = render(view(0)); - const input = await screen.findByRole("textbox", { name: "Message input" }); - input.blur(); - expect(input).not.toHaveFocus(); - - rerender(view(1)); - - await waitFor(() => expect(input).toHaveFocus()); - }); - it("keeps image generation controls out of the composer", async () => { const client = makeClient(); const disabledSettings = modelSettings("deepseek-v4-pro", "deepseek");