creative_studio/frontend/src/pages/SkillManagement.tsx
hjjjj 5487450f34 feat: 实现审核系统核心功能与UI优化
- 新增审核卡片和确认卡片模型,支持Agent推送审核任务和用户确认
- 实现审核卡片API服务,支持创建、更新、批准、驳回等操作
- 扩展审核维度配置,新增角色一致性、剧情连贯性等维度
- 优化前端审核配置页面,修复API路径错误和状态枚举问题
- 改进剧集创作平台布局,新增左侧边栏用于剧集管理和上下文查看
- 增强Skill管理,支持从审核系统跳转创建/编辑Skill
- 修复episodes.json数据问题,清理聊天历史记录
- 更新Agent提示词,明确Skill引用加载流程
- 统一前端主题配置,优化整体UI体验
2026-01-30 18:32:48 +08:00

1580 lines
51 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.

/**
* Skills 管理中心 - 列表页面
*/
import { useEffect, useState } from 'react'
import {
Card,
Table,
Tag,
Button,
Space,
Input,
message,
Tabs,
Drawer,
Modal,
Form,
Select,
Popconfirm,
Badge,
Divider,
Tooltip,
Switch,
Empty,
Spin,
Alert,
Progress,
List,
Typography
} from 'antd'
import { SkillCreate } from '@/components/SkillCreate'
import {
PlusOutlined,
EyeOutlined,
EditOutlined,
PlayCircleOutlined,
SearchOutlined,
CopyOutlined,
DeleteOutlined,
AppstoreOutlined,
SettingOutlined,
CheckCircleOutlined,
LockOutlined,
RobotOutlined,
EditFilled,
ReloadOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
CheckOutlined
} from '@ant-design/icons'
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'
import { useSkillStore, Skill } from '@/stores/skillStore'
import { skillService, SkillDraft } from '@/services/skillService'
import { taskService } from '@/services/taskService'
import type { ColumnsType } from 'antd/es/table'
import { TablePaginationConfig } from 'antd/es/table'
import dayjs from 'dayjs'
const { Search, TextArea } = Input
const { TabPane } = Tabs
const { Option } = Select
// Keep Input component available
const InputComponent = Input
interface SkillFormData {
id: string
name: string
category: string
behavior_guide: string
tags: string[]
config?: Record<string, any>
}
// AI 创建模式
type CreateMode = 'ai' | 'manual' | 'template'
export const SkillManagement = () => {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const location = useLocation()
const { skills, builtinSkills, userSkills, loading, fetchSkills, testSkill } = useSkillStore()
// List view states
const [searchText, setSearchText] = useState('')
const [activeTab, setActiveTab] = useState('all')
const [viewMode, setViewMode] = useState<'list' | 'grid'>('list')
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
// Detail view states
const [detailDrawerOpen, setDetailDrawerOpen] = useState(false)
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null)
// Test states
const [testModalOpen, setTestModalOpen] = useState(false)
const [testInput, setTestInput] = useState('')
const [testResult, setTestResult] = useState('')
const [testing, setTesting] = useState(false)
// CRUD states
const [editModalOpen, setEditModalOpen] = useState(false)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingSkill, setEditingSkill] = useState<Skill | null>(null)
const [form] = Form.useForm()
const [submitting, setSubmitting] = useState(false)
// AI 创建状态
const [createMode, setCreateMode] = useState<CreateMode>('ai')
const [aiIdea, setAiIdea] = useState('')
const [aiCategory, setAiCategory] = useState<string | undefined>()
const [generating, setGenerating] = useState(false)
const [generatedDraft, setGeneratedDraft] = useState<SkillDraft | null>(null)
// 技能生成任务状态
const [skillGenerationTasks, setSkillGenerationTasks] = useState<any[]>([])
const [loadingTasks, setLoadingTasks] = useState(false)
const [optimizing, setOptimizing] = useState(false)
const [optimizeRequirements, setOptimizeRequirements] = useState('')
// 模板初始化状态
const [templateSkillName, setTemplateSkillName] = useState('')
const [templateOutputPath, setTemplateOutputPath] = useState('')
const [initializing, setInitializing] = useState(false)
const [initResult, setInitResult] = useState<string | null>(null)
// 向导状态
const [wizardVisible, setWizardVisible] = useState(false)
// 跳转回退信息(从外部跳转过来时使用)
const [redirectState, setRedirectState] = useState<{ path: string; activeTab?: string; reviewSubTab?: string } | null>(null)
useEffect(() => {
fetchSkills()
// 轮询技能生成任务 - 使用递归 setTimeout 而不是 setInterval
// 这样可以确保前一个请求完成后才开始下一个请求
let isMounted = true
const pollTasks = async () => {
if (!isMounted) return
try {
await fetchSkillGenerationTasks()
} catch (error) {
console.error('Error polling tasks:', error)
}
// 等待 3 秒后再次轮询
if (isMounted) {
setTimeout(pollTasks, 3000)
}
}
// 启动轮询
pollTasks()
return () => {
isMounted = false
}
}, [])
// 处理URL参数和location.state - 支持通过URL参数打开编辑或创建
useEffect(() => {
// 从 location.state 读取 redirect 信息
const state = location.state as any
if (state?.redirect) {
setRedirectState({
path: state.redirect,
activeTab: state.activeTab,
reviewSubTab: state.reviewSubTab
})
// 清除 state 避免重复处理
location.state = null
}
// 处理编辑参数 - 使用新的 SkillCreate 向导组件
const editSkillId = searchParams.get('edit')
if (editSkillId) {
const skillToEdit = skills.find(s => s.id === editSkillId)
if (skillToEdit) {
setEditingSkill(skillToEdit)
setWizardVisible(true)
// 清除URL参数
searchParams.delete('edit')
navigate(`/skills?${searchParams.toString()}`, { replace: true })
return
} else {
// Skill 不在本地列表中,尝试从服务器直接获取
// 这允许编辑在 review 配置中引用但不在主列表中的技能
const fetchSkillForEdit = async () => {
try {
const result = await skillService.getSkillWithReferences(editSkillId, false)
if (result?.skill) {
setEditingSkill(result.skill)
setWizardVisible(true)
} else {
message.error(`Skill ID "${editSkillId}" 不存在`)
}
} catch (error) {
console.error('Failed to fetch skill for edit:', error)
message.error(`无法加载 Skill "${editSkillId}": ${(error as Error).message}`)
} finally {
// 清除URL参数
searchParams.delete('edit')
navigate(`/skills?${searchParams.toString()}`, { replace: true })
}
}
fetchSkillForEdit()
return
}
}
// 处理创建参数 - 使用新的 SkillCreate 向导组件
const shouldCreate = searchParams.get('create')
if (shouldCreate === 'true') {
// 确保重置 editingSkill避免误用之前的编辑状态
setEditingSkill(null)
setWizardVisible(true)
// 清除URL参数
searchParams.delete('create')
navigate(`/skills?${searchParams.toString()}`, { replace: true })
}
}, [skills, searchParams, navigate, location])
// 获取技能生成任务
const fetchSkillGenerationTasks = async () => {
try {
const tasks = await taskService.listTasks({
type: 'generate_skill',
status: 'running'
})
setSkillGenerationTasks(tasks)
// 如果有任务完成,刷新技能列表
const completedTasks = tasks.filter(task => task.status === 'completed')
if (completedTasks.length > 0) {
fetchSkills()
}
} catch (error) {
console.error('Failed to fetch skill generation tasks:', error)
}
}
// Get unique categories
const categories = Array.from(new Set(skills.map(s => s.category)))
// Filter skills
const filteredSkills = skills.filter(skill => {
const matchSearch =
skill.name.toLowerCase().includes(searchText.toLowerCase()) ||
skill.category.toLowerCase().includes(searchText.toLowerCase()) ||
skill.id.toLowerCase().includes(searchText.toLowerCase())
const matchTab =
activeTab === 'all' ||
(activeTab === 'builtin' && skill.type === 'builtin') ||
(activeTab === 'user' && skill.type === 'user')
const matchCategory = selectedCategories.length === 0 || selectedCategories.includes(skill.category)
return matchSearch && matchTab && matchCategory
})
// Handlers
const handleView = (skill: Skill) => {
setSelectedSkill(skill)
setDetailDrawerOpen(true)
}
const handleTest = (skill: Skill) => {
setSelectedSkill(skill)
setTestInput('')
setTestResult('')
setTestModalOpen(true)
}
const handleRunTest = async () => {
if (!selectedSkill) return
setTesting(true)
setTestResult('')
try {
const result = await testSkill(selectedSkill.id, testInput || '请创作一段示例内容')
setTestResult(result)
message.success('测试成功!')
} catch (error) {
message.error(`测试失败: ${(error as Error).message}`)
} finally {
setTesting(false)
}
}
const handleEdit = (skill: Skill) => {
if (skill.type === 'builtin') {
message.warning('内置 Skill 不允许编辑')
return
}
// 使用新的 SkillCreate 组件进行编辑
setEditingSkill(skill)
setWizardVisible(true)
}
const handleCopy = (skill: Skill) => {
form.setFieldsValue({
id: `${skill.id}-copy-${Date.now()}`,
name: `${skill.name} (副本)`,
category: skill.category,
behavior_guide: skill.behavior_guide,
tags: skill.tags || []
})
setEditingSkill(null)
setCreateModalOpen(true)
message.info('已复制 Skill 内容,请修改后保存')
}
const handleDelete = async (skill: Skill) => {
if (skill.type === 'builtin') {
message.warning('内置 Skill 不允许删除')
return
}
try {
await skillService.deleteSkill(skill.id)
message.success('删除成功')
fetchSkills()
} catch (error) {
message.error(`删除失败: ${(error as Error).message}`)
}
}
const handleCreate = () => {
// 打开新的向导
setWizardVisible(true)
}
// 从模板初始化 Skill使用 skill-creator 的 init_skill.py 脚本)
const handleInitFromTemplate = async () => {
if (!templateSkillName.trim()) {
message.warning('请输入 Skill 名称')
return
}
// 验证格式hyphen-case
if (!/^[a-z0-9-]+$/.test(templateSkillName)) {
message.warning('Skill 名称只能包含小写字母、数字和连字符 (hyphen-case)')
return
}
setInitializing(true)
setInitResult(null)
try {
const result = await skillService.initSkillTemplate(
templateSkillName,
templateOutputPath || undefined
)
if (result.success) {
setInitResult(result.output || 'Skill 模板初始化成功!')
message.success('Skill 模板初始化成功!')
// 自动填充表单
form.setFieldsValue({
id: templateSkillName,
name: templateSkillName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
category: 'general',
behavior_guide: `# ${form.getFieldValue('name') || templateSkillName}\n\n## Overview\n\n[TODO: Describe what this skill does]\n\n## Behavior Guide\n\n[TODO: Add behavior guide content]`,
tags: []
})
}
} catch (error) {
message.error(`初始化失败: ${(error as Error).message}`)
} finally {
setInitializing(false)
}
}
// AI 生成 Skill
const handleGenerate = async () => {
if (!aiIdea.trim()) {
message.warning('请输入您的想法或需求')
return
}
setGenerating(true)
try {
const draft = await skillService.generateSkill(aiIdea, aiCategory)
setGeneratedDraft(draft)
// 将生成的数据填充到表单
form.setFieldsValue({
id: draft.suggested_id,
name: draft.suggested_name,
category: draft.suggested_category,
behavior_guide: draft.behavior_guide,
tags: draft.suggested_tags
})
message.success('AI 生成成功!您可以继续编辑后保存')
} catch (error) {
message.error(`AI 生成失败: ${(error as Error).message}`)
} finally {
setGenerating(false)
}
}
// AI 优化 Skill
const handleOptimize = async () => {
if (!generatedDraft && !editingSkill) {
message.warning('请先生成或加载 Skill 内容')
return
}
if (!optimizeRequirements.trim()) {
message.warning('请输入优化需求')
return
}
const currentContent = form.getFieldValue('behavior_guide')
if (!currentContent) {
message.warning('没有可优化的内容')
return
}
setOptimizing(true)
try {
const skillId = form.getFieldValue('id') || 'temp'
const result = await skillService.optimizeSkill(skillId, currentContent, optimizeRequirements)
// 更新行为指导
form.setFieldsValue({
behavior_guide: result.behavior_guide
})
// 更新 generatedDraft
if (generatedDraft) {
setGeneratedDraft({
...generatedDraft,
behavior_guide: result.behavior_guide
})
}
message.success('优化成功!')
// 显示改进说明
if (result.improvements && result.improvements.length > 0) {
Modal.info({
title: '优化改进',
content: (
<ul>
{result.improvements.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
),
width: 600
})
}
} catch (error) {
message.error(`优化失败: ${(error as Error).message}`)
} finally {
setOptimizing(false)
}
}
// 重新生成
const handleRegenerate = () => {
setGeneratedDraft(null)
handleGenerate()
}
const handleCreateSubmit = async () => {
try {
const values = await form.validateFields()
setSubmitting(true)
await skillService.createSkill({
id: values.id,
name: values.name,
content: values.behavior_guide,
category: values.category
})
message.success('创建成功')
setCreateModalOpen(false)
form.resetFields()
fetchSkills()
} catch (error) {
message.error(`创建失败: ${(error as Error).message}`)
} finally {
setSubmitting(false)
}
}
const handleEditSubmit = async () => {
if (!editingSkill) return
try {
const values = await form.validateFields()
setSubmitting(true)
await skillService.updateSkill(editingSkill.id, {
name: values.name,
content: values.behavior_guide
})
message.success('更新成功')
setEditModalOpen(false)
setEditingSkill(null)
form.resetFields()
fetchSkills()
} catch (error) {
message.error(`更新失败: ${(error as Error).message}`)
} finally {
setSubmitting(false)
}
}
const columns: ColumnsType<Skill> = [
{
title: 'Skill ID',
dataIndex: 'id',
key: 'id',
width: 200,
render: (id: string, record: Skill) => (
<Space>
<span style={{ fontFamily: 'monospace' }}>{id}</span>
{record.type === 'builtin' && (
<Tooltip title="内置 Skill">
<LockOutlined style={{ color: '#1890ff' }} />
</Tooltip>
)}
</Space>
)
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (name: string, record: Skill) => (
<Space>
<span>{name}</span>
{record.type === 'builtin' && <Badge count="系统" style={{ backgroundColor: '#1890ff' }} />}
</Space>
)
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
filters: [
{ text: '内置', value: 'builtin' },
{ text: '用户', value: 'user' }
],
onFilter: (value, record) => record.type === value,
render: (type: string) => (
<Tag color={type === 'builtin' ? 'blue' : 'green'} icon={type === 'builtin' ? <LockOutlined /> : undefined}>
{type === 'builtin' ? '内置' : '用户'}
</Tag>
)
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
filters: categories.map(cat => ({ text: cat, value: cat })),
onFilter: (value, record) => record.category === value,
render: (category: string) => <Tag>{category}</Tag>
},
{
title: '版本',
dataIndex: 'version',
key: 'version',
width: 100,
},
{
title: '标签',
dataIndex: 'tags',
key: 'tags',
width: 150,
render: (tags: string[]) => (
<Space size={[4, 4]} wrap>
{tags?.slice(0, 3).map(tag => (
<Tag key={tag} size="small">{tag}</Tag>
))}
{tags?.length > 3 && <Tag size="small">+{tags.length - 3}</Tag>}
</Space>
)
},
{
title: '操作',
key: 'actions',
width: 280,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Tooltip title="查看详情">
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleView(record)}>
</Button>
</Tooltip>
<Tooltip title="测试 Skill">
<Button type="link" size="small" icon={<PlayCircleOutlined />} onClick={() => handleTest(record)}>
</Button>
</Tooltip>
{record.type === 'user' && (
<>
<Tooltip title="编辑">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
</Button>
</Tooltip>
<Tooltip title="复制">
<Button type="link" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(record)}>
</Button>
</Tooltip>
<Popconfirm
title="确认删除"
description="确定要删除这个 Skill 吗?"
onConfirm={() => handleDelete(record)}
okText="确认"
cancelText="取消"
>
<Tooltip title="删除">
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Tooltip>
</Popconfirm>
</>
)}
{record.type === 'builtin' && (
<>
<Tooltip title="复制内置 Skill">
<Button type="link" size="small" icon={<CopyOutlined />} onClick={() => handleCopy(record)}>
</Button>
</Tooltip>
<Tooltip title="内置 Skill 不可编辑">
<Button type="link" size="small" icon={<LockOutlined />} disabled>
</Button>
</Tooltip>
</>
)}
</Space>
)
}
]
return (
<div style={{ padding: '24px' }}>
<Card
title={
<Space>
<AppstoreOutlined />
<span>Skills </span>
</Space>
}
extra={
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Skill
</Button>
</Space>
}
>
{/* 技能生成任务状态 */}
{skillGenerationTasks.length > 0 && (
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
action={
<Button size="small" onClick={fetchSkillGenerationTasks}>
</Button>
}
>
<Space>
<ClockCircleOutlined />
<span> {skillGenerationTasks.length} Skill </span>
</Space>
</Alert>
)}
{/* 搜索和筛选 */}
<div style={{ marginBottom: '16px' }}>
<Space size="large" wrap>
<Search
placeholder="搜索 Skills (名称/ID/分类)..."
allowClear
style={{ width: 350 }}
onChange={(e) => setSearchText(e.target.value)}
prefix={<SearchOutlined />}
/>
<Space>
<span>:</span>
<Select
mode="multiple"
placeholder="选择分类"
style={{ width: 250 }}
value={selectedCategories}
onChange={setSelectedCategories}
allowClear
>
{categories.map(cat => (
<Option key={cat} value={cat}>{cat}</Option>
))}
</Select>
</Space>
<Space>
<span>:</span>
<Switch
checked={viewMode === 'grid'}
onChange={(checked) => setViewMode(checked ? 'grid' : 'list')}
checkedChildren="网格"
unCheckedChildren="列表"
/>
</Space>
<Divider type="vertical" />
<Tag color="blue">: {builtinSkills.length}</Tag>
<Tag color="green">: {userSkills.length}</Tag>
<Tag>: {skills.length}</Tag>
</Space>
</div>
{/* Skills 列表 */}
<Table
columns={columns}
dataSource={filteredSkills}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total} 个 Skills`
}}
scroll={{ x: 1200 }}
/>
</Card>
{/* Skill 详情抽屉 - 使用 Drawer 替代 Modal 避免重叠 */}
<Drawer
title={
<Space>
<EyeOutlined />
<span>{selectedSkill?.name}</span>
{selectedSkill?.type === 'builtin' && <Badge count="系统" style={{ backgroundColor: '#1890ff' }} />}
</Space>
}
placement="right"
width={720}
open={detailDrawerOpen}
onClose={() => setDetailDrawerOpen(false)}
extra={
<Space>
<Button
icon={<PlayCircleOutlined />}
onClick={() => {
setDetailDrawerOpen(false)
handleTest(selectedSkill!)
}}
>
</Button>
{selectedSkill?.type === 'user' && (
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => {
setDetailDrawerOpen(false)
handleEdit(selectedSkill!)
}}
>
</Button>
)}
</Space>
}
>
{selectedSkill && (
<Tabs defaultActiveKey="info">
<TabPane tab="基础信息" key="info">
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Card size="small" title="基本信息">
<Space direction="vertical" style={{ width: '100%' }} size="small">
<div>
<strong>Skill ID:</strong>
<p style={{ fontFamily: 'monospace', color: '#666', marginTop: '4px' }}>{selectedSkill.id}</p>
</div>
<Divider style={{ margin: '8px 0' }} />
<div>
<strong>:</strong>
<p style={{ marginTop: '4px' }}>{selectedSkill.name}</p>
</div>
<div>
<strong>:</strong>
<Tag color="blue" style={{ marginLeft: '8px' }}>{selectedSkill.version}</Tag>
</div>
<div>
<strong>:</strong>
<Tag
color={selectedSkill.type === 'builtin' ? 'blue' : 'green'}
icon={selectedSkill.type === 'builtin' ? <LockOutlined /> : undefined}
style={{ marginLeft: '8px' }}
>
{selectedSkill.type === 'builtin' ? '内置 (只读)' : '用户 (可编辑)'}
</Tag>
</div>
<div>
<strong>:</strong>
<Tag style={{ marginLeft: '8px' }}>{selectedSkill.category}</Tag>
</div>
{selectedSkill.created_at && (
<div>
<strong>:</strong>
<p style={{ color: '#666', marginTop: '4px' }}>
{dayjs(selectedSkill.created_at).format('YYYY-MM-DD HH:mm:ss')}
</p>
</div>
)}
</Space>
</Card>
<Card size="small" title="标签">
<Space size={[8, 8]} wrap>
{selectedSkill.tags && selectedSkill.tags.length > 0 ? (
selectedSkill.tags.map(tag => <Tag key={tag}>{tag}</Tag>)
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无标签" />
)}
</Space>
</Card>
</Space>
</TabPane>
<TabPane tab="行为指导 (核心)" key="behavior">
<Card
size="small"
title="核心行为指导"
extra={
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => {
navigator.clipboard.writeText(selectedSkill.behavior_guide)
message.success('已复制到剪贴板')
}}
>
</Button>
}
>
<pre style={{
background: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
maxHeight: '70vh',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: '1.6'
}}>
{selectedSkill.behavior_guide}
</pre>
</Card>
</TabPane>
<TabPane tab="配置" key="config">
<Card size="small" title="配置参数">
<pre style={{
background: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
maxHeight: '70vh',
overflowY: 'auto'
}}>
{JSON.stringify(selectedSkill, null, 2)}
</pre>
</Card>
</TabPane>
</Tabs>
)}
</Drawer>
{/* Skill 测试弹窗 */}
<Modal
title={
<Space>
<PlayCircleOutlined />
<span> Skill: {selectedSkill?.name}</span>
</Space>
}
open={testModalOpen}
onCancel={() => {
setTestModalOpen(false)
setTestInput('')
setTestResult('')
}}
width={800}
footer={null}
destroyOnHidden
>
{selectedSkill && (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div>
<div style={{ marginBottom: '8px' }}>
<strong>:</strong>
<span style={{ color: '#999', marginLeft: '8px', fontSize: '12px' }}>
Skill
</span>
</div>
<TextArea
rows={4}
placeholder="输入测试内容... 例如:请写一段关于春天的诗歌"
value={testInput}
onChange={(e) => setTestInput(e.target.value)}
/>
</div>
<Button
type="primary"
size="large"
loading={testing}
onClick={handleRunTest}
disabled={!testInput.trim()}
block
icon={<PlayCircleOutlined />}
>
{testing ? '测试中...' : '开始测试'}
</Button>
{testResult && (
<Card
title={
<Space>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<span></span>
</Space>
}
size="small"
extra={
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => {
navigator.clipboard.writeText(testResult)
message.success('已复制到剪贴板')
}}
>
</Button>
}
>
<pre style={{
whiteSpace: 'pre-wrap',
background: '#f5f5f5',
padding: '12px',
borderRadius: '4px',
maxHeight: '60vh',
overflowY: 'auto',
fontSize: '13px',
lineHeight: '1.6'
}}>
{testResult}
</pre>
</Card>
)}
{!testing && !testResult && (
<Card size="small" style={{ background: '#fafafa' }}>
<Space direction="vertical" size="small">
<p style={{ margin: 0, color: '#666' }}>
<strong>:</strong> "开始测试"
</p>
<p style={{ margin: 0, color: '#999', fontSize: '12px' }}>
使 Skill
</p>
</Space>
</Card>
)}
</Space>
)}
</Modal>
{/* 创建 Skill 弹窗 - AI 辅助版本 */}
<Modal
title={
<Space>
<PlusOutlined />
<span> Skill</span>
</Space>
}
open={createModalOpen}
onCancel={() => {
setCreateModalOpen(false)
form.resetFields()
setGeneratedDraft(null)
setAiIdea('')
setAiCategory(undefined)
}}
width={900}
footer={null}
destroyOnHidden
>
<Tabs
activeKey={createMode}
onChange={(key) => setCreateMode(key as CreateMode)}
type="card"
>
{/* AI 辅助创建 */}
<TabPane
tab={
<Space>
<RobotOutlined />
<span>AI </span>
</Space>
}
key="ai"
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* AI 输入区域 */}
<Card size="small" title="1. 描述您的想法">
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<div style={{ marginBottom: '8px' }}>
<strong>:</strong>
<span style={{ color: '#999', marginLeft: '8px', fontSize: '12px' }}>
Skill
</span>
</div>
<TextArea
rows={3}
placeholder="例如: 我想要一个能写现代都市对话的 Skill要求对话自然、口语化适合年轻人的交流方式..."
value={aiIdea}
onChange={(e) => setAiIdea(e.target.value)}
/>
</div>
<div>
<strong>:</strong>
<Select
placeholder="选择分类(可选)"
style={{ width: '100%', marginTop: '8px' }}
value={aiCategory}
onChange={setAiCategory}
allowClear
>
<Option value="writing"></Option>
<Option value="dialogue"></Option>
<Option value="analysis"></Option>
<Option value="translation"></Option>
<Option value="general"></Option>
</Select>
</div>
<Space>
<Button
type="primary"
size="large"
icon={<RobotOutlined />}
loading={generating}
onClick={handleGenerate}
disabled={!aiIdea.trim()}
>
{generating ? 'AI 生成中...' : 'AI 生成 Skill'}
</Button>
{generatedDraft && (
<Button
icon={<ReloadOutlined />}
onClick={handleRegenerate}
disabled={generating}
>
</Button>
)}
</Space>
</Space>
</Card>
{/* 生成的结果 */}
{generatedDraft && (
<>
<Alert
message="AI 生成成功"
description="您可以继续编辑下面的内容,或使用优化功能进一步改进"
type="success"
showIcon
closable
/>
<Card
size="small"
title="2. 编辑和优化"
extra={
<Space>
<Button
size="small"
icon={<EditFilled />}
onClick={() => {
Modal.info({
title: '优化 Skill',
content: (
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<strong>:</strong>
<TextArea
rows={3}
placeholder="描述您希望如何改进这个 Skill..."
value={optimizeRequirements}
onChange={(e) => setOptimizeRequirements(e.target.value)}
/>
</div>
<Button
type="primary"
loading={optimizing}
onClick={() => {
handleOptimize()
}}
block
>
</Button>
</Space>
),
width: 600,
okText: '取消'
})
}}
>
</Button>
</Space>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
tags: [],
category: 'general'
}}
>
<Form.Item
label="Skill ID"
name="id"
rules={[
{ required: true, message: '请输入 Skill ID' },
{ pattern: /^[a-z0-9-]+$/, message: '只能包含小写字母、数字和连字符' }
]}
>
<InputComponent placeholder="例如: my-custom-skill" prefix={<AppstoreOutlined />} />
</Form.Item>
<Form.Item
label="Skill 名称"
name="name"
rules={[{ required: true, message: '请输入 Skill 名称' }]}
>
<InputComponent placeholder="例如: 自定义创作助手" />
</Form.Item>
<Form.Item
label="分类"
name="category"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder="选择分类">
<Option value="writing"></Option>
<Option value="dialogue"></Option>
<Option value="analysis"></Option>
<Option value="translation"></Option>
<Option value="general"></Option>
</Select>
</Form.Item>
<Form.Item
label="行为指导 (核心)"
name="behavior_guide"
rules={[{ required: true, message: '请输入行为指导' }]}
extra="这是 AI 执行任务时遵循的核心指导原则"
>
<TextArea
rows={10}
placeholder="AI 生成的行为指导将显示在这里..."
style={{ fontFamily: 'monospace', fontSize: '13px' }}
/>
</Form.Item>
<Form.Item
label="标签"
name="tags"
>
<Select mode="tags" placeholder="输入标签后按回车">
</Select>
</Form.Item>
</Form>
</Card>
</>
)}
{/* 保存按钮 */}
{generatedDraft && (
<Card size="small">
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setCreateModalOpen(false)}>
</Button>
<Button
type="primary"
loading={submitting}
onClick={handleCreateSubmit}
size="large"
>
Skill
</Button>
</Space>
</Card>
)}
</Space>
</TabPane>
{/* 手动创建 */}
<TabPane
tab={
<Space>
<EditOutlined />
<span></span>
</Space>
}
key="manual"
>
<Form
form={form}
layout="vertical"
initialValues={{
tags: [],
category: 'general'
}}
>
<Form.Item
label="Skill ID"
name="id"
rules={[
{ required: true, message: '请输入 Skill ID' },
{ pattern: /^[a-z0-9-]+$/, message: '只能包含小写字母、数字和连字符' }
]}
>
<InputComponent placeholder="例如: my-custom-skill" prefix={<AppstoreOutlined />} />
</Form.Item>
<Form.Item
label="Skill 名称"
name="name"
rules={[{ required: true, message: '请输入 Skill 名称' }]}
>
<InputComponent placeholder="例如: 自定义创作助手" />
</Form.Item>
<Form.Item
label="分类"
name="category"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder="选择分类">
<Option value="writing"></Option>
<Option value="dialogue"></Option>
<Option value="analysis"></Option>
<Option value="translation"></Option>
<Option value="general"></Option>
</Select>
</Form.Item>
<Form.Item
label="行为指导 (核心)"
name="behavior_guide"
rules={[{ required: true, message: '请输入行为指导' }]}
extra="这是 AI 执行任务时遵循的核心指导原则"
>
<TextArea
rows={8}
placeholder="输入详细的行为指导... 例如:你是一个专业的编剧助手,擅长创作古风对话..."
/>
</Form.Item>
<Form.Item
label="标签"
name="tags"
>
<Select mode="tags" placeholder="输入标签后按回车">
</Select>
</Form.Item>
</Form>
<Card size="small" style={{ marginTop: '16px' }}>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setCreateModalOpen(false)}>
</Button>
<Button
type="primary"
loading={submitting}
onClick={handleCreateSubmit}
size="large"
>
Skill
</Button>
</Space>
</Card>
</TabPane>
{/* 从模板创建 - 快速创建 Skill */}
<TabPane
tab={
<Space>
<AppstoreOutlined />
<span></span>
</Space>
}
key="template"
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card
size="small"
title="创建新 Skill"
>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<div style={{ marginBottom: '8px' }}>
<strong>Skill ID:</strong>
<span style={{ color: '#999', marginLeft: '8px', fontSize: '12px' }}>
: my-awesome-skill
</span>
</div>
<InputComponent
placeholder="例如: my-awesome-skill"
value={templateSkillName}
onChange={(e) => setTemplateSkillName(e.target.value)}
prefix={<AppstoreOutlined />}
/>
</div>
<Button
type="primary"
size="large"
icon={<AppstoreOutlined />}
loading={initializing}
onClick={handleInitFromTemplate}
disabled={!templateSkillName.trim()}
block
>
{initializing ? '创建中...' : '创建 Skill'}
</Button>
{initResult && (
<Alert
message="Skill 创建成功!"
description="请在下方完善 Skill 的内容"
type="success"
showIcon
closable
/>
)}
</Space>
</Card>
{initResult && (
<>
<Alert
message="请完善 Skill 内容"
description="模板已创建,现在请完善下面的 Skill 信息"
type="info"
showIcon
/>
<Card size="small" title="完善 Skill 信息">
<Form
form={form}
layout="vertical"
initialValues={{
tags: [],
category: 'general'
}}
>
<Form.Item
label="Skill ID"
name="id"
rules={[
{ required: true, message: '请输入 Skill ID' },
{ pattern: /^[a-z0-9-]+$/, message: '只能包含小写字母、数字和连字符' }
]}
>
<InputComponent disabled prefix={<AppstoreOutlined />} />
</Form.Item>
<Form.Item
label="Skill 名称"
name="name"
rules={[{ required: true, message: '请输入 Skill 名称' }]}
>
<InputComponent placeholder="例如: 自定义创作助手" />
</Form.Item>
<Form.Item
label="分类"
name="category"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder="选择分类">
<Option value="writing"></Option>
<Option value="dialogue"></Option>
<Option value="analysis"></Option>
<Option value="translation"></Option>
<Option value="general"></Option>
</Select>
</Form.Item>
<Form.Item
label="行为指导 (核心)"
name="behavior_guide"
rules={[{ required: true, message: '请输入行为指导' }]}
extra="这是 AI 执行任务时遵循的核心指导原则"
>
<TextArea
rows={10}
placeholder="输入详细的行为指导..."
style={{ fontFamily: 'monospace', fontSize: '13px' }}
/>
</Form.Item>
<Form.Item
label="标签"
name="tags"
>
<Select mode="tags" placeholder="输入标签后按回车">
</Select>
</Form.Item>
</Form>
</Card>
<Card size="small">
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setCreateModalOpen(false)}>
</Button>
<Button
type="primary"
loading={submitting}
onClick={handleCreateSubmit}
size="large"
>
Skill
</Button>
</Space>
</Card>
</>
)}
</Space>
</TabPane>
</Tabs>
</Modal>
{/* 编辑 Skill 弹窗 */}
<Modal
title={
<Space>
<EditOutlined />
<span> Skill: {editingSkill?.name}</span>
</Space>
}
open={editModalOpen}
onCancel={() => {
setEditModalOpen(false)
setEditingSkill(null)
form.resetFields()
}}
width={700}
footer={[
<Button key="cancel" onClick={() => setEditModalOpen(false)}>
</Button>,
<Button
key="submit"
type="primary"
loading={submitting}
onClick={handleEditSubmit}
>
</Button>
]}
destroyOnHidden
>
<Form
form={form}
layout="vertical"
>
<Form.Item
label="Skill ID"
name="id"
>
<InputComponent disabled prefix={<AppstoreOutlined />} />
</Form.Item>
<Form.Item
label="Skill 名称"
name="name"
rules={[{ required: true, message: '请输入 Skill 名称' }]}
>
<InputComponent placeholder="例如: 自定义创作助手" />
</Form.Item>
<Form.Item
label="分类"
name="category"
>
<Select disabled>
<Option value="writing"></Option>
<Option value="dialogue"></Option>
<Option value="analysis"></Option>
<Option value="translation"></Option>
<Option value="general"></Option>
</Select>
</Form.Item>
<Form.Item
label="行为指导 (核心)"
name="behavior_guide"
rules={[{ required: true, message: '请输入行为指导' }]}
>
<TextArea
rows={8}
placeholder="输入详细的行为指导..."
/>
</Form.Item>
<Form.Item
label="标签"
name="tags"
>
<Select mode="tags" placeholder="输入标签后按回车">
</Select>
</Form.Item>
</Form>
</Modal>
{/* Skill 创建 - AI 自动生成 */}
<SkillCreate
visible={wizardVisible}
onClose={() => {
setWizardVisible(false)
setEditingSkill(null)
// 如果有跳转回退信息,关闭时跳转回去
if (redirectState) {
navigate(redirectState.path, {
state: { activeTab: redirectState.activeTab, reviewSubTab: redirectState.reviewSubTab }
})
setRedirectState(null)
}
}}
onSuccess={() => {
fetchSkills()
// 不自动关闭,让用户查看创建结果后主动关闭
// setWizardVisible(false)
// setEditingSkill(null)
}}
editingSkillId={editingSkill?.id}
/>
</div>
)
}