From 74df246d7e5146be0540e1ab048358d727678e31 Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 22 Jun 2026 17:22:17 +0800 Subject: [PATCH] feat(ai): improve model selection with searchable dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): toast 消息支持换行显示 - 添加 max-w-3xl 限制最大宽度 - 使用 whitespace-pre-wrap 允许自动换行 - 添加 break-words 防止长单词溢出 * fix(ai): 优化模型选择下拉列表 --------- Co-authored-by: t8y2 <1156263951@qq.com> Co-authored-by: staff --- .../src/components/editor/AiAssistant.vue | 59 ++++++++++--------- .../ui/searchable-select/SearchableSelect.vue | 43 +++++++++++++- 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index b4433f5e1..e56766317 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import LightDropdown from "@/components/ui/LightDropdown.vue"; +import { SearchableSelect } from "@/components/ui/searchable-select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useTheme } from "@/composables/useTheme"; import { useSettingsStore } from "@/stores/settingsStore"; @@ -88,14 +89,6 @@ 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[] = []; @@ -129,19 +122,19 @@ function handleModelSelect(modelId: string) { settings.updateAiConfig({ model: modelId }); } -const modelDropdownItems = computed(() => { +const modelOptionIds = 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 }); + const ids = modelOptions.value.map((model) => model.id); + if (currentModel && !ids.includes(currentModel)) { + return [currentModel, ...ids]; } - return items; + return ids; }); +function displayModelName(modelId: string) { + return modelOptions.value.find((model) => model.id === modelId)?.displayName || modelId; +} + /** Deferred context compaction info; applied after stream ends to avoid shifting assistantIdx. */ const pendingCompaction = ref<{ summary: string; compactedMessages: number } | null>(null); @@ -1240,20 +1233,32 @@ const messageRenderer = computed(() => { - + @update:open="(open: boolean) => open && fetchModelOptions()" + > + + + diff --git a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue index c52515142..41fa1dd85 100644 --- a/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue +++ b/apps/desktop/src/components/ui/searchable-select/SearchableSelect.vue @@ -20,6 +20,7 @@ const props = withDefaults( allowCustom?: boolean; triggerClass?: HTMLAttributes["class"]; contentClass?: HTMLAttributes["class"]; + itemClass?: HTMLAttributes["class"]; displayName?: (option: string) => string; normalizeCustom?: (value: string) => string; }>(), @@ -51,6 +52,11 @@ const filteredOptions = computed(() => filterDatabaseOptions(props.options, sear const customOptionValue = computed(() => props.normalizeCustom(searchText.value.trim())); const canSelectCustom = computed(() => props.allowCustom && !!customOptionValue.value && !props.options.includes(customOptionValue.value)); +function highlightSelectedOption() { + const selectedIndex = filteredOptions.value.findIndex((option) => option === props.modelValue); + highlightIndex.value = selectedIndex >= 0 ? selectedIndex : 0; +} + watch(open, async (value) => { emit("update:open", value); if (!value) { @@ -61,12 +67,22 @@ watch(open, async (value) => { await nextTick(); const input = searchInput.value?.$el as HTMLInputElement | undefined; input?.focus(); + highlightSelectedOption(); }); watch(searchText, () => { highlightIndex.value = 0; }); +watch( + () => [props.modelValue, props.options], + () => { + if (!open.value || searchText.value) return; + highlightSelectedOption(); + }, + { deep: true }, +); + watch(highlightIndex, async () => { await nextTick(); const container = listContainer.value; @@ -86,6 +102,11 @@ function selectCustomOption() { selectOption(customOptionValue.value); } +function optionTitle(option: string) { + const label = props.displayName(option); + return label === option ? option : `${label}\n${option}`; +} + function optionCount() { return filteredOptions.value.length + (canSelectCustom.value ? 1 : 0); } @@ -118,7 +139,7 @@ function handleKeydown(event: KeyboardEvent) {