wendajia/frontend/src/CreateAskTasksPage.jsx

407 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 } from "react";
import { Check, Link2, Plus, Sparkles } from "lucide-react";
const blankForm = {
product_link: "",
product_name: "",
category: "",
benefits: "",
selling_points: "",
language_style: "口语(亲切随性)",
target_group: "女性",
usage_scene: "居家",
purchase_channel: "电商平台",
question_count: 5,
};
async function readReferenceFile(file) {
if (!file) return "";
return (await file.text()).slice(0, 20000);
}
export default function CreateAskTasksPage({ api }) {
const [form, setForm] = useState(blankForm);
const [task, setTask] = useState(null);
const [questions, setQuestions] = useState([]);
const [editing, setEditing] = useState(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [referenceFile, setReferenceFile] = useState(null);
const update = (key, value) =>
setForm((current) => ({ ...current, [key]: value }));
async function generate() {
setBusy(true);
setError("");
try {
const reference_qa = await readReferenceFile(referenceFile);
const result = await api("/api/admin/ask-tasks/generate", {
method: "POST",
body: JSON.stringify({
...form,
question_count: Number(form.question_count),
reference_qa,
}),
});
setTask(result.data);
setQuestions(
result.data.selected_questions.map((item) => ({
...item,
selected: true,
})),
);
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
async function saveQuestion(index, value) {
setQuestions((current) =>
current.map((item, i) =>
i === index ? { ...item, question: value } : item,
),
);
const question = questions[index];
if (
task &&
question &&
!String(question.id).startsWith("new-") &&
value.trim()
)
await api(`/api/admin/ask-tasks/${task.id}/questions/${question.id}`, {
method: "PATCH",
body: JSON.stringify({ question: value.trim() }),
});
}
async function toggle(index) {
const question = questions[index];
const selected = !question.selected;
setQuestions((current) =>
current.map((item, i) => (i === index ? { ...item, selected } : item)),
);
if (task && !String(question.id).startsWith("new-"))
await api(`/api/admin/ask-tasks/${task.id}/questions/${question.id}`, {
method: "PATCH",
body: JSON.stringify({ selected }),
});
}
async function addQuestion() {
const value = window.prompt("请输入提问内容");
if (!value?.trim()) return;
if (!task)
return setQuestions((current) => [
...current,
{ id: `new-${Date.now()}`, question: value.trim(), selected: true },
]);
try {
const result = await api(`/api/admin/ask-tasks/${task.id}/questions`, {
method: "POST",
body: JSON.stringify({ question: value.trim() }),
});
setQuestions((current) => [...current, result.data]);
} catch (err) {
setError(err.message);
}
}
async function publish() {
if (!task) return setError("请先生成提问");
setBusy(true);
setError("");
try {
const result = await api(`/api/admin/ask-tasks/${task.id}/publish`, {
method: "POST",
});
setTask(result.data);
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
if (task?.status === "已发布")
return (
<>
<div className="create-page-heading">
<div>
<h2>创建任务</h2>
<p>提问任务已发布,可在任务看板查看。</p>
</div>
</div>
<section className="ask-created-card panel">
<div className="ask-created-icon">
<Check size={34} />
</div>
<h2>任务创建成功!</h2>
<p>
任务ID:<strong>{task.task_id}</strong>
</p>
<div className="ask-link-box">
<span>
<Link2 size={14} />
提问任务链接
</span>
<code>
{window.location.origin}/ask/{task.task_id}
</code>
<small>当前链接为任务预留入口,后续接入提问用户页面。</small>
</div>
<div>
<button
className="primary-button"
onClick={() =>
window.dispatchEvent(new CustomEvent("open-task-board"))
}
>
前往任务看板查看
</button>
<button
className="ghost-button"
onClick={() => {
setForm(blankForm);
setQuestions([]);
setTask(null);
}}
>
继续创建新任务
</button>
</div>
</section>
</>
);
return (
<>
<div className="create-page-heading">
<div>
<h2>创建任务</h2>
<p>通过顶部切换创建「提问任务」或「回答任务」。</p>
</div>
<div className="create-mode-tabs">
<button className="active">创建提问任务</button>
<button
onClick={() =>
window.dispatchEvent(new CustomEvent("switch-create-answer"))
}
>
创建回答任务
</button>
</div>
</div>
<div className="ask-stepper">
<b>1</b>
<strong>Step 1 生成并勾选提问</strong>
<i />
<span>2 Step 2 发布成功</span>
</div>
<div className="ask-workspace panel">
<section className="ask-config">
<div className="panel-title">
<Sparkles size={17} />
生成条件配置 <small>* 商品链接为必填项</small>
</div>
<label>
商品链接 *
<input
value={form.product_link}
onChange={(event) => update("product_link", event.target.value)}
placeholder="粘贴商品链接,例如:https://item.jd.com/100028981.html"
/>
</label>
<h4>商品信息</h4>
<label>
商品名称
<input
value={form.product_name}
onChange={(event) => update("product_name", event.target.value)}
placeholder="请输入商品名称"
/>
</label>
<div className="two-col">
<label>
商品品类
<input
value={form.category}
onChange={(event) => update("category", event.target.value)}
placeholder="例如:面部精华"
/>
</label>
<label>
商品功效
<input
value={form.benefits}
onChange={(event) => update("benefits", event.target.value)}
placeholder="例如:舒缓泛红、修护屏障"
/>
</label>
</div>
<label>
商品卖点
<textarea
value={form.selling_points}
onChange={(event) => update("selling_points", event.target.value)}
placeholder="例如:油敏肌救星,植萃配方"
/>
</label>
<label className="reference-qa-upload">
真实问答参考(选填)
<input type="file" accept=".txt,.csv,.json,.md,text/plain,text/csv,application/json,text/markdown" onChange={(event) => setReferenceFile(event.target.files?.[0] || null)} />
<small>{referenceFile ? `已选择:${referenceFile.name}` : "支持 TXT、CSV、JSON、Markdown,生成提问时会参考其中的问题角度"}</small>
</label>
<h4>生成偏好与数量</h4>
<div className="two-col">
<label>
语言风格
<select
value={form.language_style}
onChange={(event) =>
update("language_style", event.target.value)
}
>
<option>口语(亲切随性)</option>
<option>专业</option>
<option>幽默</option>
</select>
</label>
<label>
目标人群
<select
value={form.target_group}
onChange={(event) => update("target_group", event.target.value)}
>
<option>女性</option>
<option>男性</option>
<option>学生</option>
</select>
</label>
</div>
<label>
提问生成条数
<input
type="number"
min="1"
value={form.question_count}
onChange={(event) => update("question_count", event.target.value)}
/>
</label>
<div className="two-col">
<label>
使用场景
<select
value={form.usage_scene}
onChange={(event) => update("usage_scene", event.target.value)}
>
<option>居家</option>
<option>旅行</option>
<option>通勤</option>
</select>
</label>
<label>
购买渠道
<select
value={form.purchase_channel}
onChange={(event) =>
update("purchase_channel", event.target.value)
}
>
<option>电商平台</option>
<option>大型商超</option>
<option>药房</option>
</select>
</label>
</div>
{error && <div className="form-error">{error}</div>}
<button
className="answer-v2-primary wide"
disabled={busy}
onClick={generate}
>
<Sparkles size={16} />
{busy ? "生成中…" : "生成提问"}
</button>
</section>
<section className="ask-results">
<div className="ask-toolbar">
<label>
<input
type="checkbox"
checked={
questions.length > 0 &&
questions.every((item) => item.selected)
}
onChange={(event) =>
setQuestions((current) =>
current.map((item) => ({
...item,
selected: event.target.checked,
})),
)
}
/>{" "}
全选 ({questions.filter((item) => item.selected).length}/
{questions.length})
</label>
<button className="ghost-button" onClick={addQuestion}>
<Plus size={14} />
添加提问
</button>
<button
className="primary-button"
disabled={
!questions.some((item) => item.selected) || !task || busy
}
onClick={publish}
>
发布任务
</button>
</div>
{questions.length === 0 ? (
<div className="ask-empty">
点击左侧「生成提问」,AI 生成的提问将展示在这里,可逐条勾选。
</div>
) : (
questions.map((question, index) => (
<article
className={question.selected ? "selected" : ""}
key={question.id}
>
<input
type="checkbox"
checked={question.selected}
onChange={() => toggle(index)}
/>
<div>
<header>
<b>Q{index + 1}</b>
{editing === index ? (
<input
autoFocus
value={question.question}
onChange={(event) =>
saveQuestion(index, event.target.value)
}
onBlur={() => setEditing(null)}
/>
) : (
<strong>{question.question}</strong>
)}
<button
onClick={() =>
setEditing(editing === index ? null : index)
}
>
{editing === index ? "完成" : "编辑"}
</button>
</header>
<small>来源:AI生成 · 可编辑后发布</small>
</div>
</article>
))
)}
</section>
</div>
</>
);
}