💻 源码
const questionSelect = document.getElementById("question-select");
const questionDetail = document.getElementById("question-detail");
const runBtn = document.getElementById("run-btn");
const progress = document.getElementById("progress");
const progressFill = document.getElementById("progress-fill");
const progressText = document.getElementById("progress-text");
const resultsGrid = document.getElementById("results-grid");
const flowCard = document.getElementById("flow-card");
const flowSteps = document.getElementById("flow-steps");
const summaryCard = document.getElementById("summary-card");
const runtimeInfo = document.getElementById("runtime-info");

let questions = [];
let currentSource = null;

const MODE_COLORS = {
  direct: "模式 A",
  zero_shot: "模式 B",
  few_shot: "模式 C",
};

async function init() {
  const [qRes, rRes] = await Promise.all([
    fetch("/api/questions"),
    fetch("/api/runtime"),
  ]);
  const qData = await qRes.json();
  const runtime = await rRes.json();
  questions = qData.questions;

  questionSelect.innerHTML = questions
    .map(
      (q) =>
        `<option value="${q.index}">题目${q.index}:${q.title}</option>`
    )
    .join("");

  renderRuntime(runtime);
  renderQuestionDetail(questions[0]);
  renderPlaceholders();

  questionSelect.addEventListener("change", () => {
    const q = questions.find((item) => item.index === Number(questionSelect.value));
    renderQuestionDetail(q);
    resetUI();
  });

  runBtn.addEventListener("click", startDemo);
}

function renderRuntime(runtime) {
  const items = [
    ["调用方式", runtime.call_path],
    ["模型", runtime.model],
    ["API Key", runtime.api_configured ? runtime.api_key_status : "未配置"],
    ["参数", `temperature=${runtime.temperature}, seed=${runtime.seed}`],
    ["是否真实运行", "是 · 每次回复均来自百炼实时 API"],
  ];
  runtimeInfo.innerHTML = items
    .map(([label, value]) => `<div class="runtime-item"><span>${label}</span><strong>${value}</strong></div>`)
    .join("");
}

function renderQuestionDetail(q) {
  if (!q) return;
  questionDetail.innerHTML = `
    <p><strong>题目:</strong>${escapeHtml(q.question)}</p>
    <p><strong>参考答案:</strong>${escapeHtml(q.expected_answer)}</p>
    <p><strong>为何适合演示 CoT:</strong>${escapeHtml(q.why_hard)}</p>
  `;
}

function renderPlaceholders() {
  const ids = ["direct", "zero_shot", "few_shot"];
  resultsGrid.innerHTML = ids
    .map(
      (id) => `
      <article class="mode-card placeholder" data-mode="${id}" id="card-${id}">
        <div class="mode-head">
          <h3>${MODE_COLORS[id] || id}</h3>
          <p>等待运行…</p>
        </div>
        <div class="mode-body">选择题目后点击「开始对比演示」</div>
      </article>`
    )
    .join("");
}

function resetUI() {
  summaryCard.hidden = true;
  flowCard.hidden = true;
  progress.hidden = true;
  progressFill.style.width = "0%";
  flowSteps.querySelectorAll("li").forEach((li) => {
    li.className = "";
    li.querySelector("em").textContent = "等待";
  });
  renderPlaceholders();
}

