248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
||
import { Layout, Typography, Spin, Empty, Button, Card, Tooltip, message, Modal } from 'antd';
|
||
import { LoadingOutlined, WarningOutlined, SaveOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons';
|
||
|
||
const { Content } = Layout;
|
||
const { Title, Text } = Typography;
|
||
|
||
interface SmartCanvasProps {
|
||
content: string;
|
||
streaming: boolean;
|
||
annotations?: any[];
|
||
onStartGenerate?: () => void;
|
||
onContentChange?: (content: string) => void;
|
||
onContentSave?: (content: string) => void;
|
||
episodeTitle?: string;
|
||
episodeNumber?: number;
|
||
}
|
||
|
||
export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
||
content,
|
||
streaming,
|
||
annotations = [],
|
||
onStartGenerate,
|
||
onContentChange,
|
||
onContentSave,
|
||
episodeTitle = '未命名草稿',
|
||
episodeNumber = 5
|
||
}) => {
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [editContent, setEditContent] = useState(content);
|
||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||
const [selectedText, setSelectedText] = useState('');
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
|
||
// Update editContent when content changes (e.g., from agent streaming)
|
||
useEffect(() => {
|
||
if (!isEditing) {
|
||
setEditContent(content);
|
||
}
|
||
}, [content, isEditing]);
|
||
|
||
const handleEditToggle = () => {
|
||
if (isEditing) {
|
||
// Save and exit edit mode
|
||
setIsEditing(false);
|
||
if (onContentChange) {
|
||
onContentChange(editContent);
|
||
}
|
||
message.success('内容已更新');
|
||
} else {
|
||
// Enter edit mode
|
||
setIsEditing(true);
|
||
setEditContent(content);
|
||
}
|
||
};
|
||
|
||
const handleSave = () => {
|
||
if (onContentSave) {
|
||
onContentSave(editContent);
|
||
message.success('内容已保存');
|
||
}
|
||
setIsEditing(false);
|
||
};
|
||
|
||
const handleTextSelection = () => {
|
||
const selection = window.getSelection();
|
||
const text = selection?.toString() || '';
|
||
setSelectedText(text);
|
||
|
||
if (text.length > 0) {
|
||
setShowSaveModal(true);
|
||
}
|
||
};
|
||
|
||
const handleInsertReference = () => {
|
||
// This will be handled by parent component through callback
|
||
setShowSaveModal(false);
|
||
// Notify parent to insert reference into chat
|
||
if (onContentChange) {
|
||
onContentChange(`【引用】: ${selectedText}`);
|
||
}
|
||
message.info('已复制到剪贴板,可以在对话框中粘贴引用');
|
||
navigator.clipboard.writeText(selectedText);
|
||
};
|
||
|
||
return (
|
||
<Content style={{
|
||
padding: '24px 48px',
|
||
background: '#fff',
|
||
overflowY: 'auto',
|
||
height: '100%',
|
||
position: 'relative',
|
||
display: 'flex',
|
||
gap: '24px'
|
||
}}>
|
||
<div style={{ flex: 1, maxWidth: '800px', margin: '0 auto' }}>
|
||
<Title level={3} style={{ textAlign: 'center', marginBottom: '48px', color: '#333' }}>
|
||
第 {episodeNumber} 集:{episodeTitle}
|
||
</Title>
|
||
|
||
{/* 操作按钮 */}
|
||
{!streaming && content && (
|
||
<div style={{ position: 'absolute', top: '24px', right: '24px', display: 'flex', gap: '8px' }}>
|
||
<Tooltip title={isEditing ? '保存编辑' : '编辑内容'}>
|
||
<Button
|
||
type={isEditing ? 'primary' : 'default'}
|
||
size="small"
|
||
icon={isEditing ? <CheckOutlined /> : <EditOutlined />}
|
||
onClick={handleEditToggle}
|
||
>
|
||
{isEditing ? '完成' : '编辑'}
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title="保存到草稿">
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
icon={<SaveOutlined />}
|
||
onClick={handleSave}
|
||
disabled={isEditing}
|
||
>
|
||
保存
|
||
</Button>
|
||
</Tooltip>
|
||
</div>
|
||
)}
|
||
|
||
{content ? (
|
||
isEditing ? (
|
||
// 编辑模式
|
||
<textarea
|
||
ref={textareaRef}
|
||
value={editContent}
|
||
onChange={(e) => setEditContent(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
minHeight: '500px',
|
||
padding: '16px',
|
||
fontSize: '16px',
|
||
lineHeight: '1.8',
|
||
color: '#262626',
|
||
fontFamily: "'Merriweather', 'Georgia', serif",
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '6px',
|
||
resize: 'vertical',
|
||
outline: 'none',
|
||
whiteSpace: 'pre-wrap'
|
||
}}
|
||
onMouseUp={handleTextSelection}
|
||
/>
|
||
) : (
|
||
// 查看模式 - 支持选择文本引用
|
||
<div
|
||
style={{
|
||
fontSize: '16px',
|
||
lineHeight: '1.8',
|
||
color: '#262626',
|
||
whiteSpace: 'pre-wrap',
|
||
fontFamily: "'Merriweather', 'Georgia', serif",
|
||
userSelect: 'text',
|
||
cursor: 'text'
|
||
}}
|
||
onMouseUp={handleTextSelection}
|
||
>
|
||
{editContent}
|
||
{streaming && <span className="cursor-blink" style={{ borderLeft: '2px solid #1890ff', marginLeft: '2px' }}></span>}
|
||
</div>
|
||
)
|
||
) : (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="画布准备就绪,等待创作..."
|
||
style={{ marginTop: '100px' }}
|
||
>
|
||
<Button type="primary" onClick={onStartGenerate}>开始生成大纲</Button>
|
||
</Empty>
|
||
)}
|
||
</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>
|
||
)}
|
||
|
||
{/* 悬浮状态指示 */}
|
||
{streaming && (
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '20px',
|
||
right: '20px',
|
||
background: 'rgba(24, 144, 255, 0.1)',
|
||
padding: '4px 12px',
|
||
borderRadius: '14px',
|
||
color: '#1890ff',
|
||
display: 'flex',
|
||
alignItems: 'center'
|
||
}}>
|
||
<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} style={{ marginRight: '8px' }} />
|
||
正在实时生成...
|
||
</div>
|
||
)}
|
||
|
||
{/* 文本引用模态框 */}
|
||
<Modal
|
||
title="引用文本"
|
||
open={showSaveModal}
|
||
onOk={handleInsertReference}
|
||
onCancel={() => setShowSaveModal(false)}
|
||
okText="复制并引用"
|
||
cancelText="取消"
|
||
>
|
||
<p style={{ marginBottom: '8px', color: '#666' }}>选中的文本:</p>
|
||
<div style={{
|
||
padding: '12px',
|
||
background: '#f5f5f5',
|
||
borderRadius: '4px',
|
||
maxHeight: '200px',
|
||
overflow: 'auto',
|
||
fontSize: '14px',
|
||
lineHeight: '1.6'
|
||
}}>
|
||
{selectedText}
|
||
</div>
|
||
<p style={{ marginTop: '12px', color: '#999', fontSize: '12px' }}>
|
||
点击"复制并引用"将复制到剪贴板,可以在对话框中粘贴使用
|
||
</p>
|
||
</Modal>
|
||
</Content>
|
||
);
|
||
};
|