<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{ title or app_name }}</title>
    <link rel="icon" type="image/png" href="/static/favicon.png" />
    <link rel="apple-touch-icon" href="/static/favicon.png" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap"
      rel="stylesheet"
    />
    <style>
      :root {
        --bg-0: #070707;
        --bg-1: #0c0c0d;
        --bg-2: #131114;
        --surface-0: rgba(18, 17, 19, 0.9);
        --surface-1: rgba(24, 22, 25, 0.92);
        --surface-2: rgba(34, 28, 31, 0.9);
        --line: rgba(255, 255, 255, 0.08);
        --line-soft: rgba(255, 255, 255, 0.04);
        --ink: #f1efef;
        --ink-soft: rgba(241, 239, 239, 0.74);
        --danger: #ff7070;
        --ok: #98e2b5;
        --accent: #8f1f2d;
        --accent-strong: #b12a3d;
        --accent-soft: rgba(177, 42, 61, 0.2);
        --shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
        --radius-xl: 20px;
        --radius-lg: 14px;
        --radius-md: 10px;
      }

      * {
        box-sizing: border-box;
      }

      html,
      body {
        margin: 0;
        padding: 0;
        min-height: 100%;
      }

      body {
        font-family: "Space Grotesk", "Segoe UI", sans-serif;
        color: var(--ink);
        background:
          radial-gradient(circle at 8% 0%, rgba(115, 20, 34, 0.32), transparent 38%),
          radial-gradient(circle at 98% 0%, rgba(95, 18, 30, 0.26), transparent 40%),
          radial-gradient(circle at 45% 120%, rgba(74, 14, 22, 0.26), transparent 44%),
          linear-gradient(180deg, var(--bg-1), var(--bg-0) 60%);
      }

      a {
        color: inherit;
        text-decoration: none;
      }

      .app-shell {
        display: grid;
        grid-template-columns: 270px 1fr;
        min-height: 100vh;
      }

      .sidebar {
        border-right: 1px solid var(--line-soft);
        background:
          linear-gradient(180deg, rgba(23, 18, 20, 0.92), rgba(11, 10, 11, 0.95)),
          var(--surface-0);
        padding: 22px 16px;
        position: sticky;
        top: 0;
        height: 100vh;
      }

      .brand {
        font-size: 1.15rem;
        font-weight: 700;
        letter-spacing: 0.03em;
        margin-bottom: 4px;
      }

      .brand-sub {
        color: var(--ink-soft);
        font-size: 0.82rem;
        margin-bottom: 18px;
      }

      .nav-group {
        display: grid;
        gap: 8px;
      }

      .nav-item {
        border: 1px solid var(--line-soft);
        border-radius: var(--radius-md);
        padding: 10px 12px;
        background: rgba(255, 255, 255, 0.02);
        color: var(--ink-soft);
        transition: 180ms ease;
      }

      .nav-item:hover {
        border-color: rgba(177, 42, 61, 0.45);
        color: var(--ink);
        background: rgba(177, 42, 61, 0.14);
        box-shadow: 0 0 0 1px rgba(177, 42, 61, 0.12) inset;
      }

      .nav-item.active {
        color: #fff;
        border-color: rgba(177, 42, 61, 0.65);
        background: linear-gradient(135deg, rgba(177, 42, 61, 0.26), rgba(90, 20, 31, 0.36));
        box-shadow: 0 10px 24px rgba(104, 23, 36, 0.4);
      }

      .nav-caption {
        margin-top: 22px;
        color: var(--ink-soft);
        font-size: 0.76rem;
        text-transform: uppercase;
        letter-spacing: 0.08em;
      }

      .nav-meta {
        margin-top: 8px;
        color: var(--ink-soft);
        font-size: 0.84rem;
        line-height: 1.45;
      }

      .main {
        padding: 24px;
      }

      .topbar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        border: 1px solid var(--line);
        background: linear-gradient(180deg, rgba(33, 28, 31, 0.84), rgba(20, 18, 20, 0.84));
        border-radius: var(--radius-lg);
        padding: 14px 16px;
        box-shadow: var(--shadow);
        margin-bottom: 16px;
      }

      .topbar h1 {
        margin: 0;
        font-size: 1.02rem;
      }

      .topbar-meta {
        display: flex;
        gap: 8px;
        align-items: center;
      }

      .chip {
        border: 1px solid var(--line);
        border-radius: 999px;
        padding: 4px 10px;
        font-size: 0.77rem;
        color: var(--ink-soft);
        background: rgba(255, 255, 255, 0.02);
      }

      .mono {
        font-family: "IBM Plex Mono", monospace;
      }

      .banner {
        margin-bottom: 12px;
        border-radius: var(--radius-md);
        padding: 10px 12px;
        font-size: 0.9rem;
      }

      .banner.ok {
        border: 1px solid rgba(120, 196, 148, 0.45);
        background: rgba(34, 74, 48, 0.36);
        color: #b6ebca;
      }

      .banner.error {
        border: 1px solid rgba(255, 112, 112, 0.45);
        background: rgba(96, 21, 29, 0.38);
        color: #ffb7b7;
      }

      .grid {
        display: grid;
        gap: 14px;
      }

      .two-col {
        grid-template-columns: minmax(280px, 360px) 1fr;
      }

      .card {
        border: 1px solid var(--line);
        border-radius: var(--radius-lg);
        padding: 16px;
        background:
          linear-gradient(180deg, rgba(33, 27, 30, 0.72), rgba(20, 18, 19, 0.85)),
          var(--surface-1);
        box-shadow: var(--shadow);
      }

      .card h2,
      .card h3 {
        margin: 0 0 10px;
        font-size: 1.02rem;
      }

      .kicker {
        margin-bottom: 8px;
        font-size: 0.72rem;
        text-transform: uppercase;
        letter-spacing: 0.1em;
        color: var(--ink-soft);
      }

      .muted {
        color: var(--ink-soft);
      }

      .field-grid {
        display: grid;
        gap: 10px;
      }

      .field-row {
        display: grid;
        gap: 10px;
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }

      label {
        display: grid;
        gap: 6px;
        font-size: 0.85rem;
        color: var(--ink-soft);
      }

      .toggle-row {
        display: flex;
        align-items: center;
        gap: 10px;
        min-height: 38px;
        margin: 0;
        padding: 8px 12px;
        border: 1px solid var(--line);
        border-radius: var(--radius-md);
        background: rgba(255, 255, 255, 0.03);
      }

      .toggle-row input[type="checkbox"],
      .toggle-control {
        -webkit-appearance: none;
        appearance: none;
        width: 18px;
        height: 18px;
        margin: 0;
        border: 1px solid rgba(255, 255, 255, 0.36);
        border-radius: 5px;
        background: rgba(0, 0, 0, 0.56);
        display: grid;
        place-content: center;
        cursor: pointer;
        flex: 0 0 auto;
        transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
      }

      .toggle-row input[type="checkbox"]::before,
      .toggle-control::before {
        content: "";
        width: 9px;
        height: 9px;
        border-radius: 2px;
        background: #d84f4f;
        transform: scale(0);
        transition: transform 160ms ease;
      }

      .toggle-row input[type="checkbox"]:checked,
      .toggle-control:checked {
        border-color: rgba(221, 90, 90, 0.85);
        background: rgba(120, 14, 24, 0.48);
      }

      .toggle-row input[type="checkbox"]:checked::before,
      .toggle-control:checked::before {
        transform: scale(1);
      }

      .toggle-row input[type="checkbox"]:focus-visible,
      .toggle-control:focus-visible {
        outline: none;
        box-shadow: 0 0 0 2px rgba(221, 90, 90, 0.22);
      }

      .toggle-label {
        margin: 0;
        color: rgba(241, 239, 239, 0.82);
        font-size: 0.84rem;
        line-height: 1.35;
        cursor: pointer;
      }

      input,
      select,
      textarea {
        width: 100%;
        border: 1px solid var(--line);
        border-radius: var(--radius-md);
        background: rgba(7, 7, 8, 0.6);
        color: var(--ink);
        font: inherit;
        padding: 9px 10px;
        transition: 170ms ease;
      }

      input:focus,
      select:focus,
      textarea:focus {
        outline: none;
        border-color: rgba(177, 42, 61, 0.65);
        box-shadow: 0 0 0 3px rgba(177, 42, 61, 0.18), 0 0 30px rgba(177, 42, 61, 0.16);
      }

      textarea {
        min-height: 90px;
        resize: vertical;
      }

      .btn-row {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
      }

      .btn {
        border: 1px solid rgba(177, 42, 61, 0.52);
        color: #ffdce0;
        background: linear-gradient(135deg, rgba(140, 31, 45, 0.45), rgba(77, 15, 25, 0.5));
        border-radius: 999px;
        padding: 8px 14px;
        font-size: 0.85rem;
        font-family: inherit;
        cursor: pointer;
        transition: 180ms ease;
      }

      .btn:hover {
        transform: translateY(-1px);
        box-shadow: 0 10px 24px rgba(116, 22, 35, 0.36);
      }

      .btn.secondary {
        border-color: var(--line);
        background: rgba(255, 255, 255, 0.03);
        color: var(--ink-soft);
      }

      .btn.danger {
        border-color: rgba(255, 109, 109, 0.5);
        background: rgba(125, 28, 38, 0.45);
        color: #ffc0c0;
      }

      .metric {
        font-size: 2.3rem;
        font-weight: 700;
        margin: 4px 0;
      }

      .pill {
        display: inline-block;
        padding: 3px 9px;
        border-radius: 999px;
        border: 1px solid var(--line);
        background: rgba(255, 255, 255, 0.03);
        font-size: 0.76rem;
        color: var(--ink-soft);
      }

      .table-wrap {
        overflow-x: auto;
      }

      table {
        width: 100%;
        border-collapse: collapse;
        font-size: 0.88rem;
      }

      th,
      td {
        text-align: left;
        padding: 9px 8px;
        border-bottom: 1px solid var(--line-soft);
        vertical-align: top;
      }

      th {
        color: var(--ink-soft);
        font-weight: 600;
      }

      .list {
        display: grid;
        gap: 9px;
      }

      .list-item {
        border: 1px solid var(--line-soft);
        border-radius: var(--radius-md);
        padding: 10px;
        background: rgba(255, 255, 255, 0.02);
        transition: 150ms ease;
      }

      .list-item:hover {
        border-color: rgba(177, 42, 61, 0.35);
        background: rgba(177, 42, 61, 0.12);
      }

      .report-html h1,
      .report-html h2,
      .report-html h3 {
        margin-top: 1.1em;
      }

      .report-html p,
      .report-html li {
        line-height: 1.55;
        color: var(--ink);
      }

      .assistant-toggle {
        position: fixed;
        right: 20px;
        bottom: 20px;
        z-index: 100;
        border: 1px solid rgba(177, 42, 61, 0.58);
        border-radius: 999px;
        background: linear-gradient(135deg, rgba(150, 35, 49, 0.88), rgba(91, 21, 31, 0.92));
        color: #ffecef;
        font-family: inherit;
        font-size: 0.85rem;
        padding: 10px 14px;
        cursor: pointer;
        box-shadow: 0 14px 30px rgba(71, 14, 23, 0.44);
      }

      .assistant-panel {
        position: fixed;
        right: 20px;
        bottom: 70px;
        width: min(420px, calc(100vw - 24px));
        height: min(560px, calc(100vh - 110px));
        border: 1px solid rgba(177, 42, 61, 0.38);
        border-radius: 18px;
        background:
          radial-gradient(circle at 100% 0%, rgba(114, 21, 33, 0.28), transparent 40%),
          linear-gradient(180deg, rgba(26, 20, 23, 0.96), rgba(13, 12, 13, 0.97));
        box-shadow: 0 22px 60px rgba(0, 0, 0, 0.62);
        display: grid;
        grid-template-rows: auto auto 1fr auto;
        overflow: hidden;
        z-index: 100;
        opacity: 0;
        transform: translateY(10px);
        pointer-events: none;
        transition: 170ms ease;
      }

      .assistant-panel.open {
        opacity: 1;
        transform: translateY(0);
        pointer-events: auto;
      }

      .assistant-head {
        border-bottom: 1px solid var(--line-soft);
        padding: 12px 12px 10px;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }

      .assistant-head h3 {
        margin: 0;
        font-size: 0.95rem;
      }

      .assistant-status {
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 0.77rem;
        color: var(--ink-soft);
      }

      .status-dot {
        width: 8px;
        height: 8px;
        border-radius: 999px;
        background: #ff7b8d;
        box-shadow: 0 0 14px rgba(255, 123, 141, 0.7);
      }

      .assistant-state {
        border-bottom: 1px solid var(--line-soft);
        padding: 8px 12px;
        font-size: 0.77rem;
        color: var(--ink-soft);
      }

      .assistant-log {
        overflow-y: auto;
        padding: 12px;
        display: grid;
        gap: 8px;
      }

      .msg {
        border: 1px solid var(--line-soft);
        border-radius: 12px;
        padding: 9px 10px;
        font-size: 0.86rem;
        line-height: 1.5;
      }

      .msg.user {
        background: rgba(177, 42, 61, 0.23);
        border-color: rgba(177, 42, 61, 0.45);
      }

      .msg.assistant {
        background: rgba(255, 255, 255, 0.03);
      }

      .msg .msg-role {
        display: block;
        margin-bottom: 4px;
        color: var(--ink-soft);
        font-size: 0.73rem;
        text-transform: uppercase;
        letter-spacing: 0.08em;
      }

      .msg .msg-body p,
      .msg .msg-body blockquote,
      .msg .msg-body pre,
      .msg .msg-body ul,
      .msg .msg-body ol {
        margin: 0.55em 0;
      }

      .msg .msg-body p:first-child,
      .msg .msg-body blockquote:first-child,
      .msg .msg-body pre:first-child,
      .msg .msg-body ul:first-child,
      .msg .msg-body ol:first-child {
        margin-top: 0;
      }

      .msg .msg-body p:last-child,
      .msg .msg-body blockquote:last-child,
      .msg .msg-body pre:last-child,
      .msg .msg-body ul:last-child,
      .msg .msg-body ol:last-child {
        margin-bottom: 0;
      }

      .msg .msg-body h1,
      .msg .msg-body h2,
      .msg .msg-body h3,
      .msg .msg-body h4,
      .msg .msg-body h5,
      .msg .msg-body h6 {
        margin: 0.45em 0;
        line-height: 1.25;
      }

      .msg .msg-body ul,
      .msg .msg-body ol {
        padding-left: 20px;
      }

      .msg .msg-body pre {
        border: 1px solid var(--line);
        border-radius: 9px;
        padding: 10px;
        overflow-x: auto;
        background: rgba(0, 0, 0, 0.45);
      }

      .msg .msg-body code {
        font-family: "IBM Plex Mono", monospace;
        font-size: 0.82em;
        border: 1px solid rgba(255, 255, 255, 0.08);
        border-radius: 6px;
        background: rgba(255, 255, 255, 0.04);
        padding: 1px 5px;
      }

      .msg .msg-body pre code {
        border: none;
        border-radius: 0;
        background: transparent;
        padding: 0;
      }

      .msg .msg-body blockquote {
        border-left: 2px solid rgba(255, 255, 255, 0.18);
        padding-left: 10px;
        color: var(--ink-soft);
      }

      .msg .msg-body a {
        color: #f6c6cd;
        text-decoration: underline;
      }

      .msg .msg-sources {
        margin: 8px 0 0;
        padding-left: 18px;
      }

      .msg-word {
        display: inline-block;
        opacity: 0;
        transform: translateY(8px);
        filter: blur(4px);
        transition: opacity 320ms ease, transform 320ms ease, filter 320ms ease;
      }

      .msg-word.reveal {
        opacity: 1;
        transform: translateY(0);
        filter: blur(0);
      }

      .assistant-form {
        border-top: 1px solid var(--line-soft);
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 8px;
        padding: 10px;
      }

      .assistant-form input {
        min-width: 0;
      }

      .assistant-form button {
        white-space: nowrap;
      }

      .hidden {
        display: none !important;
      }

      @media (max-width: 1160px) {
        .app-shell {
          grid-template-columns: 1fr;
        }

        .sidebar {
          position: static;
          height: auto;
          border-right: none;
          border-bottom: 1px solid var(--line-soft);
        }
      }

      @media (max-width: 760px) {
        .main {
          padding: 14px;
        }

        .field-row,
        .two-col {
          grid-template-columns: 1fr;
        }

        .assistant-panel {
          right: 12px;
          bottom: 64px;
          width: calc(100vw - 24px);
          height: min(520px, calc(100vh - 88px));
        }
      }
    </style>
  </head>
  <body>
    {% set focus = engagement if engagement is defined and engagement else (selected_engagement if selected_engagement is defined and selected_engagement else None) %}
    {% set current_path = request.url.path if request and request.url else "" %}
    {% set nav_section = "operations" %}
    {% if current_path.startswith("/dashboard/vault") %}
      {% set nav_section = "vault" %}
    {% elif current_path == "/dashboard/report" or current_path.startswith("/dashboard/report/") %}
      {% set nav_section = "reporting" %}
    {% elif current_path == "/dashboard/notes" or current_path.startswith("/dashboard/notes/") %}
      {% set nav_section = "notes" %}
    {% elif current_path.startswith("/dashboard/tasks") %}
      {% set nav_section = "tasks" %}
    {% elif current_path.startswith("/dashboard/config") or current_path.startswith("/dashboard/settings") %}
      {% set nav_section = "config" %}
    {% endif %}
    {% set reporting_href = "/dashboard/report/" ~ focus.id if focus else "/dashboard/report" %}
    {% set notes_href = "/dashboard/notes/" ~ focus.id if focus else "/dashboard/notes" %}
    <div class="app-shell">
      <aside class="sidebar">
        <div class="brand">{{ app_name }}</div>
        <div class="brand-sub">Operator Console</div>
        <nav class="nav-group">
          <a class="nav-item {% if nav_section == 'operations' %}active{% endif %}" href="/">Operations</a>
          <a class="nav-item {% if nav_section == 'vault' %}active{% endif %}" href="/dashboard/vault">Vault</a>
          <a class="nav-item {% if nav_section == 'reporting' %}active{% endif %}" href="{{ reporting_href }}">Reporting</a>
          <a class="nav-item {% if nav_section == 'notes' %}active{% endif %}" href="{{ notes_href }}">Notes</a>
          <a class="nav-item {% if nav_section == 'tasks' %}active{% endif %}" href="/dashboard/tasks">Tasks</a>
          <a class="nav-item {% if nav_section == 'config' %}active{% endif %}" href="/dashboard/config">Config</a>
        </nav>
        <div class="nav-caption">Runtime</div>
        <div class="nav-meta mono">API: <a href="/health">/health</a></div>
        {% if focus %}
          <div class="nav-meta">Engagement: <span class="mono">#{{ focus.id }}</span><br />{{ focus.name }}</div>
        {% endif %}
      </aside>

      <main class="main">
        <header class="topbar">
          <h1>{{ title or app_name }}</h1>
          <div class="topbar-meta">
            <span class="chip mono">API + UI</span>
            <span class="chip mono" id="global-voice-phase">voice: idle</span>
            <span class="chip mono" id="global-ws-state">ws: connecting</span>
          </div>
        </header>

        {% if message %}
          <div class="banner ok">{{ message }}</div>
        {% endif %}
        {% if error %}
          <div class="banner error">{{ error }}</div>
        {% endif %}

        {% block content %}{% endblock %}
      </main>
    </div>

    <button id="assistant-toggle" class="assistant-toggle" type="button">Assistant</button>
    <aside id="assistant-panel" class="assistant-panel" aria-label="Assistant panel">
      <div class="assistant-head">
        <h3>Assistant</h3>
        <div class="assistant-status">
          <span id="assistant-dot" class="status-dot"></span>
          <span id="assistant-ws-label" class="mono">connecting</span>
        </div>
      </div>
      <div id="assistant-live-state" class="assistant-state mono">voice_state=idle</div>
      <div id="assistant-log" class="assistant-log"></div>
      <form id="assistant-form" class="assistant-form">
        <input id="assistant-input" type="text" placeholder="Ask about coverage, notes, tasks, or events..." required />
        <button class="btn" type="submit">Send</button>
      </form>
    </aside>

    <script>
      (() => {
        const panel = document.getElementById("assistant-panel")
        const toggle = document.getElementById("assistant-toggle")
        const form = document.getElementById("assistant-form")
        const input = document.getElementById("assistant-input")
        const log = document.getElementById("assistant-log")
        const dot = document.getElementById("assistant-dot")
        const wsLabel = document.getElementById("assistant-ws-label")
        const liveState = document.getElementById("assistant-live-state")
        const globalVoice = document.getElementById("global-voice-phase")
        const globalWs = document.getElementById("global-ws-state")

        let ws = null
        let wsPing = null
        let voiceState = "idle"
        const markdownCache = new Map()

        const parseAssistantText = (raw) => {
          const text = String(raw || "").trim()
          const parts = text.split(/\nSources:\n/i)
          if (parts.length < 2) {
            return { body: text, sources: [] }
          }
          const body = parts[0].trim()
          const tail = parts.slice(1).join("\nSources:\n")
          const sources = tail
            .split("\n")
            .map((line) => line.trim())
            .filter((line) => line.startsWith("- "))
            .map((line) => line.slice(2).trim())
          return { body, sources }
        }

        const escapeHtml = (value) =>
          String(value || "")
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;")

        const renderMarkdownHtml = async (rawText) => {
          const text = String(rawText || "")
          if (!text.trim()) return ""
          if (markdownCache.has(text)) {
            return markdownCache.get(text)
          }
          const renderPromise = fetch("/assistant/render-markdown", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ text }),
          })
            .then(async (response) => {
              const payload = await response.json().catch(() => ({}))
              if (!response.ok) {
                throw new Error(String(payload?.detail || `HTTP ${response.status}`))
              }
              return String(payload?.html || "")
            })
            .catch(() => `<p>${escapeHtml(text)}</p>`)
          markdownCache.set(text, renderPromise)
          return renderPromise
        }

        const tokenizeMarkdownWords = (root, wordClass) => {
          const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
            acceptNode(node) {
              const value = String(node.nodeValue || "")
              if (!value.trim()) return NodeFilter.FILTER_REJECT
              const parent = node.parentElement
              if (parent && parent.closest("pre, code")) return NodeFilter.FILTER_REJECT
              return NodeFilter.FILTER_ACCEPT
            },
          })
          const textNodes = []
          let current = walker.nextNode()
          while (current) {
            textNodes.push(current)
            current = walker.nextNode()
          }

          const words = []
          for (const node of textNodes) {
            if (!node.parentNode) continue
            const fragment = document.createDocumentFragment()
            const tokens = String(node.nodeValue || "").split(/(\s+)/)
            for (const token of tokens) {
              if (!token) continue
              if (/^\s+$/.test(token)) {
                fragment.appendChild(document.createTextNode(token))
                continue
              }
              const span = document.createElement("span")
              span.className = wordClass
              span.textContent = token
              fragment.appendChild(span)
              words.push(span)
            }
            node.parentNode.replaceChild(fragment, node)
          }
          return words
        }

        const renderAssistantMessage = async (target, text) => {
          const clean = String(text || "").trim()
          if (!clean) {
            target.innerHTML = ""
            return
          }
          const rendered = await renderMarkdownHtml(clean)
          if (!target.isConnected) return
          target.innerHTML = rendered
          log.scrollTop = log.scrollHeight
        }

        const addMessage = (role, text, sources = [], typing = false) => {
          const item = document.createElement("article")
          item.className = `msg ${role}`

          const roleLabel = document.createElement("span")
          roleLabel.className = "msg-role"
          roleLabel.textContent = role === "user" ? "Operator" : "Assistant"
          item.appendChild(roleLabel)

          const body = document.createElement("div")
          body.className = "msg-body"
          if (role === "assistant" && !typing) {
            void renderAssistantMessage(body, text)
          } else {
            body.textContent = text
          }
          item.appendChild(body)

          if (sources.length) {
            const ul = document.createElement("ul")
            ul.className = "msg-sources"
            for (const source of sources) {
              const li = document.createElement("li")
              li.className = "mono"
              li.textContent = source
              ul.appendChild(li)
            }
            item.appendChild(ul)
          }

          if (typing) {
            item.dataset.typing = "1"
          }

          log.appendChild(item)
          log.scrollTop = log.scrollHeight
          return item
        }

        const setWsConnected = (connected) => {
          wsLabel.textContent = connected ? "connected" : "offline"
          dot.style.background = connected ? "#98e2b5" : "#ff7b8d"
          dot.style.boxShadow = connected
            ? "0 0 14px rgba(152, 226, 181, 0.65)"
            : "0 0 14px rgba(255, 123, 141, 0.7)"
          globalWs.textContent = `ws: ${connected ? "connected" : "offline"}`
        }

        const setVoiceState = (state) => {
          voiceState = state || "idle"
          liveState.textContent = `voice_state=${voiceState}`
          globalVoice.textContent = `voice: ${voiceState}`
        }

        const hydrateState = async () => {
          try {
            const response = await fetch("/assistant/ws/state", { cache: "no-store" })
            if (!response.ok) return
            const snapshot = await response.json()
            const phase = snapshot.uiPhase || "idle"
            if (phase === "hold") {
              setVoiceState("cooldown")
              return
            }
            setVoiceState(phase === "active" ? "speaking" : "idle")
          } catch {
            // Non-blocking assistant hydration.
          }
        }

        const connectWs = () => {
          const proto = window.location.protocol === "https:" ? "wss" : "ws"
          ws = new WebSocket(`${proto}://${window.location.host}/assistant/ws`)

          ws.addEventListener("open", () => {
            setWsConnected(true)
            hydrateState()
            if (wsPing) clearInterval(wsPing)
            wsPing = setInterval(() => {
              if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send("ping")
              }
            }, 12000)
          })

          ws.addEventListener("message", (event) => {
            let payload = null
            try {
              payload = JSON.parse(event.data)
            } catch {
              return
            }
            if (!payload || typeof payload !== "object") return
            if (payload.type === "state") {
              const state = String(payload.state || "")
              if (state) setVoiceState(state)
            }
          })

          ws.addEventListener("close", () => {
            setWsConnected(false)
            if (wsPing) {
              clearInterval(wsPing)
              wsPing = null
            }
            setTimeout(connectWs, 1500)
          })

          ws.addEventListener("error", () => {
            setWsConnected(false)
            if (ws && ws.readyState <= WebSocket.OPEN) {
              ws.close()
            }
          })
        }

        const revealAssistantText = async (target, text) => {
          const clean = String(text || "").trim()
          if (!clean) {
            target.innerHTML = ""
            return
          }
          const rendered = await renderMarkdownHtml(clean)
          if (!target.isConnected) return
          target.innerHTML = rendered
          const words = tokenizeMarkdownWords(target, "msg-word")
          if (!words.length) {
            log.scrollTop = log.scrollHeight
            return
          }
          let index = 0
          const timer = setInterval(() => {
            if (!target.isConnected) {
              clearInterval(timer)
              return
            }
            words[index].classList.add("reveal")
            index += 1
            if (index >= words.length) clearInterval(timer)
            log.scrollTop = log.scrollHeight
          }, 22)
        }

        toggle.addEventListener("click", () => {
          panel.classList.toggle("open")
          if (panel.classList.contains("open")) {
            input.focus()
          }
        })

        form.addEventListener("submit", async (event) => {
          event.preventDefault()
          const message = input.value.trim()
          if (!message) return

          addMessage("user", message)
          input.value = ""

          const typingBubble = addMessage("assistant", "Thinking...", [], true)

          try {
            const response = await fetch("/assistant/query", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ message }),
            })
            const payload = await response.json()
            if (!response.ok) {
              throw new Error(payload?.detail || `HTTP ${response.status}`)
            }
            const parsed = parseAssistantText(payload?.response || "")
            typingBubble.remove()
            const item = addMessage("assistant", "", parsed.sources)
            const textNode = item.querySelector(".msg-body")
            if (textNode) void revealAssistantText(textNode, parsed.body || "(no response)")
          } catch (error) {
            typingBubble.remove()
            addMessage("assistant", `Request failed: ${error instanceof Error ? error.message : String(error)}`)
          }
        })

        addMessage("assistant", "Assistant connected. Ask for coverage, timeline, notes, or tasks.")
        setWsConnected(false)
        setVoiceState("idle")
        connectWs()
      })()
    </script>
    {% block page_scripts %}{% endblock %}
  </body>
</html>
