syntheogenesis / dee /static /assistant.html
Tengo Gzirishvili
Highlight mutated codons in red on generated variant DNA
d748f97
Raw
History Blame Contribute Delete
33.1 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TuringDNA Β· Assistant</title>
<meta name="description" content="TuringDNA Assistant β€” a conversational orchestrator for the Design β†’ Build β†’ Edit β†’ Learn loop. Talk to the engine; it runs your tools." />
<meta name="theme-color" content="#1b1a17" />
<meta name="color-scheme" content="dark light" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<meta name="robots" content="noindex,follow" />
<!-- Theme bootstrap β€” external (not inline) so it clears the site CSP
(script-src 'self' 'nonce-…' refuses un-nonced inline scripts on a
statically-served page). Loaded synchronously in <head> BEFORE app.css
so <html data-theme> is set before first paint (no flash). -->
<script src="/static/assistant-boot.js?v=20260707-repaint2"></script>
<!-- Reuse the engine's editorial design system verbatim: same tokens, same
quad font stack (Playfair / Source Serif / Inter / JetBrains Mono), same
warm-paper Γ— graphite palette + dark theme. This page only ADDS a
chat-workspace layout scoped under `.asst`. -->
<link rel="stylesheet" href="/static/app.css?v=20260707-paintkick" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
<link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
<style>
/* ══════════════════════════════════════════════════════════════════════
TuringDNA Β· Assistant β€” a conversational orchestrator surface.
Structure mirrors a professional coding agent (Claude Code): a left rail
of recent conversations, a phase-aware header, a reading-width thread of
turns with inline tool-execution cards, and a docked composer.
Every value below is drawn from app.css tokens so it stays in lockstep
with the rest of the OS.
══════════════════════════════════════════════════════════════════════ */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background: var(--bg-app);
color: var(--ink);
font-family: var(--font-ui);
-webkit-font-smoothing: antialiased;
}
.asst {
display: flex;
/* --vh (set in assistant-boot.js, before first paint) is
window.innerHeight-based rather than the vh/dvh unit itself β€”
Safari 15.6.1 was resolving 100dvh taller than this iframe's real
visible box, clipping the composer at the bottom. var(..., 1vh)
falls back to the native unit for the one frame before that
script runs. */
height: calc(var(--vh, 1vh) * 100);
position: relative;
z-index: 1;
}
/* ───────────────────── HISTORY (dropdown, not a rail) ─────────────────
Recent conversations used to live in a permanent left rail β€” a second
sidebar sitting right next to the engine's own tool nav. Now they're a
compact popover anchored under the conversation title: one nav rail
total (the engine's), and switching chats is one click away instead of
a permanent fixture. */
.asst-back {
display: inline-flex; align-items: center; justify-content: center;
width: 26px; height: 26px; flex: none;
color: var(--ink-soft); border-radius: var(--r-2);
transition: background var(--t-fast), color var(--t-fast);
}
.asst-back:hover { color: var(--ink-strong); background: var(--bg-hover); }
.asst-title-btn {
display: flex; align-items: center; gap: 6px; min-width: 0;
padding: 4px 8px 4px 6px; margin-left: -6px;
background: none; border: none; border-radius: var(--r-2);
cursor: pointer; text-align: left;
transition: background var(--t-fast);
}
.asst-title-btn:hover { background: var(--bg-hover); }
.asst-title-chev {
width: 13px; height: 13px; flex: none; color: var(--ink-faint);
transition: transform var(--t-fast) var(--ease);
}
.asst-top-lead.open .asst-title-chev { transform: rotate(180deg); }
.asst-history {
position: absolute; top: calc(100% + 8px); left: 0;
width: 300px; max-width: min(320px, 90vw);
max-height: min(560px, 70vh);
display: flex; flex-direction: column;
background: var(--bg-card);
border: 1px solid var(--line-strong);
border-radius: var(--r-4);
box-shadow: var(--elev-4);
z-index: 30;
opacity: 0; transform: translateY(-6px) scale(0.98);
pointer-events: none;
transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.asst-top-lead.open .asst-history {
opacity: 1; transform: translateY(0) scale(1);
pointer-events: auto;
}
.asst-new {
margin: 8px 8px 4px;
display: flex; align-items: center; gap: 9px;
padding: 9px 11px;
font-family: var(--font-ui);
font-size: 13px; font-weight: 600;
color: var(--ink-strong);
background: var(--bg-app);
border: 1px solid var(--line-strong);
border-radius: var(--r-3);
cursor: pointer;
flex: none;
transition: background var(--t-fast) var(--ease-snap), color var(--t-fast), border-color var(--t-fast);
}
.asst-new:hover { background: var(--hover-fill); color: var(--hover-text); border-color: var(--hover-fill); }
.asst-new svg { width: 15px; height: 15px; }
.asst-chats { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 2px 8px 10px; }
.asst-group-label {
font-family: var(--font-mono);
font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--ink-faint);
padding: 14px 10px 6px;
}
.asst-chat {
position: relative;
display: block;
padding: 8px 30px 8px 11px;
border-radius: var(--r-2);
cursor: pointer;
color: var(--ink-soft);
text-decoration: none;
transition: background var(--t-fast);
}
.asst-chat:hover { background: var(--bg-hover); }
.asst-chat.active { background: var(--bg-hover); }
.asst-chat.active::before {
content: ""; position: absolute; left: 0; top: 7px; bottom: 7px;
width: 2px; border-radius: 2px; background: var(--brand);
}
.asst-chat-title {
font-size: 12.5px; font-weight: 500; color: var(--ink);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.35;
}
.asst-chat.active .asst-chat-title { color: var(--ink-strong); }
.asst-chat-meta {
font-family: var(--font-mono);
font-size: 9.5px; color: var(--ink-faint); margin-top: 2px;
letter-spacing: 0.02em;
}
.asst-chat-del {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
width: 22px; height: 22px; display: none; place-items: center;
border: none; background: transparent; color: var(--ink-faint);
border-radius: var(--r-1); cursor: pointer;
}
.asst-chat:hover .asst-chat-del { display: grid; }
.asst-chat-del:hover { color: var(--danger); background: var(--bg-card); }
/* ─────────────────────────── MAIN ─────────────────────────── */
.asst-main { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
.asst-top {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 13px 24px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--bg-app) 82%, transparent);
backdrop-filter: blur(8px);
}
.asst-top-lead { display: flex; align-items: center; gap: 2px; min-width: 0; position: relative; }
.asst-top-title { min-width: 0; }
.asst-top-title h1 {
margin: 0; font-family: var(--font-ui); font-weight: 600; font-size: 14px; color: var(--ink-strong);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.asst-top-sub {
font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em; color: var(--ink-faint); margin-top: 2px;
}
.asst-top-trail { display: flex; align-items: center; gap: 16px; flex: none; }
.asst-context[hidden] { display: none; }
.asst-context { display: flex; align-items: center; gap: 6px; }
.asst-context-bar {
width: 34px; height: 4px; border-radius: 2px; background: var(--line-strong);
overflow: hidden; display: block;
}
.asst-context-fill { display: block; height: 100%; background: var(--ink-faint); border-radius: 2px; transition: width 0.3s ease; }
.asst-context-pct { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); white-space: nowrap; }
/* Escalation as a conversation fills the model's context window β€” set
on #contextMeter by renderThread(). Under ~70% stays the quiet
default styling above; nothing to warn about yet. */
.asst-context.warn .asst-context-fill { background: var(--warning); }
.asst-context.warn .asst-context-pct { color: var(--warning); }
.asst-context.crit .asst-context-fill { background: var(--danger); }
.asst-context.crit .asst-context-pct { color: var(--danger); }
/* Preview banner β€” flags that Turing isn't wired to a backend yet.
[hidden] override needed: plain author-stylesheet display:flex below
otherwise beats the UA stylesheet's [hidden]{display:none} regardless
of selector specificity, since author origin always wins over UA
origin in the cascade β€” the hidden attribute alone would silently
do nothing here. */
.asst-preview[hidden] { display: none; }
.asst-preview {
display: flex; align-items: center; gap: 9px;
padding: 8px 24px;
background: color-mix(in srgb, var(--brand) 9%, var(--bg-app));
border-bottom: 1px solid var(--line);
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.01em;
color: var(--ink-soft);
}
.asst-preview b { color: var(--ink-strong); font-weight: 600; }
.asst-preview-dot {
width: 6px; height: 6px; border-radius: 50%; flex: none;
background: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);
}
.asst-preview-txt { min-width: 0; }
/* Embed mode (?embed=1) β€” panel lives inside the engine's #turing view, so
its own ambient backdrop + "back to engine" link are dropped (the engine
already provides brand, nav, topbar, and a way back β€” it's the page
you're already on). Standalone mode keeps them for direct-URL access. */
html.asst-embed .bg-atmos { display: none; }
html.asst-embed .asst-back { display: none; }
/* Phase rail β€” the Design β†’ Build β†’ Edit β†’ Learn loop, always visible. */
.asst-phases { display: flex; align-items: center; gap: 0; flex: none; }
.asst-phase {
display: flex; flex-direction: column; align-items: center; gap: 2px;
padding: 4px 13px; position: relative;
}
.asst-phase + .asst-phase::before {
content: ""; position: absolute; left: -1px; top: 50%; width: 14px; height: 1px;
background: var(--line-strong); transform: translate(-50%, 6px);
}
.asst-phase-num { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.1em; color: var(--ink-faint); }
.asst-phase-name {
font-family: var(--font-ui); font-size: 11px; font-weight: 600; color: var(--ink-faint);
padding: 2px 8px; border-radius: 999px; transition: all var(--t-base) var(--ease);
}
.asst-phase.on .asst-phase-name { color: var(--hover-text); background: var(--brand); }
.asst-phase.on .asst-phase-num { color: var(--ink); }
/* Thread */
.asst-thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; scroll-behavior: smooth; }
.asst-thread-inner { max-width: 48rem; margin: 0 auto; padding: 30px 24px 20px; }
.asst-turn { margin-bottom: 26px; }
.asst-role {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--ink-faint); margin-bottom: 8px; display: flex; align-items: center; gap: 7px;
}
.asst-role .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand); }
.asst-body {
font-family: var(--font-body); font-size: 15.5px; line-height: 1.62; color: var(--ink);
}
.asst-body p { margin: 0 0 0.7em; }
.asst-body p:last-child { margin-bottom: 0; }
.asst-body p.asst-h { font-weight: 600; color: var(--ink-strong); margin: 0.9em 0 0.35em; }
.asst-body p.asst-h:first-child { margin-top: 0; }
.asst-body ul { margin: 0 0 0.7em; padding-left: 1.3em; }
.asst-body ul:last-child { margin-bottom: 0; }
.asst-body li { margin: 0 0 0.25em; }
.asst-body code { font-family: var(--font-mono); font-size: 0.86em; background: var(--bg-hover); padding: 1px 5px; border-radius: var(--r-1); overflow-wrap: anywhere; word-break: break-word; }
/* Fenced code block that wasn't long enough to collapse into .asst-seq
(see fencedBlock() in assistant.js) β€” still wraps instead of forcing
the message wide, same fix as the .asst-body code rule above. */
.asst-code {
margin: 6px 0; padding: 10px 12px;
background: var(--bg-hover); border: 1px solid var(--line); border-radius: var(--r-2);
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; color: var(--ink-soft);
white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
}
/* User turn β€” a discrete right-offset card, ink-forward. */
.asst-turn.user { display: flex; justify-content: flex-end; }
.asst-turn.user .asst-bubble {
max-width: 84%;
background: var(--bg-card);
border: 1px solid var(--line);
border-radius: var(--r-4);
padding: 11px 15px;
font-family: var(--font-body); font-size: 15px; line-height: 1.55; color: var(--ink-strong);
box-shadow: var(--elev-1);
}
/* A long pasted sequence collapses into this instead of running
straight into the surrounding prose as one unreadable wall of text
(reported 2026-07-11). */
.asst-seq {
margin: 6px 0; padding: 8px 10px;
background: var(--bg-hover); border: 1px solid var(--line); border-radius: var(--r-2);
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.asst-seq.expanded { flex-direction: column; align-items: stretch; }
.asst-seq-preview {
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.5;
color: var(--ink-soft); word-break: break-all; flex: 1; min-width: 0;
}
/* The mutated codon(s) in a generated variant sequence β€” NOT a raw diff
against WT (see the long comment on mutated_positions in
agent_tools.py), so this only ever appears on real, known changes. */
.asst-seq-mut {
background: none; color: var(--danger); font-weight: 700;
}
.asst-seq-meta { font-size: 10.5px; font-family: var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
.asst-seq-toggle, .asst-seq-copy, .asst-seq-dl {
font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.03em;
color: var(--brand-light); background: none; border: none; cursor: pointer;
padding: 2px 6px; border-radius: var(--r-1); white-space: nowrap;
}
.asst-seq-toggle:hover, .asst-seq-copy:hover, .asst-seq-dl:hover { background: var(--bg-card); color: var(--ink-strong); }
/* Copy/download sit slightly quieter than the primary Show-full toggle
until hovered β€” three same-weight actions in a row reads noisier
than one primary + two secondary. */
.asst-seq-copy, .asst-seq-dl { color: var(--ink-faint); }
/* Tool-execution card β€” the "it runs your tools" moment. */
.asst-tool {
margin: 12px 0 2px;
border: 1px solid var(--line-strong);
border-left: 2px solid var(--brand);
border-radius: var(--r-3);
background: var(--bg-card);
overflow: hidden;
}
.asst-tool-head {
display: flex; align-items: center; gap: 9px;
padding: 9px 13px;
font-family: var(--font-mono); font-size: 11px; color: var(--ink-strong);
border-bottom: 1px solid var(--line);
}
.asst-tool-head .glyph { color: var(--brand-light); }
.asst-tool-name { font-weight: 600; letter-spacing: 0.02em; }
.asst-tool-status {
margin-left: auto; font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--success); display: inline-flex; align-items: center; gap: 5px;
}
.asst-tool-status .tick { width: 5px; height: 5px; border-radius: 50%; background: var(--success); }
.asst-tool-status.failed { color: var(--danger); }
.asst-tool-status.failed .tick { background: var(--danger); }
.asst-tool-body { padding: 10px 13px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.6; color: var(--ink-soft); }
.asst-tool-body b { color: var(--ink-strong); font-weight: 600; }
.asst-tool-body .kv { display: flex; justify-content: space-between; gap: 16px; padding: 1px 0; }
/* Compact preview list under the summary stats β€” top guides/pairs/
variants a tool actually produced, so "the engineering" reads as a
real result, not just aggregate numbers. */
.asst-tool-items { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.asst-tool-item { display: flex; justify-content: space-between; gap: 12px; padding: 2px 0; }
.asst-tool-item span:first-child { color: var(--ink-strong); }
/* One per real, orderable sequence a tool produced (e.g. each DE
variant's DNA) β€” a label line + the same seqBlock used for pasted
sequences elsewhere, so copy/download/expand all work identically. */
.asst-tool-seq { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.asst-tool-seq:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.asst-tool-seq-label { color: var(--ink-strong); font-size: 11px; margin-bottom: 4px; }
.asst-tool-seq .asst-seq { margin: 0; }
/* System / offline notice, rendered as a quiet inline line. */
.asst-sys {
font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint);
border: 1px dashed var(--line-strong); border-radius: var(--r-3);
padding: 9px 13px; margin: 4px 0; line-height: 1.55;
}
/* Daily-limit / upgrade card β€” shown when the message budget is spent. */
.asst-limit {
border: 1px solid var(--line-strong); border-radius: var(--r-4);
background: var(--bg-card); padding: 15px 16px; box-shadow: var(--elev-1);
}
.asst-limit-t { font-family: var(--font-body); font-size: 15px; color: var(--ink-strong); }
.asst-limit-s { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-faint); margin-top: 4px; letter-spacing: 0.02em; }
.asst-limit-cta {
display: inline-block; margin-top: 12px; padding: 8px 14px;
font-family: var(--font-ui); font-size: 12.5px; font-weight: 600; text-decoration: none;
color: var(--hover-text); background: var(--brand); border-radius: var(--r-3);
transition: background var(--t-fast);
}
.asst-limit-cta:hover { background: var(--hover-fill); }
/* Thinking indicator β€” a caption cycles through workflow-flavored steps
(guessTrack() in assistant.js) alongside the pulse dots, so the wait
reads as "the engine is working through Design/Build/Edit/Learn"
instead of a content-free spinner. */
.asst-thinking { display: flex; align-items: center; gap: 9px; }
.asst-think { display: inline-flex; gap: 5px; align-items: center; padding: 2px 0; flex: none; }
.asst-think span { width: 5px; height: 5px; border-radius: 50%; background: var(--ink-faint); animation: asstpulse 1.1s var(--ease) infinite; }
.asst-think span:nth-child(2) { animation-delay: 0.15s; }
.asst-think span:nth-child(3) { animation-delay: 0.30s; }
.asst-think-caption {
font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-faint); letter-spacing: 0.01em;
}
@keyframes asstpulse { 0%,100% { opacity: 0.25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-2px); } }
@media (prefers-reduced-motion: reduce) { .asst-think span { animation: none; opacity: 0.6; } }
/* Pulses the currently-active phase pill while a request is in flight β€”
the existing Design β†’ Build β†’ Edit β†’ Learn rail doubling as a live
workflow indicator instead of only reflecting the last-completed
phase. */
#phaseRail.busy .asst-phase.on .asst-phase-name { animation: asstphasebusy 1.3s var(--ease) infinite; }
@keyframes asstphasebusy { 0%,100% { opacity: 1; } 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) { #phaseRail.busy .asst-phase.on .asst-phase-name { animation: none; } }
/* Empty / welcome state */
.asst-empty { max-width: 44rem; margin: 0 auto; padding: 8vh 24px 0; text-align: center; }
.asst-empty h2 {
font-family: var(--font-display); font-style: italic; font-weight: 400;
font-size: clamp(26px, 4.4vw, 40px); line-height: 1.1; color: var(--ink-strong); margin: 0 0 10px;
}
.asst-empty p {
font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.02em; color: var(--ink-faint);
margin: 0 auto 30px; max-width: 30rem; line-height: 1.7;
}
.asst-chips { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 34rem; margin: 0 auto; }
.asst-chip {
text-align: left; cursor: pointer;
background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--r-4);
padding: 14px 15px; transition: border-color var(--t-base), transform var(--t-base) var(--ease), box-shadow var(--t-base);
}
.asst-chip:hover { border-color: var(--line-bold); transform: translateY(-1px); box-shadow: var(--elev-2); }
.asst-chip .k { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--brand-light); }
.asst-chip .t { display: block; font-family: var(--font-body); font-size: 14px; color: var(--ink-strong); margin-top: 5px; line-height: 1.35; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Composer */
.asst-composer { border-top: 1px solid var(--line); padding: 14px 24px 16px; background: var(--bg-app); }
.asst-composer-inner { max-width: 48rem; margin: 0 auto; }
.asst-field {
display: flex; align-items: flex-end; gap: 10px;
background: var(--bg-card); border: 1px solid var(--line-strong); border-radius: var(--r-4);
padding: 8px 8px 8px 14px; box-shadow: var(--elev-1);
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.asst-field:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-ring); }
.asst-input {
flex: 1 1 auto; resize: none; border: none; outline: none; background: transparent;
font-family: var(--font-body); font-size: 15px; line-height: 1.5; color: var(--ink-strong);
max-height: 180px; padding: 6px 0; min-height: 24px;
}
.asst-input::placeholder { color: var(--ink-faint); }
.asst-send {
flex: none; width: 34px; height: 34px; border-radius: var(--r-3); border: none; cursor: pointer;
background: var(--brand); color: var(--hover-text); display: grid; place-items: center;
transition: background var(--t-fast), opacity var(--t-fast);
}
.asst-send:hover { background: var(--hover-fill); }
.asst-send:disabled { opacity: 0.4; cursor: default; }
.asst-send svg { width: 16px; height: 16px; }
.asst-hint {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.05em; color: var(--ink-faint);
text-align: center; margin-top: 9px;
}
.asst-hint b { color: var(--ink-soft); font-weight: 600; }
.asst-char-count[hidden] { display: none; }
.asst-char-count {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.05em; color: var(--ink-faint);
text-align: right; margin-top: 6px;
}
.asst-char-count.warn { color: var(--warning); }
.asst-char-count.over { color: var(--danger); font-weight: 600; }
/* Narrow widths: the phase rail is dropped (no room) and the thread/
composer get side padding back (no rail to butt up against). The
history dropdown needs no special-casing β€” its max-width already caps
at 90vw, so it never overflows a narrow screen. */
@media (max-width: 720px) {
.asst-phases { display: none; }
.asst-thread-inner, .asst-composer-inner { padding-left: 16px; padding-right: 16px; }
}
@media (prefers-reduced-motion: reduce) {
.asst-history, .asst-title-chev { transition: none; }
}
</style>
</head>
<body>
<!-- Same ambient backdrop as the engine (dark-mode only), for continuity. -->
<div class="bg-atmos lit" aria-hidden="true"></div>
<div class="asst" id="asst">
<!-- ══════════════════════ MAIN ══════════════════════ -->
<section class="asst-main">
<!-- Preview banner β€” hidden unless assistant.js's PREVIEW_MODE is
true (it un-hides this itself; see the Boot section there). -->
<div class="asst-preview" id="previewBar" role="status" hidden>
<span class="asst-preview-dot" aria-hidden="true"></span>
<span class="asst-preview-txt"><b>Preview</b> β€” Turing isn't live yet. Browse the example sessions to see how it runs your tools; the composer is disabled for now.</span>
</div>
<header class="asst-top">
<div class="asst-top-lead" id="topLead">
<!-- Standalone-mode only (embed hides this β€” the engine IS
the way back). -->
<a class="asst-back" href="/" aria-label="Back to engine">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
</a>
<!-- Conversation title doubles as the history trigger β€” recent
chats used to live in a permanent rail beside this one;
now they're a popover so there's only ONE nav rail
total (the engine's) instead of two side by side. -->
<button class="asst-title-btn" id="titleBtn" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="historyMenu">
<div class="asst-top-title">
<h1 id="topTitle">New conversation</h1>
<div class="asst-top-sub" id="topSub">Turing Β· the engine runs your tools</div>
</div>
<svg class="asst-title-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="asst-history" id="historyMenu" role="menu" aria-label="Conversations">
<button class="asst-new" id="newChat" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
New conversation
</button>
<nav class="asst-chats" id="chatList" aria-label="Recent conversations"></nav>
</div>
</div>
<!-- .asst-top is a 2-item space-between flex row (lead / this
group) β€” phase rail + context meter share this ONE
wrapper so adding the meter doesn't turn it into a
3-item row (which would break the existing spacing,
and .asst-phases is display:none on mobile with no
fallback layout for a stray third child). -->
<div class="asst-top-trail">
<div class="asst-phases" id="phaseRail" aria-label="Design, Build, Edit, Learn loop">
<div class="asst-phase" data-phase="Design"><span class="asst-phase-num">Β§01</span><span class="asst-phase-name">Design</span></div>
<div class="asst-phase" data-phase="Build"><span class="asst-phase-num">Β§02</span><span class="asst-phase-name">Build</span></div>
<div class="asst-phase" data-phase="Edit"><span class="asst-phase-num">Β§03</span><span class="asst-phase-name">Edit</span></div>
<div class="asst-phase" data-phase="Learn"><span class="asst-phase-num">Β§04</span><span class="asst-phase-name">Learn</span></div>
</div>
<!-- Hidden until a chat has an actual token count (i.e.
after the first real reply). Escalates color via
.warn/.crit classes as usage climbs, set in
renderThread(). -->
<div class="asst-context" id="contextMeter" title="How much of the model's context window this conversation has used" hidden>
<span class="asst-context-bar"><span class="asst-context-fill" id="contextFill"></span></span>
<span class="asst-context-pct" id="contextPct"></span>
</div>
</div>
</header>
<div class="asst-thread" id="thread"><div class="asst-thread-inner" id="threadInner"></div></div>
<!-- Deliberately NOT aria-live on #thread itself β€” it fully
rebuilds via innerHTML on every render, including every
tick of the typewriter reveal, which would have a screen
reader re-announce the whole conversation dozens of times
per reply. This is a separate, silent-until-used region
announced once, when a reply finishes revealing. -->
<div class="sr-only" id="liveRegion" role="status" aria-live="polite"></div>
<footer class="asst-composer">
<div class="asst-composer-inner">
<div class="asst-field">
<textarea class="asst-input" id="input" rows="1" placeholder="Describe what you want to engineer β€” the assistant will run the right tools…"></textarea>
<button class="asst-send" id="send" type="button" aria-label="Send" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 11l5-5 5 5M12 6v13"/></svg>
</button>
</div>
<!-- Server hard-caps a message at 4,000 chars (was a
SILENT truncation β€” a pasted plasmid could get cut
off mid-sequence with zero indication, which is
exactly the kind of thing that can't happen in a
tool feeding real lab decisions). Hidden until the
user is actually getting close, via autoGrow(). -->
<div class="asst-char-count" id="charCount" hidden></div>
<div class="asst-hint" id="composerHint">Enter to send Β· Shift+Enter for a new line Β· the assistant routes across <b>Design</b> Β· <b>Build</b> Β· <b>Edit</b> Β· <b>Learn</b></div>
</div>
</footer>
</section>
</div>
<!-- Main client β€” external for CSP (see assistant-boot.js note). `defer`
runs it after the DOM is parsed. -->
<script src="/static/assistant.js?v=20260712-redmut1" defer></script>
</body>
</html>