Files
zclaw_openfang/admin/src/app/(dashboard)/agent-templates/page.tsx
iven 5fdf96c3f5 chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
2026-03-29 10:46:41 +08:00

291 lines
14 KiB
TypeScript
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.

'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { api } from '@/lib/api-client'
import type { AgentTemplate } from '@/lib/types'
import { ErrorBanner, EmptyState } from '@/components/ui/state'
import { TableSkeleton } from '@/components/ui/skeleton'
export default function AgentTemplatesPage() {
const [page, setPage] = useState(1)
const [error, setError] = useState('')
const [showCreate, setShowCreate] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const { data, isLoading, mutate } = useSWR(
['agentTemplates.list', page],
() => api.agentTemplates.list({ page, page_size: 50 }),
)
const templates = data?.items ?? []
const total = data?.total ?? 0
const handleCreate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
try {
const tools = (fd.get('tools') as string || '').split(',').map(s => s.trim()).filter(Boolean)
const capabilities = (fd.get('capabilities') as string || '').split(',').map(s => s.trim()).filter(Boolean)
await api.agentTemplates.create({
name: fd.get('name') as string,
description: (fd.get('description') as string) || undefined,
category: (fd.get('category') as string) || 'general',
model: (fd.get('model') as string) || undefined,
system_prompt: (fd.get('system_prompt') as string) || undefined,
tools: tools.length > 0 ? tools : undefined,
capabilities: capabilities.length > 0 ? capabilities : undefined,
temperature: (fd.get('temperature') as string) ? parseFloat(fd.get('temperature') as string) : undefined,
max_tokens: (fd.get('max_tokens') as string) ? parseInt(fd.get('max_tokens') as string, 10) : undefined,
visibility: (fd.get('visibility') as string) || 'public',
})
setShowCreate(false)
mutate()
} catch {
setError('创建失败')
}
}
const handleArchive = async (id: string, name: string) => {
if (!confirm(`确认归档模板 "${name}"`)) return
try {
await api.agentTemplates.archive(id)
mutate()
} catch {
setError('归档失败')
}
}
const statusBadge = (status: string) => {
const colors: Record<string, string> = {
active: 'bg-emerald-500/20 text-emerald-400',
archived: 'bg-zinc-500/20 text-zinc-400',
}
return <span className={`px-2 py-0.5 text-xs rounded-full ${colors[status] || colors.archived}`}>{status}</span>
}
const sourceBadge = (source: string) => {
const colors: Record<string, string> = {
builtin: 'bg-blue-500/20 text-blue-400',
custom: 'bg-purple-500/20 text-purple-400',
}
return (
<span className={`px-2 py-0.5 text-xs rounded-full ${colors[source] || ''}`}>
{source === 'builtin' ? '内置' : '自定义'}
</span>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Agent </h1>
<p className="text-sm text-zinc-400 mt-1"> Agent </p>
</div>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
>
+
</button>
</div>
{error && <ErrorBanner message={error} onDismiss={() => setError('')} />}
<div className="bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800">
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-left px-4 py-3 text-zinc-400 font-medium"></th>
<th className="text-right px-4 py-3 text-zinc-400 font-medium"></th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={9}>
<TableSkeleton rows={5} cols={9} hasToolbar={false} />
</td>
</tr>
) : templates.length === 0 ? (
<tr><td colSpan={9}><EmptyState message="暂无 Agent 模板" /></td></tr>
) : (
templates.map(t => (
<tr key={t.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
<td className="px-4 py-3">
<div>
<span className="text-white font-medium">{t.name}</span>
{t.description && (
<p className="text-xs text-zinc-500 mt-0.5 truncate max-w-[200px]">{t.description}</p>
)}
</div>
</td>
<td className="px-4 py-3 text-zinc-400">{t.category}</td>
<td className="px-4 py-3">{sourceBadge(t.source)}</td>
<td className="px-4 py-3 text-zinc-300 font-mono text-xs">{t.model || '-'}</td>
<td className="px-4 py-3 text-zinc-400">{t.tools.length}</td>
<td className="px-4 py-3 text-zinc-400">{t.visibility}</td>
<td className="px-4 py-3">{statusBadge(t.status)}</td>
<td className="px-4 py-3 text-zinc-500 text-xs">
{new Date(t.updated_at).toLocaleString('zh-CN')}
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => setEditingId(editingId === t.id ? null : t.id)}
className="text-zinc-400 hover:text-white mr-2"
>
</button>
{t.source === 'custom' && (
<button
onClick={() => handleArchive(t.id, t.name)}
className="text-red-400 hover:text-red-300"
>
</button>
)}
</td>
</tr>
))
)}
</tbody>
</table>
<div className="px-4 py-2 text-xs text-zinc-500 border-t border-zinc-800">
{total}
</div>
</div>
{/* 展开详情 */}
{editingId && (() => {
const t = templates.find(t => t.id === editingId)
if (!t) return null
return (
<div className="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-white">{t.name} </h2>
<button onClick={() => setEditingId(null)} className="text-zinc-400 hover:text-white text-sm"></button>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-zinc-500"></span>
<span className="text-zinc-300">{t.category}</span>
</div>
<div>
<span className="text-zinc-500"></span>
<span className="text-zinc-300 font-mono">{t.model || '未指定'}</span>
</div>
<div>
<span className="text-zinc-500"></span>
<span className="text-zinc-300">{t.temperature?.toFixed(2) || '默认'}</span>
</div>
<div>
<span className="text-zinc-500"> Token</span>
<span className="text-zinc-300">{t.max_tokens || '未限制'}</span>
</div>
<div className="col-span-2">
<span className="text-zinc-500"></span>
<div className="flex flex-wrap gap-1 mt-1">
{t.tools.length > 0 ? t.tools.map(tool => (
<span key={tool} className="px-2 py-0.5 bg-zinc-800 rounded text-xs text-zinc-300">{tool}</span>
)) : <span className="text-zinc-600"></span>}
</div>
</div>
<div className="col-span-2">
<span className="text-zinc-500"></span>
<div className="flex flex-wrap gap-1 mt-1">
{t.capabilities.length > 0 ? t.capabilities.map(cap => (
<span key={cap} className="px-2 py-0.5 bg-blue-500/10 rounded text-xs text-blue-400">{cap}</span>
)) : <span className="text-zinc-600"></span>}
</div>
</div>
{t.system_prompt && (
<div className="col-span-2">
<span className="text-zinc-500"></span>
<pre className="text-xs text-zinc-400 bg-zinc-800/50 rounded p-2 mt-1 overflow-x-auto max-h-32">
{t.system_prompt.substring(0, 500)}{t.system_prompt.length > 500 ? '...' : ''}
</pre>
</div>
)}
</div>
</div>
)
})()}
{/* Create Modal */}
{showCreate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<form onSubmit={handleCreate} className="bg-zinc-900 rounded-xl border border-zinc-700 p-6 w-full max-w-lg space-y-4 max-h-[80vh] overflow-y-auto">
<h2 className="text-lg font-semibold text-white"> Agent </h2>
<div>
<label className="block text-sm text-zinc-400 mb-1"> *</label>
<input name="name" required className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="my_agent" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="description" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="可选" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<select name="category" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm">
<option value="general"></option>
<option value="coding"></option>
<option value="research"></option>
<option value="creative"></option>
<option value="assistant"></option>
</select>
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="model" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="如 glm-4-plus" />
</div>
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<textarea name="system_prompt" rows={4} className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm font-mono" placeholder="Agent 系统提示词" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="tools" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="browser, file_system, code_execute" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="capabilities" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="streaming, vision, function_calling" />
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="temperature" type="number" step="0.1" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="默认" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"> Token</label>
<input name="max_tokens" type="number" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="不限" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<select name="visibility" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm">
<option value="public"></option>
<option value="team"></option>
<option value="private"></option>
</select>
</div>
</div>
<div className="flex gap-2 justify-end">
<button type="button" onClick={() => setShowCreate(false)} className="px-4 py-2 bg-zinc-700 text-white rounded-lg hover:bg-zinc-600 text-sm"></button>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm"></button>
</div>
</form>
</div>
)}
</div>
)
}