function escapeHtml(text) {
  return String(text)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function renderMessages(messages) {
  return messages
    .map(
      (msg) => `
      <details class="message-item" open>
        <summary>消息 ${msg.index} · ${escapeHtml(msg.role_label)}</summary>
        <pre>${escapeHtml(msg.content)}</pre>
      </details>`
    )
    .join("");
}

function renderModeCard(result) {
  const card = document.getElementById(`card-${result.id}`);
  if (!card) return;
  card.className = "mode-card";
  card.innerHTML = `
    <div class="mode-head">
      <h3>${escapeHtml(result.title)} · ${escapeHtml(result.subtitle)}</h3>
      <p>${escapeHtml(result.badge)} · ${result.message_count} 条消息</p>
      <div class="badges">
        <span class="badge ${result.correct ? "ok" : "bad"}">${result.correct ? "答案正确" : "答案错误"}</span>
        <span class="badge info">${result.has_steps ? "含分步推理" : "无分步推理"}</span>
      </div>
    </div>
    <div class="mode-body">
      <div>
        <div class="section-title">发送给模型的提示词</div>
        <div class="prompt-summary">${escapeHtml(result.prompt_summary)}</div>
        <div class="message-list">${renderMessages(result.messages)}</div>
      </div>
      <div>
        <div class="section-title">模型回复(百炼实时返回)</div>
        <div class="reply-box">${escapeHtml(result.reply)}</div>
        <p><strong>最终答案:</strong>${escapeHtml(result.final_answer)}</p>
      </div>
    </div>
  `;
}

function setFlowStep(step, status, text) {
  const li = flowSteps.querySelector(`li[data-step="${step}"]`);
  if (!li) return;
  li.className = status;
  li.querySelector("em").textContent = text;
}

function renderSummary(summary) {
  summaryCard.hidden = false;
  const promptBody = summary.prompt_diff
    .map(
      (row) =>
        `<tr><td>${escapeHtml(row.title)}</td><td>${row.message_count}</td><td>${escapeHtml(row.prompt_summary)}</td></tr>`
    )
    .join("");
  const resultBody = summary.result_diff
    .map(
      (row) =>
        `<tr>
          <td>${escapeHtml(row.title)}</td>
          <td>${escapeHtml(row.final_answer)}</td>
          <td>${row.correct ? "正确" : "错误"}</td>
          <td>${row.has_steps ? "有" : "无"}</td>
        </tr>`
    )
    .join("");
  document.querySelector("#prompt-table tbody").innerHTML = promptBody;
  document.querySelector("#result-table tbody").innerHTML = resultBody;
}

function startDemo() {
  if (currentSource) {
    currentSource.close();
    currentSource = null;
  }

  const questionIndex = Number(questionSelect.value);
  resetUI();
  runBtn.disabled = true;
  progress.hidden = false;
  flowCard.hidden = false;
  progressText.textContent = "连接演示服务…";
  progressFill.style.width = "5%";

  ["direct", "zero_shot", "few_shot"].forEach((id) => {
    const card = document.getElementById(`card-${id}`);
    if (card) {
      card.className = "mode-card loading placeholder";
      card.querySelector(".mode-body").textContent = "等待调用…";
    }
  });

  currentSource = new EventSource(`/api/stream/${questionIndex}`);

  currentSource.onmessage = (event) => {
    const data = JSON.parse(event.data);

    if (data.type === "error") {
      progressText.textContent = `错误:${data.message}`;
      runBtn.disabled = false;
      currentSource.close();
      return;
    }

    if (data.type === "init") {
      renderQuestionDetail(data.question);
      renderRuntime(data.runtime);
      progressText.textContent = `已加载题目:${data.question.title}`;
      progressFill.style.width = "10%";
    }

    if (data.type === "mode_start") {
      setFlowStep(data.step, "running", "正在调用百炼 API…");
      progressText.textContent = `步骤 ${data.step}/${data.total}:${data.title} · ${data.subtitle}`;
      progressFill.style.width = `${10 + ((data.step - 1) / data.total) * 70}%`;
      const card = document.getElementById(`card-${data.mode_id}`);
      if (card) {
        card.className = "mode-card loading";
        card.innerHTML = `
          <div class="mode-head"><h3>${escapeHtml(data.title)}</h3><p>${escapeHtml(data.subtitle)}</p></div>
          <div class="mode-body">正在调用百炼 API,请稍候…</div>`;
      }
    }

    if (data.type === "mode_done") {
      setFlowStep(data.step, "done", "完成");
      renderModeCard(data.result);
      progressFill.style.width = `${10 + (data.step / data.total) * 80}%`;
    }

    if (data.type === "summary") {
      renderSummary(data);
      progressFill.style.width = "95%";
    }

    if (data.type === "done") {
      progressText.textContent = `演示完成,共发起 ${data.api_calls} 次百炼 API 真实调用`;
      progressFill.style.width = "100%";
      runBtn.disabled = false;
      currentSource.close();
      currentSource = null;
    }
  };

  currentSource.onerror = () => {
    progressText.textContent = "连接中断,请确认 web_app.py 正在运行";
    runBtn.disabled = false;
    if (currentSource) currentSource.close();
  };
}

init();