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 = ({ 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([]); const [contentSelectedSkills, setContentSelectedSkills] = useState([]); const [createSelectedSkills, setCreateSelectedSkills] = useState([]); 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(null); const outlineTextareaRef = useRef(null); const containerRef = useRef(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(''); 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) => { const value = e.target.value; setEditOutline(value); if (onOutlineChange) { onOutlineChange(value); } }, [onOutlineChange]); const handleContentChange = useCallback((e: React.ChangeEvent) => { 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 (
{/* 标题区域 */}
{isEditingTitle ? ( 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="输入剧集名称" /> 提示:只需输入剧集名称,"第X集"会自动添加 ) : (
{displayTitle}
)}
{/* 大纲编辑区域 */}
剧集大纲