💻 源码
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("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

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();