diff --git a/web/.gitignore b/web/.gitignore index a547bf3..c493a56 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -9,6 +9,7 @@ lerna-debug.log* node_modules dist +public/tinymce dist-ssr *.local diff --git a/web/package-lock.json b/web/package-lock.json index 81deebe..8254ea8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons": "^6.3.2", + "@tinymce/tinymce-react": "^6.3.0", "antd": "^6.5.0", "axios": "^1.18.1", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "tinymce": "^8.7.0" }, "devDependencies": { "@types/node": "^24.13.2", @@ -1486,6 +1488,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@tinymce/tinymce-react": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz", + "integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0", + "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0", + "tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1" + }, + "peerDependenciesMeta": { + "tinymce": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1991,6 +2012,12 @@ "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/json2mq": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", @@ -2261,6 +2288,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2316,6 +2355,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/oxlint": { "version": "1.72.0", "resolved": "https://registry.npmmirror.com/oxlint/-/oxlint-1.72.0.tgz", @@ -2414,6 +2462,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -2591,6 +2656,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinymce": { + "version": "8.7.0", + "resolved": "https://registry.npmmirror.com/tinymce/-/tinymce-8.7.0.tgz", + "integrity": "sha512-V3fBgEzKxKT5d41/3qkXs//2SjANGHzo1MWnU2ai8nss8YrzPprrRFlDbX2X9Z6ClyQAknojs7CzEgcsTdqhPQ==", + "license": "SEE LICENSE IN license.md" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", diff --git a/web/package.json b/web/package.json index 684eb53..68f8e73 100644 --- a/web/package.json +++ b/web/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@ant-design/icons": "^6.3.2", + "@tinymce/tinymce-react": "^6.3.0", "antd": "^6.5.0", "axios": "^1.18.1", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "tinymce": "^8.7.0" }, "devDependencies": { "@types/node": "^24.13.2", diff --git a/web/src/api/generationPoints.ts b/web/src/api/generationPoints.ts new file mode 100644 index 0000000..26c2a3c --- /dev/null +++ b/web/src/api/generationPoints.ts @@ -0,0 +1,51 @@ +import request from "./request"; +import type { GenerationPoint } from "../types"; + +export async function listGenerationPoints(template_id: string) { + const { data } = await request.get("/generation-points", { + params: { template_id }, + }); + return data; +} + +export async function createGenerationPoint(body: { + template_id: string; + position: string; + prompt: string; + model_id?: string; + order?: number; + ref_file?: File; +}) { + const formData = new FormData(); + formData.append("template_id", body.template_id); + formData.append("position", body.position); + formData.append("prompt", body.prompt); + if (body.model_id) formData.append("model_id", body.model_id); + if (body.order !== undefined) formData.append("order", String(body.order)); + if (body.ref_file) formData.append("ref_file", body.ref_file); + const { data } = await request.post("/generation-points", formData); + return data; +} + +export async function updateGenerationPoint( + id: string, + body: Partial +) { + const { data } = await request.put(`/generation-points/${id}`, body); + return data; +} + +export async function deleteGenerationPoint(id: string) { + await request.delete(`/generation-points/${id}`); +} + +export async function batchUpdateOrder(points: { id: string; order: number }[]) { + await request.post("/generation-points/batch-order", { points }); +} + +export async function testGenerationPoint(pointId: string) { + const { data } = await request.post<{ result: string }>( + `/generation-points/${pointId}/test` + ); + return data.result; +} diff --git a/web/src/api/models.ts b/web/src/api/models.ts new file mode 100644 index 0000000..66b1fea --- /dev/null +++ b/web/src/api/models.ts @@ -0,0 +1,31 @@ +import request from "./request"; +import type { AIModel } from "../types"; + +export async function listModels(params?: { enabled?: boolean }) { + const { data } = await request.get("/models", { params }); + return data; +} + +export async function getModel(id: string) { + const { data } = await request.get(`/models/${id}`); + return data; +} + +export async function createModel(body: Partial & { api_key: string }) { + const { data } = await request.post("/models", body); + return data; +} + +export async function updateModel(id: string, body: Partial) { + const { data } = await request.put(`/models/${id}`, body); + return data; +} + +export async function deleteModel(id: string) { + await request.delete(`/models/${id}`); +} + +export async function toggleModel(id: string, is_enabled: boolean) { + const { data } = await request.patch(`/models/${id}/toggle`, { is_enabled }); + return data; +} diff --git a/web/src/api/request.ts b/web/src/api/request.ts new file mode 100644 index 0000000..3e26e83 --- /dev/null +++ b/web/src/api/request.ts @@ -0,0 +1,16 @@ +import axios from "axios"; + +const request = axios.create({ + baseURL: "/api/v1", + timeout: 30000, +}); + +request.interceptors.response.use( + (response) => response, + (error) => { + const message = error.response?.data?.detail || error.message || "请求失败"; + return Promise.reject(new Error(message)); + } +); + +export default request; diff --git a/web/src/api/tasks.ts b/web/src/api/tasks.ts new file mode 100644 index 0000000..496809c --- /dev/null +++ b/web/src/api/tasks.ts @@ -0,0 +1,27 @@ +import request from "./request"; +import type { GenerationTask } from "../types"; + +export async function triggerGeneration(templateId: string) { + const { data } = await request.post<{ task_id: string; status: string }>( + `/templates/${templateId}/generate` + ); + return data; +} + +export async function getTaskStatus(taskId: string) { + const { data } = await request.get(`/tasks/${taskId}`); + return data; +} + +export async function listTasks() { + const { data } = await request.get("/tasks"); + return data; +} + +export async function cancelTask(taskId: string) { + await request.post(`/tasks/${taskId}/cancel`); +} + +export function getTaskDownloadUrl(taskId: string, format: "docx" | "pdf" = "docx") { + return `/api/v1/tasks/${taskId}/download?format=${format}`; +} diff --git a/web/src/api/templates.ts b/web/src/api/templates.ts new file mode 100644 index 0000000..e84216e --- /dev/null +++ b/web/src/api/templates.ts @@ -0,0 +1,37 @@ +import request from "./request"; +import type { Template, TemplateListItem } from "../types"; + +export async function listTemplates() { + const { data } = await request.get("/templates"); + return data; +} + +export async function getTemplate(id: string) { + const { data } = await request.get