- 新增审核卡片和确认卡片模型,支持Agent推送审核任务和用户确认 - 实现审核卡片API服务,支持创建、更新、批准、驳回等操作 - 扩展审核维度配置,新增角色一致性、剧情连贯性等维度 - 优化前端审核配置页面,修复API路径错误和状态枚举问题 - 改进剧集创作平台布局,新增左侧边栏用于剧集管理和上下文查看 - 增强Skill管理,支持从审核系统跳转创建/编辑Skill - 修复episodes.json数据问题,清理聊天历史记录 - 更新Agent提示词,明确Skill引用加载流程 - 统一前端主题配置,优化整体UI体验
978 lines
35 KiB
TypeScript
978 lines
35 KiB
TypeScript
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||
import { Layout, Typography, Spin, Button, Card, Tooltip, message, Modal, Select, Input, Space } from 'antd';
|
||
import { LoadingOutlined, WarningOutlined, SaveOutlined, EditOutlined, CheckOutlined, RobotOutlined, PlayCircleOutlined, FileTextOutlined } from '@ant-design/icons';
|
||
|
||
const { Content } = Layout;
|
||
const { Title, Text } = Typography;
|
||
const { TextArea } = Input;
|
||
|
||
interface SmartCanvasProps {
|
||
content: string;
|
||
streaming: boolean;
|
||
annotations?: any[];
|
||
onStartWriting?: () => void;
|
||
onContentChange?: (content: string) => void;
|
||
onContentSave?: (content: string) => void;
|
||
onAIAssist?: (content: string, options?: AIAssistOptions) => void;
|
||
episodeTitle?: string;
|
||
episodeNumber?: number | null;
|
||
episodeStatus?: 'pending' | 'draft' | 'writing' | 'completed';
|
||
availableSkills?: any[];
|
||
projectId?: string;
|
||
onTitleChange?: (title: string) => void;
|
||
onConfirmComplete?: () => void;
|
||
// 新增:大纲相关props
|
||
outlineContent?: string;
|
||
onOutlineChange?: (outline: string) => void;
|
||
onOutlineGenerate?: () => void;
|
||
onOutlineSave?: (outline: string) => void;
|
||
onOutlineAIAssist?: (outline: string, options?: AIAssistOptions) => void;
|
||
outlineStreaming?: boolean;
|
||
}
|
||
|
||
interface AIAssistOptions {
|
||
skills?: any[];
|
||
customPrompt?: string;
|
||
injectAgent?: boolean;
|
||
injectOriginalContent?: boolean;
|
||
}
|
||
|
||
export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
||
content,
|
||
streaming,
|
||
annotations = [],
|
||
onStartWriting,
|
||
onContentChange,
|
||
onContentSave,
|
||
onAIAssist,
|
||
episodeTitle = '未命名草稿',
|
||
episodeNumber = null,
|
||
episodeStatus = 'pending',
|
||
availableSkills = [],
|
||
projectId,
|
||
onTitleChange,
|
||
onConfirmComplete,
|
||
// 新增:大纲相关props
|
||
outlineContent = '',
|
||
onOutlineChange,
|
||
onOutlineGenerate,
|
||
onOutlineSave,
|
||
onOutlineAIAssist,
|
||
outlineStreaming = false,
|
||
}) => {
|
||
// 标题编辑状态
|
||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||
const [editTitle, setEditTitle] = useState(episodeTitle);
|
||
|
||
// 内容编辑状态
|
||
const [editContent, setEditContent] = useState(content);
|
||
|
||
// 大纲编辑状态
|
||
const [editOutline, setEditOutline] = useState(outlineContent);
|
||
|
||
// 分割线相关状态(大纲和内容的分割)
|
||
const [outlineHeightPercent, setOutlineHeightPercent] = useState(40); // 大纲占40%
|
||
const [isResizingOutline, setIsResizingOutline] = useState(false);
|
||
|
||
// AI辅助相关状态
|
||
const [showOutlineAIAssistModal, setShowOutlineAIAssistModal] = useState(false);
|
||
const [showContentAIAssistModal, setShowContentAIAssistModal] = useState(false);
|
||
const [showCreateConfigModal, setShowCreateConfigModal] = useState(false);
|
||
const [outlineSelectedSkills, setOutlineSelectedSkills] = useState<any[]>([]);
|
||
const [contentSelectedSkills, setContentSelectedSkills] = useState<any[]>([]);
|
||
const [createSelectedSkills, setCreateSelectedSkills] = useState<any[]>([]);
|
||
const [outlineCustomPrompt, setOutlineCustomPrompt] = useState('');
|
||
const [contentCustomPrompt, setContentCustomPrompt] = useState('');
|
||
const [createCustomPrompt, setCreateCustomPrompt] = useState('');
|
||
const [outlineInjectAgent, setOutlineInjectAgent] = useState(true);
|
||
const [outlineInjectOriginal, setOutlineInjectOriginal] = useState(true);
|
||
const [contentInjectAgent, setContentInjectAgent] = useState(true);
|
||
const [contentInjectOriginal, setContentInjectOriginal] = useState(true);
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
const outlineTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 显示标题处理
|
||
const displayTitle = useMemo(() => {
|
||
if (episodeTitle && episodeTitle !== '未命名草稿') {
|
||
if (episodeNumber !== null && !episodeTitle.includes(`第${episodeNumber}集`)) {
|
||
return `第${episodeNumber}集:${episodeTitle}`;
|
||
}
|
||
return episodeTitle;
|
||
}
|
||
if (episodeNumber !== null && episodeNumber !== undefined) {
|
||
return `第${episodeNumber}集`;
|
||
}
|
||
return '请选择剧集';
|
||
}, [episodeNumber, episodeTitle]);
|
||
|
||
// 当props变化时更新本地状态(仅在剧集切换时执行,避免输入时被覆盖)
|
||
const prevEpisodeRef = useRef<{ number: number | null; title: string }>({ number: null, title: '' });
|
||
const prevContentRef = useRef<string>('');
|
||
|
||
useEffect(() => {
|
||
const currentEpisodeKey = `${episodeNumber}_${episodeTitle}`;
|
||
const prevEpisodeKey = `${prevEpisodeRef.current.number}_${prevEpisodeRef.current.title}`;
|
||
|
||
// 只在剧集切换或内容从外部更新时才同步
|
||
const isEpisodeChanged = currentEpisodeKey !== prevEpisodeKey;
|
||
const isContentFromExternal = content !== prevContentRef.current && content !== editContent;
|
||
|
||
if (isEpisodeChanged || isContentFromExternal) {
|
||
let cleaned = content?.trim() || '';
|
||
if (episodeNumber !== null) {
|
||
const titlePrefixPattern = new RegExp(`^第\\s*${episodeNumber}\\s*集[::\\s]*.*?(?:\\n|$)`, 'i');
|
||
cleaned = cleaned.replace(titlePrefixPattern, '').trim();
|
||
}
|
||
if (episodeTitle && episodeTitle !== '未命名草稿') {
|
||
const escapedTitle = episodeTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
const titlePattern = new RegExp(`^${escapedTitle}[::\\s]*(?:\\n|$)`, 'i');
|
||
cleaned = cleaned.replace(titlePattern, '').trim();
|
||
}
|
||
setEditContent(cleaned);
|
||
}
|
||
|
||
// 更新 ref
|
||
prevEpisodeRef.current = { number: episodeNumber, title: episodeTitle };
|
||
prevContentRef.current = content;
|
||
}, [content, episodeNumber, episodeTitle, editContent]);
|
||
|
||
useEffect(() => {
|
||
setEditOutline(outlineContent);
|
||
}, [outlineContent]);
|
||
|
||
// 只在 displayTitle 实际变化时更新 editTitle(避免不必要的更新)
|
||
useEffect(() => {
|
||
if (editTitle !== displayTitle) {
|
||
setEditTitle(displayTitle);
|
||
}
|
||
}, [displayTitle]);
|
||
|
||
// 使用 useCallback 优化 onChange 处理,避免频繁重新渲染
|
||
const handleOutlineChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
const value = e.target.value;
|
||
setEditOutline(value);
|
||
if (onOutlineChange) {
|
||
onOutlineChange(value);
|
||
}
|
||
}, [onOutlineChange]);
|
||
|
||
const handleContentChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
const value = e.target.value;
|
||
setEditContent(value);
|
||
if (onContentChange) {
|
||
onContentChange(value);
|
||
}
|
||
}, [onContentChange]);
|
||
|
||
// 处理内容保存
|
||
const handleSave = () => {
|
||
if (onContentSave) {
|
||
onContentSave(editContent);
|
||
message.success('内容已保存');
|
||
}
|
||
};
|
||
|
||
// 处理大纲保存
|
||
const handleOutlineSave = () => {
|
||
if (onOutlineSave) {
|
||
onOutlineSave(editOutline);
|
||
message.success('大纲已保存');
|
||
}
|
||
};
|
||
|
||
// 处理大纲AI辅助
|
||
const handleOutlineAIAssistConfirm = () => {
|
||
if (onOutlineAIAssist) {
|
||
onOutlineAIAssist(editOutline, {
|
||
skills: outlineSelectedSkills,
|
||
customPrompt: outlineCustomPrompt || undefined,
|
||
injectAgent: outlineInjectAgent,
|
||
injectOriginalContent: outlineInjectOriginal
|
||
});
|
||
setShowOutlineAIAssistModal(false);
|
||
}
|
||
};
|
||
|
||
// 处理内容AI辅助
|
||
const handleContentAIAssistConfirm = () => {
|
||
if (onAIAssist) {
|
||
onAIAssist(editContent, {
|
||
skills: contentSelectedSkills,
|
||
customPrompt: contentCustomPrompt || undefined,
|
||
injectAgent: contentInjectAgent,
|
||
injectOriginalContent: contentInjectOriginal
|
||
});
|
||
setShowContentAIAssistModal(false);
|
||
}
|
||
};
|
||
|
||
// 分割线拖动处理
|
||
useEffect(() => {
|
||
if (!isResizingOutline) return;
|
||
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (!containerRef.current) return;
|
||
const containerRect = containerRef.current.getBoundingClientRect();
|
||
const newHeight = ((e.clientY - containerRect.top) / containerRect.height) * 100;
|
||
setOutlineHeightPercent(Math.max(20, Math.min(60, newHeight)));
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
setIsResizingOutline(false);
|
||
};
|
||
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
|
||
return () => {
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
};
|
||
}, [isResizingOutline]);
|
||
|
||
const startOutlineResize = (e: React.MouseEvent) => {
|
||
e.preventDefault();
|
||
setIsResizingOutline(true);
|
||
};
|
||
|
||
return (
|
||
<Content style={{
|
||
padding: '24px 32px',
|
||
background: '#ffffff',
|
||
height: '100%',
|
||
position: 'relative',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
borderLeft: '1px solid #e8eaed',
|
||
borderRight: '1px solid #e8eaed',
|
||
boxShadow: 'inset 0 0 20px rgba(0,0,0,0.02)'
|
||
}}>
|
||
<div ref={containerRef} style={{
|
||
flex: 1,
|
||
maxWidth: '900px',
|
||
margin: '0 auto',
|
||
width: '100%',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
overflow: 'hidden'
|
||
}}>
|
||
{/* 标题区域 */}
|
||
<div style={{ textAlign: 'center', marginBottom: '16px', position: 'relative', flexShrink: 0 }}>
|
||
{isEditingTitle ? (
|
||
<Space direction="vertical" align="center">
|
||
<Space>
|
||
<Input
|
||
value={editTitle}
|
||
onChange={(e) => setEditTitle(e.target.value)}
|
||
onPressEnter={() => {
|
||
setIsEditingTitle(false);
|
||
const titleOnly = editTitle.replace(/^第\s*\d+\s*集/, '');
|
||
onTitleChange?.(titleOnly || editTitle);
|
||
}}
|
||
style={{
|
||
width: '320px',
|
||
fontSize: '18px',
|
||
fontWeight: 'bold',
|
||
borderRadius: '8px',
|
||
border: '2px solid #667eea'
|
||
}}
|
||
placeholder="输入剧集名称"
|
||
/>
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
icon={<CheckOutlined />}
|
||
onClick={() => {
|
||
setIsEditingTitle(false);
|
||
const titleOnly = editTitle.replace(/^第\s*\d+\s*集/, '');
|
||
onTitleChange?.(titleOnly || editTitle);
|
||
}}
|
||
style={{ borderRadius: '6px' }}
|
||
>
|
||
保存
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
onClick={() => {
|
||
setIsEditingTitle(false);
|
||
setEditTitle(displayTitle);
|
||
}}
|
||
style={{ borderRadius: '6px' }}
|
||
>
|
||
取消
|
||
</Button>
|
||
</Space>
|
||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||
提示:只需输入剧集名称,"第X集"会自动添加
|
||
</Text>
|
||
</Space>
|
||
) : (
|
||
<div style={{ display: 'inline-block', position: 'relative' }}>
|
||
<Title level={3} style={{
|
||
display: 'inline-block',
|
||
margin: 0,
|
||
color: '#1a1a1a',
|
||
paddingRight: '40px',
|
||
fontWeight: 600
|
||
}}>
|
||
{displayTitle}
|
||
</Title>
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => {
|
||
const titleOnly = displayTitle.replace(/^第\s*\d+\s*集/, '');
|
||
setEditTitle(titleOnly || displayTitle);
|
||
setIsEditingTitle(true);
|
||
}}
|
||
style={{
|
||
position: 'absolute',
|
||
right: '0',
|
||
top: '50%',
|
||
transform: 'translateY(-50%)',
|
||
color: '#999'
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 大纲编辑区域 */}
|
||
<div style={{
|
||
height: `${outlineHeightPercent}%`,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
minHeight: '150px',
|
||
position: 'relative'
|
||
}}>
|
||
<div style={{
|
||
marginBottom: '8px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
flexShrink: 0
|
||
}}>
|
||
<Space>
|
||
<FileTextOutlined style={{ color: '#667eea' }} />
|
||
<Text strong style={{ fontSize: '14px' }}>剧集大纲</Text>
|
||
</Space>
|
||
<Space size="small">
|
||
<Tooltip title="AI 辅助修改大纲">
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
icon={<RobotOutlined />}
|
||
onClick={() => setShowOutlineAIAssistModal(true)}
|
||
style={{ color: '#667eea' }}
|
||
>
|
||
AI 辅助
|
||
</Button>
|
||
</Tooltip>
|
||
</Space>
|
||
</div>
|
||
|
||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||
<textarea
|
||
ref={outlineTextareaRef}
|
||
value={editOutline}
|
||
onChange={handleOutlineChange}
|
||
placeholder="在此编辑剧集大纲..."
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
padding: '16px',
|
||
fontSize: '14px',
|
||
lineHeight: '1.6',
|
||
color: '#1a1a1a',
|
||
fontFamily: "'Merriweather', 'Georgia', serif",
|
||
border: '2px solid #e5e7eb',
|
||
borderRadius: '12px',
|
||
resize: 'none',
|
||
outline: 'none',
|
||
whiteSpace: 'pre-wrap',
|
||
backgroundColor: '#fafbfc',
|
||
transition: 'all 0.2s',
|
||
boxSizing: 'border-box',
|
||
boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
|
||
}}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = '#667eea';
|
||
e.currentTarget.style.backgroundColor = '#fff';
|
||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.15), 0 2px 8px rgba(102, 126, 234, 0.2)';
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = '#e5e7eb';
|
||
e.currentTarget.style.backgroundColor = '#fafbfc';
|
||
e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
|
||
}}
|
||
/>
|
||
{outlineStreaming && (
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '12px',
|
||
right: '12px',
|
||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||
color: '#fff',
|
||
padding: '4px 12px',
|
||
borderRadius: '12px',
|
||
fontSize: '12px',
|
||
fontWeight: 500,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)'
|
||
}}>
|
||
<Spin indicator={<LoadingOutlined style={{ fontSize: 10 }} spin />} />
|
||
生成中...
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 大纲操作按钮 */}
|
||
{!outlineStreaming && (
|
||
<div style={{
|
||
marginTop: '8px',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
gap: '12px',
|
||
flexShrink: 0
|
||
}}>
|
||
<Button
|
||
size="middle"
|
||
icon={<RobotOutlined />}
|
||
onClick={() => {
|
||
// 打开AI辅助模态框,带有生成大纲的预设提示
|
||
setOutlineCustomPrompt('请生成当前剧集的大纲');
|
||
setShowOutlineAIAssistModal(true);
|
||
}}
|
||
style={{
|
||
borderRadius: '6px',
|
||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||
border: 'none',
|
||
color: '#fff'
|
||
}}
|
||
>
|
||
生成大纲
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
size="middle"
|
||
icon={<SaveOutlined />}
|
||
onClick={handleOutlineSave}
|
||
style={{
|
||
borderRadius: '6px'
|
||
}}
|
||
>
|
||
保存大纲
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 水平分割线 */}
|
||
<div
|
||
style={{
|
||
height: '6px',
|
||
cursor: 'row-resize',
|
||
backgroundColor: '#e8e8e8',
|
||
margin: '8px 0',
|
||
borderRadius: '3px',
|
||
transition: 'all 0.2s',
|
||
flexShrink: 0,
|
||
position: 'relative',
|
||
zIndex: 100
|
||
}}
|
||
onMouseDown={startOutlineResize}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.backgroundColor = '#667eea';
|
||
e.currentTarget.style.height = '8px';
|
||
e.currentTarget.style.boxShadow = '0 0 8px rgba(102, 126, 234, 0.4)';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
if (!isResizingOutline) {
|
||
e.currentTarget.style.backgroundColor = '#e8e8e8';
|
||
e.currentTarget.style.height = '6px';
|
||
e.currentTarget.style.boxShadow = 'none';
|
||
}
|
||
}}
|
||
>
|
||
<div style={{
|
||
position: 'absolute',
|
||
left: '50%',
|
||
top: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
color: '#999',
|
||
fontSize: '10px',
|
||
pointerEvents: 'none'
|
||
}}>
|
||
≡
|
||
</div>
|
||
</div>
|
||
|
||
{/* 剧集内容编辑区域 */}
|
||
<div style={{
|
||
height: `${100 - outlineHeightPercent - 3}%`,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
minHeight: '200px',
|
||
position: 'relative'
|
||
}}>
|
||
<div style={{
|
||
marginBottom: '8px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
flexShrink: 0
|
||
}}>
|
||
<Space>
|
||
<EditOutlined style={{ color: '#52c41a' }} />
|
||
<Text strong style={{ fontSize: '14px' }}>剧集内容</Text>
|
||
</Space>
|
||
<Space size="small">
|
||
<Tooltip title="AI 辅助修改内容">
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
icon={<RobotOutlined />}
|
||
onClick={() => setShowContentAIAssistModal(true)}
|
||
style={{ color: '#52c41a' }}
|
||
>
|
||
AI 辅助
|
||
</Button>
|
||
</Tooltip>
|
||
</Space>
|
||
</div>
|
||
|
||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||
<textarea
|
||
ref={textareaRef}
|
||
value={editContent}
|
||
onChange={handleContentChange}
|
||
placeholder={episodeStatus === 'pending' ? '等待开始创作...' : '在此编辑剧集内容...'}
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
padding: '16px',
|
||
fontSize: '15px',
|
||
lineHeight: '1.8',
|
||
color: '#1a1a1a',
|
||
fontFamily: "'Merriweather', 'Georgia', serif",
|
||
border: '2px solid #e5e7eb',
|
||
borderRadius: '12px',
|
||
resize: 'none',
|
||
outline: 'none',
|
||
whiteSpace: 'pre-wrap',
|
||
backgroundColor: '#fafbfc',
|
||
transition: 'all 0.2s',
|
||
boxSizing: 'border-box',
|
||
boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
|
||
}}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = '#667eea';
|
||
e.currentTarget.style.backgroundColor = '#fff';
|
||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.15), 0 2px 8px rgba(102, 126, 234, 0.2)';
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = '#e5e7eb';
|
||
e.currentTarget.style.backgroundColor = '#fafbfc';
|
||
e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
|
||
}}
|
||
/>
|
||
{streaming && (
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '12px',
|
||
right: '12px',
|
||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||
color: '#fff',
|
||
padding: '4px 12px',
|
||
borderRadius: '12px',
|
||
fontSize: '12px',
|
||
fontWeight: 500,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)'
|
||
}}>
|
||
<Spin indicator={<LoadingOutlined style={{ fontSize: 10 }} spin />} />
|
||
创作中...
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 内容操作按钮 */}
|
||
{!streaming && (
|
||
<div style={{
|
||
marginTop: '8px',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
gap: '12px',
|
||
flexShrink: 0
|
||
}}>
|
||
{/* 左侧:创作按钮 */}
|
||
<div style={{ display: 'flex', justifyContent: 'center', gap: '12px' }}>
|
||
<Button
|
||
type="primary"
|
||
size="middle"
|
||
icon={<PlayCircleOutlined />}
|
||
onClick={() => setShowCreateConfigModal(true)}
|
||
style={{
|
||
borderRadius: '6px',
|
||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||
border: 'none'
|
||
}}
|
||
>
|
||
开始创作
|
||
</Button>
|
||
{/* 当有内容且状态为草稿或创作中时显示确认按钮 */}
|
||
{content && (episodeStatus === 'draft' || episodeStatus === 'writing') && onConfirmComplete && (
|
||
<Button
|
||
type="primary"
|
||
size="middle"
|
||
icon={<CheckOutlined />}
|
||
onClick={onConfirmComplete}
|
||
style={{
|
||
backgroundColor: '#52c41a',
|
||
borderColor: '#52c41a',
|
||
borderRadius: '6px'
|
||
}}
|
||
>
|
||
确认剧集内容
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧:保存按钮 */}
|
||
<Button
|
||
type="primary"
|
||
size="middle"
|
||
icon={<SaveOutlined />}
|
||
onClick={handleSave}
|
||
style={{
|
||
borderRadius: '6px'
|
||
}}
|
||
>
|
||
保存剧集内容
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Annotations Sidebar */}
|
||
{annotations.length > 0 && (
|
||
<div style={{ width: '250px', borderLeft: '1px solid #f0f0f0', paddingLeft: '16px' }}>
|
||
<Title level={5} style={{ fontSize: '14px', marginBottom: '16px' }}>批注 (Annotations)</Title>
|
||
{annotations.map((note, idx) => (
|
||
<Card
|
||
key={idx}
|
||
size="small"
|
||
style={{ marginBottom: '8px', borderColor: '#ffccc7', background: '#fff1f0' }}
|
||
title={<span style={{ color: '#cf1322', fontSize: '12px' }}><WarningOutlined /> {note.type || 'Review Issue'}</span>}
|
||
>
|
||
<Text style={{ fontSize: '12px' }}>{note.content || note.description}</Text>
|
||
{note.suggestion && (
|
||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||
建议: {note.suggestion}
|
||
</div>
|
||
)}
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 大纲AI辅助配置模态框 */}
|
||
<Modal
|
||
title="AI 辅助修改大纲"
|
||
open={showOutlineAIAssistModal}
|
||
onOk={handleOutlineAIAssistConfirm}
|
||
onCancel={() => setShowOutlineAIAssistModal(false)}
|
||
okText="开始优化"
|
||
cancelText="取消"
|
||
width={600}
|
||
>
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
选择 Skills(可选):
|
||
</label>
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择要应用的 Skills"
|
||
style={{ width: '100%' }}
|
||
value={outlineSelectedSkills}
|
||
onChange={setOutlineSelectedSkills}
|
||
options={availableSkills.map((skill: any) => ({
|
||
label: skill.name,
|
||
value: skill.id,
|
||
description: skill.description
|
||
}))}
|
||
optionRender={(option) => (
|
||
<div>
|
||
<div>{option.data.label}</div>
|
||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||
{option.data.description}
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
<p style={{ marginTop: '4px', fontSize: '12px', color: '#999' }}>
|
||
选择的 Skills 将融入 AI 辅助修改的行为指导中
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
自定义提示词(可选):
|
||
</label>
|
||
<TextArea
|
||
placeholder="输入自定义的修改要求或指导..."
|
||
value={outlineCustomPrompt}
|
||
onChange={(e) => setOutlineCustomPrompt(e.target.value)}
|
||
rows={3}
|
||
maxLength={500}
|
||
showCount
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={outlineInjectAgent}
|
||
onChange={(e) => setOutlineInjectAgent(e.target.checked)}
|
||
style={{ marginRight: '8px' }}
|
||
/>
|
||
<span>
|
||
启用 Agent 注入
|
||
</span>
|
||
</label>
|
||
<p style={{ marginTop: '4px', marginLeft: '24px', fontSize: '12px', color: '#999' }}>
|
||
开启后,AI 将使用 Agent 模式进行后台优化,优化过程对用户透明
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={outlineInjectOriginal}
|
||
onChange={(e) => setOutlineInjectOriginal(e.target.checked)}
|
||
style={{ marginRight: '8px' }}
|
||
/>
|
||
<span>
|
||
注入原始大纲内容
|
||
</span>
|
||
</label>
|
||
<p style={{ marginTop: '4px', marginLeft: '24px', fontSize: '12px', color: '#999' }}>
|
||
开启后,AI 将在优化时参考原始大纲内容
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ padding: '12px', background: '#f0f5ff', borderRadius: '4px', border: '1px solid #adc6ff' }}>
|
||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||
<strong>功能说明:</strong>
|
||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||
<li>AI 将分析当前大纲并给出优化建议</li>
|
||
<li>支持的 Skills 会自动融入修改指导</li>
|
||
<li>Agent 注入模式确保优化过程对用户透明</li>
|
||
<li>优化后的大纲将直接更新到编辑区</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
|
||
{/* 内容AI辅助配置模态框 */}
|
||
<Modal
|
||
title="AI 辅助修改内容"
|
||
open={showContentAIAssistModal}
|
||
onOk={handleContentAIAssistConfirm}
|
||
onCancel={() => setShowContentAIAssistModal(false)}
|
||
okText="开始优化"
|
||
cancelText="取消"
|
||
width={600}
|
||
>
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
选择 Skills(可选):
|
||
</label>
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择要应用的 Skills"
|
||
style={{ width: '100%' }}
|
||
value={contentSelectedSkills}
|
||
onChange={setContentSelectedSkills}
|
||
options={availableSkills.map((skill: any) => ({
|
||
label: skill.name,
|
||
value: skill.id,
|
||
description: skill.description
|
||
}))}
|
||
optionRender={(option) => (
|
||
<div>
|
||
<div>{option.data.label}</div>
|
||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||
{option.data.description}
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
<p style={{ marginTop: '4px', fontSize: '12px', color: '#999' }}>
|
||
选择的 Skills 将融入 AI 辅助修改的行为指导中
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
自定义提示词(可选):
|
||
</label>
|
||
<TextArea
|
||
placeholder="输入自定义的修改要求或指导..."
|
||
value={contentCustomPrompt}
|
||
onChange={(e) => setContentCustomPrompt(e.target.value)}
|
||
rows={3}
|
||
maxLength={500}
|
||
showCount
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={contentInjectAgent}
|
||
onChange={(e) => setContentInjectAgent(e.target.checked)}
|
||
style={{ marginRight: '8px' }}
|
||
/>
|
||
<span>
|
||
启用 Agent 注入
|
||
</span>
|
||
</label>
|
||
<p style={{ marginTop: '4px', marginLeft: '24px', fontSize: '12px', color: '#999' }}>
|
||
开启后,AI 将使用 Agent 模式进行后台优化,优化过程对用户透明
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={contentInjectOriginal}
|
||
onChange={(e) => setContentInjectOriginal(e.target.checked)}
|
||
style={{ marginRight: '8px' }}
|
||
/>
|
||
<span>
|
||
注入原始内容
|
||
</span>
|
||
</label>
|
||
<p style={{ marginTop: '4px', marginLeft: '24px', fontSize: '12px', color: '#999' }}>
|
||
开启后,AI 将在优化时参考原始内容
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ padding: '12px', background: '#f0f5ff', borderRadius: '4px', border: '1px solid #adc6ff' }}>
|
||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||
<strong>功能说明:</strong>
|
||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||
<li>AI 将分析当前内容并给出优化建议</li>
|
||
<li>支持的 Skills 会自动融入修改指导</li>
|
||
<li>Agent 注入模式确保优化过程对用户透明</li>
|
||
<li>优化后的内容将直接更新到编辑区</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
|
||
{/* 创作配置模态框 */}
|
||
<Modal
|
||
title="配置创作选项"
|
||
open={showCreateConfigModal}
|
||
onOk={() => {
|
||
setShowCreateConfigModal(false);
|
||
if (onStartWriting) {
|
||
// 构建包含skills信息的创作消息
|
||
let message = `开始创作第${episodeNumber}集完整内容`;
|
||
if (createSelectedSkills.length > 0) {
|
||
const skillNames = createSelectedSkills.map(s => s.name).join('、');
|
||
message += `\n使用Skills:${skillNames}`;
|
||
}
|
||
if (createCustomPrompt) {
|
||
message += `\n要求:${createCustomPrompt}`;
|
||
}
|
||
// 通过onAIAssist发送(这会使用WebSocket和agent)
|
||
if (onAIAssist) {
|
||
onAIAssist('', {
|
||
skills: createSelectedSkills,
|
||
customPrompt: message
|
||
});
|
||
} else if (onStartWriting) {
|
||
onStartWriting();
|
||
}
|
||
}
|
||
}}
|
||
onCancel={() => setShowCreateConfigModal(false)}
|
||
okText="开始创作"
|
||
cancelText="取消"
|
||
width={600}
|
||
>
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
选择创作Skills(可选):
|
||
</label>
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择要使用的Skills,不选择则AI自动判断"
|
||
style={{ width: '100%' }}
|
||
value={createSelectedSkills}
|
||
onChange={setCreateSelectedSkills}
|
||
options={availableSkills.map((skill: any) => ({
|
||
label: skill.name,
|
||
value: skill.id,
|
||
description: skill.description
|
||
}))}
|
||
optionRender={(option) => (
|
||
<div>
|
||
<div>{option.data.label}</div>
|
||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||
{option.data.description}
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
<p style={{ marginTop: '4px', fontSize: '12px', color: '#999' }}>
|
||
不选择则AI将根据项目配置自动选择合适的Skills
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
|
||
创作要求(可选):
|
||
</label>
|
||
<TextArea
|
||
placeholder="输入特殊的创作要求或指导..."
|
||
value={createCustomPrompt}
|
||
onChange={(e) => setCreateCustomPrompt(e.target.value)}
|
||
rows={3}
|
||
maxLength={500}
|
||
showCount
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ padding: '12px', background: '#e6f7ff', borderRadius: '4px', border: '1px solid #91d5ff' }}>
|
||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||
<strong>创作说明:</strong>
|
||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||
<li>AI将按照保存的大纲进行创作</li>
|
||
<li>选择的Skills将融入创作过程</li>
|
||
<li>创作过程中可以随时停止</li>
|
||
<li>创作完成后会进行质量审核</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</Content>
|
||
);
|
||
};
|