Spaces:
Sleeping
Sleeping
| <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> | |