← Files ClaraARCHIVED FILE

modules/distribution-analysis/assets/distribution-review-widget.html

21.5 KB · Oct 5, 2026 · 00:02 UTC

↓ Download file

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Distribution Review</title>
  <style>
    :root {
      color-scheme: light;
      --paper: #ffffff;
      --surface: #ffffff;
      --surface-muted: #f7f9f6;
      --surface-strong: #eef3ed;
      --soft: #f7f9f6;
      --soft-strong: #eef3ed;
      --ink: #243027;
      --text: #465148;
      --muted: #6d776f;
      --border: #dfe6dd;
      --border-strong: #cfd9d0;
      --accent: #496a60;
      --accent-strong: #263b34;
      --accent-soft: #e7eee9;
      --danger: #8f4438;
      --danger-bg: #fbf4f2;
      --warning: #7d642d;
      --warning-bg: #fbf7ee;
      --ok: #3f6b55;
      --ok-bg: #f0f7f2;
      --info: #425f72;
      --info-bg: #f1f6f8;
      --radius: 8px;
      --content: 1240px;
      --shadow: none;
    }
    * { box-sizing: border-box; }
    html, body { background: var(--paper); }
    body {
      margin: 0;
      color: var(--text);
      font-family: "Instrument Sans", "Aptos", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
      font-size: 1rem;
      line-height: 1.5;
      font-kerning: normal;
      letter-spacing: 0;
      text-rendering: optimizeLegibility;
    }
    button, input, textarea, select {
      font: inherit;
      letter-spacing: 0;
    }
    button { -webkit-tap-highlight-color: transparent; }
    .app {
      min-height: 100vh;
      padding: 1.25rem;
      background: var(--paper);
    }
    .shell {
      max-width: var(--content);
      margin: 0 auto;
    }
    .topbar {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 1rem;
      padding-bottom: 1rem;
      margin-bottom: 1rem;
      border-bottom: 1px solid var(--border);
    }
    h1 {
      margin: 0 0 0.35rem;
      color: var(--ink);
      font-size: 1.35rem;
      font-weight: 650;
      line-height: 1.18;
      letter-spacing: 0;
    }
    .subtitle {
      max-width: 68ch;
      margin: 0;
      color: var(--muted);
      font-size: 0.95rem;
      line-height: 1.45;
    }
    .pill {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 2rem;
      max-width: 18rem;
      border: 1px solid var(--border);
      border-radius: 999px;
      padding: 0.25rem 0.75rem;
      background: var(--surface);
      color: var(--accent-strong);
      font-size: 0.78rem;
      font-weight: 650;
      line-height: 1.2;
      text-align: center;
      overflow-wrap: anywhere;
    }
    .summary {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
      gap: 0.65rem;
      margin-bottom: 1rem;
    }
    .metric {
      min-width: 0;
      min-height: 5.25rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 0.85rem;
      background: var(--surface);
    }
    .metric span {
      display: block;
      color: var(--muted);
      font-size: 0.78rem;
      font-weight: 520;
      line-height: 1.25;
      overflow-wrap: anywhere;
    }
    .metric strong {
      display: block;
      margin-top: 0.3rem;
      color: var(--ink);
      font-size: 1.28rem;
      font-weight: 650;
      line-height: 1.18;
      letter-spacing: 0;
      overflow-wrap: anywhere;
      font-variant-numeric: tabular-nums;
    }
    .toolbar {
      display: grid;
      grid-template-columns: minmax(14rem, 1fr) auto;
      gap: 0.65rem;
      align-items: start;
      margin-bottom: 0.85rem;
    }
    .search {
      width: 100%;
      min-height: 2.75rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 0.65rem 0.8rem;
      background: var(--surface);
      color: var(--ink);
      outline: none;
    }
    .search::placeholder { color: var(--muted); }
    .search:focus-visible,
    .tab:focus-visible,
    .row:focus-visible,
    button:focus-visible {
      outline: 2px solid var(--accent-soft);
      outline-offset: 2px;
    }
    .tabs {
      display: flex;
      flex-wrap: wrap;
      gap: 0.4rem;
      justify-content: flex-end;
    }
    .tab {
      min-height: 2.25rem;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 0.48rem 0.7rem;
      background: var(--surface);
      color: var(--text);
      cursor: pointer;
      font-size: 0.9rem;
      line-height: 1.2;
      overflow-wrap: anywhere;
    }
    .tab:hover { background: var(--soft); }
    .tab[aria-pressed="true"] {
      border-color: var(--border-strong);
      background: var(--accent-soft);
      color: var(--accent-strong);
      font-weight: 650;
    }
    .content {
      display: grid;
      grid-template-columns: minmax(0, 1.08fr) minmax(20rem, 0.92fr);
      gap: 0.9rem;
      align-items: start;
    }
    .panel {
      min-width: 0;
      overflow: hidden;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      background: var(--surface);
    }
    .panel-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 0.75rem;
      min-height: 3rem;
      border-bottom: 1px solid var(--border);
      padding: 0.75rem 0.9rem;
      background: var(--soft);
      color: var(--muted);
      font-size: 0.84rem;
    }
    .panel-header h2 {
      margin: 0;
      color: var(--ink);
      font-size: 0.95rem;
      font-weight: 650;
      line-height: 1.2;
      letter-spacing: 0;
    }
    .rows {
      max-height: 700px;
      overflow: auto;
    }
    .row {
      display: grid;
      grid-template-columns: minmax(7.5rem, 0.32fr) minmax(0, 1fr) minmax(6.75rem, 10rem);
      gap: 0.75rem;
      width: 100%;
      min-height: 3.75rem;
      border: 0;
      border-left: 2px solid transparent;
      border-bottom: 1px solid var(--border);
      padding: 0.72rem 0.85rem 0.72rem calc(0.85rem - 2px);
      background: var(--surface);
      color: var(--text);
      text-align: left;
      cursor: pointer;
    }
    .row:last-child { border-bottom: 0; }
    .row:hover,
    .row.is-selected {
      background: var(--soft);
    }
    .row.is-selected {
      border-left-color: var(--accent);
    }
    .type {
      color: var(--muted);
      font-size: 0.78rem;
      line-height: 1.25;
      overflow-wrap: anywhere;
    }
    .title {
      color: var(--ink);
      font-weight: 650;
      line-height: 1.3;
      overflow-wrap: anywhere;
    }
    .path {
      display: block;
      margin-top: 0.22rem;
      color: var(--muted);
      font-size: 0.78rem;
      line-height: 1.3;
      overflow-wrap: anywhere;
    }
    .action,
    .tag {
      justify-self: end;
      align-self: start;
      max-width: 10rem;
      border: 1px solid var(--border);
      border-radius: 999px;
      padding: 0.25rem 0.55rem;
      background: var(--warning-bg);
      color: var(--warning);
      font-size: 0.76rem;
      font-weight: 620;
      line-height: 1.2;
      text-align: center;
      overflow-wrap: anywhere;
    }
    .row[data-kind*="artifact"] .action,
    .row[data-kind*="source"] .action,
    .row[data-kind*="source"] .action,
    .row[data-kind*="inventory"] .action,
    .row[data-kind*="supported"] .action,
    .row[data-kind*="section"] .action,
    .row[data-kind*="table"] .action,
    .row[data-kind*="chart"] .action,
    .row[data-kind*="context"] .action,
    .row[data-kind*="field"] .action,
    .row[data-kind*="memo"] .action,
    .row[data-kind*="email"] .action,
    .row[data-kind*="prompt"] .action,
    .row[data-kind*="domain"] .action,
    .row[data-kind*="driver"] .action,
    .row[data-kind*="movement"] .action,
    .row[data-kind*="window"] .action,
    .row[data-kind*="match"] .action {
      background: var(--ok-bg);
      color: var(--ok);
    }
    .row[data-kind*="error"] .action,
    .row[data-kind*="missing"] .action,
    .row[data-kind*="mismatch"] .action,
    .row[data-kind*="unmatched"] .action,
    .row[data-kind*="issue"] .action,
    .row[data-kind*="failed"] .action,
    .row[data-kind*="anomaly"] .action,
    .row[data-kind*="warning"] .action,
    .row[data-kind*="duplicate"] .action,
    .row[data-kind*="followup"] .action {
      background: var(--danger-bg);
      color: var(--danger);
    }
    .details {
      padding: 0.95rem;
    }
    .details h3 {
      margin: 0 0 0.55rem;
      color: var(--ink);
      font-size: 1.05rem;
      font-weight: 650;
      line-height: 1.25;
      letter-spacing: 0;
      overflow-wrap: anywhere;
    }
    .details dl {
      display: grid;
      grid-template-columns: minmax(7.5rem, 0.32fr) minmax(0, 1fr);
      gap: 0.45rem 0.8rem;
      margin: 0.85rem 0;
    }
    .details dt {
      color: var(--muted);
      font-size: 0.8rem;
      line-height: 1.3;
    }
    .details dd {
      margin: 0;
      color: var(--text);
      overflow-wrap: anywhere;
    }
    h4 {
      margin: 1rem 0 0.45rem;
      color: var(--ink);
      font-size: 0.86rem;
      font-weight: 650;
      line-height: 1.25;
      letter-spacing: 0;
    }
    pre {
      max-height: 24rem;
      overflow: auto;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 0.8rem;
      background: var(--soft);
      color: var(--text);
      white-space: pre-wrap;
      overflow-wrap: anywhere;
      font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
      font-size: 0.78rem;
      line-height: 1.45;
    }
    .empty {
      padding: 1.35rem;
      color: var(--muted);
    }
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        scroll-behavior: auto !important;
        transition-duration: 0.001ms !important;
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
      }
    }
    @media (max-width: 980px) {
      .content { grid-template-columns: 1fr; }
      .toolbar { grid-template-columns: 1fr; }
      .tabs { justify-content: flex-start; }
    }
    @media (max-width: 760px) {
      .app { padding: 1rem; }
      .topbar { flex-direction: column; align-items: stretch; }
      .pill { align-self: flex-start; }
      .summary { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }
      .row { grid-template-columns: 1fr; }
      .action, .tag { justify-self: start; max-width: 100%; }
      .details dl { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <main class="app">
    <div class="shell">
      <section class="topbar">
        <div>
          <h1 id="page-title">Distribution Review</h1>
          <p class="subtitle" id="subtitle">Loading payload...</p>
        </div>
        <span class="pill" id="status-pill">MCP widget</span>
      </section>
      <section class="summary" id="summary"></section>
      <section class="toolbar">
        <input id="search" class="search" type="search" placeholder="Search period, metric, chart, context, follow-up, artifact">
        <div class="tabs" id="tabs"></div>
      </section>
      <section class="content">
        <div class="panel">
          <div class="panel-header">
            <h2 id="items-title">Review items</h2>
            <span id="count"></span>
          </div>
          <div class="rows" id="rows"></div>
        </div>
        <div class="panel">
          <div class="panel-header">
            <h2 id="detail-title">Detail</h2>
          </div>
          <div class="details" id="details"></div>
        </div>
      </section>
    </div>
  </main>
  <script>
    const UI_TEXT = {
      en: {
        documentTitle: "Distribution Review",
        title: "Distribution Review",
        loading: "Loading payload...",
        widget: "MCP widget",
        search: "Search period, metric, chart, context, follow-up, artifact",
        reviewItems: "Review items",
        detail: "Detail",
        run: "Run",
        unavailable: "n/a",
        all: "All",
        review: "review",
        noMatch: "No item matches the current filters.",
        selectItem: "Select a review item.",
        data: "Data",
        sourceRecords: "Source Records",
        types: {
          distribution_summary: "Distribution",
          chart_artifact: "Chart",
          context_artifact: "Context",
          followup_request: "Follow-up",
          report_artifact: "Report",
          review_artifact: "Artifact",
        },
        fields: { type: "Type", action: "Action", source: "Source", output: "Output", status: "Status" },
        metrics: { metric: "Metric", dimension: "Dimension", widest: "Widest", range: "Range", charts: "Charts", written: "Written" },
        actions: { accept: "Accept", reject: "Reject", edit: "Edit", mark_unclear: "Mark unclear", request_more_documents: "Request documents", skip: "Skip" },
        statuses: { empty: "Empty", ready_for_review: "Ready for review", needs_review: "Needs review" },
      },
      es: {
        documentTitle: "Revisión de distribución",
        title: "Revisión de distribución",
        loading: "Cargando datos...",
        widget: "Widget MCP",
        search: "Buscar periodo, métrica, gráfico, contexto, seguimiento o artefacto",
        reviewItems: "Elementos para revisar",
        detail: "Detalle",
        run: "Ejecución",
        unavailable: "no disponible",
        all: "Todos",
        review: "revisar",
        noMatch: "Ningún elemento coincide con los filtros actuales.",
        selectItem: "Seleccione un elemento para revisar.",
        data: "Datos",
        sourceRecords: "Registros de origen",
        types: {
          distribution_summary: "Distribución",
          chart_artifact: "Gráfico",
          context_artifact: "Contexto",
          followup_request: "Seguimiento",
          report_artifact: "Informe",
          review_artifact: "Artefacto",
        },
        fields: { type: "Tipo", action: "Acción", source: "Fuente", output: "Salida", status: "Estado" },
        metrics: { metric: "Métrica", dimension: "Dimensión", widest: "Mayor amplitud", range: "Rango", charts: "Gráficos", written: "Generados" },
        actions: { accept: "Aceptar", reject: "Rechazar", edit: "Editar", mark_unclear: "Marcar como incierto", request_more_documents: "Solicitar documentos", skip: "Omitir" },
        statuses: { empty: "Vacío", ready_for_review: "Listo para revisión", needs_review: "Pendiente de revisión" },
      },
    };
    const FALLBACK = {
      widget_type: "distribution_review",
      review_payload: {
        language: "en",
        run_id: "preview",
        status: "empty",
        item_count: 0,
        items: [],
        summary: {},
      },
    };
    const state = {
      payload: FALLBACK,
      selectedType: "all",
      selectedId: null,
      query: "",
    };

    function toolOutput() {
      return window.openai?.toolOutput || window.openai?.structuredContent || FALLBACK;
    }

    function esc(value) {
      return String(value ?? "").replace(/[&<>"']/g, (char) => ({
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        '"': "&quot;",
        "'": "&#39;",
      })[char]);
    }

    function reviewPayload() {
      return state.payload.review_payload || FALLBACK.review_payload;
    }

    function normalizeLanguage(value) {
      const normalized = String(value || "en").trim().toLowerCase().replaceAll("_", "-");
      return ["es", "spa", "spanish", "espanol", "español"].includes(normalized.split("-", 1)[0]) ? "es" : "en";
    }

    function language() {
      return normalizeLanguage(reviewPayload().language || state.payload.run_intake?.language);
    }

    function ui() {
      return UI_TEXT[language()] || UI_TEXT.en;
    }

    function typeLabel(value) {
      return ui().types[value] || value;
    }

    function actionLabel(value) {
      if (value === "review") return ui().review;
      return ui().actions[value] || value;
    }

    function statusLabel(value) {
      return ui().statuses[value] || value;
    }

    function applyLanguage() {
      const copy = ui();
      document.documentElement.lang = language();
      document.title = copy.documentTitle;
      document.getElementById("page-title").textContent = copy.title;
      document.getElementById("subtitle").textContent = copy.loading;
      document.getElementById("status-pill").textContent = copy.widget;
      document.getElementById("search").placeholder = copy.search;
      document.getElementById("items-title").textContent = copy.reviewItems;
      document.getElementById("detail-title").textContent = copy.detail;
    }

    function items() {
      return Array.isArray(reviewPayload().items) ? reviewPayload().items : [];
    }

    function filteredItems() {
      const q = state.query.trim().toLowerCase();
      return items().filter((item) => {
        const typeOk = state.selectedType === "all" || item.item_type === state.selectedType;
        if (!typeOk) return false;
        if (!q) return true;
        return JSON.stringify(item).toLowerCase().includes(q);
      });
    }

    function formatNumber(value) {
      if (value == null || value === "") return ui().unavailable;
      const number = Number(value);
      if (!Number.isFinite(number)) return String(value);
      return new Intl.NumberFormat(language() === "es" ? "es-ES" : undefined, { maximumFractionDigits: 1 }).format(number);
    }

    function metric(label, value) {
      return `<article class="metric"><span>${esc(label)}</span><strong>${esc(value)}</strong></article>`;
    }

    function renderSummary() {
      const payload = reviewPayload();
      const summary = payload.summary || {};
      const copy = ui();
      document.getElementById("subtitle").textContent = `${copy.run} ${payload.run_id || copy.unavailable}`;
      document.getElementById("status-pill").textContent = statusLabel(payload.status || "ready_for_review");
      document.getElementById("summary").innerHTML = [
        metric(copy.metrics.metric, summary.metric || copy.unavailable),
        metric(copy.metrics.dimension, summary.distribution_dimension || copy.unavailable),
        metric(copy.metrics.widest, summary.widest_period || copy.unavailable),
        metric(copy.metrics.range, formatNumber(summary.widest_range)),
        metric(copy.metrics.charts, summary.chart_count ?? 0),
        metric(copy.metrics.written, summary.legacy_chart_written_count ?? 0),
      ].join("");
    }

    function renderTabs() {
      const counts = new Map([["all", items().length]]);
      for (const item of items()) counts.set(item.item_type, (counts.get(item.item_type) || 0) + 1);
      document.getElementById("tabs").innerHTML = Array.from(counts.entries()).map(([type, count]) => {
        const label = type === "all" ? ui().all : typeLabel(type);
        return `<button class="tab" data-type="${esc(type)}" aria-pressed="${type === state.selectedType}">${esc(label)} (${count})</button>`;
      }).join("");
    }

    function rowHtml(item) {
      const selected = item.id === state.selectedId ? " is-selected" : "";
      return `<button class="row${selected}" data-id="${esc(item.id)}" data-kind="${esc(item.item_type)}">
        <span class="type">${esc(typeLabel(item.item_type))}</span>
        <span>
          <span class="title">${esc(item.title)}</span>
          <span class="path">${esc(item.source_path || item.output_path || "")}</span>
        </span>
        <span class="action">${esc(actionLabel(item.recommended_action || "review"))}</span>
      </button>`;
    }

    function renderRows() {
      const rows = filteredItems();
      document.getElementById("count").textContent = `${rows.length} / ${items().length}`;
      document.getElementById("rows").innerHTML = rows.length
        ? rows.map(rowHtml).join("")
        : `<div class="empty">${esc(ui().noMatch)}</div>`;
      if (!state.selectedId && rows[0]) state.selectedId = rows[0].id;
      if (state.selectedId && !items().some((item) => item.id === state.selectedId)) {
        state.selectedId = rows[0]?.id || null;
      }
      renderDetails();
    }

    function detailsHtml(item) {
      if (!item) return `<div class="empty">${esc(ui().selectItem)}</div>`;
      const copy = ui();
      const fields = [
        [copy.fields.type, typeLabel(item.item_type)],
        [copy.fields.action, actionLabel(item.recommended_action || "")],
        [copy.fields.source, item.source_path || ""],
        [copy.fields.output, item.output_path || ""],
        [copy.fields.status, statusLabel(item.status || "")],
      ].filter(([, value]) => value);
      return `
        <h3>${esc(item.title)}</h3>
        <dl>${fields.map(([key, value]) => `<dt>${esc(key)}</dt><dd>${esc(value)}</dd>`).join("")}</dl>
        <h4>${esc(copy.data)}</h4>
        <pre>${esc(JSON.stringify(item.data || {}, null, 2))}</pre>
        <h4>${esc(copy.sourceRecords)}</h4>
        <pre>${esc(JSON.stringify(item.source_records || [], null, 2))}</pre>
      `;
    }

    function renderDetails() {
      const item = items().find((candidate) => candidate.id === state.selectedId);
      document.getElementById("details").innerHTML = detailsHtml(item);
    }

    function render() {
      applyLanguage();
      renderSummary();
      renderTabs();
      renderRows();
    }

    document.getElementById("search").addEventListener("input", (event) => {
      state.query = event.target.value;
      renderRows();
    });
    document.getElementById("tabs").addEventListener("click", (event) => {
      const button = event.target.closest("button[data-type]");
      if (!button) return;
      state.selectedType = button.dataset.type;
      state.selectedId = null;
      render();
    });
    document.getElementById("rows").addEventListener("click", (event) => {
      const button = event.target.closest("button[data-id]");
      if (!button) return;
      state.selectedId = button.dataset.id;
      renderRows();
    });

    state.payload = toolOutput();
    render();
  </script>
</body>
</html>

SHA-256: 5604e25936a623c5bdfee7d2a174315a7535f8770cf1c9dd74f30e6db1b7e307