diff --git a/mateclaw-ui/src/components/chat/ModelSelector.vue b/mateclaw-ui/src/components/chat/ModelSelector.vue new file mode 100644 index 00000000..7d120f71 --- /dev/null +++ b/mateclaw-ui/src/components/chat/ModelSelector.vue @@ -0,0 +1,406 @@ + + + + + + + diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 639cb962..298123d6 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -118,6 +118,8 @@ export default { deleteConversationFailed: 'Failed to delete conversation', deleteConfirm: 'Delete this conversation? This cannot be undone.', switchModelFailed: 'Failed to switch model', + searchModel: 'Search models…', + noMatchModel: 'No matching models', uploadFailed: 'File upload failed', dropToUpload: 'Drop files or folders here', copyFailed: 'Copy failed', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 39cb1f81..351bb46e 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -118,6 +118,8 @@ export default { deleteConversationFailed: '删除会话失败', deleteConfirm: '确定删除这个会话?此操作不可恢复。', switchModelFailed: '切换模型失败', + searchModel: '搜索模型…', + noMatchModel: '没有匹配的模型', uploadFailed: '文件上传失败', dropToUpload: '拖放文件或文件夹到此处', copyFailed: '复制失败', diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index 0efc006f..14a8cfcf 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -136,31 +136,14 @@
-
- - -
-
- -
-
- {{ item.label }} - - - -
-
-
-
+ @@ -284,6 +267,7 @@ import MessageList from '@/components/chat/MessageList.vue' import ChatInput from '@/components/chat/ChatInput.vue' import StreamLoadingBar from '@/components/chat/StreamLoadingBar.vue' import TalkMode from '@/components/chat/TalkMode.vue' +import ModelSelector from '@/components/chat/ModelSelector.vue' import { useEChartsRenderer } from '@/composables/useEChartsRenderer' // ============ Talk Mode ============ @@ -362,7 +346,7 @@ const uploadingAttachment = ref(false) // Dropdowns & menus const agentDropdownOpen = ref(false) -const modelDropdownOpen = ref(false) + const headerMenuOpen = ref(false) function selectAgent(agent: Agent) { @@ -374,7 +358,6 @@ function selectAgent(agent: Agent) { } async function selectModel(value: string) { - modelDropdownOpen.value = false const [providerId, model] = value.split('::') if (!providerId || !model) return modelSaving.value = true @@ -664,16 +647,18 @@ const modelPromptDesc = computed(() => { return t('chat.noAvailableModel') }) +const availableProviders = computed(() => + providers.value.filter((p) => p.available && [...(p.models || []), ...(p.extraModels || [])].length > 0) +) + const eligibleModels = computed(() => { - return providers.value - .filter((provider) => provider.available) - .flatMap((provider) => { - const allModels = [...(provider.models || []), ...(provider.extraModels || [])] - return allModels.map((model) => ({ - value: `${provider.id}::${model.id}`, - label: `${provider.name} / ${model.name || model.id}`, - })) - }) + return availableProviders.value.flatMap((provider) => { + const allModels = [...(provider.models || []), ...(provider.extraModels || [])] + return allModels.map((model) => ({ + value: `${provider.id}::${model.id}`, + label: `${provider.name} / ${model.name || model.id}`, + })) + }) }) // ============ 生命周期 ============ @@ -1883,94 +1868,7 @@ function handleCodeCopy(e: MouseEvent) { } /* Model selector */ -.model-selector-wrap { - position: relative; -} - -.model-select-trigger { - display: flex; - align-items: center; - gap: 6px; - height: 34px; - padding: 0 12px; - border: 1px solid var(--mc-border); - border-radius: 12px; - background: var(--mc-panel-raised); - color: var(--mc-text-primary); - font-size: 13px; - cursor: pointer; - transition: all 0.15s; - white-space: nowrap; - max-width: 280px; -} - -.model-select-trigger:hover { - border-color: var(--mc-primary); -} - -.model-select-trigger:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.model-select-trigger__name { - overflow: hidden; - text-overflow: ellipsis; -} - -.model-select-trigger__arrow { - flex-shrink: 0; - color: var(--mc-text-tertiary); - transition: transform 0.2s; -} - -.model-select-trigger__arrow.open { - transform: rotate(180deg); -} - -.model-dropdown { - position: absolute; - top: calc(100% + 4px); - right: 0; - z-index: 100; - min-width: 260px; - background: var(--mc-bg-elevated); - border: 1px solid var(--mc-border); - border-radius: 14px; - padding: 6px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); - max-height: 360px; - overflow-y: auto; -} - -.model-dropdown-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 9px 12px; - border-radius: 10px; - cursor: pointer; - transition: background 0.12s; -} - -.model-dropdown-item:hover { - background: var(--mc-bg-sunken); -} - -.model-dropdown-item.active { - background: var(--mc-primary-bg); -} - -.model-dropdown-item__name { - font-size: 13px; - color: var(--mc-text-primary); -} - -.model-dropdown-item__check { - flex-shrink: 0; - color: var(--mc-primary); -} +/* Model selector styles moved to ModelSelector.vue */ /* Header overflow menu */ .header-overflow-wrap {