From 320336e84edba72ebd8f616560c74eea0a4a5e4a Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Thu, 18 Jun 2026 18:10:28 +0800 Subject: [PATCH] feat(ai): add inline model selector to AI assistant toolbar --- .../src/components/editor/AiAssistant.vue | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 11bc011efa..fd7f41cf86 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -23,7 +23,7 @@ import { buildAiAgentStepItems, type AiAgentStepItem, type AiAgentStepTone } fro import { createAiShikiCodeHighlighter, type AiCodeHighlighter } from "@/lib/aiCodeHighlighter"; import { createAiMessageRenderer } from "@/lib/aiMessageRender"; import { Marked } from "marked"; -import { aiCancelStream, saveAiConversation, loadAiConversations, deleteAiConversation, listSchemas, listTables, type AiConversation } from "@/lib/api"; +import { aiCancelStream, aiListModels, saveAiConversation, loadAiConversations, deleteAiConversation, listSchemas, listTables, type AiConversation, type AiModelInfo } from "@/lib/api"; import type { AiMessage } from "@/lib/api"; import type { ConnectionConfig, QueryTab, TableInfo } from "@/types/database"; import { useDatabaseOptions } from "@/composables/useDatabaseOptions"; @@ -80,6 +80,65 @@ const promptCompositionActive = ref(false); const shikiCodeHighlighter = ref(); const agentTokens = ref<{ input: number; output: number } | null>(null); +// Inline model selector +const modelOptions = ref([]); +const modelLoading = ref(false); +let modelRequestToken = 0; + +const currentModelLabel = computed(() => { + const currentModel = settings.aiConfig.model; + const found = modelOptions.value.find((m) => m.id === currentModel); + const raw = found?.displayName || currentModel; + if (!raw) return t("ai.model"); + return raw.length > 20 ? raw.slice(0, 18) + "…" : raw; +}); + +function normalizeModelOptions(models: AiModelInfo[]): AiModelInfo[] { + const seen = new Set(); + const normalized: AiModelInfo[] = []; + for (const model of models) { + const id = model.id?.trim(); + if (!id || seen.has(id)) continue; + seen.add(id); + normalized.push({ id, displayName: model.displayName?.trim() || undefined }); + } + return normalized; +} + +async function fetchModelOptions() { + if (modelLoading.value) return; + if (!settings.isConfigured()) return; + const token = ++modelRequestToken; + modelLoading.value = true; + try { + const models = normalizeModelOptions(await aiListModels(settings.aiConfig)); + if (token !== modelRequestToken) return; + modelOptions.value = models; + } catch { + if (token !== modelRequestToken) return; + modelOptions.value = []; + } finally { + if (token === modelRequestToken) modelLoading.value = false; + } +} + +function handleModelSelect(modelId: string) { + settings.updateAiConfig({ model: modelId }); +} + +const modelDropdownItems = computed(() => { + const currentModel = settings.aiConfig.model; + const items = modelOptions.value.map((m) => ({ + value: m.id, + label: m.displayName || m.id, + title: m.id, + })); + if (currentModel && !items.some((item) => item.value === currentModel)) { + items.unshift({ value: currentModel, label: currentModel, title: currentModel }); + } + return items; +}); + /** Deferred context compaction info; applied after stream ends to avoid shifting assistantIdx. */ const pendingCompaction = ref<{ summary: string; compactedMessages: number } | null>(null); @@ -789,6 +848,9 @@ onMounted(async () => { shikiCodeHighlighter.value = await createAiShikiCodeHighlighter({ appearance: () => aiCodeAppearance.value, }).catch(() => undefined); + + // Load available AI models for inline selector + fetchModelOptions(); }); function startResize(event: MouseEvent) { @@ -1129,6 +1191,20 @@ const messageRenderer = computed(() => { +