const state = {
task: "classify",
source: "sample",
difficulty: "easy",
samples: [],
selectedSample: null,
generateStatus: "idle",
};
const taskCards = document.querySelectorAll(".task-card");
const sourceButtons = document.querySelectorAll("#sourceToggle .segment-btn");
const diffButtons = document.querySelectorAll("#difficultyBar .segment-btn");
const sampleMode = document.querySelector("#sampleMode");
const manualMode = document.querySelector("#manualMode");
const manualSingle = document.querySelector("#manualSingle");
const manualMatch = document.querySelector("#manualMatch");
const sampleList = document.querySelector("#sampleList");
const previewBox = document.querySelector("#previewBox");
const previewHint = document.querySelector("#previewHint");
const promptView = document.querySelector("#promptView");
const resultView = document.querySelector("#resultView");
const submitBtn = document.querySelector("#submitBtn");
const generateBtn = document.querySelector("#generateBtn");
const generateStatus = document.querySelector("#generateStatus");
const generateHint = document.querySelector("#generateHint");
const textInput = document.querySelector("#textInput");
const textAInput = document.querySelector("#textAInput");
const textBInput = document.querySelector("#textBInput");
const difficultyLabels = {
easy: "简单",
medium: "中等",
hard: "困难",
};
const generateStatusLabels = {
idle: "待生成",
generating: "生成中...",
done: "已生成",
error: "生成失败",
};
function prettyJson(data) {
return JSON.stringify(data, null, 2);
}
function renderResultView(data) {
if (typeof data === "string") {
resultView.innerHTML = `<span class="result-placeholder">${escapeHtml(data)}</span>`;
return;
}
const compare =
data.is_correct === true
? '<span class="result-badge result-correct">对比:正确</span>'
: data.is_correct === false
? '<span class="result-badge result-wrong">对比:错误</span>'
: "";
const statusBadge =
data.status === "success"
? '<span class="result-badge result-success">打标成功</span>'
: '<span class="result-badge result-error">打标失败</span>';
const view = {
status: data.status,
id: data.id,
difficulty: data.difficulty,
expected: data.expected,
result: data.result,
};
if (data.is_correct === true) view.compare = "正确";
if (data.is_correct === false) view.compare = "错误";
resultView.innerHTML = `
<div class="result-summary">
${statusBadge}
${compare}
</div>
<code class="result-json">${escapeHtml(prettyJson(view))}</code>
`;
}
function escapeHtml(text) {
return String(text)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
}
function setPreviewEmpty(isEmpty) {
previewBox.classList.toggle("is-empty", isEmpty);
}
function formatExtractPreview(expected) {
if (!expected || typeof expected !== "object") {
return "";
}
const parts = [];
for (const [key, value] of Object.entries(expected)) {
if (Array.isArray(value) && value.length) {
parts.push(`${key}:${value.slice(0, 2).join("、")}`);
}
}
return parts.slice(0, 3).join(" | ");
}
function truncate(text, max = 72) {
const value = String(text || "").trim();
if (value.length <= max) {
return value;
}
return `${value.slice(0, max)}...`;
}
function setActiveButtons(buttons, matcher) {
buttons.forEach((button) => {
button.classList.toggle("active", matcher(button));
});
}
async function parseResponse(response) {
const data = await response.json();
if (!response.ok) {
const detail = data.detail;
if (Array.isArray(detail)) {
throw new Error(detail.map((item) => item.msg || item).join(";"));
}
throw new Error(detail || "请求失败");
}
return data;
}
async function fetchWithTimeout(url, options = {}, timeoutMs = 90000) {
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
} catch (error) {
if (error.name === "AbortError") {
throw new Error("请求超时,请检查网络或稍后重试");
}
throw error;
} finally {
window.clearTimeout(timer);
}
}
const generateBtnLabel = document.querySelector(".btn-generate-label");
function setGenerateStatus(status, message = "") {
state.generateStatus = status;
generateStatus.className = `status-chip status-${status}`;
generateStatus.textContent = generateStatusLabels[status] || status;
if (status === "generating") {
generateBtn.disabled = true;
submitBtn.disabled = true;
if (generateBtnLabel) {
generateBtnLabel.textContent = "正在生成...";
}
return;
}
generateBtn.disabled = false;
submitBtn.disabled = false;
if (generateBtnLabel) {
generateBtnLabel.textContent = "生成测试数据";
}
if (status === "done") {
generateHint.textContent =
message ||
`已成功生成 1 条${difficultyLabels[state.difficulty]}难度样本,已加入列表顶部。`;
return;
}
if (status === "error") {
generateHint.textContent = message || "生成失败,请稍后重试。";
return;
}
updateGenerateHint();
}
function showGeneratingPlaceholder() {
sampleList.innerHTML = `
<div class="sample-card generating-card">
<div class="sample-meta">
<span class="sample-id">生成中</span>
<span class="status-chip status-generating">生成中...</span>
</div>
<div class="sample-text">大模型正在生成 ${difficultyLabels[state.difficulty]} 难度测试数据,请稍候...</div>
</div>
`;
}
async function loadPrompt() {
promptView.textContent = "正在读取提示词...";
try {
const response = await fetch(`/prompt/${state.task}`);
const data = await parseResponse(response);
promptView.textContent = data.prompt;
} catch (error) {
promptView.textContent = error.message;
}
}
async function loadSamples(preferredId = null) {
if (state.generateStatus !== "generating") {
sampleList.innerHTML =
'<div class="empty-state"><span class="empty-spinner"></span>正在加载示例数据...</div>';
}
try {
const response = await fetch(
`/samples/${state.task}?difficulty=${state.difficulty}`,
);
const data = await parseResponse(response);
state.samples = data.samples || [];
const preferred =
preferredId && state.samples.find((item) => item.id === preferredId);
const current =
state.selectedSample &&
state.samples.find((item) => item.id === state.selectedSample.id);
state.selectedSample = preferred || current || state.samples[0] || null;
renderSampleCards();
updatePreview();
} catch (error) {
sampleList.innerHTML = `<div class="sample-text">${error.message}</div>`;
}
}
function renderSampleCards() {
if (!state.samples.length) {
sampleList.innerHTML =
'<div class="empty-state">当前难度暂无示例,请点击「生成测试数据」或切换难度。</div>';
return;
}
sampleList.innerHTML = state.samples
.map((sample) => {
const isActive = state.selectedSample?.id === sample.id;
const body =
state.task === "match"
? `<div class="sample-text"><strong>文本 A:</strong>${truncate(sample.text_a)}</div>
<div class="sample-text"><strong>文本 B:</strong>${truncate(sample.text_b)}</div>`
: `<div class="sample-text">${truncate(sample.text)}</div>`;
const generatedBadge = sample.generated
? '<span class="badge badge-generated">AI生成</span>'
: "";
const expectedHint =
state.task === "classify" && sample.expected_category
? `<div class="sample-text"><strong>标准:</strong>${sample.expected_category}</div>`
: state.task === "match" && sample.expected_is_match !== undefined
? `<div class="sample-text"><strong>标准:</strong>is_match = ${sample.expected_is_match}</div>`
: state.task === "extract" && sample.expected_result
? `<div class="sample-text"><strong>标准:</strong>${formatExtractPreview(sample.expected_result) || "已含标准答案"}</div>`
: "";
return `
<button
class="sample-card ${isActive ? "active" : ""} ${sample.generated ? "generated" : ""}"
data-id="${sample.id}"
type="button"
>
<div class="sample-meta">
<span class="sample-id">${sample.id}</span>
<span class="badge badge-${sample.difficulty}">${difficultyLabels[sample.difficulty] || sample.difficulty}</span>
${generatedBadge}
</div>
${expectedHint}
${body}
</button>
`;
})
.join("");
sampleList.querySelectorAll(".sample-card").forEach((card) => {
card.addEventListener("click", () => {
const sample = state.samples.find((item) => item.id === card.dataset.id);
state.selectedSample = sample || null;
renderSampleCards();
updatePreview();
renderResultView("已切换示例,点击「开始打标」查看新结果。");
});
});
}
function updateSourceMode() {
const isSample = state.source === "sample";
sampleMode.classList.toggle("hidden", !isSample);
manualMode.classList.toggle("hidden", isSample);
if (isSample) {
loadSamples();
} else {
manualSingle.classList.toggle("hidden", state.task === "match");
manualMatch.classList.toggle("hidden", state.task !== "match");
updatePreview();
}
}
function getManualPayload() {
if (state.task === "match") {
return {
text_a: textAInput.value.trim(),
text_b: textBInput.value.trim(),
};
}
return {
text: textInput.value.trim(),
};
}
function updatePreview() {
if (state.source === "sample") {
if (!state.selectedSample) {
previewBox.textContent = "尚未选择数据";
setPreviewEmpty(true);
previewHint.textContent = "请选择一条示例。";
return;
}
setPreviewEmpty(false);
if (state.task === "match") {
previewBox.textContent = `文本 A:${state.selectedSample.text_a}\n\n文本 B:${state.selectedSample.text_b}`;
} else {
previewBox.textContent = state.selectedSample.text;
}
previewHint.textContent = `当前示例:${state.selectedSample.id}(${difficultyLabels[state.selectedSample.difficulty]})${formatExpectedHint(state.selectedSample) ? " | " + formatExpectedHint(state.selectedSample) : ""}`;
return;
}
const payload = getManualPayload();
setPreviewEmpty(!payload.text && !payload.text_a && !payload.text_b);
if (state.task === "match") {
previewBox.textContent = `文本 A:${payload.text_a || "(未填写)"}\n\n文本 B:${payload.text_b || "(未填写)"}`;
} else {
previewBox.textContent = payload.text || "(未填写)";
}
previewHint.textContent = "手动输入模式:填写文本后点击开始打标。";
}
function formatExpectedHint(sample) {
if (state.task === "classify" && sample.expected_category) {
return `标准标签:${sample.expected_category}`;
}
if (state.task === "match" && sample.expected_is_match !== undefined) {
return `标准匹配:${sample.expected_is_match}`;
}
if (state.task === "extract" && sample.expected_result) {
return formatExtractPreview(sample.expected_result) || "标准答案已就绪";
}
return "";
}
function buildPayload() {
if (state.source === "sample") {
if (!state.selectedSample) {
throw new Error("请先选择一条示例数据");
}
if (state.task === "match") {
return {
task: state.task,
sample_id: state.selectedSample.id,
text_a: state.selectedSample.text_a,
text_b: state.selectedSample.text_b,
};
}
return {
task: state.task,
sample_id: state.selectedSample.id,
text: state.selectedSample.text,
};
}
const manual = getManualPayload();
if (state.task === "match") {
if (!manual.text_a || !manual.text_b) {
throw new Error("请填写文本 A 和文本 B");
}
return { task: state.task, ...manual };
}
if (!manual.text) {
throw new Error("请填写待打标文本");
}
return { task: state.task, ...manual };
}
async function generateSample() {
if (state.generateStatus === "generating") {
return;
}
setGenerateStatus("generating");
showGeneratingPlaceholder();
try {
const response = await fetchWithTimeout(
"/generate-sample",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
task: state.task,
difficulty: state.difficulty,
}),
},
120000,
);
const data = await parseResponse(response);
const sample = data.sample;
await loadSamples(sample.id);
setGenerateStatus(
"done",
`已生成 1 条${difficultyLabels[state.difficulty]}样本:${sample.id}`,
);
renderResultView("新测试数据已生成,点击「开始打标」查看模型表现。");
} catch (error) {
setGenerateStatus("error", error.message);
await loadSamples();
}
}
function updateGenerateHint() {
if (state.generateStatus === "generating") {
return;
}
const diff = difficultyLabels[state.difficulty];
if (state.difficulty === "hard") {
generateHint.textContent =
"当前为困难难度。生成器会制造多重歧义/指代/条件嵌套,并通过程序校验拒绝过于简单的样本。";
return;
}
if (state.difficulty === "medium") {
generateHint.textContent = `当前为${diff}难度。生成器会加入多个信息点,需要读完整句才能判断。`;
return;
}
generateHint.textContent = `当前为${diff}难度。生成器会产出意图非常明显的样本。`;
}
async function submitLabeling() {
submitBtn.disabled = true;
renderResultView("模型正在打标,请稍候...");
try {
const payload = buildPayload();
const response = await fetch("/label", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await parseResponse(response);
if (data.status === "success" && data.result) {
renderResultView(data);
return;
}
renderResultView(data);
} catch (error) {
renderResultView(`请求失败:${error.message}`);
} finally {
submitBtn.disabled = false;
}
}
taskCards.forEach((card) => {
card.addEventListener("click", () => {
state.task = card.dataset.task;
setActiveButtons(taskCards, (button) => button.dataset.task === state.task);
renderResultView("已切换任务,点击「开始打标」查看结果。");
setGenerateStatus("idle");
loadPrompt();
updateGenerateHint();
updateSourceMode();
});
});
sourceButtons.forEach((button) => {
button.addEventListener("click", () => {
state.source = button.dataset.source;
setActiveButtons(
sourceButtons,
(item) => item.dataset.source === state.source,
);
updateSourceMode();
});
});
diffButtons.forEach((button) => {
button.addEventListener("click", () => {
state.difficulty = button.dataset.difficulty;
setActiveButtons(
diffButtons,
(item) => item.dataset.difficulty === state.difficulty,
);
setGenerateStatus("idle");
loadSamples();
updateGenerateHint();
});
});
generateBtn.addEventListener("click", generateSample);
[textInput, textAInput, textBInput].forEach((input) => {
input.addEventListener("input", updatePreview);
});
submitBtn.addEventListener("click", submitLabeling);
loadPrompt();
setGenerateStatus("idle");
updateSourceMode();