💻 源码
const state = {
  scenarios: {},
  currentScenario: "prompt_injection",
  currentSample: null,
};

const SCENARIO_ICONS = {
  prompt_injection: "💉",
  jailbreak: "🔓",
  data_leakage: "📤",
  combo_attack: "⚔️",
};

const LAYER_LABELS = {
  input_isolation: "输入隔离",
  input_guard: "注入检测",
  jailbreak_guard: "越狱检测",
  task_lock: "任务锁定",
  output_guard: "输出过滤",
};

const STEP_STATUS = {
  passed: { icon: "✓", label: "通过" },
  blocked: { icon: "✕", label: "拦截" },
  warning: { icon: "!", label: "风险提示" },
  skipped: { icon: "–", label: "跳过" },
};

const GUARD_CHECKBOX_MAP = {
  enable_input_isolation: "输入隔离",
  enable_injection_detection: "注入检测",
  enable_jailbreak_detection: "越狱检测",
  enable_output_filter: "输出过滤",
};

let lastPipeline = [];

const $ = (sel) => document.querySelector(sel);

async function api(path, options = {}) {
  const res = await fetch(path, {
    headers: { "Content-Type": "application/json" },
    ...options,
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

function levelBadge(level) {
  if (!level) return "";
  return `<span class="level-badge level-${level}">${level}</span>`;
}

function renderScenarios() {
  const tabs = $("#scenarioTabs");
  tabs.innerHTML = "";
  Object.entries(state.scenarios).forEach(([id, sc]) => {
    const btn = document.createElement("button");
    const icon = SCENARIO_ICONS[id] || "📌";
    btn.className = `tab${id === state.currentScenario ? " active" : ""}`;
    btn.innerHTML = `${icon}&nbsp; ${sc.name}`;
    btn.onclick = () => selectScenario(id);
    tabs.appendChild(btn);
  });
}

function renderSamples() {
  const sc = state.scenarios[state.currentScenario];
  if (!sc) return;

  $("#scenarioDesc").textContent = sc.description;
  const list = $("#sampleList");
  list.innerHTML = "";

  sc.samples.forEach((sample) => {
    const li = document.createElement("li");
    li.className = `sample-item${state.currentSample?.id === sample.id ? " active" : ""}`;
    const chain = sample.attack_chain
      ? `<span class="attack-chain">攻击链:${sample.attack_chain}</span>`
      : "";
    li.innerHTML = `<strong>${sample.id}</strong>${sample.name}${levelBadge(sample.level)}<small>${sample.intent}</small>${chain}`;
    li.onclick = () => selectSample(sample);
    list.appendChild(li);
  });
}

function renderAttackGuide(sample) {
  const guide = $("#attackGuide");
  if (!sample) {
    guide.innerHTML = '<p class="empty-hint">选择样本后,这里会显示攻击手法与预期后果</p>';
    return;
  }

  const layers = (sample.defense_layers || [])
    .map((l) => `<span class="defense-layer-tag">${LAYER_LABELS[l] || l}</span>`)
    .join("");

  const chainBlock = sample.attack_chain
    ? `<div class="guide-section attack"><h3>攻击链</h3><p>${sample.attack_chain}</p></div>`
    : "";

  guide.innerHTML = `
    <div class="guide-block">
      ${chainBlock}
      <div class="guide-section attack">
        <h3>怎么攻</h3>
        <p>${sample.intent}</p>
        ${sample.tags ? `<p class="muted" style="margin-top:0.3rem;font-size:0.8rem">标签:${sample.tags.join(" · ")}</p>` : ""}
      </div>
      <div class="guide-section consequence">
        <h3>可能造成什么后果</h3>
        <ul>${sample.consequences.map((c) => `<li>${c}</li>`).join("")}</ul>
      </div>
      <div class="guide-section defense">
        <h3>如何防御</h3>
        <p>${sample.defense_hint || "启用右侧安全设定,观察哪一层拦截了攻击。"}</p>
        ${layers ? `<div style="margin-top:0.5rem">关键防御层:${layers}</div>` : ""}
      </div>
    </div>
  `;
}

function selectScenario(id) {
  state.currentScenario = id;
  state.currentSample = null;
  renderScenarios();
  renderSamples();
  renderAttackGuide(null);
}

function selectSample(sample) {
  state.currentSample = sample;
  $("#attackInput").value = sample.input;
  renderSamples();
  renderAttackGuide(sample);
}

function setBadge(el, success, isVuln) {
  if (isVuln) {
    el.textContent = success ? "攻击成功" : "攻击未生效";
    el.className = `badge ${success ? "badge-danger" : "badge-neutral"}`;
  } else {
    el.textContent = success ? "攻击成功" : "防御成功";
    el.className = `badge ${success ? "badge-danger" : "badge-success"}`;
  }
}

function renderTags(container, items, cls = "") {
  container.innerHTML = items.length
    ? items.map((t) => `<span class="tag ${cls}">${t}</span>`).join("")
    : '<span class="muted" style="font-size:0.82rem">—</span>';
}

function renderCompareSummary(vuln, secure) {
  const box = $("#compareSummary");
  box.classList.remove("hidden");

  const vulnStatus = $("#summaryVulnStatus");
  const vulnDetail = $("#summaryVulnDetail");
  if (vuln.attack_success) {
    vulnStatus.textContent = "🔴 攻击成功";
    vulnStatus.className = "summary-status hit";
    vulnDetail.textContent = vuln.consequences.length
      ? `后果:${vuln.consequences.join("、")}`
      : "脆弱模式被攻破";
  } else {
    vulnStatus.textContent = "⚪ 攻击未生效";
    vulnStatus.className = "summary-status safe";
    vulnDetail.textContent = "攻击未能突破无防护模式";
  }

  const secStatus = $("#summarySecureStatus");
  const secDetail = $("#summarySecureDetail");
  if (!secure.attack_success) {
    secStatus.textContent = "🟢 防御成功";
    secStatus.className = "summary-status ok";
    secDetail.textContent = secure.blocked_by
      ? `拦截层:${LAYER_LABELS[secure.blocked_by] || secure.blocked_by}`
      : "攻击被安全层阻断";
  } else {
    secStatus.textContent = "🔴 攻击成功";
    secStatus.className = "summary-status fail";
    secDetail.textContent = "安全模式未能拦截,需检查配置";
  }
}

function updateDefenseWarning(cfg) {
  const box = $("#defenseWarning");
  const off = [];
  for (const [key, label] of Object.entries(GUARD_CHECKBOX_MAP)) {
    if (!cfg[key]) off.push(label);
  }
  if (off.length) {
    box.textContent = `⚠️ 当前「${off.join("」「")}」已关闭,安全模式防护不完整`;
    box.classList.remove("hidden");
  } else {
    box.classList.add("hidden");
  }
}

function renderPipelineModal(steps) {
  const box = $("#pipelineTimeline");
  if (!steps?.length) {
    box.innerHTML = '<p class="empty-hint">暂无处理流程数据</p>';
    return;
  }
  box.innerHTML = steps
    .map((step) => {
      const meta = STEP_STATUS[step.status] || STEP_STATUS.skipped;
      const detail = step.detail
        ? `<div class="step-detail">${escapeHtml(step.detail)}</div>`
        : "";
      return `
      <div class="pipeline-step status-${step.status}">
        <div class="step-icon">${meta.icon}</div>
        <div class="step-body">
          <div class="step-head">
            <span class="step-name">${step.name}</span>
            <span class="step-status-label ${step.status}">${meta.label}</span>
          </div>
          <p class="step-summary">${escapeHtml(step.summary)}</p>
          ${detail}
        </div>
      </div>`;
    })
    .join("");
}

function escapeHtml(text) {
  return String(text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function openPipelineModal() {
  if (!lastPipeline.length) return;
  renderPipelineModal(lastPipeline);
  $("#pipelineModal").classList.remove("hidden");
}

function closePipelineModal() {
  $("#pipelineModal").classList.add("hidden");
}

async function sendCompare() {
  const message = $("#attackInput").value.trim();
  if (!message) return alert("请输入攻击内容");

  $("#loadingHint").classList.remove("hidden");
  $("#btnSend").disabled = true;

  try {
    const data = await api("/api/compare", {
      method: "POST",
      body: JSON.stringify({
        message,
        scenario_id: state.currentScenario,
        sample_id: state.currentSample?.id || null,
      }),
    });

    const vuln = data.vulnerable;
    const secure = data.secure;

    renderCompareSummary(vuln, secure);

    const vulnBox = $("#vulnOutput");
    vulnBox.textContent = vuln.output;
    vulnBox.className = `output-box${vuln.attack_success ? " attack-success" : ""}`;
    setBadge($("#vulnBadge"), vuln.attack_success, true);
    renderTags($("#vulnConsequences"), vuln.consequences);
    $("#vulnExplain").textContent = vuln.defense_explanation || "";

    const secureBox = $("#secureOutput");
    secureBox.textContent = secure.output;
    secureBox.className = `output-box${!secure.attack_success ? " defense-success" : " attack-success"}`;
    setBadge($("#secureBadge"), secure.attack_success, false);

    const defenseTags = [];
    if (secure.blocked_by) {
      defenseTags.push(`拦截层: ${LAYER_LABELS[secure.blocked_by] || secure.blocked_by}`);
    }
    if (secure.consequences.length) defenseTags.push(...secure.consequences);
    renderTags($("#secureDefense"), defenseTags, "defense");
    $("#secureExplain").textContent = secure.defense_explanation || "";

    lastPipeline = secure.security_pipeline || [];
    const btnPipeline = $("#btnPipeline");
    if (lastPipeline.length) {
      btnPipeline.classList.remove("hidden");
      openPipelineModal();
    } else {
      btnPipeline.classList.add("hidden");
    }

    await loadAuditLogs();
  } catch (err) {
    alert("请求失败: " + err.message);
  } finally {
    $("#loadingHint").classList.add("hidden");
    $("#btnSend").disabled = false;
  }
}

async function loadAuditLogs() {
  const logs = await api("/api/audit/logs");
  const box = $("#auditLog");
  if (!logs.length) {
    box.innerHTML = '<p class="empty-hint">暂无记录,发送实验后显示</p>';
    return;
  }
  box.innerHTML = logs
    .slice()
    .reverse()
    .map((e) => {
      const cls = e.attack_success ? "attacked" : "defended";
      const statusCls = e.attack_success ? "bad" : "ok";
      const statusText = e.attack_success ? "攻击成功" : "已防御";
      return `
      <div class="audit-entry ${cls}">
        <strong>${e.timestamp.slice(11, 19)}</strong>
        &nbsp;[${e.mode}]&nbsp;${e.scenario}
        — <span class="audit-status ${statusCls}">${statusText}</span>
        ${e.blocked_by ? `<span class="muted"> (${e.blocked_by})</span>` : ""}
        <br/><span class="muted">${e.message_preview}</span>
      </div>`;
    })
    .join("");
}

async function loadSecurityConfig() {
  const cfg = await api("/api/security/config");
  const form = $("#securityForm");
  form.enable_input_isolation.checked = cfg.enable_input_isolation;
  form.enable_injection_detection.checked = cfg.enable_injection_detection;
  form.enable_jailbreak_detection.checked = cfg.enable_jailbreak_detection;
  form.enable_output_filter.checked = cfg.enable_output_filter;
  form.audit_enabled.checked = cfg.audit_enabled;
  form.task_whitelist.value = cfg.task_whitelist.join(",");
  form.refusal_message.value = cfg.refusal_message;
  updateDefenseWarning(cfg);
}

async function saveSecurityConfig(e) {
  e.preventDefault();
  const form = $("#securityForm");
  const body = {
    enable_input_isolation: form.enable_input_isolation.checked,
    enable_injection_detection: form.enable_injection_detection.checked,
    enable_jailbreak_detection: form.enable_jailbreak_detection.checked,
    enable_output_filter: form.enable_output_filter.checked,
    audit_enabled: form.audit_enabled.checked,
    task_whitelist: form.task_whitelist.value.split(",").map((s) => s.trim()).filter(Boolean),
    refusal_message: form.refusal_message.value,
  };
  const cfg = await api("/api/security/config", { method: "PUT", body: JSON.stringify(body) });
  updateDefenseWarning(cfg);
  $("#settingsStatus").textContent = "✓ 设定已保存";
  setTimeout(() => ($("#settingsStatus").textContent = ""), 2500);
}

async function runRedteam() {
  $("#redteamModal").classList.remove("hidden");
  $("#redteamSummary").textContent = "运行中…";
  $("#redteamResults").innerHTML = "";

  try {
    const data = await api("/api/redteam/run", { method: "POST" });
    const s = data.summary;

    let levelHtml = "";
    if (s.by_level) {
      levelHtml = "<br/>按难度:" + Object.entries(s.by_level)
        .map(([lv, v]) => `${lv} ${v.blocked}/${v.total}(${v.block_rate})`)
        .join(" · ");
    }

    $("#redteamSummary").innerHTML = `
      共 <strong>${s.total}</strong> 个样本 &nbsp;·&nbsp;
      脆弱模式攻破 <strong>${s.vulnerable_hit}</strong> 个 &nbsp;·&nbsp;
      安全模式拦截 <strong>${s.secure_blocked}</strong> 个 &nbsp;·&nbsp;
      拦截率 <strong>${s.block_rate}</strong>
      ${levelHtml}
    `;
    $("#redteamResults").innerHTML = `
      <div class="redteam-row header">
        <span>编号</span><span>难度</span><span>场景</span><span>脆弱</span><span>安全拦截</span>
      </div>
      ${data.results
        .map((r) => {
          const vulnCls = r.vulnerable_success ? "fail" : "pass";
          const secCls = r.secure_blocked ? "pass" : "fail";
          return `
        <div class="redteam-row">
          <span><strong>${r.sample_id}</strong></span>
          <span>${r.level || "—"}</span>
          <span>${r.scenario}</span>
          <span class="${vulnCls}">${r.vulnerable_success ? "成功" : "失败"}</span>
          <span class="${secCls}">${r.secure_blocked ? (LAYER_LABELS[r.blocked_by] || r.blocked_by || "已防御") : "未拦截"}</span>
        </div>`;
        })
        .join("")}
    `;
  } catch (err) {
    $("#redteamSummary").textContent = "失败: " + err.message;
  }
}

$("#redteamModal")?.addEventListener("click", (e) => {
  if (e.target === $("#redteamModal")) $("#redteamModal").classList.add("hidden");
});

$("#pipelineModal")?.addEventListener("click", (e) => {
  if (e.target === $("#pipelineModal")) closePipelineModal();
});

$("#securityForm")?.addEventListener("change", () => {
  const form = $("#securityForm");
  updateDefenseWarning({
    enable_input_isolation: form.enable_input_isolation.checked,
    enable_injection_detection: form.enable_injection_detection.checked,
    enable_jailbreak_detection: form.enable_jailbreak_detection.checked,
    enable_output_filter: form.enable_output_filter.checked,
  });
});

async function init() {
  state.scenarios = await api("/api/scenarios");
  renderScenarios();
  renderSamples();
  await loadSecurityConfig();

  $("#btnSend").onclick = sendCompare;
  $("#securityForm").onsubmit = saveSecurityConfig;
  $("#btnRedteam").onclick = runRedteam;
  $("#btnCloseModal").onclick = () => $("#redteamModal").classList.add("hidden");
  $("#btnPipeline").onclick = openPipelineModal;
  $("#btnClosePipeline").onclick = closePipelineModal;

  const first = state.scenarios[state.currentScenario]?.samples?.[0];
  if (first) selectSample(first);
}

init();