From 5816c409a125cdee364015643d7ca0c1f56d9d75 Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Thu, 7 May 2026 16:12:55 +0100 Subject: [PATCH] feat: polish providers settings form UI --- .../frontend/src/features/chat/chat-page.tsx | 73 +- .../features/chat/chat-providers-dialog.tsx | 1047 +++++++++++------ .../src/features/chat/external-providers.ts | 5 +- .../chat/stores/external-providers-store.ts | 24 + .../src/features/settings/settings-dialog.tsx | 24 +- .../settings/stores/settings-dialog-store.ts | 15 +- .../settings/tabs/connections-tab.tsx | 17 + 7 files changed, 767 insertions(+), 438 deletions(-) create mode 100644 studio/frontend/src/features/chat/stores/external-providers-store.ts create mode 100644 studio/frontend/src/features/settings/tabs/connections-tab.tsx diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 9e13582ffd..17cbdae74c 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -15,6 +15,7 @@ import { useChooseNativeModel } from "@/features/native-intents/use-native-dialo import { useNativeModelDrop } from "@/features/native-intents/use-native-drop"; import { useNativePathLeasesSupported } from "@/features/native-intents/use-native-readiness"; import { useNativeIntentStore } from "@/features/native-intents/store"; +import { useSettingsDialogStore } from "@/features/settings"; import type { NativeIntent } from "@/features/native-intents/types"; import { isTauri } from "@/lib/api-base"; import { cn } from "@/lib/utils"; @@ -37,18 +38,11 @@ import { import { toast } from "sonner"; import type { ChatSearch } from "@/app/routes/chat"; import { listLocalModels } from "./api/chat-api"; -import { ChatProvidersDialog } from "./chat-providers-dialog"; import { ChatSettingsPanel } from "./chat-settings-sheet"; import { ContextUsageBar } from "./components/context-usage-bar"; import { ModelLoadInlineStatus } from "./components/model-load-status"; import { db } from "./db"; -import { - buildExternalModelId, - isExternalModelId, - loadExternalProviders, - saveExternalProviders, - type ExternalProviderConfig, -} from "./external-providers"; +import { buildExternalModelId, isExternalModelId } from "./external-providers"; import { useChatModelRuntime } from "./hooks/use-chat-model-runtime"; import { clearTrainingCompareHandoff, @@ -63,6 +57,7 @@ import { SharedComposer, } from "./shared-composer"; import { useChatRuntimeStore } from "./stores/chat-runtime-store"; +import { useExternalProvidersStore } from "./stores/external-providers-store"; import { buildChatTourSteps } from "./tour"; import type { ChatView, MessageRecord } from "./types"; @@ -545,7 +540,7 @@ export function ChatPage(): ReactElement { const settingsOpen = useChatRuntimeStore((s) => s.settingsPanelOpen); const setSettingsOpen = useChatRuntimeStore((s) => s.setSettingsPanelOpen); - const [providersOpen, setProvidersOpen] = useState(false); + const externalProviders = useExternalProvidersStore((s) => s.providers); useEffect(() => { const threadId = search.thread; @@ -576,12 +571,9 @@ export function ChatPage(): ReactElement { canceled = true; }; }, [navigate, search.thread]); - + const [modelSelectorOpen, setModelSelectorOpen] = useState(false); const [modelSelectorLocked, setModelSelectorLocked] = useState(false); - const [externalProviders, setExternalProviders] = useState(() => - loadExternalProviders(), - ); const viewBeforeCompareRef = useRef(null); const inferenceParams = useChatRuntimeStore((state) => state.params); const setInferenceParams = useChatRuntimeStore((state) => state.setParams); @@ -609,7 +601,9 @@ export function ChatPage(): ReactElement { loadProgress, loadToastDismissed, } = useChatModelRuntime(); - const pendingNativeModelIntent = useNativeIntentStore((state) => state.pendingModelIntent); + const pendingNativeModelIntent = useNativeIntentStore( + (state) => state.pendingModelIntent, + ); const nativePathLeasesSupported = useNativePathLeasesSupported(); const refreshRef = useRef(refresh); const selectModelRef = useRef(selectModel); @@ -649,7 +643,8 @@ export function ChatPage(): ReactElement { const hasActiveModel = Boolean(inferenceParams.checkpoint); const loadNativeModelIntent = useCallback( async (intent: NativeIntent, loadingDescription: string) => { - const label = intent.path.displayLabel || intent.displayLabel || "Local GGUF model"; + const label = + intent.path.displayLabel || intent.displayLabel || "Local GGUF model"; await selectModel({ id: label, nativePathToken: intent.path.token, @@ -885,20 +880,24 @@ export function ChatPage(): ReactElement { .catch(() => {}); }, [navigate]); - const refreshModelLists = useCallback((deletedModel?: DeletedModelRef) => { - const { checkpoint } = useChatRuntimeStore.getState().params; - const activeGgufVariant = useChatRuntimeStore.getState().activeGgufVariant; - if ( - modelMatchesDeleted( - { id: checkpoint, ggufVariant: activeGgufVariant }, - deletedModel, - ) - ) { - useChatRuntimeStore.getState().clearCheckpoint(); - } - void refresh(); - refreshLocalModels(); - }, [refresh, refreshLocalModels]); + const refreshModelLists = useCallback( + (deletedModel?: DeletedModelRef) => { + const { checkpoint } = useChatRuntimeStore.getState().params; + const activeGgufVariant = + useChatRuntimeStore.getState().activeGgufVariant; + if ( + modelMatchesDeleted( + { id: checkpoint, ggufVariant: activeGgufVariant }, + deletedModel, + ) + ) { + useChatRuntimeStore.getState().clearCheckpoint(); + } + void refresh(); + refreshLocalModels(); + }, + [refresh, refreshLocalModels], + ); const loraModels = useMemo(() => { const fromLoras = lorasFromStore.map((lora) => ({ @@ -918,10 +917,6 @@ export function ChatPage(): ReactElement { refreshLocalModels(); }, [refresh, refreshLocalModels]); - useEffect(() => { - void saveExternalProviders(externalProviders); - }, [externalProviders]); - useEffect(() => { const handoff = getTrainingCompareHandoff(); if (!handoff) return; @@ -1115,7 +1110,11 @@ export function ChatPage(): ReactElement { +
+
+ + Providers + + + + {editingProviderId ? "Edit" : "New"}
-
- {providers.length === 0 ? ( -
- No providers yet. Add one using the form on the right. -
- ) : ( - providers.map((provider) => { - const registryEntry = registryByType.get(provider.providerType); - const detail = provider.baseUrl || registryEntry?.base_url || ""; - const visibleModels = provider.models.slice(0, 5); - const hiddenModelsCount = Math.max(0, provider.models.length - visibleModels.length); - return ( -
-
-
- -
-
- {provider.name} -
-
- {provider.providerType} - {" · "} - {registryEntry?.display_name ?? provider.providerType} - {detail ? ` · ${detail}` : ""} -
-
-
-
- - - -
-
-
- {visibleModels.map((model) => ( - - {model} - - ))} - {hiddenModelsCount > 0 ? ( - - +{hiddenModelsCount} - - ) : null} -
-
- ); - }) - )} -
-
-
-

+

{editingProviderId ? "Edit provider" : "Add provider"} -

-
-
- + +
+ + +
+
+
+
+
+ +

+ Supported registry or Custom. +

+
-

- Choose a provider from Studio's supported list, or Custom. -

-
- +
+
+ +

+ Stored locally. +

+
setApiKey(event.target.value)} placeholder="Enter API key" - className="h-10 text-sm" + className="h-9 text-sm" />
{isCustomProvider ? ( -
-
-
- - + + +
+
+ +

+ {modelStatusLabel} +

+
+ -
- {isCustomProvider ? ( -
-

- Enter exact model IDs served by your custom endpoint. -

-
- -