Files
huihuiSquare/frontend/src/views/AIModels.vue
T

263 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-container">
<div class="page-header">
<span class="page-title">AI模型配置</span>
<el-button type="primary" size="small" @click="openCreate">
<el-icon><Plus /></el-icon> 添加模型
</el-button>
</div>
<div class="models-grid">
<div v-for="m in models" :key="m.id" class="model-card" :class="{ 'is-default': m.is_default }">
<div class="model-head">
<div class="model-name">
<span class="provider-badge" :class="m.provider">{{ providerLabels[m.provider] || m.provider }}</span>
<span class="model-title">{{ m.model_name }}</span>
</div>
<div style="display:flex;gap:6px;align-items:center">
<el-tag :type="m.usage_scope === 'digital_avatar' ? 'warning' : 'info'" size="small">
{{ scopeLabels[m.usage_scope] || '通用业务' }}
</el-tag>
<el-tag v-if="m.is_default" type="success" size="small">默认</el-tag>
<el-tag v-if="!m.is_enabled" type="danger" size="small">禁用</el-tag>
</div>
</div>
<div class="model-meta">
<span>版本: {{ m.model_version || '--' }}</span>
<span v-if="m.usage_scope === 'digital_avatar'">视觉: {{ m.vision_model_version || 'qwen3.6-flash' }}</span>
<span v-if="m.usage_scope === 'digital_avatar'">病例OCR: {{ m.ocr_model_version || 'qwen-vl-ocr' }}</span>
<span>温度: {{ m.temperature }}</span>
<span>Max Tokens: {{ m.max_tokens }}</span>
<span>超时: {{ m.timeout_seconds }}s</span>
<span>API Key: {{ m.has_api_key ? '✓ 已配置' : '✗ 未配置' }}</span>
</div>
<div class="model-actions">
<el-button size="small" @click="openTest(m)">测试</el-button>
<el-button size="small" @click="openEdit(m)">编辑</el-button>
<el-button v-if="!m.is_default" size="small" type="primary" @click="setDefault(m)">设为默认</el-button>
<el-button size="small" type="danger" @click="doDelete(m)">删除</el-button>
</div>
</div>
<div v-if="!models.length" class="empty-state">
<el-empty description="暂无模型配置,请添加AI模型" />
</div>
</div>
<!-- Create/Edit Dialog -->
<el-dialog v-model="dialogVisible" :title="editModel ? '编辑模型' : '添加AI模型'" width="540px">
<el-form :model="form" label-width="110px" :rules="rules" ref="formRef">
<el-form-item label="模型名称" prop="model_name">
<el-input v-model="form.model_name" placeholder="如: GPT-4 生产环境" />
</el-form-item>
<el-form-item label="提供商" prop="provider">
<el-select v-model="form.provider" style="width:100%" @change="onProviderChange">
<el-option v-for="(l,v) in providerLabels" :key="v" :label="l" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="使用场景" prop="usage_scope">
<el-radio-group v-model="form.usage_scope">
<el-radio-button value="general">通用业务</el-radio-button>
<el-radio-button value="digital_avatar">数字分身专用</el-radio-button>
</el-radio-group>
<div class="scope-tip">数字分身专用模型仅用于分身对话和主动接管回复</div>
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="form.api_base_url" placeholder="留空使用默认地址" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="form.api_key" type="password" show-password :placeholder="editModel ? '不修改请留空' : '输入API Key'" />
</el-form-item>
<el-form-item label="模型版本">
<el-input v-model="form.model_version" placeholder="如: gpt-4-turbo, glm-4" />
</el-form-item>
<template v-if="form.usage_scope === 'digital_avatar'">
<el-form-item label="视觉模型">
<el-input v-model="form.vision_model_version" placeholder="如: qwen3.6-flash" />
</el-form-item>
<el-form-item label="病例OCR模型">
<el-input v-model="form.ocr_model_version" placeholder="如: qwen-vl-ocr" />
<div class="scope-tip">识别为病例、处方、检查单后自动调用,普通图片不会重复调用。</div>
</el-form-item>
</template>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="温度">
<el-input-number v-model="form.temperature" :min="0" :max="2" :step="0.1" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Max Tokens">
<el-input-number v-model="form.max_tokens" :min="100" :max="32000" :step="100" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="超时(秒)">
<el-input-number v-model="form.timeout_seconds" :min="5" :max="300" style="width:160px" />
</el-form-item>
<el-form-item label="设为默认">
<el-switch v-model="form.is_default" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit" :loading="submitting">保存</el-button>
</template>
</el-dialog>
<!-- Test Dialog -->
<el-dialog v-model="testVisible" title="模型测试" width="560px">
<el-form label-width="90px">
<el-form-item label="测试指令">
<el-input v-model="testPrompt" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<div v-if="testResult" class="test-result">
<div class="result-meta">
<el-tag :type="testResult.success ? 'success' : 'danger'">
{{ testResult.success ? '成功' : '失败' }}
</el-tag>
<span v-if="testResult.success" style="font-size:12px;color:var(--color-text-muted)">
耗时 {{ testResult.elapsed_seconds }}s · 消耗 {{ testResult.tokens }} tokens
</span>
</div>
<div class="result-content" style="white-space:pre-wrap;word-break:break-all;">
{{ testResult.success ? (testResult.content || '(响应为空)') : testResult.error }}
</div>
<div v-if="testResult.success" style="margin-top:8px;font-size:12px;color:var(--color-text-muted)">
耗时 {{ testResult.elapsed_seconds }}s &nbsp;·&nbsp; Token {{ testResult.tokens }}
</div>
</div>
<template #footer>
<el-button @click="testVisible = false">关闭</el-button>
<el-button type="primary" @click="runTest" :loading="testing">发送测试</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAIModels, createAIModel, updateAIModel, deleteAIModel, testAIModel } from '@/api'
const models = ref([])
const dialogVisible = ref(false)
const editModel = ref(null)
const submitting = ref(false)
const formRef = ref()
const testVisible = ref(false)
const testingModel = ref(null)
const testPrompt = ref('你好,请简单介绍一下你自己,并写一条关于"人工智能改变新闻业"的新闻评论。')
const testResult = ref(null)
const testing = ref(false)
const providerLabels = { openai: 'OpenAI', zhipu: '智谱GLM', wenxin: '文心一言', qianwen: '通义千问', local: '本地模型' }
const scopeLabels = { general: '通用业务', digital_avatar: '数字分身专用' }
const form = reactive({ model_name: '', provider: 'openai', usage_scope: 'general', api_base_url: '', api_key: '', model_version: '', vision_model_version: 'qwen3.6-flash', ocr_model_version: 'qwen-vl-ocr', temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
const rules = { model_name: [{ required: true, message: '请输入模型名称' }], provider: [{ required: true }], usage_scope: [{ required: true }] }
async function load() {
const res = await getAIModels()
models.value = res.data || []
}
const PROVIDER_DEFAULTS = {
openai: { api_base_url: 'https://api.openai.com/v1', model_version: 'gpt-4-turbo' },
zhipu: { api_base_url: 'https://open.bigmodel.cn/api/paas/v4', model_version: 'glm-4' },
wenxin: { api_base_url: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat', model_version: 'ERNIE-Bot-4' },
qianwen: { api_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model_version: 'qwen-turbo' },
local: { api_base_url: 'http://127.0.0.1:11434/v1', model_version: 'qwen2' },
}
function onProviderChange(provider) {
const def = PROVIDER_DEFAULTS[provider] || {}
form.api_base_url = def.api_base_url || ''
form.model_version = def.model_version || ''
}
function openCreate() {
editModel.value = null
Object.assign(form, { model_name: '', provider: 'openai', usage_scope: 'general', api_base_url: PROVIDER_DEFAULTS.openai.api_base_url, api_key: '', model_version: PROVIDER_DEFAULTS.openai.model_version, vision_model_version: 'qwen3.6-flash', ocr_model_version: 'qwen-vl-ocr', temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
dialogVisible.value = true
}
function openEdit(m) {
editModel.value = m
Object.assign(form, { model_name: m.model_name, provider: m.provider, usage_scope: m.usage_scope || 'general', api_base_url: m.api_base_url || '', api_key: '', model_version: m.model_version || '', vision_model_version: m.vision_model_version || 'qwen3.6-flash', ocr_model_version: m.ocr_model_version || 'qwen-vl-ocr', temperature: m.temperature, max_tokens: m.max_tokens, timeout_seconds: m.timeout_seconds, is_default: m.is_default })
dialogVisible.value = true
}
async function submit() {
await formRef.value.validate()
submitting.value = true
try {
const data = { ...form }
if (editModel.value) {
if (!data.api_key) delete data.api_key
await updateAIModel(editModel.value.id, data)
} else {
await createAIModel(data)
}
ElMessage.success('保存成功')
dialogVisible.value = false
load()
} finally { submitting.value = false }
}
async function doDelete(m) {
await ElMessageBox.confirm(`确认删除模型 "${m.model_name}"?`, '确认', { type: 'warning' })
await deleteAIModel(m.id)
ElMessage.success('删除成功')
load()
}
async function setDefault(m) {
await updateAIModel(m.id, { is_default: 1 })
ElMessage.success(`"${m.model_name}" 已设为默认模型`)
load()
}
function openTest(m) {
testingModel.value = m
testResult.value = null
testVisible.value = true
}
async function runTest() {
testing.value = true
testResult.value = null
try {
const res = await testAIModel({ model_id: testingModel.value.id, test_prompt: testPrompt.value })
testResult.value = res.data
} finally { testing.value = false }
}
onMounted(load)
</script>
<style scoped>
.models-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; }
.model-card {
background: var(--color-bg-card); border: 1px solid var(--color-border);
border-radius: 12px; padding: 18px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.model-card.is-default { border-color: var(--color-accent-green); box-shadow: 0 0 12px rgba(63,185,80,0.15); }
.model-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.model-name { display: flex; align-items: center; gap: 8px; }
.provider-badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600; }
.provider-badge.openai { background: rgba(16,163,127,0.15); color: #10a37f; }
.provider-badge.zhipu { background: rgba(88,166,255,0.15); color: var(--color-accent); }
.provider-badge.wenxin { background: rgba(240,136,62,0.15); color: var(--color-accent-orange); }
.provider-badge.qianwen { background: rgba(210,153,34,0.15); color: var(--color-accent-yellow); }
.provider-badge.local { background: rgba(188,140,255,0.15); color: var(--color-accent-purple); }
.model-title { font-size: 15px; font-weight: 600; }
.model-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: var(--color-text-muted); margin-bottom: 14px; }
.model-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.test-result { margin-top: 16px; }
.result-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.result-content { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 8px; padding: 12px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }
.empty-state { grid-column: 1/-1; padding: 40px; }
.scope-tip { margin-top: 6px; color: var(--color-text-muted); font-size: 12px; line-height: 1.5; }
</style>