248 lines
7.7 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.

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>
);
};