import { useEffect, useState, useRef, useCallback } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { Layout, Button, Modal, Form, Input, Select, Upload, Space, message, Spin, Progress, Popconfirm, Card, Collapse, } from "antd"; import { PlusOutlined, DeleteOutlined, EditOutlined, SaveOutlined, ArrowLeftOutlined, ThunderboltOutlined, DownloadOutlined, ExperimentOutlined, MenuFoldOutlined, MenuUnfoldOutlined, } from "@ant-design/icons"; import { Editor } from "@tinymce/tinymce-react"; import type { Editor as TinyMCEEditor } from "tinymce"; import type { GenerationPoint, AIModel, GenerationTask } from "../types"; import * as templateApi from "../api/templates"; import * as pointApi from "../api/generationPoints"; import * as modelApi from "../api/models"; import * as taskApi from "../api/tasks"; const { Sider, Content } = Layout; export default function TemplateEditor() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const editorRef = useRef(null); const selectionRef = useRef<{ start: number; end: number; text: string } | null>(null); const [htmlContent, setHtmlContent] = useState(""); const [templateName, setTemplateName] = useState(""); const [points, setPoints] = useState([]); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [pointModalOpen, setPointModalOpen] = useState(false); const [editingPoint, setEditingPoint] = useState(null); const [showSelectionBtn, setShowSelectionBtn] = useState(false); const [selBtnPos, setSelBtnPos] = useState({ x: 0, y: 0 }); const [pointForm] = Form.useForm(); const [panelCollapsed, setPanelCollapsed] = useState(false); const [generating, setGenerating] = useState(false); const [taskStatus, setTaskStatus] = useState(null); const pollingRef = useRef | null>(null); const loadData = useCallback(async () => { if (!id) return; setLoading(true); try { const [template, pointsData, modelsData] = await Promise.all([ templateApi.getTemplate(id), pointApi.listGenerationPoints(id), modelApi.listModels({ enabled: true }), ]); setTemplateName(template.name); setHtmlContent(template.html_content || ""); setPoints(pointsData); setModels(modelsData); } catch { message.error("加载数据失败"); } finally { setLoading(false); } }, [id]); useEffect(() => { loadData(); return () => { if (pollingRef.current) clearInterval(pollingRef.current); }; }, [loadData]); const handleSave = async () => { if (!id) return; setSaving(true); try { const html = editorRef.current?.getContent() || htmlContent; await templateApi.updateTemplateHtml(id, html); setHtmlContent(html); message.success("保存成功"); } catch { message.error("保存失败"); } finally { setSaving(false); } }; const handleEditorInit = (_evt: unknown, editor: TinyMCEEditor) => { editorRef.current = editor; editor.on("selectionchange", () => { const sel = editor.selection.getContent({ format: "text" }); if (sel && sel.trim().length > 0) { const content = editor.getContent(); const rng = editor.selection.getRng(); if (rng) { const preRange = rng.cloneRange(); preRange.selectNodeContents(editor.getBody()); preRange.setEnd(rng.startContainer, rng.startOffset); const start = preRange.toString().length; const end = start + sel.length; selectionRef.current = { start, end, text: sel.trim() }; } const ed = editor.getContainer(); const rect = ed.getBoundingClientRect(); setSelBtnPos({ x: rect.left + 100, y: rect.top + 10 }); setShowSelectionBtn(true); } else { setShowSelectionBtn(false); } }); }; const handleCreatePoint = () => { setEditingPoint(null); pointForm.resetFields(); if (selectionRef.current) { pointForm.setFieldsValue({ prompt: `根据上下文生成关于「${selectionRef.current.text.slice(0, 20)}...」的内容`, }); } setPointModalOpen(true); }; const handleEditPoint = (point: GenerationPoint) => { setEditingPoint(point); pointForm.setFieldsValue({ prompt: point.prompt, model_id: point.model_id, }); setPointModalOpen(true); }; const handlePointSubmit = async () => { if (!id) return; try { const values = await pointForm.validateFields(); const position = selectionRef.current ? { start: selectionRef.current.start, end: selectionRef.current.end } : editingPoint?.position; const refFile = values.ref_file?.fileList?.[0]?.originFileObj; if (editingPoint) { const updateData: Record = {}; if (values.prompt) updateData.prompt = values.prompt; if (values.model_id) updateData.model_id = values.model_id; await pointApi.updateGenerationPoint(editingPoint.id, updateData); message.success("更新成功"); } else if (position) { await pointApi.createGenerationPoint({ template_id: id, position: JSON.stringify(position), prompt: values.prompt, model_id: values.model_id, order: points.length, ref_file: refFile, }); message.success("创建成功"); selectionRef.current = null; setShowSelectionBtn(false); } else { message.warning("请先在编辑器中选中文本"); return; } setPointModalOpen(false); const updated = await pointApi.listGenerationPoints(id); setPoints(updated); } catch (err: unknown) { if (err instanceof Error) message.error(err.message); } }; const handleDeletePoint = async (pointId: string) => { await pointApi.deleteGenerationPoint(pointId); message.success("删除成功"); const updated = await pointApi.listGenerationPoints(id!); setPoints(updated); }; const handleTestPoint = async (pointId: string) => { try { message.loading({ content: "测试中...", key: "test" }); const result = await pointApi.testGenerationPoint(pointId); message.success({ content: "测试完成", key: "test" }); Modal.info({ title: "AI 生成结果", content: result, width: 600 }); } catch { message.error({ content: "测试失败", key: "test" }); } }; const handleGenerate = async () => { if (!id) return; if (points.length === 0) { message.warning("请先添加至少一个生成点"); return; } setGenerating(true); try { const { task_id } = await taskApi.triggerGeneration(id); pollingRef.current = setInterval(async () => { try { const status = await taskApi.getTaskStatus(task_id); setTaskStatus(status); if (status.status === "done" || status.status === "failed") { if (pollingRef.current) clearInterval(pollingRef.current); if (status.status === "done") { message.success("生成完成!"); } else { message.error(`生成失败: ${status.error_msg}`); } setGenerating(false); } } catch { if (pollingRef.current) clearInterval(pollingRef.current); setGenerating(false); } }, 2000); } catch (err: unknown) { if (err instanceof Error) message.error(err.message); setGenerating(false); } }; const handleDownload = (format: "docx" | "pdf" = "docx") => { if (taskStatus?.id) { window.open(taskApi.getTaskDownloadUrl(taskStatus.id, format)); } }; if (loading) { return (
); } return (

{templateName}

{taskStatus?.status === "done" && ( <> )}
{taskStatus && taskStatus.status !== "done" && taskStatus.status !== "failed" && (
taskStatus.status === "pending" ? "等待中..." : "生成中..."} />
)} {taskStatus?.status === "failed" && (
失败: {taskStatus.error_msg}
)} {showSelectionBtn && ( )} { editor.on("init", () => { handleEditorInit(null, editor); }); }, }} /> {!panelCollapsed && (

生成点列表 ({points.length})

{points.map((point, index) => { const model = models.find((m) => m.id === point.model_id); return ( #{index + 1} {model && ( {model.name} )} } extra={