chatcad / static /how_it_works_light.html
Samarjithbiswas's picture
chat_cad live (Docker app + weights)
32c7c43 verified
Raw
History Blame Contribute Delete
34.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>How chat_cad works — light theme</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f6f8fb; --bg-card: #ffffff; --border: #e2e6ec;
--text: #15181d; --text-dim: #5a6373;
--blue: #2461c2; --green: #2f9e44; --purple: #7c3aed;
--orange: #f59e0b; --red: #e23636; --gold: #d4a017;
}
body { background: var(--bg); color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
min-height: 100vh; padding: 24px;
display: flex; flex-direction: column; align-items: center; gap: 18px; }
h1 { font-size: 28px; font-weight: 700; color: var(--text);
letter-spacing: -0.4px; }
h1 .accent { background: linear-gradient(90deg, var(--blue), var(--purple));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; }
.sub { font-size: 14px; color: var(--text-dim); margin-top: -10px; }
#ctl { display: flex; gap: 10px; align-items: center; }
button, select { font-size: 13px; padding: 8px 14px; border-radius: 6px;
border: 1px solid var(--border); background: #fff;
color: var(--text); cursor: pointer; font-weight: 500; }
button.primary { background: var(--blue); color: #fff; border-color: var(--blue); }
button.primary:hover { background: #1c4e9e; }
#stage { width: 1180px; height: 660px;
display: grid; grid-template-columns: 1fr 460px; gap: 18px; }
#flow, #workbench { background: var(--bg-card); border-radius: 14px;
border: 1px solid var(--border); position: relative;
box-shadow: 0 4px 20px rgba(20,30,50,0.06); }
/* ---- FLOW (left side) ---- */
#flow { padding: 20px; overflow: hidden; }
#flow h3 { font-size: 12px; color: var(--text-dim); font-weight: 600;
letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 12px; }
.node { position: absolute; padding: 10px 14px; border-radius: 10px;
background: #fafbfd; border: 1.5px solid var(--border);
min-width: 130px; text-align: center;
transition: all 0.4s cubic-bezier(.2,.8,.2,1); }
.node .icn { font-size: 22px; line-height: 1; margin-bottom: 3px; }
.node .ttl { font-size: 12px; font-weight: 700; color: var(--text); }
.node .sub2 { font-size: 9.5px; color: var(--text-dim);
font-family: ui-monospace, monospace; margin-top: 2px; }
.node.active { background: #fff; border-color: var(--glow);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--glow) 18%, transparent),
0 6px 20px color-mix(in srgb, var(--glow) 22%, transparent);
transform: scale(1.05); }
.node.user { left: 24px; top: 240px; --glow: var(--blue); }
.node.chat { left: 184px; top: 240px; --glow: var(--blue); }
.node.router { left: 344px; top: 240px; --glow: var(--orange); }
.node.parser { left: 504px; top: 90px; --glow: var(--green); }
.node.llm { left: 504px; top: 200px; --glow: var(--purple); }
.node.agent { left: 504px; top: 310px; --glow: var(--gold); }
.node.intent { left: 504px; top: 420px; --glow: var(--orange); }
.node.engine { left: 504px; top: 540px; --glow: var(--blue); }
svg.wires { position: absolute; inset: 0; pointer-events: none; }
.wire { stroke: #d4dae3; stroke-width: 2; fill: none;
transition: stroke 0.4s, stroke-width 0.4s; }
.wire.active { stroke: var(--wirecolor); stroke-width: 3;
filter: drop-shadow(0 0 6px color-mix(in srgb, var(--wirecolor) 60%, transparent)); }
.packet { fill: var(--orange); r: 6;
filter: drop-shadow(0 0 8px var(--orange)); }
/* ---- WORKBENCH (right side, where 3D object materializes) ---- */
#workbench { padding: 18px 20px; display: flex; flex-direction: column; gap: 8px; }
#workbench h3 { font-size: 12px; color: var(--text-dim); font-weight: 600;
letter-spacing: 0.08em; text-transform: uppercase; }
#wbtitle { font-size: 16px; font-weight: 700; color: var(--text); }
#wbsub { font-size: 11px; color: var(--text-dim);
font-family: ui-monospace, monospace; margin-bottom: 4px; }
#canvas { flex: 1; background: linear-gradient(180deg, #f3f6fa 0%, #e8edf3 100%);
border-radius: 10px; border: 1px solid var(--border);
position: relative; overflow: hidden; }
#canvas svg { width: 100%; height: 100%; display: block; }
/* the build-up steps fade in one-by-one via opacity transitions */
.step { opacity: 0; transition: opacity 0.55s ease-out; }
.step.on { opacity: 1; }
#steps { font-size: 11px; color: var(--text-dim);
font-family: ui-monospace, monospace; line-height: 1.7;
background: #f6f8fb; border-radius: 6px; padding: 8px 12px;
min-height: 88px; max-height: 88px; overflow-y: auto; }
#steps .line { opacity: 0.35; transition: opacity 0.3s; }
#steps .line.done { opacity: 1; color: var(--green); }
#steps .line.active { opacity: 1; color: var(--blue); font-weight: 600; }
/* ---- bottom ribbon ---- */
#ribbon { display: flex; align-items: center; gap: 14px;
background: #fff; border: 1px solid var(--border);
border-radius: 10px; padding: 10px 18px; width: 1180px;
font-family: ui-monospace, monospace; font-size: 13px; }
#typing { color: var(--blue); }
#typing .cursor { display: inline-block; width: 7px; height: 14px;
background: var(--blue); vertical-align: middle;
margin-left: 2px; animation: blink 1s steps(2) infinite; }
@keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
#status { color: var(--text-dim); flex: 1; text-align: right;
font-family: ui-sans-serif, system-ui; font-size: 12px; }
#legend { display: flex; gap: 12px; flex-wrap: wrap;
font-size: 11px; font-family: ui-monospace, monospace;
max-width: 1180px; }
#legend .pill { padding: 4px 12px; border-radius: 999px;
background: #fff; border: 1px solid var(--border); }
#legend .pill .dot { display: inline-block; width: 8px; height: 8px;
border-radius: 50%; margin-right: 6px;
vertical-align: middle; }
footer { font-size: 11px; color: var(--text-dim); margin-top: 8px; }
</style>
</head>
<body>
<h1>How <span class="accent">chat_cad</span> works</h1>
<div class="sub">Watch how a plain-English request becomes real parametric geometry, step by step.</div>
<div id="ctl">
<button id="play" class="primary">▶ Play</button>
<button id="restart">↻ Restart</button>
<select id="scenario">
<option value="bolt">▣ Build an M8 hex bolt</option>
<option value="nut">▣ Build an M8 hex nut</option>
<option value="bracket">▣ Recognise an L-bracket from English</option>
<option value="stress">▣ Run stress analysis with color plot</option>
</select>
</div>
<div id="stage">
<!-- LEFT: pipeline flowchart -->
<div id="flow">
<h3>Pipeline</h3>
<svg class="wires" viewBox="0 0 600 660" preserveAspectRatio="none">
<path class="wire" id="w_user_chat" d="M 154 270 L 184 270"/>
<path class="wire" id="w_chat_router" d="M 314 270 L 344 270"/>
<path class="wire" id="w_router_parser" d="M 474 270 Q 490 270 490 120 L 504 120"/>
<path class="wire" id="w_router_llm" d="M 474 270 Q 490 270 490 230 L 504 230"/>
<path class="wire" id="w_router_agent" d="M 474 270 Q 490 270 490 340 L 504 340"/>
<path class="wire" id="w_router_intent" d="M 474 270 Q 490 270 490 450 L 504 450"/>
<path class="wire" id="w_parser_engine" d="M 569 150 Q 569 350 569 540"/>
<path class="wire" id="w_llm_engine" d="M 569 260 L 569 540"/>
<path class="wire" id="w_agent_engine" d="M 569 370 L 569 540"/>
<path class="wire" id="w_intent_engine" d="M 569 480 L 569 540"/>
<circle id="packet" class="packet" r="6" cx="0" cy="0" opacity="0"/>
</svg>
<div class="node user"><div class="icn">🧑‍💼</div>
<div class="ttl">You</div><div class="sub2">prompt</div></div>
<div class="node chat"><div class="icn">💬</div>
<div class="ttl">Chat box</div><div class="sub2">browser</div></div>
<div class="node router"><div class="icn">🔀</div>
<div class="ttl">Smart router</div><div class="sub2">classifier</div></div>
<div class="node parser"><div class="icn">⚙️</div>
<div class="ttl">Parser</div><div class="sub2">grammar match</div></div>
<div class="node llm"><div class="icn">🧠</div>
<div class="ttl">LLM</div><div class="sub2">Claude / Gemini</div></div>
<div class="node agent"><div class="icn">🤖</div>
<div class="ttl">Design agent</div><div class="sub2">multi-step loop</div></div>
<div class="node intent"><div class="icn">💡</div>
<div class="ttl">Intent fallback</div><div class="sub2">47 keywords</div></div>
<div class="node engine"><div class="icn">🏗️</div>
<div class="ttl">CadQuery engine</div><div class="sub2">B-rep solid model</div></div>
</div>
<!-- RIGHT: workbench where the object materialises -->
<div id="workbench">
<h3>Workbench</h3>
<div id="wbtitle">M8 Hex Bolt</div>
<div id="wbsub">bolt b1 M8 30</div>
<div id="canvas">
<!-- One SVG with multiple part scenes; only the active one is shown -->
<svg viewBox="0 0 460 380" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="steel" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#dadfe6"/>
<stop offset="0.55" stop-color="#a4adb7"/>
<stop offset="1" stop-color="#6b7280"/>
</linearGradient>
<linearGradient id="steel_dark" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#9aa3ad"/>
<stop offset="1" stop-color="#5a626d"/>
</linearGradient>
<linearGradient id="steel_face" x1="0" y1="0" x2="1" y2="0.5">
<stop offset="0" stop-color="#e8ecf2"/>
<stop offset="1" stop-color="#9fa8b3"/>
</linearGradient>
<radialGradient id="floor" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="rgba(50,80,140,0.10)"/>
<stop offset="1" stop-color="rgba(50,80,140,0)"/>
</radialGradient>
<filter id="shadow"><feGaussianBlur stdDeviation="3.5"/></filter>
<linearGradient id="stress" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#1a3aa3"/>
<stop offset="0.25" stop-color="#1a8cd9"/>
<stop offset="0.5" stop-color="#33cc4d"/>
<stop offset="0.75" stop-color="#f2d91a"/>
<stop offset="1" stop-color="#e6261a"/>
</linearGradient>
</defs>
<!-- contact shadow under part -->
<ellipse cx="230" cy="335" rx="160" ry="14" fill="url(#floor)"/>
<!-- ============== BOLT SCENE ============== -->
<g id="scn_bolt" style="display:none">
<!-- step 1: shank cylinder -->
<g class="step" data-step="1">
<rect x="200" y="100" width="60" height="200" fill="url(#steel)" stroke="#5a626d" stroke-width="1.2"/>
<ellipse cx="230" cy="100" rx="30" ry="9" fill="url(#steel_face)" stroke="#5a626d" stroke-width="1"/>
<ellipse cx="230" cy="300" rx="30" ry="9" fill="url(#steel_dark)" stroke="#5a626d" stroke-width="1"/>
</g>
<!-- step 2: hex head added on top -->
<g class="step" data-step="2">
<polygon points="172,82 230,55 288,82 288,118 230,145 172,118"
fill="url(#steel)" stroke="#5a626d" stroke-width="1.4"/>
<polygon points="172,82 230,109 230,145 172,118" fill="url(#steel_dark)" opacity="0.55"/>
<polygon points="288,82 230,109 230,145 288,118" fill="url(#steel_face)" opacity="0.45"/>
<polygon points="172,82 230,55 288,82 230,109" fill="url(#steel_face)"/>
</g>
<!-- step 3: helical thread bands -->
<g class="step" data-step="3">
<g stroke="#3a414b" stroke-width="1" fill="none">
<!-- thread rings (front-facing arcs only) -->
<ellipse cx="230" cy="170" rx="30" ry="3"/>
<ellipse cx="230" cy="180" rx="30" ry="3"/>
<ellipse cx="230" cy="190" rx="30" ry="3"/>
<ellipse cx="230" cy="200" rx="30" ry="3"/>
<ellipse cx="230" cy="210" rx="30" ry="3"/>
<ellipse cx="230" cy="220" rx="30" ry="3"/>
<ellipse cx="230" cy="230" rx="30" ry="3"/>
<ellipse cx="230" cy="240" rx="30" ry="3"/>
<ellipse cx="230" cy="250" rx="30" ry="3"/>
<ellipse cx="230" cy="260" rx="30" ry="3"/>
<ellipse cx="230" cy="270" rx="30" ry="3"/>
<ellipse cx="230" cy="280" rx="30" ry="3"/>
<ellipse cx="230" cy="290" rx="30" ry="3"/>
</g>
</g>
<!-- step 4: dimension lines + label -->
<g class="step" data-step="4">
<line x1="310" y1="60" x2="310" y2="295" stroke="#2461c2" stroke-width="1"/>
<line x1="306" y1="60" x2="314" y2="60" stroke="#2461c2" stroke-width="1"/>
<line x1="306" y1="295" x2="314" y2="295" stroke="#2461c2" stroke-width="1"/>
<text x="318" y="180" font-family="ui-monospace" font-size="11" fill="#2461c2">30 mm</text>
<line x1="172" y1="68" x2="288" y2="68" stroke="#2461c2" stroke-width="1"/>
<text x="195" y="60" font-family="ui-monospace" font-size="11" fill="#2461c2">M8 hex 13 AF</text>
<circle cx="288" cy="55" r="14" fill="#2f9e44" opacity="0"/>
<text x="276" y="60" font-family="ui-sans-serif" font-size="11" font-weight="bold" fill="#fff" opacity="0"></text>
<animate xlink:href="#dummy"/>
</g>
</g>
<!-- ============== NUT SCENE ============== -->
<g id="scn_nut" style="display:none">
<g class="step" data-step="1">
<!-- outer hex prism -->
<polygon points="120,200 230,135 340,200 340,260 230,325 120,260"
fill="url(#steel)" stroke="#5a626d" stroke-width="1.4"/>
<polygon points="120,200 230,265 230,325 120,260" fill="url(#steel_dark)" opacity="0.55"/>
<polygon points="340,200 230,265 230,325 340,260" fill="url(#steel_face)" opacity="0.45"/>
<polygon points="120,200 230,135 340,200 230,265" fill="url(#steel_face)"/>
</g>
<g class="step" data-step="2">
<!-- bore (top circle visible) -->
<ellipse cx="230" cy="200" rx="42" ry="12" fill="#5a626d"/>
<ellipse cx="230" cy="200" rx="34" ry="9" fill="#2a2f37"/>
</g>
<g class="step" data-step="3">
<!-- internal thread rings inside bore -->
<g stroke="#1a1d22" stroke-width="0.7" fill="none">
<ellipse cx="230" cy="208" rx="33" ry="8.6"/>
<ellipse cx="230" cy="218" rx="33" ry="8.6"/>
<ellipse cx="230" cy="228" rx="33" ry="8.6"/>
<ellipse cx="230" cy="238" rx="33" ry="8.6"/>
<ellipse cx="230" cy="248" rx="33" ry="8.6"/>
<ellipse cx="230" cy="258" rx="33" ry="8.6"/>
</g>
</g>
<g class="step" data-step="4">
<line x1="120" y1="115" x2="340" y2="115" stroke="#2461c2" stroke-width="1"/>
<text x="180" y="108" font-family="ui-monospace" font-size="11" fill="#2461c2">M8 hex 13 AF</text>
<line x1="370" y1="135" x2="370" y2="325" stroke="#2461c2" stroke-width="1"/>
<text x="375" y="240" font-family="ui-monospace" font-size="11" fill="#2461c2">h = 6.5</text>
</g>
</g>
<!-- ============== L-BRACKET SCENE ============== -->
<g id="scn_bracket" style="display:none">
<g class="step" data-step="1">
<!-- horizontal arm (isometric) -->
<polygon points="80,260 240,200 360,225 200,285" fill="url(#steel)" stroke="#5a626d" stroke-width="1.2"/>
<polygon points="80,260 200,285 200,310 80,285" fill="url(#steel_dark)" opacity="0.6"/>
<polygon points="360,225 200,285 200,310 360,250" fill="url(#steel_face)" opacity="0.45"/>
</g>
<g class="step" data-step="2">
<!-- vertical arm -->
<polygon points="80,260 240,200 240,80 80,140" fill="url(#steel)" stroke="#5a626d" stroke-width="1.2"/>
<polygon points="80,260 80,140 80,140" fill="url(#steel_dark)" opacity="0.55"/>
<polygon points="240,200 240,80 220,90 220,210" fill="url(#steel_dark)" opacity="0.4"/>
</g>
<g class="step" data-step="3">
<!-- mounting holes on horizontal arm -->
<ellipse cx="245" cy="244" rx="9" ry="4" fill="#2a2f37"/>
<ellipse cx="290" cy="234" rx="9" ry="4" fill="#2a2f37"/>
<!-- mounting holes on vertical arm -->
<ellipse cx="140" cy="180" rx="9" ry="4" fill="#2a2f37"/>
<ellipse cx="185" cy="160" rx="9" ry="4" fill="#2a2f37"/>
</g>
<g class="step" data-step="4">
<text x="100" y="60" font-family="ui-monospace" font-size="11" fill="#2461c2">leg a = 50 mm</text>
<text x="100" y="78" font-family="ui-monospace" font-size="11" fill="#2461c2">leg b = 40 mm</text>
<text x="100" y="96" font-family="ui-monospace" font-size="11" fill="#2461c2">t = 4 mm, 4× ⌀5</text>
</g>
</g>
<!-- ============== STRESS SCENE ============== -->
<g id="scn_stress" style="display:none">
<g class="step" data-step="1">
<!-- gray bar before analysis -->
<rect x="60" y="180" width="340" height="50" fill="url(#steel)"
stroke="#5a626d" stroke-width="1.2"/>
<polygon points="60,180 80,170 420,170 400,180" fill="url(#steel_face)"/>
<polygon points="400,180 420,170 420,220 400,230" fill="url(#steel_dark)"/>
</g>
<g class="step" data-step="2">
<!-- mesh wireframe overlay -->
<g stroke="#6b7280" stroke-width="0.4" fill="none" opacity="0.7">
<!-- vertical lines -->
<line x1="60" y1="180" x2="60" y2="230"/>
<line x1="92" y1="180" x2="92" y2="230"/>
<line x1="124" y1="180" x2="124" y2="230"/>
<line x1="156" y1="180" x2="156" y2="230"/>
<line x1="188" y1="180" x2="188" y2="230"/>
<line x1="220" y1="180" x2="220" y2="230"/>
<line x1="252" y1="180" x2="252" y2="230"/>
<line x1="284" y1="180" x2="284" y2="230"/>
<line x1="316" y1="180" x2="316" y2="230"/>
<line x1="348" y1="180" x2="348" y2="230"/>
<line x1="380" y1="180" x2="380" y2="230"/>
<line x1="400" y1="180" x2="400" y2="230"/>
<!-- horizontals + diagonals -->
<line x1="60" y1="205" x2="400" y2="205"/>
<line x1="60" y1="180" x2="92" y2="205"/>
<line x1="92" y1="180" x2="124" y2="205"/>
<line x1="124" y1="180" x2="156" y2="205"/>
<line x1="156" y1="180" x2="188" y2="205"/>
<line x1="188" y1="180" x2="220" y2="205"/>
<line x1="220" y1="180" x2="252" y2="205"/>
<line x1="252" y1="180" x2="284" y2="205"/>
<line x1="284" y1="180" x2="316" y2="205"/>
<line x1="316" y1="180" x2="348" y2="205"/>
<line x1="348" y1="180" x2="380" y2="205"/>
<line x1="380" y1="180" x2="400" y2="205"/>
<line x1="60" y1="230" x2="92" y2="205"/>
<line x1="92" y1="230" x2="124" y2="205"/>
<line x1="124" y1="230" x2="156" y2="205"/>
<line x1="156" y1="230" x2="188" y2="205"/>
<line x1="188" y1="230" x2="220" y2="205"/>
<line x1="220" y1="230" x2="252" y2="205"/>
<line x1="252" y1="230" x2="284" y2="205"/>
<line x1="284" y1="230" x2="316" y2="205"/>
<line x1="316" y1="230" x2="348" y2="205"/>
<line x1="348" y1="230" x2="380" y2="205"/>
<line x1="380" y1="230" x2="400" y2="205"/>
</g>
</g>
<g class="step" data-step="3">
<!-- color-mapped stress (rainbow gradient overlay) -->
<rect x="60" y="180" width="340" height="50" fill="url(#stress)"
stroke="#5a626d" stroke-width="1.2" opacity="0.92"/>
<polygon points="60,180 80,170 420,170 400,180" fill="url(#stress)" opacity="0.85"/>
<polygon points="400,180 420,170 420,220 400,230" fill="url(#stress)" opacity="0.65"/>
<!-- force arrow -->
<line x1="60" y1="205" x2="40" y2="205" stroke="#222" stroke-width="2"/>
<polygon points="40,200 30,205 40,210" fill="#222"/>
<text x="15" y="190" font-family="ui-monospace" font-size="10" fill="#222">F</text>
<!-- fixed face hatching -->
<g stroke="#222" stroke-width="0.8">
<line x1="420" y1="170" x2="430" y2="160"/>
<line x1="420" y1="180" x2="430" y2="170"/>
<line x1="420" y1="190" x2="430" y2="180"/>
<line x1="420" y1="200" x2="430" y2="190"/>
<line x1="420" y1="210" x2="430" y2="200"/>
<line x1="420" y1="220" x2="430" y2="210"/>
</g>
</g>
<g class="step" data-step="4">
<text x="60" y="295" font-family="ui-monospace" font-size="11" fill="#222">
max σ = 138 MPa • Sy = 250 MPa • SF = 1.81 ✓ OK
</text>
<!-- legend -->
<rect x="60" y="320" width="200" height="10" fill="url(#stress)"/>
<text x="60" y="345" font-family="ui-monospace" font-size="9" fill="#5a6373">0 MPa</text>
<text x="220" y="345" font-family="ui-monospace" font-size="9" fill="#5a6373">138 MPa</text>
</g>
</g>
</svg>
</div>
<div id="steps">
<div class="line" data-i="1">→ awaiting prompt</div>
</div>
</div>
</div>
<div id="ribbon">
<span id="typing"></span>
<span id="status">ready</span>
</div>
<div id="legend">
<span class="pill"><span class="dot" style="background:var(--blue)"></span>3D engine</span>
<span class="pill"><span class="dot" style="background:var(--green)"></span>parser</span>
<span class="pill"><span class="dot" style="background:var(--purple)"></span>LLM</span>
<span class="pill"><span class="dot" style="background:var(--gold)"></span>design agent</span>
<span class="pill"><span class="dot" style="background:var(--orange)"></span>intent fallback</span>
<span class="pill"><span class="dot" style="background:var(--red)"></span>FEA / stress</span>
</div>
<footer>chat_cad — plain English to real parametric CAD in one chat line.</footer>
<script>
// ---- scenario timelines ----
// Each step lights up a node + animates a packet on a wire + advances
// the workbench construction (.step.on) and the steps log.
const SCENARIOS = {
bolt: {
title: 'M8 Hex Bolt', cmd: 'bolt b1 M8 30', scene: 'bolt',
steps: [
"1. user types 'bolt b1 M8 30'",
"2. router classifies → known parser keyword",
"3. parser → CadQuery: cylinder shaft",
"4. union hex head (across-flats 13 mm)",
"5. helical thread valley cut along shank",
"6. dimension annotations + safety check",
],
timeline: [
{ ms: 0, op: 'type' },
{ ms: 1300, op: 'glow', node: 'chat', },
{ ms: 1300, op: 'wire', id: 'w_user_chat', color: 'var(--blue)' },
{ ms: 1300, op: 'log', i: 0 },
{ ms: 1900, op: 'glow', node: 'router' },
{ ms: 1900, op: 'wire', id: 'w_chat_router', color: 'var(--blue)' },
{ ms: 1900, op: 'log', i: 1 },
{ ms: 2200, op: 'status', text: 'router sees "bolt" — known parser keyword' },
{ ms: 2600, op: 'glow', node: 'parser' },
{ ms: 2600, op: 'wire', id: 'w_router_parser', color: 'var(--green)' },
{ ms: 2600, op: 'log', i: 2 },
{ ms: 3200, op: 'glow', node: 'engine' },
{ ms: 3200, op: 'wire', id: 'w_parser_engine', color: 'var(--green)' },
{ ms: 3200, op: 'step', n: 1, msg: 'building shank cylinder…' },
{ ms: 4200, op: 'step', n: 2, msg: 'attaching hex head' },
{ ms: 4200, op: 'log', i: 3 },
{ ms: 5400, op: 'step', n: 3, msg: 'sweeping helical thread valleys' },
{ ms: 5400, op: 'log', i: 4 },
{ ms: 6800, op: 'step', n: 4, msg: '✓ M8×30 bolt ready' },
{ ms: 6800, op: 'log', i: 5 },
],
},
nut: {
title: 'M8 Hex Nut', cmd: 'nut n1 M8', scene: 'nut',
steps: [
"1. user types 'nut n1 M8'",
"2. router → known parser keyword",
"3. extrude hex prism (13 AF, height 6.5)",
"4. drill through-bore (⌀8 mm)",
"5. sweep internal thread crests",
"6. dimension callouts",
],
timeline: [
{ ms: 0, op: 'type' },
{ ms: 1200, op: 'glow', node: 'chat' }, { ms: 1200, op: 'wire', id: 'w_user_chat', color: 'var(--blue)' }, { ms: 1200, op: 'log', i: 0 },
{ ms: 1800, op: 'glow', node: 'router' }, { ms: 1800, op: 'wire', id: 'w_chat_router', color: 'var(--blue)' }, { ms: 1800, op: 'log', i: 1 },
{ ms: 2500, op: 'glow', node: 'parser' }, { ms: 2500, op: 'wire', id: 'w_router_parser', color: 'var(--green)' },
{ ms: 3100, op: 'glow', node: 'engine' }, { ms: 3100, op: 'wire', id: 'w_parser_engine', color: 'var(--green)' },
{ ms: 3100, op: 'step', n: 1, msg: 'hex prism extruded' }, { ms: 3100, op: 'log', i: 2 },
{ ms: 4300, op: 'step', n: 2, msg: 'through-bore drilled' }, { ms: 4300, op: 'log', i: 3 },
{ ms: 5500, op: 'step', n: 3, msg: 'internal thread swept' }, { ms: 5500, op: 'log', i: 4 },
{ ms: 6700, op: 'step', n: 4, msg: '✓ M8 nut ready' }, { ms: 6700, op: 'log', i: 5 },
],
},
bracket: {
title: 'L-Bracket (50 × 40 mm)', cmd: 'Draw me a L bracket', scene: 'bracket',
steps: [
"1. user types plain English: 'Draw me a L bracket'",
"2. router → not a parser keyword",
"3. intent fallback matches 'bracket' (47-keyword list)",
"4. emits l_bracket b1 50 40 4 30 5 2",
"5. CadQuery builds two perpendicular plates + holes",
"6. ready — and the LLM was never called",
],
timeline: [
{ ms: 0, op: 'type' },
{ ms: 1700, op: 'glow', node: 'chat' }, { ms: 1700, op: 'wire', id: 'w_user_chat', color: 'var(--blue)' }, { ms: 1700, op: 'log', i: 0 },
{ ms: 2300, op: 'glow', node: 'router' }, { ms: 2300, op: 'wire', id: 'w_chat_router', color: 'var(--blue)' }, { ms: 2300, op: 'log', i: 1 },
{ ms: 2700, op: 'status', text: 'plain English — no parser keyword. Try intent fallback.' },
{ ms: 3100, op: 'glow', node: 'intent' }, { ms: 3100, op: 'wire', id: 'w_router_intent', color: 'var(--orange)' }, { ms: 3100, op: 'log', i: 2 },
{ ms: 3600, op: 'status', text: 'matched keyword "bracket" → emits l_bracket b1 50 40 4 30 5 2' }, { ms: 3600, op: 'log', i: 3 },
{ ms: 4200, op: 'glow', node: 'engine' }, { ms: 4200, op: 'wire', id: 'w_intent_engine', color: 'var(--orange)' },
{ ms: 4200, op: 'step', n: 1, msg: 'first leg extruded' },
{ ms: 5300, op: 'step', n: 2, msg: 'second leg attached at 90°' }, { ms: 5300, op: 'log', i: 4 },
{ ms: 6300, op: 'step', n: 3, msg: '4 × ⌀5 mounting holes' },
{ ms: 7000, op: 'step', n: 4, msg: '✓ L-bracket ready (LLM never called)' }, { ms: 7000, op: 'log', i: 5 },
],
},
stress: {
title: 'Stress Analysis — 100×10×5 steel bar', cmd: 'stress bar1 load=2000 axis=X', scene: 'stress',
steps: [
"1. user types 'stress bar1 load=2000 axis=X'",
"2. parser routes to FEA worker (gmsh + skfem)",
"3. mesh the bar into tetrahedra",
"4. solve K·u = f under fixed-free BCs",
"5. compute von Mises stress per node",
"6. colour-map: blue (low) → red (high)",
],
timeline: [
{ ms: 0, op: 'type' },
{ ms: 1400, op: 'glow', node: 'chat' }, { ms: 1400, op: 'wire', id: 'w_user_chat', color: 'var(--blue)' }, { ms: 1400, op: 'log', i: 0 },
{ ms: 2000, op: 'glow', node: 'router' }, { ms: 2000, op: 'wire', id: 'w_chat_router', color: 'var(--blue)' },
{ ms: 2400, op: 'glow', node: 'parser' }, { ms: 2400, op: 'wire', id: 'w_router_parser', color: 'var(--green)' }, { ms: 2400, op: 'log', i: 1 },
{ ms: 3000, op: 'glow', node: 'engine' }, { ms: 3000, op: 'wire', id: 'w_parser_engine', color: 'var(--green)' },
{ ms: 3000, op: 'step', n: 1, msg: 'STL extracted from B-rep' },
{ ms: 3800, op: 'step', n: 2, msg: 'gmsh meshes — 170 nodes, 377 tetrahedra' }, { ms: 3800, op: 'log', i: 2 },
{ ms: 5000, op: 'status', text: 'skfem assembles K, solves K·u = f' }, { ms: 5000, op: 'log', i: 3 },
{ ms: 5500, op: 'log', i: 4 },
{ ms: 6000, op: 'step', n: 3, msg: 'von Mises per node → colour map' }, { ms: 6000, op: 'log', i: 5 },
{ ms: 7200, op: 'step', n: 4, msg: '✓ max σ 138 MPa, SF 1.81 — OK' },
],
},
};
// ---- runtime ----
let TIMERS = [];
function clearTimers() { TIMERS.forEach(clearTimeout); TIMERS = []; }
function resetVisuals() {
document.querySelectorAll('.node').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.wire').forEach(w => { w.classList.remove('active'); w.style.removeProperty('--wirecolor'); });
document.querySelectorAll('.step').forEach(s => s.classList.remove('on'));
document.querySelectorAll('#steps .line').forEach(l => l.classList.remove('done', 'active'));
document.getElementById('packet').setAttribute('opacity', '0');
document.getElementById('typing').textContent = '';
document.getElementById('status').textContent = 'ready';
}
function typeInto(el, text, ms_total = 900) {
const start = performance.now();
function step(t) {
const u = Math.min(1, (t - start) / ms_total);
const n = Math.floor(text.length * u);
el.innerHTML = text.slice(0, n) + '<span class="cursor"></span>';
if (u < 1) requestAnimationFrame(step);
else el.innerHTML = text + '<span class="cursor"></span>';
}
requestAnimationFrame(step);
}
function animatePacket(wireId) {
const path = document.getElementById(wireId);
if (!path) return;
const len = path.getTotalLength();
const packet = document.getElementById('packet');
packet.setAttribute('opacity', '1');
const t0 = performance.now();
function step(t) {
const u = Math.min(1, (t - t0) / 600);
const pt = path.getPointAtLength(len * u);
packet.setAttribute('cx', pt.x);
packet.setAttribute('cy', pt.y);
if (u < 1) requestAnimationFrame(step);
else setTimeout(() => packet.setAttribute('opacity', '0'), 150);
}
requestAnimationFrame(step);
}
function setScene(name) {
document.querySelectorAll('[id^="scn_"]').forEach(g => g.style.display = 'none');
const el = document.getElementById('scn_' + name);
if (el) el.style.display = 'block';
}
function renderStepLog(steps) {
const el = document.getElementById('steps');
el.innerHTML = steps.map((s, i) =>
`<div class="line" data-i="${i}">${s}</div>`).join('');
}
function runScenario(key) {
clearTimers();
resetVisuals();
const sc = SCENARIOS[key];
document.getElementById('wbtitle').textContent = sc.title;
document.getElementById('wbsub').textContent = sc.cmd;
setScene(sc.scene);
renderStepLog(sc.steps);
sc.timeline.forEach(ev => {
TIMERS.push(setTimeout(() => {
if (ev.op === 'type') typeInto(document.getElementById('typing'), sc.cmd);
else if (ev.op === 'glow') document.querySelector('.node.' + ev.node)?.classList.add('active');
else if (ev.op === 'wire') {
const w = document.getElementById(ev.id);
if (w) { w.classList.add('active'); w.style.setProperty('--wirecolor', ev.color); }
animatePacket(ev.id);
}
else if (ev.op === 'step') {
const s = document.querySelector(`.step[data-step="${ev.n}"]`);
if (s) s.classList.add('on');
if (ev.msg) document.getElementById('status').textContent = ev.msg;
}
else if (ev.op === 'status') document.getElementById('status').textContent = ev.text;
else if (ev.op === 'log') {
const lines = document.querySelectorAll('#steps .line');
lines.forEach((l, i) => {
if (i < ev.i) l.classList.add('done');
else if (i === ev.i) l.classList.add('active');
});
const target = lines[ev.i];
if (target) target.scrollIntoView({block:'nearest', behavior:'smooth'});
}
}, ev.ms));
});
}
let _loop = null;
function startLoop() {
const sel = document.getElementById('scenario');
runScenario(sel.value);
// last step ms + a small pad
const sc = SCENARIOS[sel.value];
const lastMs = sc.timeline[sc.timeline.length - 1].ms;
_loop = setTimeout(startLoop, lastMs + 1800);
}
document.getElementById('play').addEventListener('click', () => {
clearTimeout(_loop); startLoop();
});
document.getElementById('restart').addEventListener('click', () => {
clearTimeout(_loop); resetVisuals(); setTimeout(startLoop, 200);
});
document.getElementById('scenario').addEventListener('change', () => {
clearTimeout(_loop); resetVisuals(); setTimeout(startLoop, 200);
});
window.addEventListener('load', () => setTimeout(startLoop, 400));
</script>
</body>
</html>