feat: 新版生成流程 - 确认弹窗 + 任务详情页
- 生成弹窗: 展示所有AI块(prompt/模型/备注),支持多文件上传/删除/重传
- 点击生成 → 创建任务 → 提示前往任务历史查看
- 任务详情页: 显示模板名/状态/AI块/提示词/上传文件/下载DOCX+PDF
- 任务历史: 行可点击进入详情,增加详情按钮
- 后端: /tasks/{id}?detail=true 返回含生成点信息的完整详情
- 编辑器: 移除旧进度条+下载按钮,接入新GenerateModal
This commit is contained in:
parent
dd2af6be4a
commit
5f87f695fe
|
|
@ -13,7 +13,7 @@ from app.models.generation_task import GenerationTask
|
|||
from app.models.generation_point import GenerationPoint
|
||||
from app.models.template import Template
|
||||
from app.models.ai_model import AIModel
|
||||
from app.schemas.generation_task import TaskResponse, GenerateResponse, SingleTestRequest, SingleTestResponse
|
||||
from app.schemas.generation_task import TaskResponse, GenerateResponse, SingleTestRequest, SingleTestResponse, TaskDetailResponse
|
||||
from app.services.document_processor import docx_to_pdf_bytes
|
||||
from app.services.file_storage import get_file_content, save_upload, REF_FILES_DIR
|
||||
from app.services.ai_adapter import call_ai_model
|
||||
|
|
@ -51,12 +51,41 @@ async def trigger_generation(template_id: str, db: AsyncSession = Depends(get_db
|
|||
return GenerateResponse(task_id=task.id, status="pending")
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||
async def get_task_status(task_id: str, db: AsyncSession = Depends(get_db)):
|
||||
@router.get("/tasks/{task_id}")
|
||||
async def get_task_status(task_id: str, detail: bool = Query(False), db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GenerationTask).where(GenerationTask.id == task_id))
|
||||
task = result.scalar_one_or_none()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
if detail:
|
||||
tpl_result = await db.execute(select(Template).where(Template.id == task.template_id))
|
||||
tpl = tpl_result.scalar_one_or_none()
|
||||
points_result = await db.execute(
|
||||
select(GenerationPoint).where(GenerationPoint.template_id == task.template_id).order_by(GenerationPoint.order.asc())
|
||||
)
|
||||
points = points_result.scalars().all()
|
||||
return TaskDetailResponse(
|
||||
id=task.id,
|
||||
template_id=task.template_id,
|
||||
template_name=tpl.name if tpl else "",
|
||||
status=task.status,
|
||||
result_file_path=task.result_file_path,
|
||||
error_msg=task.error_msg,
|
||||
created_at=task.created_at,
|
||||
finished_at=task.finished_at,
|
||||
points=[{
|
||||
"id": str(p.id),
|
||||
"prompt": p.prompt,
|
||||
"position": p.position,
|
||||
"model_id": str(p.model_id) if p.model_id else None,
|
||||
"ref_file_path": p.ref_file_path,
|
||||
"selected_text": p.selected_text,
|
||||
"need_ref_file": p.need_ref_file,
|
||||
"remark": p.remark,
|
||||
"order": p.order,
|
||||
} for p in points],
|
||||
)
|
||||
return task
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,20 @@ class TaskResponse(BaseModel):
|
|||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class TaskDetailResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
template_id: uuid.UUID
|
||||
template_name: str = ""
|
||||
status: str
|
||||
result_file_path: str | None = None
|
||||
error_msg: str | None = None
|
||||
created_at: datetime
|
||||
finished_at: datetime | None = None
|
||||
points: list[dict] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class GenerateResponse(BaseModel):
|
||||
task_id: uuid.UUID
|
||||
status: str
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import TemplateList from "./pages/TemplateList";
|
|||
import TemplateEditor from "./pages/TemplateEditor";
|
||||
import ModelList from "./pages/ModelList";
|
||||
import TaskList from "./pages/TaskList";
|
||||
import TaskDetail from "./pages/TaskDetail";
|
||||
import Settings from "./pages/Settings";
|
||||
|
||||
function App() {
|
||||
|
|
@ -15,6 +16,7 @@ function App() {
|
|||
<Route path="/editor/:id" element={<TemplateEditor />} />
|
||||
<Route path="/models" element={<ModelList />} />
|
||||
<Route path="/tasks" element={<TaskList />} />
|
||||
<Route path="/tasks/:id" element={<TaskDetail />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Modal, Button, Space, message, Card, Tag, Upload, List, Popconfirm,
|
||||
} from "antd";
|
||||
import {
|
||||
UploadOutlined, DeleteOutlined, FileTextOutlined,
|
||||
ThunderboltOutlined, PaperClipOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { GenerationPoint, AIModel } from "../types";
|
||||
import * as taskApi from "../api/tasks";
|
||||
import request from "../api/request";
|
||||
|
||||
interface GenerateModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
templateId: string;
|
||||
points: GenerationPoint[];
|
||||
models: AIModel[];
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
uid: string;
|
||||
name: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export default function GenerateModal({
|
||||
open, onClose, templateId, points, models,
|
||||
}: GenerateModalProps) {
|
||||
const [filesMap, setFilesMap] = useState<Record<string, FileItem[]>>({});
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const handleUpload = async (pointId: string, fileList: File[]) => {
|
||||
if (fileList.length === 0) return;
|
||||
const formData = new FormData();
|
||||
fileList.forEach((f) => formData.append("ref_files", f));
|
||||
await request.post(`/generation-points/${pointId}/upload-ref`, formData);
|
||||
const newFiles = fileList.map((f) => ({
|
||||
uid: f.name + Date.now(),
|
||||
name: f.name,
|
||||
}));
|
||||
setFilesMap((prev) => ({
|
||||
...prev,
|
||||
[pointId]: [...(prev[pointId] || []), ...newFiles],
|
||||
}));
|
||||
message.success(`已上传 ${fileList.length} 个文件`);
|
||||
};
|
||||
|
||||
const handleRemoveFile = (pointId: string, fileName: string) => {
|
||||
setFilesMap((prev) => ({
|
||||
...prev,
|
||||
[pointId]: (prev[pointId] || []).filter((f) => f.name !== fileName),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const { task_id } = await taskApi.triggerGeneration(templateId);
|
||||
message.success("已创建生成任务,请前往任务历史查看");
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) message.error(err.message);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getModelName = (modelId: string | null) => {
|
||||
if (!modelId) return "默认模型";
|
||||
const m = models.find((x) => x.id === modelId);
|
||||
return m?.name || modelId;
|
||||
};
|
||||
|
||||
const pointsNeedRef = points.filter((p) => p.need_ref_file);
|
||||
const pointsNoRef = points.filter((p) => !p.need_ref_file);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="确认生成文档"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={700}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={handleGenerate}
|
||||
loading={generating}
|
||||
>
|
||||
开始生成
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<p style={{ marginBottom: 16, color: "#666" }}>
|
||||
共 {points.length} 个 AI 生成点,请确认提示词并上传参考文件后点击生成。
|
||||
</p>
|
||||
|
||||
{pointsNeedRef.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Tag color="orange">需要上传文件 ({pointsNeedRef.length})</Tag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{points.map((point, idx) => {
|
||||
const uploadedFiles = filesMap[point.id] || [];
|
||||
const modelName = getModelName(point.model_id);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={point.id}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={
|
||||
<Space size={4}>
|
||||
<span style={{ fontSize: 12, color: "#999" }}>#{idx + 1}</span>
|
||||
<span style={{ fontSize: 13 }}>{point.prompt.slice(0, 30)}{point.prompt.length > 30 ? "..." : ""}</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Tag color="blue" style={{ fontSize: 11 }}>{modelName}</Tag>
|
||||
}
|
||||
>
|
||||
{point.remark && (
|
||||
<p style={{ fontSize: 12, color: "#999", marginBottom: 8 }}>
|
||||
备注:{point.remark}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{point.need_ref_file && (
|
||||
<div>
|
||||
<Upload
|
||||
multiple
|
||||
accept=".txt,.docx,.pdf"
|
||||
showUploadList={false}
|
||||
beforeUpload={(file) => {
|
||||
handleUpload(point.id, [file]);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button size="small" icon={<UploadOutlined />}>
|
||||
上传参考文件
|
||||
</Button>
|
||||
</Upload>
|
||||
|
||||
{uploadedFiles.length > 0 && (
|
||||
<List
|
||||
size="small"
|
||||
style={{ marginTop: 8 }}
|
||||
dataSource={uploadedFiles}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
extra={
|
||||
<Popconfirm
|
||||
title="删除此文件?"
|
||||
onConfirm={() => handleRemoveFile(point.id, item.name)}
|
||||
>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
}
|
||||
>
|
||||
<FileTextOutlined style={{ marginRight: 6 }} />
|
||||
<span style={{ fontSize: 12 }}>{item.name}</span>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card, Tag, Button, Space, Spin, Descriptions, List, Empty,
|
||||
} from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined, DownloadOutlined, FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import request from "../api/request";
|
||||
import type { GenerationPoint, AIModel } from "../types";
|
||||
import * as modelApi from "../api/models";
|
||||
import * as taskApi from "../api/tasks";
|
||||
|
||||
interface TaskDetailData {
|
||||
id: string;
|
||||
template_id: string;
|
||||
template_name: string;
|
||||
status: string;
|
||||
result_file_path: string | null;
|
||||
error_msg: string | null;
|
||||
created_at: string;
|
||||
finished_at: string | null;
|
||||
points: GenerationPoint[];
|
||||
}
|
||||
|
||||
const STATUS_MAP: Record<string, { color: string; label: string }> = {
|
||||
pending: { color: "default", label: "等待中" },
|
||||
processing: { color: "processing", label: "生成中" },
|
||||
done: { color: "success", label: "已完成" },
|
||||
failed: { color: "error", label: "失败" },
|
||||
};
|
||||
|
||||
export default function TaskDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [task, setTask] = useState<TaskDetailData | null>(null);
|
||||
const [models, setModels] = useState<AIModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchTask = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await request.get<TaskDetailData>(`/tasks/${id}`, { params: { detail: true } });
|
||||
setTask(data);
|
||||
} catch {
|
||||
setTask(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetchTask(),
|
||||
modelApi.listModels({ enabled: true }).then(setModels).catch(() => {}),
|
||||
]);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!task || task.status === "done" || task.status === "failed") return;
|
||||
const timer = setInterval(fetchTask, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [task?.status]);
|
||||
|
||||
const getModelName = (modelId: string | null) => {
|
||||
if (!modelId) return "默认模型";
|
||||
return models.find((m) => m.id === modelId)?.name || modelId;
|
||||
};
|
||||
|
||||
if (loading) return <Spin style={{ display: "block", marginTop: 100 }} />;
|
||||
if (!task) return <Empty description="任务不存在" />;
|
||||
|
||||
const status = STATUS_MAP[task.status] || { color: "default", label: task.status };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Space>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate("/tasks")}>返回</Button>
|
||||
<h3 style={{ margin: 0 }}>任务详情</h3>
|
||||
</Space>
|
||||
{task.status === "done" && (
|
||||
<Space>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => window.open(taskApi.getTaskDownloadUrl(task.id))}>
|
||||
DOCX
|
||||
</Button>
|
||||
<Button onClick={() => window.open(taskApi.getTaskDownloadUrl(task.id, "pdf"))}>
|
||||
PDF
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Descriptions column={3} size="small">
|
||||
<Descriptions.Item label="模板名称">{task.template_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态"><Tag color={status.color}>{status.label}</Tag></Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{new Date(task.created_at).toLocaleString()}</Descriptions.Item>
|
||||
{task.finished_at && (
|
||||
<Descriptions.Item label="完成时间">{new Date(task.finished_at).toLocaleString()}</Descriptions.Item>
|
||||
)}
|
||||
{task.error_msg && (
|
||||
<Descriptions.Item label="错误信息" span={3}>
|
||||
<span style={{ color: "red" }}>{task.error_msg}</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<h4 style={{ marginBottom: 12 }}>AI 生成点 ({task.points.length})</h4>
|
||||
{task.points.map((point, idx) => {
|
||||
const modelName = getModelName(point.model_id);
|
||||
const hasFiles = point.ref_file_path && point.ref_file_path !== "[]";
|
||||
let fileNames: string[] = [];
|
||||
if (hasFiles) {
|
||||
try { fileNames = JSON.parse(point.ref_file_path || "[]").map((f: string) => f.split("/").pop() || f); } catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={point.id}
|
||||
size="small"
|
||||
style={{ marginBottom: 8 }}
|
||||
title={
|
||||
<Space size={4}>
|
||||
<span style={{ fontSize: 12, color: "#999" }}>#{idx + 1}</span>
|
||||
<span style={{ fontSize: 13 }}>{point.prompt.slice(0, 40)}{point.prompt.length > 40 ? "..." : ""}</span>
|
||||
</Space>
|
||||
}
|
||||
extra={<Tag color="blue">{modelName}</Tag>}
|
||||
>
|
||||
{point.remark && <p style={{ fontSize: 12, color: "#999", marginBottom: 8 }}>备注:{point.remark}</p>}
|
||||
{point.need_ref_file && (
|
||||
<div>
|
||||
{hasFiles && fileNames.length > 0 ? (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={fileNames}
|
||||
renderItem={(name: string) => (
|
||||
<List.Item><FileTextOutlined style={{ marginRight: 6 }} />{name}</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: "#999" }}>未上传参考文件</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Table, Button, Space, Tag, message, Popconfirm } from "antd";
|
||||
import { DownloadOutlined, StopOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import { DownloadOutlined, StopOutlined, ReloadOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import type { GenerationTask } from "../types";
|
||||
import * as taskApi from "../api/tasks";
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ const STATUS_MAP: Record<string, { color: string; label: string }> = {
|
|||
};
|
||||
|
||||
export default function TaskList() {
|
||||
const navigate = useNavigate();
|
||||
const [tasks, setTasks] = useState<GenerationTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
|
@ -81,6 +83,13 @@ export default function TaskList() {
|
|||
width: 200,
|
||||
render: (_: unknown, record: GenerationTask) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/tasks/${record.id}`)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
{record.status === "done" && (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -124,6 +133,10 @@ export default function TaskList() {
|
|||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
onRow={(record) => ({
|
||||
style: { cursor: "pointer" },
|
||||
onClick: () => navigate(`/tasks/${record.id}`),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
|
|||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Layout, Button, Modal, Form, Input, Select,
|
||||
Space, message, Spin, Progress, Popconfirm, Card, Switch,
|
||||
Space, message, Spin, Popconfirm, Card, Switch,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined, DeleteOutlined, EditOutlined, SaveOutlined,
|
||||
|
|
@ -12,11 +12,11 @@ import {
|
|||
} 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 type { GenerationPoint, AIModel } 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";
|
||||
import GenerateModal from "../components/GenerateModal";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
|
|
@ -38,10 +38,8 @@ export default function TemplateEditor() {
|
|||
const [selBtnPos, setSelBtnPos] = useState({ x: 0, y: 0 });
|
||||
const [pointForm] = Form.useForm();
|
||||
const [panelCollapsed, setPanelCollapsed] = useState(false);
|
||||
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
||||
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [taskStatus, setTaskStatus] = useState<GenerationTask | null>(null);
|
||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const dragItem = useRef<number | null>(null);
|
||||
const dragOverItem = useRef<number | null>(null);
|
||||
|
||||
|
|
@ -266,94 +264,13 @@ export default function TemplateEditor() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const handleGenerate = () => {
|
||||
if (!id) return;
|
||||
if (points.length === 0) {
|
||||
message.warning("请先添加至少一个生成点");
|
||||
return;
|
||||
}
|
||||
|
||||
const needsRef = points.filter((p) => p.need_ref_file && !p.ref_file_path);
|
||||
if (needsRef.length > 0) {
|
||||
Modal.confirm({
|
||||
title: "需要上传参考文件",
|
||||
content: `有 ${needsRef.length} 个生成点需要参考文件但未上传,请先上传后再生成。`,
|
||||
okText: "去上传",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
// 逐个提示上传
|
||||
uploadRefForPoints(needsRef.map((p) => p.id));
|
||||
},
|
||||
});
|
||||
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 uploadRefForPoints = async (pointIds: string[]) => {
|
||||
for (const pointId of pointIds) {
|
||||
const point = points.find((p) => p.id === pointId);
|
||||
if (!point) continue;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.multiple = true;
|
||||
fileInput.accept = ".txt,.docx,.pdf";
|
||||
fileInput.onchange = async (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (!files || files.length === 0) { resolve(); return; }
|
||||
try {
|
||||
message.loading({ content: `上传 ${point.prompt.slice(0, 20)}...`, key: "upload" });
|
||||
const formData = new FormData();
|
||||
Array.from(files).forEach((f) => formData.append("ref_files", f));
|
||||
const { default: request } = await import("../api/request");
|
||||
await request.post(`/generation-points/${pointId}/upload-ref`, formData);
|
||||
message.success({ content: "上传成功", key: "upload" });
|
||||
// 刷新列表
|
||||
const updated = await pointApi.listGenerationPoints(id!);
|
||||
setPoints(updated);
|
||||
} catch {
|
||||
message.error({ content: "上传失败", key: "upload" });
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
message.info(`请为「${point.prompt.slice(0, 30)}」上传参考文件`);
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
message.success("所有参考文件上传完成,请再次点击生成文档");
|
||||
};
|
||||
|
||||
const handleDownload = (format: "docx" | "pdf" = "docx") => {
|
||||
if (taskStatus?.id) {
|
||||
window.open(taskApi.getTaskDownloadUrl(taskStatus.id, format));
|
||||
}
|
||||
setGenerateModalOpen(true);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -393,32 +310,11 @@ export default function TemplateEditor() {
|
|||
danger
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={handleGenerate}
|
||||
loading={generating}
|
||||
>
|
||||
生成文档
|
||||
</Button>
|
||||
{taskStatus?.status === "done" && (
|
||||
<>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => handleDownload("docx")}>
|
||||
DOCX
|
||||
</Button>
|
||||
<Button onClick={() => handleDownload("pdf")}>PDF</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
{taskStatus && taskStatus.status !== "done" && taskStatus.status !== "failed" && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Progress
|
||||
percent={taskStatus.status === "processing" ? 50 : 10}
|
||||
status="active"
|
||||
format={() => taskStatus.status === "pending" ? "等待中..." : "生成中..."}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{taskStatus?.status === "failed" && (
|
||||
<div style={{ marginBottom: 12, color: "red" }}>失败: {taskStatus.error_msg}</div>
|
||||
)}
|
||||
<Layout style={{ height: "calc(100% - 50px)", background: "#fff" }}>
|
||||
<Content style={{ position: "relative", overflow: "hidden" }}>
|
||||
{showSelectionBtn && (
|
||||
|
|
@ -582,6 +478,14 @@ export default function TemplateEditor() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<GenerateModal
|
||||
open={generateModalOpen}
|
||||
onClose={() => setGenerateModalOpen(false)}
|
||||
templateId={id || ""}
|
||||
points={points}
|
||||
models={models}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface GenerationPoint {
|
|||
export interface GenerationTask {
|
||||
id: string;
|
||||
template_id: string;
|
||||
template_name?: string;
|
||||
status: "pending" | "processing" | "done" | "failed";
|
||||
result_file_path: string | null;
|
||||
error_msg: string | null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue