mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui): dashboard model-config card + onboarding provider enablement
This commit is contained in:
parent
56a226a8c7
commit
8edf71cd64
@ -3173,7 +3173,9 @@ export default {
|
||||
testConnection: 'Test Connection',
|
||||
testSuccess: 'Connection Successful',
|
||||
testFailed: 'Connection Failed',
|
||||
saveAndContinue: 'Save & Continue',
|
||||
saveAndContinue: 'Save & Enable',
|
||||
testBeforeSave: 'Test the connection first — once it passes you can enable it',
|
||||
saveFailed: 'Save failed, please try again',
|
||||
verifyTitle: 'Verify Your Model',
|
||||
verifyMessage: 'Hello! Tell me briefly about yourself.',
|
||||
send: 'Send',
|
||||
@ -3231,6 +3233,16 @@ export default {
|
||||
thisWeek: 'This Week',
|
||||
thisMonth: 'This Month',
|
||||
},
|
||||
models: {
|
||||
title: 'Model Configuration',
|
||||
subtitle: 'Enabled LLM providers and the active model.',
|
||||
activeModel: 'Active model',
|
||||
notSet: 'Not set',
|
||||
configured: 'ready',
|
||||
manage: 'Manage',
|
||||
empty: 'No LLM providers enabled yet.',
|
||||
emptyCta: 'Configure',
|
||||
},
|
||||
recentRuns: 'Recent Cron Job Runs',
|
||||
noRuns: 'No runs yet',
|
||||
runColumns: {
|
||||
|
||||
@ -3265,7 +3265,9 @@ export default {
|
||||
testConnection: '测试连接',
|
||||
testSuccess: '连接成功',
|
||||
testFailed: '连接失败',
|
||||
saveAndContinue: '保存并继续',
|
||||
saveAndContinue: '保存并启用',
|
||||
testBeforeSave: '请先测试连接,通过后即可启用',
|
||||
saveFailed: '保存失败,请重试',
|
||||
verifyTitle: '验证你的模型',
|
||||
verifyMessage: '你好!简单介绍一下你自己。',
|
||||
send: '发送',
|
||||
@ -3323,6 +3325,16 @@ export default {
|
||||
thisWeek: '本周',
|
||||
thisMonth: '本月',
|
||||
},
|
||||
models: {
|
||||
title: '模型配置',
|
||||
subtitle: '已启用的 LLM Provider 与当前生效模型。',
|
||||
activeModel: '当前模型',
|
||||
notSet: '未设置',
|
||||
configured: '已就绪',
|
||||
manage: '管理',
|
||||
empty: '尚未启用任何模型 Provider。',
|
||||
emptyCta: '去配置',
|
||||
},
|
||||
recentRuns: '最近定时任务执行',
|
||||
noRuns: '暂无执行记录',
|
||||
runColumns: {
|
||||
|
||||
54
mateclaw-ui/src/utils/providerIcons.ts
Normal file
54
mateclaw-ui/src/utils/providerIcons.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// Provider brand icon resolution. Icon assets live in public/icons/providers/.
|
||||
// Shared by the Models settings page and the dashboard model-config card so the
|
||||
// id → icon mapping has a single source of truth.
|
||||
|
||||
const providerIconMap: Record<string, string> = {
|
||||
'dashscope': '/icons/providers/dashscope.png',
|
||||
// dashscope-compat shares the same Aliyun DashScope brand — same logo.
|
||||
'dashscope-compat': '/icons/providers/dashscope.png',
|
||||
'modelscope': '/icons/providers/modelscope.svg',
|
||||
'aliyun-codingplan': '/icons/providers/aliyun-codingplan.svg',
|
||||
'aliyun-codingplan-intl': '/icons/providers/aliyun-codingplan.svg',
|
||||
// bailian-team is an Aliyun product line — reuse the aliyun mark.
|
||||
'bailian-team': '/icons/providers/aliyun-codingplan.svg',
|
||||
'openai': '/icons/providers/openai.svg',
|
||||
'azure-openai': '/icons/providers/azure-openai.svg',
|
||||
'minimax': '/icons/providers/minimax.png',
|
||||
'minimax-cn': '/icons/providers/minimax.png',
|
||||
'kimi-cn': '/icons/providers/kimi.svg',
|
||||
'kimi-intl': '/icons/providers/kimi.svg',
|
||||
'kimi-code': '/icons/providers/kimi.svg',
|
||||
'deepseek': '/icons/providers/deepseek.svg',
|
||||
'anthropic': '/icons/providers/anthropic.svg',
|
||||
'gemini': '/icons/providers/gemini.svg',
|
||||
'ollama': '/icons/providers/ollama.svg',
|
||||
'lmstudio': '/icons/providers/lmstudio.svg',
|
||||
'llamacpp': '/icons/providers/llamacpp.svg',
|
||||
'mlx': '/icons/providers/mlx.svg',
|
||||
'openrouter': '/icons/providers/openrouter.svg',
|
||||
'zhipu-cn': '/icons/providers/zhipu.svg',
|
||||
'zhipu-intl': '/icons/providers/zhipu.svg',
|
||||
// Coding Plan subscription endpoints — same brand, reuse mark.
|
||||
'zhipu-cn-codingplan': '/icons/providers/zhipu.svg',
|
||||
'zhipu-intl-codingplan': '/icons/providers/zhipu.svg',
|
||||
'volcengine': '/icons/providers/volcengine.svg',
|
||||
// volcengine-plan = "Volcano Engine Coding Plan" — same brand, reuse mark.
|
||||
'volcengine-plan': '/icons/providers/volcengine.svg',
|
||||
'xiaomi-mimo': '/icons/providers/xiaomimimo.svg',
|
||||
'hunyuan-3d': '/icons/providers/hunyuan-color.svg',
|
||||
'opencode': '/icons/providers/opencode.svg',
|
||||
'siliconflow-cn': '/icons/providers/siliconcloud.svg',
|
||||
'siliconflow-intl': '/icons/providers/siliconcloud.svg',
|
||||
'openai-chatgpt': '/icons/providers/openai.svg',
|
||||
'anthropic-claude-code': '/icons/providers/anthropic.svg',
|
||||
}
|
||||
|
||||
export function getProviderIcon(providerId: string): string {
|
||||
return providerIconMap[providerId] || '/icons/providers/default.svg'
|
||||
}
|
||||
|
||||
// Hide the <img> when the brand icon fails to load so the UI falls back cleanly.
|
||||
export function onProviderIconError(e: Event) {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
}
|
||||
@ -55,6 +55,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="models-section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">{{ t('dashboard.models.title') }}</h2>
|
||||
<p class="section-subtitle">{{ t('dashboard.models.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="models-card mc-surface-card">
|
||||
<div class="models-card__head">
|
||||
<div class="active-model">
|
||||
<span class="active-model__label">{{ t('dashboard.models.activeModel') }}</span>
|
||||
<span v-if="activeModel" class="active-model__value">
|
||||
<span class="active-model__dot"></span>
|
||||
{{ activeProviderLabel }} · {{ activeModel.model }}
|
||||
</span>
|
||||
<span v-else class="active-model__value active-model__value--empty">
|
||||
{{ t('dashboard.models.notSet') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="models-card__actions">
|
||||
<span v-if="modelProviders.length" class="models-count">
|
||||
{{ readyProviderCount }}/{{ modelProviders.length }} {{ t('dashboard.models.configured') }}
|
||||
</span>
|
||||
<button class="models-manage" @click="goToModels">
|
||||
{{ t('dashboard.models.manage') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="modelProviders.length" class="provider-chips">
|
||||
<button
|
||||
v-for="p in modelProviders"
|
||||
:key="p.id"
|
||||
class="provider-chip"
|
||||
:class="'provider-chip--' + providerChipStatus(p)"
|
||||
:title="p.name"
|
||||
@click="goToModels"
|
||||
>
|
||||
<span class="provider-chip__dot"></span>
|
||||
<img
|
||||
class="provider-chip__icon"
|
||||
:src="getProviderIcon(p.id)"
|
||||
:alt="p.name"
|
||||
@error="onProviderIconError"
|
||||
/>
|
||||
<span class="provider-chip__name">{{ p.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="models-empty">
|
||||
<span class="models-empty__text">{{ t('dashboard.models.empty') }}</span>
|
||||
<button class="models-empty__btn" @click="goToModels">{{ t('dashboard.models.emptyCta') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="trendData.length" class="trend-section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">{{ t('dashboard.trend.title', '7-Day Trend') }}</h2>
|
||||
@ -133,10 +186,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue'
|
||||
import { dashboardApi } from '@/api'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowRight, ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue'
|
||||
import { dashboardApi, modelApi } from '@/api'
|
||||
import { getProviderIcon, onProviderIconError } from '@/utils/providerIcons'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
@ -145,6 +200,7 @@ import { CanvasRenderer } from 'echarts/renderers'
|
||||
echarts.use([LineChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const overview = ref<Record<string, any>>({})
|
||||
const recentRuns = ref<any[]>([])
|
||||
@ -160,6 +216,42 @@ const todayStats = reactive({
|
||||
errors: 0,
|
||||
})
|
||||
|
||||
// ── Model configuration card ──
|
||||
const modelProviders = ref<any[]>([])
|
||||
const activeModel = ref<{ providerId: string; model: string } | null>(null)
|
||||
|
||||
const readyProviderCount = computed(
|
||||
() => modelProviders.value.filter((p) => providerChipStatus(p) === 'ready').length,
|
||||
)
|
||||
|
||||
const activeProviderLabel = computed(() => {
|
||||
if (!activeModel.value) return ''
|
||||
const match = modelProviders.value.find((p) => p.id === activeModel.value!.providerId)
|
||||
return match?.name || activeModel.value.providerId
|
||||
})
|
||||
|
||||
// Maps a provider's liveness to a chip status, falling back to the legacy
|
||||
// configured/available booleans for backends that don't report liveness.
|
||||
function providerChipStatus(p: any): 'ready' | 'partial' | 'down' {
|
||||
switch (p.liveness) {
|
||||
case 'LIVE':
|
||||
return 'ready'
|
||||
case 'COOLDOWN':
|
||||
case 'UNPROBED':
|
||||
return 'partial'
|
||||
case 'REMOVED':
|
||||
case 'UNCONFIGURED':
|
||||
return 'down'
|
||||
}
|
||||
if (p.available) return 'ready'
|
||||
if (p.configured || (p.models?.length || 0) + (p.extraModels?.length || 0) > 0) return 'partial'
|
||||
return 'down'
|
||||
}
|
||||
|
||||
function goToModels() {
|
||||
router.push('/settings/models')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [overviewRes, runsRes, trendRes] = await Promise.all([
|
||||
@ -179,6 +271,19 @@ onMounted(async () => {
|
||||
} catch {
|
||||
// Dashboard data is non-critical
|
||||
}
|
||||
|
||||
// Model configuration card — loaded independently so a failure here never
|
||||
// blanks the analytics above, and vice versa.
|
||||
try {
|
||||
const [provRes, activeRes] = await Promise.all([
|
||||
modelApi.listProviders().catch(() => ({ data: [] })),
|
||||
modelApi.getActive().catch(() => ({ data: null })),
|
||||
])
|
||||
modelProviders.value = (provRes as any).data || []
|
||||
activeModel.value = (activeRes as any).data?.activeLlm || null
|
||||
} catch {
|
||||
// Non-critical
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -380,6 +485,75 @@ function calcDuration(run: any): string {
|
||||
.stat-card--secondary .stat-icon { background: linear-gradient(135deg, rgba(148, 163, 184, 0.12), rgba(148, 163, 184, 0.06)); color: var(--mc-text-secondary); }
|
||||
.stat-card--secondary .stat-value { font-size: 24px; }
|
||||
|
||||
/* Model Configuration card */
|
||||
.models-section { margin-bottom: 22px; }
|
||||
.models-card { padding: 18px; }
|
||||
.models-card__head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px; flex-wrap: wrap;
|
||||
}
|
||||
.active-model { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.active-model__label {
|
||||
font-size: 12px; color: var(--mc-text-tertiary);
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
.active-model__value {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
font-size: 14px; font-weight: 700; color: var(--mc-text-primary);
|
||||
}
|
||||
.active-model__value--empty { color: var(--mc-text-tertiary); font-weight: 600; }
|
||||
.active-model__dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: #10b981; flex-shrink: 0;
|
||||
}
|
||||
.models-card__actions { display: flex; align-items: center; gap: 14px; }
|
||||
.models-count { font-size: 12px; color: var(--mc-text-tertiary); white-space: nowrap; }
|
||||
.models-manage {
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
font-size: 13px; font-weight: 600; color: var(--mc-primary);
|
||||
background: none; border: none; cursor: pointer;
|
||||
padding: 4px 6px; border-radius: 6px; transition: background 0.15s ease;
|
||||
}
|
||||
.models-manage:hover { background: rgba(217, 109, 70, 0.08); }
|
||||
|
||||
.provider-chips {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
margin-top: 16px; padding-top: 16px;
|
||||
border-top: 1px solid var(--mc-border-light);
|
||||
}
|
||||
.provider-chip {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 5px 11px; border-radius: 999px;
|
||||
border: 1px solid var(--mc-border-light);
|
||||
background: var(--mc-bg-muted);
|
||||
font-size: 12px; font-weight: 600; color: var(--mc-text-secondary);
|
||||
cursor: pointer; transition: border-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.provider-chip:hover { border-color: var(--mc-primary); transform: translateY(-1px); }
|
||||
.provider-chip__dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.provider-chip__icon {
|
||||
width: 15px; height: 15px; border-radius: 4px;
|
||||
object-fit: contain; flex-shrink: 0;
|
||||
}
|
||||
.provider-chip--ready .provider-chip__dot { background: #10b981; }
|
||||
.provider-chip--partial .provider-chip__dot { background: #f59e0b; }
|
||||
.provider-chip--down .provider-chip__dot { background: var(--mc-text-tertiary); }
|
||||
.provider-chip--down { opacity: 0.7; }
|
||||
|
||||
.models-empty {
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
margin-top: 16px; padding-top: 16px;
|
||||
border-top: 1px solid var(--mc-border-light);
|
||||
}
|
||||
.models-empty__text { font-size: 13px; color: var(--mc-text-tertiary); }
|
||||
.models-empty__btn {
|
||||
padding: 6px 14px; border-radius: 8px;
|
||||
background: var(--mc-primary); color: #fff;
|
||||
border: none; font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.models-empty__btn:hover { opacity: 0.9; }
|
||||
|
||||
.trend-section { margin-bottom: 22px; }
|
||||
.trend-chart { padding: 18px; }
|
||||
.chart-container { width: 100%; height: 240px; }
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
:disabled="!selectedModel || applying"
|
||||
@click="applyLocalModel"
|
||||
>{{ applying ? t('common.loading') : t('onboarding.setDefault') }}</button>
|
||||
<span v-if="applyError" class="test-failed">{{ applyError }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Cloud path: provider cards -->
|
||||
@ -68,16 +69,18 @@
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="!apiKey || saving"
|
||||
:disabled="testResult !== 'success' || saving"
|
||||
@click="saveCloudConfig"
|
||||
>{{ saving ? t('common.loading') : t('onboarding.saveAndContinue') }}</button>
|
||||
<p v-if="testResult !== 'success'" class="save-hint">{{ t('onboarding.testBeforeSave') }}</p>
|
||||
<span v-if="saveError" class="test-failed">{{ saveError }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { modelApi } from '@/api/index'
|
||||
|
||||
@ -90,6 +93,7 @@ const loading = ref(false)
|
||||
const models = ref<string[]>([])
|
||||
const selectedModel = ref('')
|
||||
const applying = ref(false)
|
||||
const applyError = ref('')
|
||||
|
||||
// Cloud path state
|
||||
const providers = [
|
||||
@ -102,6 +106,14 @@ const apiKey = ref('')
|
||||
const testing = ref(false)
|
||||
const testResult = ref<'' | 'success' | 'failed'>('')
|
||||
const saving = ref(false)
|
||||
const saveError = ref('')
|
||||
|
||||
// A key or provider edit invalidates the previous connection test, so the
|
||||
// user must re-test before the provider can be enabled.
|
||||
watch([apiKey, selectedProvider], () => {
|
||||
testResult.value = ''
|
||||
saveError.value = ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.path === 'local') {
|
||||
@ -124,12 +136,16 @@ onMounted(async () => {
|
||||
async function applyLocalModel() {
|
||||
if (!selectedModel.value) return
|
||||
applying.value = true
|
||||
applyError.value = ''
|
||||
try {
|
||||
await modelApi.applyDiscoveredModels('ollama', [selectedModel.value])
|
||||
// Local providers ship disabled — enable Ollama so it appears in the
|
||||
// model dropdown and the chat runtime can route to it.
|
||||
await modelApi.enableProvider('ollama')
|
||||
await modelApi.setActive({ providerId: 'ollama', model: selectedModel.value })
|
||||
emit('done')
|
||||
} catch {
|
||||
// Handle error silently, user can retry
|
||||
} catch (err) {
|
||||
applyError.value = err instanceof Error ? err.message : t('onboarding.saveFailed')
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
@ -139,6 +155,7 @@ async function testConnection() {
|
||||
if (!selectedProvider.value || !apiKey.value) return
|
||||
testing.value = true
|
||||
testResult.value = ''
|
||||
saveError.value = ''
|
||||
try {
|
||||
// Save key first so test can use it
|
||||
await modelApi.updateProviderConfig(selectedProvider.value, { apiKey: apiKey.value })
|
||||
@ -154,11 +171,23 @@ async function testConnection() {
|
||||
async function saveCloudConfig() {
|
||||
if (!selectedProvider.value || !apiKey.value) return
|
||||
saving.value = true
|
||||
saveError.value = ''
|
||||
try {
|
||||
await modelApi.updateProviderConfig(selectedProvider.value, { apiKey: apiKey.value })
|
||||
const res: any = await modelApi.updateProviderConfig(selectedProvider.value, {
|
||||
apiKey: apiKey.value,
|
||||
})
|
||||
// Cloud providers ship disabled — enable this one so it surfaces in the
|
||||
// model dropdown and the chat runtime can route to it.
|
||||
await modelApi.enableProvider(selectedProvider.value)
|
||||
// Promote one of its models to the active default so the verify step,
|
||||
// and the rest of the app, has a usable model immediately.
|
||||
const firstModel = (res?.data?.models || [])[0]
|
||||
if (firstModel?.id) {
|
||||
await modelApi.setActive({ providerId: selectedProvider.value, model: firstModel.id })
|
||||
}
|
||||
emit('done')
|
||||
} catch {
|
||||
// Handle error silently
|
||||
} catch (err) {
|
||||
saveError.value = err instanceof Error ? err.message : t('onboarding.saveFailed')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@ -309,6 +338,12 @@ async function saveCloudConfig() {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-hint {
|
||||
font-size: 12px;
|
||||
color: var(--mc-text-tertiary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
background: var(--mc-primary);
|
||||
|
||||
@ -2,6 +2,7 @@ import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { modelApi } from '@/api'
|
||||
import type { ActiveModelsInfo, ProviderInfo, ProviderModelInfo } from '@/types'
|
||||
import { getProviderIcon, onProviderIconError as onIconError } from '@/utils/providerIcons'
|
||||
|
||||
/**
|
||||
* RFC-074 PR-1: list / active-model / display helpers.
|
||||
@ -70,56 +71,6 @@ export function useProviderList() {
|
||||
return { type: 'unavailable', label: t('settings.model.unavailable') }
|
||||
}
|
||||
|
||||
const providerIconMap: Record<string, string> = {
|
||||
'dashscope': '/icons/providers/dashscope.png',
|
||||
// dashscope-compat shares the same Aliyun DashScope brand — same logo.
|
||||
'dashscope-compat': '/icons/providers/dashscope.png',
|
||||
'modelscope': '/icons/providers/modelscope.svg',
|
||||
'aliyun-codingplan': '/icons/providers/aliyun-codingplan.svg',
|
||||
'aliyun-codingplan-intl': '/icons/providers/aliyun-codingplan.svg',
|
||||
// bailian-team is an Aliyun product line — reuse the aliyun mark.
|
||||
'bailian-team': '/icons/providers/aliyun-codingplan.svg',
|
||||
'openai': '/icons/providers/openai.svg',
|
||||
'azure-openai': '/icons/providers/azure-openai.svg',
|
||||
'minimax': '/icons/providers/minimax.png',
|
||||
'minimax-cn': '/icons/providers/minimax.png',
|
||||
'kimi-cn': '/icons/providers/kimi.svg',
|
||||
'kimi-intl': '/icons/providers/kimi.svg',
|
||||
'kimi-code': '/icons/providers/kimi.svg',
|
||||
'deepseek': '/icons/providers/deepseek.svg',
|
||||
'anthropic': '/icons/providers/anthropic.svg',
|
||||
'gemini': '/icons/providers/gemini.svg',
|
||||
'ollama': '/icons/providers/ollama.svg',
|
||||
'lmstudio': '/icons/providers/lmstudio.svg',
|
||||
'llamacpp': '/icons/providers/llamacpp.svg',
|
||||
'mlx': '/icons/providers/mlx.svg',
|
||||
'openrouter': '/icons/providers/openrouter.svg',
|
||||
'zhipu-cn': '/icons/providers/zhipu.svg',
|
||||
'zhipu-intl': '/icons/providers/zhipu.svg',
|
||||
// Coding Plan subscription endpoints — same brand, reuse mark.
|
||||
'zhipu-cn-codingplan': '/icons/providers/zhipu.svg',
|
||||
'zhipu-intl-codingplan': '/icons/providers/zhipu.svg',
|
||||
'volcengine': '/icons/providers/volcengine.svg',
|
||||
// volcengine-plan = "Volcano Engine Coding Plan" — same brand, reuse mark.
|
||||
'volcengine-plan': '/icons/providers/volcengine.svg',
|
||||
'xiaomi-mimo': '/icons/providers/xiaomimimo.svg',
|
||||
'hunyuan-3d': '/icons/providers/hunyuan-color.svg',
|
||||
'opencode': '/icons/providers/opencode.svg',
|
||||
'siliconflow-cn': '/icons/providers/siliconcloud.svg',
|
||||
'siliconflow-intl': '/icons/providers/siliconcloud.svg',
|
||||
'openai-chatgpt': '/icons/providers/openai.svg',
|
||||
'anthropic-claude-code': '/icons/providers/anthropic.svg',
|
||||
}
|
||||
|
||||
function getProviderIcon(providerId: string): string {
|
||||
return providerIconMap[providerId] || '/icons/providers/default.svg'
|
||||
}
|
||||
|
||||
function onIconError(e: Event) {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
}
|
||||
|
||||
return {
|
||||
providers,
|
||||
currentProvider,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user