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

342 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 { PromptTemplate, PromptVersion } from '@/lib/types'
import { EmptyState } from '@/components/ui/state'
import { TableSkeleton } from '@/components/ui/skeleton'
export default function PromptsPage() {
const [page, setPage] = useState(1)
const [selectedName, setSelectedName] = useState<string | null>(null)
const [versions, setVersions] = useState<PromptVersion[]>([])
const [showCreate, setShowCreate] = useState(false)
const [showNewVersion, setShowNewVersion] = useState(false)
const [filter, setFilter] = useState<{ source?: string; status?: string }>({})
const { data, error, isLoading, mutate } = useSWR(
['prompts.list', page, filter.source, filter.status],
() => api.prompts.list({ page, page_size: 50, ...filter }),
)
const templates = data?.items ?? []
const total = data?.total ?? 0
const fetchVersions = async (name: string) => {
try {
const res = await api.prompts.listVersions(name)
setVersions(res)
setSelectedName(name)
} catch (err) {
console.error('Failed to fetch versions:', err)
}
}
const handleCreate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
try {
await api.prompts.create({
name: fd.get('name') as string,
category: fd.get('category') as string,
description: (fd.get('description') as string) || undefined,
source: 'custom',
system_prompt: fd.get('system_prompt') as string,
})
setShowCreate(false)
mutate()
} catch (err) {
console.error('Failed to create prompt:', err)
}
}
const handleNewVersion = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!selectedName) return
const fd = new FormData(e.currentTarget)
try {
await api.prompts.createVersion(selectedName, {
system_prompt: fd.get('system_prompt') as string,
changelog: (fd.get('changelog') as string) || undefined,
})
setShowNewVersion(false)
fetchVersions(selectedName)
} catch (err) {
console.error('Failed to create version:', err)
}
}
const handleRollback = async (name: string, version: number) => {
if (!confirm(`确认回退到版本 ${version}`)) return
try {
await api.prompts.rollback(name, version)
fetchVersions(name)
mutate()
} catch (err) {
console.error('Failed to rollback:', err)
}
}
const handleArchive = async (name: string) => {
if (!confirm(`确认归档 ${name}`)) return
try {
await api.prompts.archive(name)
mutate()
} catch (err) {
console.error('Failed to archive:', err)
}
}
const statusBadge = (status: string) => {
const colors: Record<string, string> = {
active: 'bg-emerald-500/20 text-emerald-400',
deprecated: 'bg-amber-500/20 text-amber-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">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white"></h1>
<p className="text-sm text-zinc-400 mt-1"> OTA </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>
{/* Filters */}
<div className="flex gap-2">
{(['all', 'builtin', 'custom'] as const).map(s => (
<button
key={s}
onClick={() => setFilter(s === 'all' ? {} : { source: s })}
className={`px-3 py-1 text-sm rounded-lg transition-colors ${
(filter.source || 'all') === s
? 'bg-zinc-700 text-white'
: 'bg-zinc-800 text-zinc-400 hover:text-white'
}`}
>
{s === 'all' ? '全部' : s === 'builtin' ? '内置' : '自定义'}
</button>
))}
</div>
{/* Template List */}
<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-right px-4 py-3 text-zinc-400 font-medium"></th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={7}>
<TableSkeleton rows={5} cols={7} hasToolbar={false} />
</td>
</tr>
) : error ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-red-400"></td></tr>
) : templates.length === 0 ? (
<tr><td colSpan={7}><EmptyState message="暂无提示词模板" /></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">
<button
onClick={() => fetchVersions(t.name)}
className="text-blue-400 hover:text-blue-300 font-mono"
>
{t.name}
</button>
</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">v{t.current_version}</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={() => fetchVersions(t.name)}
className="text-zinc-400 hover:text-white mr-2"
>
</button>
{t.source === 'custom' && (
<button
onClick={() => handleArchive(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>
{/* Version History Panel */}
{selectedName && (
<div className="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">
{selectedName}
</h2>
<div className="flex gap-2">
<button
onClick={() => setShowNewVersion(true)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-xs"
>
</button>
<button
onClick={() => { setSelectedName(null); setVersions([]) }}
className="px-3 py-1.5 bg-zinc-700 text-white rounded-lg hover:bg-zinc-600 text-xs"
>
</button>
</div>
</div>
<div className="space-y-3">
{versions.map(v => (
<div key={v.id} className="bg-zinc-800/50 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-mono text-zinc-300">v{v.version}</span>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">
{new Date(v.created_at).toLocaleString('zh-CN')}
</span>
{v.changelog && (
<span className="text-xs text-zinc-400"> {v.changelog}</span>
)}
{v.min_app_version && (
<span className="text-xs text-amber-400">: {v.min_app_version}</span>
)}
</div>
</div>
<pre className="text-xs text-zinc-400 bg-zinc-900 rounded p-2 overflow-x-auto max-h-32">
{v.system_prompt.substring(0, 300)}{v.system_prompt.length > 300 ? '...' : ''}
</pre>
<div className="mt-2 flex gap-2">
<button
onClick={() => {
navigator.clipboard.writeText(v.system_prompt)
}}
className="text-xs text-zinc-500 hover:text-white"
>
</button>
<button
onClick={() => handleRollback(selectedName, v.version)}
className="text-xs text-amber-500 hover:text-amber-400"
>
退
</button>
</div>
</div>
))}
{versions.length === 0 && (
<EmptyState message="暂无版本历史" />
)}
</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">
<h2 className="text-lg font-semibold text-white"></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_prompt" />
</div>
<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="custom_system"></option>
<option value="custom_extraction"></option>
<option value="custom_compaction"></option>
<option value="custom_other"></option>
</select>
</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>
<label className="block text-sm text-zinc-400 mb-1"></label>
<textarea name="system_prompt" required rows={6} className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm font-mono" />
</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>
)}
{/* New Version Modal */}
{showNewVersion && selectedName && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<form onSubmit={handleNewVersion} className="bg-zinc-900 rounded-xl border border-zinc-700 p-6 w-full max-w-lg space-y-4">
<h2 className="text-lg font-semibold text-white"> {selectedName} </h2>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<textarea name="system_prompt" required rows={6} className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm font-mono" />
</div>
<div>
<label className="block text-sm text-zinc-400 mb-1"></label>
<input name="changelog" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="描述本次变更" />
</div>
<div className="flex gap-2 justify-end">
<button type="button" onClick={() => setShowNewVersion(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>
)
}