2026-02-03 01:12:39 +08:00

1104 lines
37 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.

/**
* Skill 创建组件 - 统一流程
*
* **正确的创建流程**(遵循 skill-creator 标准):
* 1. 用户输入需求
* 2. AI 使用 skill-creator 标准生成基础 SKILL.md
* 3. 用户预览完整内容(可手动编辑)
* 4. 可选择上传 references文档/GitHub/PDF
* 5. 保存为完整 Skill包含 SKILL.md + references/
*
* **关键修复**
* - AI 生成和 References 上传是同一流程的不同步骤,而非分离的模式
* - 预览内容完整显示,无高度限制
* - 支持在编辑时随意增删 references
*/
import { useState, useRef, useEffect } from 'react'
import {
Modal,
Form,
Input,
Button,
Space,
Card,
Alert,
message,
Typography,
Tag,
Upload,
Divider,
Steps,
Tabs,
Row,
Col,
List,
Popconfirm
} from 'antd'
import {
RobotOutlined,
FileTextOutlined,
UploadOutlined,
LinkOutlined,
GithubOutlined,
PlusOutlined,
DeleteOutlined,
EditOutlined,
SaveOutlined,
ArrowLeftOutlined,
EyeOutlined,
InboxOutlined,
CopyOutlined
} from '@ant-design/icons'
import { skillService } from '@/services'
import { taskService } from '@/services/taskService'
import TaskProgressTracker from '@/components/TaskProgressTracker'
const { TextArea } = Input
const { Text } = Typography
const { Step } = Steps
const { TabPane } = Tabs
interface SkillCreateProps {
visible: boolean
onClose: () => void
onSuccess: () => void
editingSkillId?: string // 如果传入,表示编辑模式
}
interface ReferenceFile {
id: string
type: 'url' | 'github' | 'upload' | 'manual'
filename: string
url?: string
content?: string
size?: number
uploadedAt: Date
}
interface GeneratedSkill {
suggested_id: string
suggested_name: string
skill_content: string
category: string
suggested_tags: string[]
explanation: string
}
type CreateStep = 'input' | 'generating' | 'preview' | 'references' | 'saving'
const SkillCreateComponent: React.FC<SkillCreateProps> = ({
visible,
onClose,
onSuccess,
editingSkillId
}) => {
const [form] = Form.useForm()
// 使用 ref 跟踪弹窗实际可见性(避免闭包问题)
const dialogVisibleRef = useRef(visible)
// 当 visible prop 变化时更新 ref
useEffect(() => {
dialogVisibleRef.current = visible
}, [visible])
// 步骤状态
const [step, setStep] = useState<CreateStep>('input')
const [aiDescription, setAiDescription] = useState('')
const [generating, setGenerating] = useState(false)
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null)
// 生成的Skill内容
const [generatedSkill, setGeneratedSkill] = useState<GeneratedSkill | null>(null)
const [editingContent, setEditingContent] = useState(false)
const [saving, setSaving] = useState(false)
// References管理
const [references, setReferences] = useState<ReferenceFile[]>([])
const [activeReferenceTab, setActiveReferenceTab] = useState<'url' | 'github' | 'upload' | 'manual'>('url')
const [urlInput, setUrlInput] = useState('')
const [githubInput, setGithubInput] = useState('')
const [manualFileName, setManualFileName] = useState('')
const [manualContent, setManualContent] = useState('')
// 预览ID用于保存
const [previewId, setPreviewId] = useState<string | null>(null)
const contentPreviewRef = useRef<HTMLDivElement>(null)
// LocalStorage key for saving task state
const SKILL_TASK_STORAGE_KEY = 'skill_generation_task'
// 重置表单
const resetForm = () => {
setStep('input')
setAiDescription('')
setGeneratedSkill(null)
setEditingContent(false)
setReferences([])
setUrlInput('')
setGithubInput('')
setManualFileName('')
setManualContent('')
setPreviewId(null)
// 不清除 currentTaskId让用户可以继续跟踪任务
form.resetFields()
}
// 加载编辑中的Skill
useEffect(() => {
if (editingSkillId && visible) {
loadSkillForEdit(editingSkillId)
} else if (visible) {
// 检查是否有正在进行的任务
const savedTask = localStorage.getItem(SKILL_TASK_STORAGE_KEY)
if (savedTask) {
try {
const taskData = JSON.parse(savedTask)
// 检查任务是否是最近的10分钟内
const taskAge = Date.now() - taskData.timestamp
if (taskAge < 10 * 60 * 1000 && taskData.taskId && taskData.description) {
setCurrentTaskId(taskData.taskId)
setAiDescription(taskData.description)
setStep('generating')
setGenerating(true)
// 继续轮询任务
const pollSavedTask = async () => {
try {
const result = await taskService.pollTask(taskData.taskId, (progress) => {
console.log('Task progress:', progress)
})
if (result.success && result.result) {
const skillData = result.result
setGeneratedSkill({
suggested_id: skillData.suggested_id,
suggested_name: skillData.suggested_name,
skill_content: skillData.skill_content,
category: skillData.category,
suggested_tags: skillData.suggested_tags,
explanation: skillData.explanation
})
form.setFieldsValue({ content: skillData.skill_content })
setStep('preview')
setGenerating(false)
setCurrentTaskId(null)
localStorage.removeItem(SKILL_TASK_STORAGE_KEY)
message.success('AI 生成成功!您可以预览和编辑内容')
}
} catch (error) {
message.error(`AI 生成失败: ${(error as Error).message}`)
setStep('input')
setGenerating(false)
setCurrentTaskId(null)
localStorage.removeItem(SKILL_TASK_STORAGE_KEY)
}
}
pollSavedTask()
message.info('检测到正在进行的生成任务,正在继续...')
return
}
} catch (error) {
console.error('Failed to parse saved task:', error)
localStorage.removeItem(SKILL_TASK_STORAGE_KEY)
}
}
resetForm()
setStep('input')
}
}, [visible, editingSkillId])
const loadSkillForEdit = async (skillId: string) => {
try {
// 检查是否是虚拟的 task ID正在生成中的 skill
// 这种 ID 以 "task-" 开头,不是真实的 skill不应该加载
if (skillId.startsWith('task-')) {
console.log('[SkillCreate] Skipping load for virtual task ID:', skillId)
message.info('该 Skill 正在生成中,请等待生成完成后再编辑')
handleClose()
return
}
const result = await skillService.getSkillWithReferences(skillId, true)
if (result) {
const { skill, references: refs } = result
// 设置生成的内容
setGeneratedSkill({
suggested_id: skill.id,
suggested_name: skill.name,
skill_content: skill.behavior_guide,
category: skill.category,
suggested_tags: skill.tags || [],
explanation: `正在编辑 Skill: ${skill.name}`
})
form.setFieldsValue({
content: skill.behavior_guide
})
// 加载references
if (refs) {
const refFiles: ReferenceFile[] = Object.entries(refs).map(([filename, content], idx) => ({
id: `ref-${idx}`,
type: 'manual',
filename,
content: content as string,
uploadedAt: new Date()
}))
setReferences(refFiles)
}
setStep('preview')
}
} catch (error) {
message.error(`加载Skill失败: ${(error as Error).message}`)
// 关闭弹窗,让父组件处理重定向
handleClose()
}
}
const handleClose = () => {
// 如果有后台任务正在运行,可以关闭弹窗让任务继续在后台运行
if (currentTaskId && generating) {
message.info('任务将在后台继续运行,您可以稍后再来查看结果')
}
resetForm()
onClose()
}
// ========== 步骤1: 输入需求 ==========
const handleGenerate = async () => {
if (!aiDescription.trim()) {
message.warning('请描述你想要创建的 Skill')
return
}
setGenerating(true)
setStep('generating')
try {
// 创建异步任务
const response = await taskService.generateSkill({
description: aiDescription,
temperature: 0.7
})
const taskId = response.taskId
setCurrentTaskId(taskId)
// 保存任务信息到 localStorage以便关闭弹窗后恢复
localStorage.setItem(SKILL_TASK_STORAGE_KEY, JSON.stringify({
taskId,
description: aiDescription,
timestamp: Date.now()
}))
message.success('任务已创建,正在后台生成中...您可以关闭此弹窗,生成完成后会自动保存')
// 启动后台轮询,但不阻塞 UI
const pollTaskInBackground = async () => {
try {
const result = await taskService.pollTask(taskId, (progress) => {
console.log('Task progress:', progress)
})
// 任务完成后,如果弹窗还开着,显示结果
if (result.success && result.result) {
const skillData = result.result
console.log('Task completed, skill data:', skillData)
// 清除 localStorage 中的任务信息
localStorage.removeItem(SKILL_TASK_STORAGE_KEY)
// 检查弹窗是否还打开(使用 ref 避免闭包问题)
if (dialogVisibleRef.current) {
// 弹窗打开:显示结果供用户查看
// 注意:后端已经自动保存了,这里只是显示结果
setGeneratedSkill({
suggested_id: skillData.suggested_id,
suggested_name: skillData.suggested_name,
skill_content: skillData.skill_content,
category: skillData.category,
suggested_tags: skillData.suggested_tags,
explanation: skillData.explanation
})
form.setFieldsValue({
content: skillData.skill_content
})
setStep('preview')
const savedInfo = skillData.auto_saved
? `${skillData.suggested_name} 已自动保存!(ID: ${skillData.saved_skill_id})`
: `${skillData.suggested_name} 生成完成!`
message.success(savedInfo)
// 通知父组件刷新列表
onSuccess()
} else {
// 弹窗已关闭:后端已经自动保存,只需通知用户
console.log('Dialog closed, skill was auto-saved by backend')
if (skillData.auto_saved) {
message.success(`${skillData.suggested_name} 已自动保存到 skills 列表!`)
// 通知父组件刷新列表
onSuccess()
} else if (skillData.save_error) {
message.warning(`生成完成但自动保存失败: ${skillData.save_error}`)
}
}
} else {
throw new Error(result.error || '生成失败')
}
} catch (error) {
message.error(`AI 生成失败: ${(error as Error).message}`)
setStep('input')
// 清除 localStorage 中的任务信息
localStorage.removeItem(SKILL_TASK_STORAGE_KEY)
} finally {
setGenerating(false)
setCurrentTaskId(null)
}
}
// 启动后台轮询
pollTaskInBackground()
} catch (error) {
message.error(`任务创建失败: ${(error as Error).message}`)
setGenerating(false)
setStep('input')
}
}
// ========== 步骤2: 预览和编辑 ==========
const handleStartEditing = () => {
setEditingContent(true)
}
const handleCancelEdit = () => {
if (generatedSkill) {
form.setFieldsValue({ content: generatedSkill.skill_content })
}
setEditingContent(false)
}
const handleConfirmEdit = () => {
const newContent = form.getFieldValue('content')
if (newContent && generatedSkill) {
setGeneratedSkill({
...generatedSkill,
skill_content: newContent
})
message.success('内容已更新')
}
setEditingContent(false)
}
// 使用AI调整内容
const [refineModalVisible, setRefineModalVisible] = useState(false)
const [refinePrompt, setRefinePrompt] = useState('')
const handleRefineWithAI = async () => {
const currentContent = form.getFieldValue('content')
if (!currentContent) {
message.warning('没有可调整的内容')
return
}
setRefinePrompt('')
setRefineModalVisible(true)
}
const confirmRefineWithAI = async () => {
if (!refinePrompt.trim()) {
message.warning('请输入调整需求')
return
}
const currentContent = form.getFieldValue('content')
setRefineModalVisible(false)
setGenerating(true)
try {
const result = await skillService.refineSkill(currentContent, refinePrompt, 0.7)
if (result.success) {
form.setFieldsValue({ content: result.refined_content })
setGeneratedSkill(prev => prev ? { ...prev, skill_content: result.refined_content } : null)
Modal.info({
title: '调整完成',
content: (
<div>
<p><strong></strong>{result.changes_summary}</p>
<p>: {result.original_length} </p>
<p>: {result.new_length} </p>
</div>
)
})
}
} catch (error) {
message.error(`调整失败: ${(error as Error).message}`)
} finally {
setGenerating(false)
}
}
// 复制内容到剪贴板
const handleCopyContent = () => {
const content = form.getFieldValue('content')
if (content) {
navigator.clipboard.writeText(content)
message.success('已复制到剪贴板')
}
}
// ========== 步骤3: References管理 ==========
const handleAddUrlReference = async () => {
if (!urlInput.trim()) {
message.warning('请输入文档URL')
return
}
try {
const result = await skillService.fetchDoc(urlInput)
if (result.success) {
const ref: ReferenceFile = {
id: `ref-${Date.now()}`,
type: 'url',
filename: result.title || 'untitled',
url: urlInput,
content: result.content,
uploadedAt: new Date()
}
setReferences([...references, ref])
setUrlInput('')
message.success('文档获取成功')
} else {
message.error(`获取文档失败: ${result.error}`)
}
} catch (error) {
message.error(`获取文档失败: ${(error as Error).message}`)
}
}
const handleAddGithubReference = async () => {
if (!githubInput.trim()) {
message.warning('请输入GitHub仓库URL')
return
}
// 解析GitHub URL
const match = githubInput.match(/github\.com\/([^/]+)\/([^/]+)/)
if (!match) {
message.warning('GitHub URL格式不正确应为: https://github.com/owner/repo')
return
}
const repoUrl = `https://github.com/${match[1]}/${match[2]}`
const docsPath = 'README.md' // 默认读取README
try {
const result = await skillService.fetchGithubDoc(repoUrl, docsPath)
if (result.success) {
const ref: ReferenceFile = {
id: `ref-${Date.now()}`,
type: 'github',
filename: `${match[2]}-${docsPath}`,
url: githubInput,
content: result.content,
uploadedAt: new Date()
}
setReferences([...references, ref])
setGithubInput('')
message.success('GitHub文档获取成功')
} else {
message.error(`获取GitHub文档失败: ${result.error}`)
}
} catch (error) {
message.error(`获取GitHub文档失败: ${(error as Error).message}`)
}
}
const handleAddManualReference = () => {
if (!manualFileName.trim() || !manualContent.trim()) {
message.warning('请输入文件名和内容')
return
}
const ref: ReferenceFile = {
id: `ref-${Date.now()}`,
type: 'manual',
filename: manualFileName.endsWith('.md') ? manualFileName : `${manualFileName}.md`,
content: manualContent,
uploadedAt: new Date()
}
setReferences([...references, ref])
setManualFileName('')
setManualContent('')
message.success('参考文档添加成功')
}
const handleUploadFiles = (file: File) => {
// 文件上传处理TODO: 实际上传到服务器)
const reader = new FileReader()
reader.onload = (e) => {
const ref: ReferenceFile = {
id: `ref-${Date.now()}`,
type: 'upload',
filename: file.name,
content: e.target?.result as string,
size: file.size,
uploadedAt: new Date()
}
setReferences([...references, ref])
}
reader.readAsText(file)
return false // 阻止自动上传
}
const handleDeleteReference = (refId: string) => {
setReferences(references.filter(r => r.id !== refId))
message.success('参考文档已删除')
}
const handleViewReference = (ref: ReferenceFile) => {
Modal.info({
title: ref.filename,
width: 900,
content: (
<div style={{
maxHeight: '70vh',
overflowY: 'auto',
background: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: '1.6'
}}>
{ref.content}
</div>
)
})
}
// ========== 步骤4: 保存 ==========
const handleSaveSkill = async () => {
if (!generatedSkill) return
setSaving(true)
try {
const skillContent = form.getFieldValue('content')
// 构建references对象
const referencesObj: Record<string, string> = {}
references.forEach(ref => {
referencesObj[ref.filename] = ref.content || ''
})
// 判断是新建还是编辑
if (editingSkillId) {
// 编辑模式更新现有Skill
await skillService.updateSkillWithReferences(
editingSkillId,
skillContent,
referencesObj
)
message.success('Skill 更新成功!')
} else {
// 新建模式创建新Skill
// 如果没有previewId先生成一个
let currentPreviewId = previewId
if (!currentPreviewId) {
const previewResult = await skillService.previewSkill({
skill_content: skillContent,
skill_name: generatedSkill.suggested_name,
category: generatedSkill.category,
tags: generatedSkill.suggested_tags
})
currentPreviewId = previewResult.preview_id
setPreviewId(currentPreviewId)
}
// 保存Skill
await skillService.saveSkillFromPreview(
currentPreviewId,
generatedSkill.suggested_id,
skillContent,
referencesObj
)
message.success('Skill 创建成功!')
}
onSuccess()
handleClose()
} catch (error) {
message.error(`保存失败: ${(error as Error).message}`)
} finally {
setSaving(false)
}
}
// 计算步骤进度
const getStepProgress = () => {
switch (step) {
case 'input': return 0
case 'generating': return 1
case 'preview': return 2
case 'references': return 2
case 'saving': return 3
default: return 0
}
}
return (
<>
<Modal
title={
<Space>
<RobotOutlined />
<span>{editingSkillId ? '编辑 Skill' : '创建 Skill'}</span>
</Space>
}
open={visible}
onCancel={handleClose}
footer={null}
width={1000}
destroyOnHidden
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 步骤指示器 */}
<Steps current={getStepProgress()} size="small">
<Step title="输入需求" />
<Step title="AI生成" />
<Step title="预览编辑" />
<Step title="添加References" />
<Step title="完成" />
</Steps>
{/* ========== 步骤1: 输入需求 ========== */}
{step === 'input' && (
<Card>
<Alert
message="描述你想要的 Skill"
description="AI 会根据你的描述,结合 skill-creator 标准,自动生成完整的 SKILL.md。之后你可以预览、编辑并选择性地添加参考文档references。"
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
<Form layout="vertical">
<Form.Item label="需求描述">
<TextArea
value={aiDescription}
onChange={(e) => setAiDescription(e.target.value)}
placeholder="描述你想要创建的 Skill 功能和要求... 例如:我想创建一个古风对话创作的 Skill让角色说话符合古代身份和性格"
rows={6}
showCount
maxLength={1000}
/>
</Form.Item>
<Button
type="primary"
size="large"
block
icon={<RobotOutlined />}
disabled={!aiDescription.trim()}
onClick={handleGenerate}
>
AI Skill
</Button>
</Form>
</Card>
)}
{/* ========== 生成中 ========== */}
{step === 'generating' && currentTaskId && (
<Card title="AI 正在生成 Skill..." style={{ marginBottom: 16 }}>
<Alert
message="任务已在后台创建"
description="您可以关闭此弹窗,任务将在后台继续运行。稍后重新打开创建弹窗即可查看生成结果。"
type="info"
showIcon
closable
style={{ marginBottom: 16 }}
/>
<TaskProgressTracker
taskId={currentTaskId}
onComplete={() => {
// 任务完成后的处理逻辑已经在 handleGenerate 中了
}}
onError={(error) => {
message.error(`生成失败: ${error}`)
}}
/>
<div style={{ marginTop: 16, textAlign: 'center' }}>
<Button onClick={handleClose}>
</Button>
</div>
</Card>
)}
{/* ========== 步骤2&3: 预览和编辑 / 添加References ========== */}
{(step === 'preview' || step === 'references') && generatedSkill && (
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 基本信息卡片 */}
<Alert
message="生成成功"
description={
<Space direction="vertical" size="small">
<Text><strong></strong>{generatedSkill.suggested_name}</Text>
<Text><strong></strong><Tag color="blue">{generatedSkill.category}</Tag></Text>
<Text><strong></strong>{generatedSkill.explanation}</Text>
<Text><strong>References</strong>{references.length} </Text>
</Space>
}
type="success"
showIcon
action={
<Button
size="small"
icon={<EditOutlined />}
onClick={() => setStep(step === 'preview' ? 'references' : 'preview')}
>
{step === 'preview' ? '添加References' : '返回预览'}
</Button>
}
/>
{/* 预览/编辑 SKILL.md */}
{step === 'preview' && (
<Card
title="SKILL.md 内容"
extra={
<Space>
{!editingContent && (
<>
<Button size="small" icon={<EditOutlined />} onClick={handleStartEditing}>
</Button>
<Button size="small" icon={<CopyOutlined />} onClick={handleCopyContent}>
</Button>
<Button size="small" onClick={handleRefineWithAI}>
AI调整
</Button>
</>
)}
{editingContent && (
<>
<Button size="small" onClick={handleCancelEdit}>
</Button>
<Button size="small" type="primary" onClick={handleConfirmEdit}>
</Button>
</>
)}
</Space>
}
>
<Form form={form} layout="vertical">
<Form.Item name="content">
{editingContent ? (
<TextArea
rows={25}
style={{
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: '1.6'
}}
/>
) : (
<div
ref={contentPreviewRef}
style={{
background: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
// 关键修复:移除高度限制,让内容完整显示
maxHeight: '70vh',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: '1.6'
}}
>
{form.getFieldValue('content')}
</div>
)}
</Form.Item>
</Form>
{/* 标签显示 */}
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text type="secondary"></Text>
<Tag color="blue">{generatedSkill.category}</Tag>
</Col>
<Col span={12}>
<Text type="secondary"></Text>
<Space size={[4, 4]} wrap>
{generatedSkill.suggested_tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Space>
</Col>
</Row>
</Card>
)}
{/* References管理 */}
{step === 'references' && (
<Card
title={
<Space>
<FileTextOutlined />
<span> (References)</span>
<Tag color="green">{references.length}</Tag>
</Space>
}
extra={
<Button
size="small"
icon={<ArrowLeftOutlined />}
onClick={() => setStep('preview')}
>
</Button>
}
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 添加References的选项卡 */}
<Tabs activeKey={activeReferenceTab} onChange={(key) => setActiveReferenceTab(key as any)}>
{/* URL获取 */}
<TabPane tab="文档URL" key="url">
<Space direction="vertical" style={{ width: '100%' }}>
<Input
placeholder="输入文档URL支持HTML/Markdown页面"
prefix={<LinkOutlined />}
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onPressEnter={handleAddUrlReference}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddUrlReference}
disabled={!urlInput.trim()}
>
</Button>
</Space>
</TabPane>
{/* GitHub获取 */}
<TabPane tab="GitHub" key="github">
<Space direction="vertical" style={{ width: '100%' }}>
<Input
placeholder="GitHub仓库URL如: https://github.com/owner/repo"
prefix={<GithubOutlined />}
value={githubInput}
onChange={(e) => setGithubInput(e.target.value)}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddGithubReference}
disabled={!githubInput.trim()}
>
README
</Button>
</Space>
</TabPane>
{/* 文件上传 */}
<TabPane tab="上传文件" key="upload">
<Upload
accept=".md,.txt,.json,.yaml,.yml"
showUploadList={false}
beforeUpload={handleUploadFiles}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Text type="secondary" style={{ marginLeft: '8px' }}>
.md, .txt, .json, .yaml, .yml
</Text>
</TabPane>
{/* 手动输入 */}
<TabPane tab="手动输入" key="manual">
<Space direction="vertical" style={{ width: '100%' }}>
<Input
placeholder="文件名 (如: api-reference.md)"
value={manualFileName}
onChange={(e) => setManualFileName(e.target.value)}
/>
<TextArea
placeholder="输入文档内容..."
rows={4}
value={manualContent}
onChange={(e) => setManualContent(e.target.value)}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddManualReference}
disabled={!manualFileName.trim() || !manualContent.trim()}
>
</Button>
</Space>
</TabPane>
</Tabs>
{/* References列表 */}
<Divider orientation="left"> References ({references.length})</Divider>
{references.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', background: '#fafafa' }}>
<InboxOutlined style={{ fontSize: '48px', color: '#ccc' }} />
<div style={{ marginTop: '16px', color: '#999' }}>
</div>
</div>
) : (
<List
dataSource={references}
renderItem={(ref) => (
<List.Item
actions={[
<Button
size="small"
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewReference(ref)}
>
</Button>,
<Popconfirm
title="确定删除这个参考文档吗?"
onConfirm={() => handleDeleteReference(ref.id)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="link" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
]}
>
<List.Item.Meta
avatar={
ref.type === 'url' ? <LinkOutlined /> :
ref.type === 'github' ? <GithubOutlined /> :
ref.type === 'upload' ? <UploadOutlined /> :
<FileTextOutlined />
}
title={
<Space>
<span>{ref.filename}</span>
<Tag color={ref.type === 'url' ? 'blue' : ref.type === 'github' ? 'purple' : 'default'}>
{ref.type}
</Tag>
{ref.size && <Text type="secondary">({(ref.size / 1024).toFixed(1)}KB)</Text>}
</Space>
}
description={
ref.url ? (
<Text copyable={{ text: ref.url }} style={{ fontSize: '12px' }}>
{ref.url}
</Text>
) : (
<Text type="secondary" style={{ fontSize: '12px' }}>
{ref.content?.substring(0, 100)}...
</Text>
)
}
/>
</List.Item>
)}
/>
)}
{/* 说明 */}
<Alert
message="关于 References"
description="References 是可选的参考文档,会被存储在 Skill 的 references/ 目录中。按照 skill-creator 标准,这些文档应该在需要时被加载到上下文中,用于提供更详细的参考信息。"
type="info"
showIcon
/>
</Space>
</Card>
)}
{/* 保存按钮区域 */}
<Card>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button onClick={handleClose}>
</Button>
<Space>
{step === 'references' && (
<Button onClick={() => setStep('preview')}>
</Button>
)}
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
loading={saving}
onClick={handleSaveSkill}
>
Skill
</Button>
</Space>
</Space>
</Card>
</Space>
)}
</Space>
</Modal>
{/* AI 调整弹窗 */}
<Modal
title="输入调整需求"
open={refineModalVisible}
onOk={confirmRefineWithAI}
onCancel={() => setRefineModalVisible(false)}
okText="开始调整"
cancelText="取消"
>
<Space direction="vertical" style={{ width: '100%' }}>
<TextArea
rows={4}
placeholder="例如:把内容改得更简洁、增加代码示例、优化描述..."
value={refinePrompt}
onChange={(e) => setRefinePrompt(e.target.value)}
/>
</Space>
</Modal>
</>
)
}
// 导出组件
const SkillCreate = SkillCreateComponent
export default SkillCreate