|
|
@@ -1,8 +1,8 @@
|
|
|
import { useState } from 'react'
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
-import { CheckCircle2, XCircle, Eye, EyeOff, Loader2, Trash2, Plus } from 'lucide-react'
|
|
|
+import { CheckCircle2, XCircle, Eye, EyeOff, Loader2, Trash2, Plus, Bot, ArrowRight } from 'lucide-react'
|
|
|
import toast from 'react-hot-toast'
|
|
|
-import { providersApi, type Provider } from '../api/providers'
|
|
|
+import { providersApi, type Provider, type ModelInUse } from '../api/providers'
|
|
|
import { Card, PageHeader, Badge, Button, Input, Spinner } from '../components/ui'
|
|
|
import { clsx } from '../lib/clsx'
|
|
|
|
|
|
@@ -146,6 +146,102 @@ function ProviderCard({ provider, onSave, onRemove, onTest, testing, testResult,
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+// ── 使用中的模型:各智能体当前模型一览 + 切换 ──────────────────────────────
|
|
|
+
|
|
|
+function ModelsInUseSection({ providers }: { providers: Provider[] }) {
|
|
|
+ const qc = useQueryClient()
|
|
|
+ // 切换草稿: agent_id -> "provider/model"
|
|
|
+ const [draft, setDraft] = useState<Record<string, string>>({})
|
|
|
+
|
|
|
+ const { data, isLoading } = useQuery({
|
|
|
+ queryKey: ['models-in-use'],
|
|
|
+ queryFn: providersApi.modelsInUse,
|
|
|
+ retry: false,
|
|
|
+ })
|
|
|
+
|
|
|
+ const switchMut = useMutation({
|
|
|
+ mutationFn: ({ agentId, provider, name }: { agentId: string; provider: string; name: string }) =>
|
|
|
+ providersApi.switchModel(agentId, provider, name),
|
|
|
+ onSuccess: (res) => {
|
|
|
+ toast.success(`已切换:${res.previous} → 新模型(v${res.version},可在版本历史回滚)`, { duration: 5000 })
|
|
|
+ qc.invalidateQueries({ queryKey: ['models-in-use'] })
|
|
|
+ qc.invalidateQueries({ queryKey: ['agents'] })
|
|
|
+ },
|
|
|
+ onError: (e: any) => toast.error(e.message ?? '切换失败'),
|
|
|
+ })
|
|
|
+
|
|
|
+ // 下拉选项:已配置的 provider 优先,其余置后并标注
|
|
|
+ const options: { value: string; label: string; configured: boolean }[] = []
|
|
|
+ for (const p of providers) {
|
|
|
+ for (const m of p.models) {
|
|
|
+ options.push({
|
|
|
+ value: `${p.id}/${m}`,
|
|
|
+ label: `${p.id} / ${m}${p.configured ? '' : '(未配置 Key)'}`,
|
|
|
+ configured: p.configured,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ options.sort((a, b) => Number(b.configured) - Number(a.configured))
|
|
|
+
|
|
|
+ if (isLoading) return null
|
|
|
+ const items = data?.items ?? []
|
|
|
+ if (items.length === 0) return null
|
|
|
+
|
|
|
+ return (
|
|
|
+ <section>
|
|
|
+ <h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
|
+ 使用中的模型({items.length} 个智能体)
|
|
|
+ </h2>
|
|
|
+ <Card className="divide-y divide-gray-100 p-0 overflow-hidden">
|
|
|
+ {items.map((it: ModelInUse) => {
|
|
|
+ const current = `${it.provider || '?'}/${it.model || '?'}`
|
|
|
+ const selected = draft[it.agent_id] ?? ''
|
|
|
+ const changed = selected && selected !== current
|
|
|
+ return (
|
|
|
+ <div key={it.agent_id} className="flex items-center gap-3 px-4 py-3 flex-wrap">
|
|
|
+ <div className="flex items-center gap-2 min-w-0 flex-1">
|
|
|
+ <Bot size={14} className="text-indigo-500 shrink-0" />
|
|
|
+ <span className="text-sm font-medium text-gray-900 truncate">{it.agent_name}</span>
|
|
|
+ <span className="text-[10px] text-gray-400 font-mono shrink-0">v{it.version}</span>
|
|
|
+ </div>
|
|
|
+ <Badge variant={it.provider ? 'green' : 'default'}>
|
|
|
+ {current}
|
|
|
+ </Badge>
|
|
|
+ <ArrowRight size={12} className="text-gray-300 shrink-0" />
|
|
|
+ <select
|
|
|
+ value={selected || current}
|
|
|
+ onChange={e => setDraft(prev => ({ ...prev, [it.agent_id]: e.target.value }))}
|
|
|
+ className="text-xs border border-gray-200 rounded-lg px-2 py-1.5 bg-white text-gray-700 max-w-[220px]"
|
|
|
+ >
|
|
|
+ {!options.some(o => o.value === current) && (
|
|
|
+ <option value={current}>{current}(当前)</option>
|
|
|
+ )}
|
|
|
+ {options.map(o => (
|
|
|
+ <option key={o.value} value={o.value}>{o.label}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ disabled={!changed || switchMut.isPending}
|
|
|
+ onClick={() => {
|
|
|
+ const [provider, ...rest] = selected.split('/')
|
|
|
+ switchMut.mutate({ agentId: it.agent_id, provider, name: rest.join('/') })
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {switchMut.isPending ? '切换中…' : '切换'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </Card>
|
|
|
+ <p className="text-[11px] text-gray-400 mt-2">
|
|
|
+ 切换会生成新版本(旧配置保留在「智能体 → 编辑 → 版本历史」中可回滚);
|
|
|
+ ollama 选项列出的是本机实际安装的模型。
|
|
|
+ </p>
|
|
|
+ </section>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
export default function Providers() {
|
|
|
const qc = useQueryClient()
|
|
|
const [testingId, setTestingId] = useState<string | null>(null)
|
|
|
@@ -219,6 +315,8 @@ export default function Providers() {
|
|
|
</div>
|
|
|
) : (
|
|
|
<div className="space-y-8">
|
|
|
+ <ModelsInUseSection providers={data?.providers ?? []} />
|
|
|
+
|
|
|
{configured.length > 0 && (
|
|
|
<section>
|
|
|
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
|