CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
4dc45c6
·
1 Parent(s): 1c6466b

feat: add chat API, MCP chat_turn tool, and rewrite frontend

Browse files

- server/routes.py: POST /api/chat, POST /api/report, GET /api/agents
- server/web.py: include chat routes
- server/mcp.py: add chat_turn MCP tool for Claude Desktop
- web/index.html: complete rewrite — fullscreen 3D viewer with
slide-out multi-agent chat panel, @mention autocomplete,
on-demand preview, code viewer modal, gallery modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (4) hide show
  1. server/mcp.py +56 -0
  2. server/routes.py +152 -0
  3. server/web.py +4 -0
  4. web/index.html +1044 -558
server/mcp.py CHANGED
@@ -391,6 +391,62 @@ def generate_from_image(
391
  return json.dumps(response, indent=2)
392
 
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  # ── Resource: System prompt (for transparency) ───────────────────────────
395
 
396
  @mcp.resource("text-to-cnc://system-prompt")
 
391
  return json.dumps(response, indent=2)
392
 
393
 
394
+ # ── Tool: chat_turn ─────────────────────────────────────────────────────
395
+
396
+ @mcp.tool()
397
+ def chat_turn(
398
+ message: str,
399
+ history: str = "[]",
400
+ mentions: str = "[]",
401
+ backend: str = "mock",
402
+ ) -> str:
403
+ """
404
+ Multi-agent chat turn for collaborative CAD design.
405
+
406
+ Send a message to the design team agents (Design, Engineering, CNC, CAD Coder).
407
+ Agents collaborate to help you design a mechanical part step by step.
408
+
409
+ Args:
410
+ message: Your message to the design team.
411
+ Use @design, @engineering, @cnc, or @cad to address specific agents.
412
+ history: JSON string of previous messages. Format:
413
+ [{"role": "user"|"agent", "agent_id": "design", "content": "..."}]
414
+ mentions: JSON string of agent IDs to address. Format: ["design", "engineering"]
415
+ Empty list = auto-route based on message content.
416
+ backend: LLM backend: "mock", "gemini", "anthropic", "openai".
417
+
418
+ Returns:
419
+ JSON string with agent responses and optional 3D preview data.
420
+ """
421
+ import json as json_mod
422
+
423
+ from agents.orchestrator import get_orchestrator
424
+ from agents.crew_orchestrator import CrewOrchestrator
425
+ from agents.prompts import parse_mentions
426
+
427
+ history_list = json_mod.loads(history) if isinstance(history, str) else history
428
+ mentions_list = json_mod.loads(mentions) if isinstance(mentions, str) else mentions
429
+
430
+ # Parse @mentions from message if not provided
431
+ if not mentions_list:
432
+ message, mentions_list = parse_mentions(message)
433
+
434
+ mentions_or_none = mentions_list if mentions_list else None
435
+
436
+ if backend in ("anthropic", "openai"):
437
+ orchestrator = CrewOrchestrator(backend_name=backend, output_dir=DEFAULT_OUTPUT_DIR)
438
+ else:
439
+ orchestrator = get_orchestrator(backend, output_dir=DEFAULT_OUTPUT_DIR)
440
+
441
+ result = orchestrator.chat_turn(
442
+ message=message,
443
+ history=history_list,
444
+ mentions=mentions_or_none,
445
+ )
446
+
447
+ return json_mod.dumps(result, indent=2)
448
+
449
+
450
  # ── Resource: System prompt (for transparency) ───────────────────────────
451
 
452
  @mcp.resource("text-to-cnc://system-prompt")
server/routes.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Chat API routes for multi-agent design conversation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from fastapi import APIRouter
8
+ from fastapi.responses import JSONResponse
9
+
10
+ from agents.orchestrator import get_orchestrator
11
+ from agents.crew_orchestrator import CrewOrchestrator
12
+ from agents.prompts import parse_mentions
13
+ from agents.definitions import AGENTS
14
+
15
+ router = APIRouter()
16
+
17
+ OUTPUT_DIR = Path(__file__).parent.parent / "output"
18
+
19
+
20
+ @router.post("/api/chat")
21
+ async def chat(body: dict):
22
+ """Multi-agent chat turn.
23
+
24
+ Request body:
25
+ {
26
+ "message": "I need a servo bracket 60mm wide",
27
+ "history": [{"role": "user"|"agent", "agent_id": "...", "content": "..."}, ...],
28
+ "mentions": ["design", "engineering"], // or [] for auto-routing
29
+ "backend": "mock"|"gemini"|"anthropic"|"openai"
30
+ }
31
+
32
+ Response:
33
+ {
34
+ "responses": [
35
+ {"agent_id", "agent_name", "message", "color", "avatar", "code"}, ...
36
+ ],
37
+ "preview": null | {"success", "part_name", "stl_url", "step_url", "execution", "validation"}
38
+ }
39
+ """
40
+ message = body.get("message", "").strip()
41
+ if not message:
42
+ return JSONResponse({"error": "Empty message"}, status_code=400)
43
+
44
+ history = body.get("history", [])
45
+ backend_name = body.get("backend", "mock")
46
+
47
+ # Parse @mentions from message if not provided
48
+ raw_mentions = body.get("mentions", [])
49
+ if not raw_mentions:
50
+ message, raw_mentions = parse_mentions(message)
51
+
52
+ mentions = raw_mentions if raw_mentions else None
53
+
54
+ # Select orchestrator based on backend
55
+ if backend_name in ("anthropic", "openai"):
56
+ orchestrator = CrewOrchestrator(
57
+ backend_name=backend_name, output_dir=OUTPUT_DIR
58
+ )
59
+ else:
60
+ orchestrator = get_orchestrator(backend_name, output_dir=OUTPUT_DIR)
61
+
62
+ # Run chat turn
63
+ try:
64
+ result = orchestrator.chat_turn(
65
+ message=message,
66
+ history=history,
67
+ mentions=mentions,
68
+ )
69
+ return JSONResponse(result)
70
+ except Exception as e:
71
+ return JSONResponse({"error": str(e)}, status_code=500)
72
+
73
+
74
+ @router.post("/api/report")
75
+ async def report(body: dict):
76
+ """Generate a design report from conversation history.
77
+
78
+ Request body:
79
+ {
80
+ "part_name": "servo_bracket",
81
+ "history": [...],
82
+ "backend": "gemini"
83
+ }
84
+ """
85
+ part_name = body.get("part_name", "part")
86
+ history = body.get("history", [])
87
+ backend_name = body.get("backend", "mock")
88
+
89
+ # Build report from conversation
90
+ report_sections = []
91
+ report_sections.append(f"# Design Report: {part_name}\n")
92
+
93
+ design_decisions = []
94
+ engineering_specs = []
95
+ cnc_notes = []
96
+
97
+ for msg in history:
98
+ agent_id = msg.get("agent_id", "")
99
+ content = msg.get("content", "")
100
+ if agent_id == "design":
101
+ design_decisions.append(content)
102
+ elif agent_id == "engineering":
103
+ engineering_specs.append(content)
104
+ elif agent_id == "cnc":
105
+ cnc_notes.append(content)
106
+
107
+ if design_decisions:
108
+ report_sections.append("## Design Decisions")
109
+ for d in design_decisions:
110
+ report_sections.append(f"- {d}")
111
+
112
+ if engineering_specs:
113
+ report_sections.append("\n## Engineering Specifications")
114
+ for s in engineering_specs:
115
+ report_sections.append(f"- {s}")
116
+
117
+ if cnc_notes:
118
+ report_sections.append("\n## Manufacturing Notes")
119
+ for n in cnc_notes:
120
+ report_sections.append(f"- {n}")
121
+
122
+ # Check if model files exist
123
+ stl_path = OUTPUT_DIR / f"{part_name}.stl"
124
+ step_path = OUTPUT_DIR / f"{part_name}.step"
125
+
126
+ report_sections.append("\n## Exported Files")
127
+ report_sections.append(f"- STEP: {'Available' if step_path.exists() else 'Not generated'}")
128
+ report_sections.append(f"- STL: {'Available' if stl_path.exists() else 'Not generated'}")
129
+
130
+ report_text = "\n".join(report_sections)
131
+
132
+ return JSONResponse({
133
+ "part_name": part_name,
134
+ "report": report_text,
135
+ })
136
+
137
+
138
+ @router.get("/api/agents")
139
+ async def list_agents():
140
+ """List available agents and their metadata."""
141
+ return JSONResponse({
142
+ "agents": [
143
+ {
144
+ "id": agent.id,
145
+ "name": agent.name,
146
+ "role": agent.role,
147
+ "color": agent.color,
148
+ "avatar": agent.avatar,
149
+ }
150
+ for agent in AGENTS.values()
151
+ ]
152
+ })
server/web.py CHANGED
@@ -31,6 +31,8 @@ from fastapi import FastAPI, File, Form, UploadFile
31
  from fastapi.middleware.cors import CORSMiddleware
32
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
33
 
 
 
34
  from mcp import ClientSession
35
  from mcp.client.sse import sse_client
36
 
@@ -106,6 +108,8 @@ app.add_middleware(
106
  allow_headers=["*"],
107
  )
108
 
 
 
109
 
110
  # ── Routes ───────────────────────────────────────────────────────────────
111
 
 
31
  from fastapi.middleware.cors import CORSMiddleware
32
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
33
 
34
+ from server.routes import router
35
+
36
  from mcp import ClientSession
37
  from mcp.client.sse import sse_client
38
 
 
108
  allow_headers=["*"],
109
  )
110
 
111
+ app.include_router(router)
112
+
113
 
114
  # ── Routes ───────────────────────────────────────────────────────────────
115
 
web/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>NeuralCAD — Text-to-CNC Pipeline</title>
7
 
8
  <!-- Three.js -->
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
@@ -37,6 +37,11 @@
37
  --machined-steel: #8899aa;
38
  --font-mono: 'JetBrains Mono', 'Cascadia Code', monospace;
39
  --font-body: 'DM Sans', system-ui, sans-serif;
 
 
 
 
 
40
  }
41
 
42
  html, body {
@@ -47,15 +52,23 @@
47
  font-family: var(--font-body);
48
  }
49
 
50
- /* ── LAYOUT ─────────────────────────────────────── */
 
 
 
 
 
 
51
 
52
  #app {
53
  display: flex;
54
  flex-direction: column;
55
  height: 100vh;
 
 
56
  }
57
 
58
- /* ── TOP BAR ────────────────────────────────────── */
59
 
60
  #topbar {
61
  flex: 0 0 44px;
@@ -65,7 +78,7 @@
65
  align-items: center;
66
  justify-content: space-between;
67
  padding: 0 16px;
68
- z-index: 10;
69
  position: relative;
70
  }
71
 
@@ -85,21 +98,10 @@
85
  gap: 10px;
86
  }
87
 
88
- .logo-mark {
89
- width: 22px; height: 22px;
90
- border: 2px solid var(--accent);
91
- transform: rotate(45deg);
92
- display: flex;
93
- align-items: center;
94
- justify-content: center;
95
- position: relative;
96
- }
97
-
98
- .logo-mark::after {
99
- content: '';
100
- width: 6px; height: 6px;
101
- background: var(--accent);
102
- border-radius: 1px;
103
  }
104
 
105
  .logo-text {
@@ -122,6 +124,12 @@
122
  border-left: 1px solid var(--border);
123
  }
124
 
 
 
 
 
 
 
125
  .topbar-right {
126
  display: flex;
127
  align-items: center;
@@ -131,7 +139,7 @@
131
  .backend-toggle {
132
  display: flex;
133
  align-items: center;
134
- gap: 2px;
135
  background: var(--bg-void);
136
  border: 1px solid var(--border);
137
  border-radius: 4px;
@@ -146,8 +154,11 @@
146
  cursor: pointer;
147
  color: var(--text-muted);
148
  transition: all 0.2s;
 
149
  }
150
 
 
 
151
  .backend-toggle button.active {
152
  background: var(--accent-glow);
153
  color: var(--accent);
@@ -157,8 +168,28 @@
157
  color: var(--text-secondary);
158
  }
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  .status-dot {
161
- width: 6px; height: 6px;
162
  border-radius: 50%;
163
  background: var(--success);
164
  box-shadow: 0 0 6px var(--success);
@@ -170,19 +201,20 @@
170
  50% { opacity: 0.4; }
171
  }
172
 
173
- .version-tag {
174
- font-family: var(--font-mono);
175
- font-size: 10px;
176
- color: var(--text-muted);
177
- padding: 2px 8px;
178
- border: 1px solid var(--border);
179
- border-radius: 3px;
 
180
  }
181
 
182
- /* ── 3D VIEWER ──────────────────────────────────── */
183
 
184
  #viewer-container {
185
- flex: 1 1 60%;
186
  position: relative;
187
  background: var(--bg-void);
188
  overflow: hidden;
@@ -195,42 +227,12 @@
195
  display: block;
196
  }
197
 
198
- /* Viewport grid effect */
199
- #viewer-container::before {
200
- content: '';
201
- position: absolute;
202
- inset: 0;
203
- background-image:
204
- linear-gradient(var(--border) 1px, transparent 1px),
205
- linear-gradient(90deg, var(--border) 1px, transparent 1px);
206
- background-size: 60px 60px;
207
- opacity: 0.15;
208
- pointer-events: none;
209
- z-index: 1;
210
- }
211
-
212
- /* Corner brackets */
213
- .corner-bracket {
214
- position: absolute;
215
- width: 20px; height: 20px;
216
- border-color: var(--accent-dim);
217
- border-style: solid;
218
- border-width: 0;
219
- opacity: 0.5;
220
- z-index: 2;
221
- pointer-events: none;
222
- }
223
- .corner-bracket.tl { top: 12px; left: 12px; border-top-width: 2px; border-left-width: 2px; }
224
- .corner-bracket.tr { top: 12px; right: 12px; border-top-width: 2px; border-right-width: 2px; }
225
- .corner-bracket.bl { bottom: 12px; left: 12px; border-bottom-width: 2px; border-left-width: 2px; }
226
- .corner-bracket.br { bottom: 12px; right: 12px; border-bottom-width: 2px; border-right-width: 2px; }
227
-
228
- /* Stats overlay */
229
  #geo-stats {
230
  position: absolute;
231
  top: 14px;
232
- right: 14px;
233
- z-index: 3;
234
  background: rgba(6, 8, 12, 0.85);
235
  border: 1px solid var(--border);
236
  border-radius: 4px;
@@ -247,17 +249,19 @@
247
  .stat-label { color: var(--text-muted); }
248
  .stat-value { color: var(--accent); }
249
 
250
- /* CNC badge */
251
  #cnc-badge {
252
  position: absolute;
253
  top: 14px;
254
- left: 14px;
255
- z-index: 3;
256
  display: none;
257
  gap: 6px;
 
258
  }
259
 
260
  #cnc-badge.visible { display: flex; }
 
261
 
262
  .badge {
263
  font-family: var(--font-mono);
@@ -293,12 +297,12 @@
293
  color: var(--accent);
294
  }
295
 
296
- /* Download buttons */
297
  #download-btns {
298
  position: absolute;
299
  bottom: 14px;
300
- right: 14px;
301
- z-index: 3;
302
  display: none;
303
  gap: 6px;
304
  }
@@ -330,20 +334,23 @@
330
  #viewer-hint {
331
  position: absolute;
332
  bottom: 16px;
333
- left: 16px;
334
- z-index: 3;
335
  font-family: var(--font-mono);
336
  font-size: 10px;
337
  color: var(--text-muted);
338
  letter-spacing: 0.5px;
339
  pointer-events: none;
 
340
  }
341
 
 
 
342
  /* Loading spinner */
343
  #viewer-loading {
344
  position: absolute;
345
  inset: 0;
346
- z-index: 4;
347
  display: none;
348
  align-items: center;
349
  justify-content: center;
@@ -376,12 +383,12 @@
376
  #viewer-empty {
377
  position: absolute;
378
  inset: 0;
379
- z-index: 2;
380
  display: flex;
381
  align-items: center;
382
  justify-content: center;
383
  flex-direction: column;
384
- gap: 12px;
385
  pointer-events: none;
386
  }
387
 
@@ -393,6 +400,7 @@
393
  align-items: center;
394
  justify-content: center;
395
  transform: rotate(45deg);
 
396
  }
397
 
398
  .empty-icon-inner {
@@ -405,267 +413,537 @@
405
 
406
  .empty-text {
407
  font-family: var(--font-mono);
408
- font-size: 11px;
409
  color: var(--text-muted);
410
  letter-spacing: 1px;
 
 
411
  }
412
 
413
- /* ── BOTTOM PANEL ───────────────────────────────── */
414
 
415
- #bottom-panel {
416
- flex: 0 0 auto;
417
- height: 40vh;
418
- min-height: 240px;
419
- max-height: 360px;
420
- background: var(--bg-panel);
421
- border-top: 1px solid var(--border);
 
 
422
  display: flex;
423
  flex-direction: column;
424
- position: relative;
 
 
425
  }
426
 
427
- #bottom-panel::before {
428
- content: '';
 
 
 
 
 
429
  position: absolute;
430
- top: -1px;
431
- left: 0; right: 0;
432
- height: 1px;
433
- background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
434
- opacity: 0.3;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  }
436
 
437
- /* Tabs */
438
- #tabs {
439
- flex: 0 0 36px;
 
 
 
 
 
440
  display: flex;
441
- gap: 0;
 
 
442
  border-bottom: 1px solid var(--border);
443
- padding: 0 12px;
444
- background: var(--bg-panel);
445
  }
446
 
447
- .tab-btn {
448
- all: unset;
 
 
 
 
 
449
  font-family: var(--font-mono);
450
  font-size: 11px;
451
- font-weight: 400;
452
- letter-spacing: 0.5px;
453
- color: var(--text-muted);
454
- padding: 0 16px;
455
- cursor: pointer;
456
- position: relative;
457
- transition: color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  display: flex;
 
 
 
 
 
 
 
 
 
459
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  gap: 6px;
 
461
  }
462
 
463
- .tab-btn:hover { color: var(--text-secondary); }
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
- .tab-btn.active {
 
466
  color: var(--accent);
 
467
  }
468
 
469
- .tab-btn.active::after {
470
- content: '';
471
- position: absolute;
472
- bottom: -1px;
473
- left: 8px; right: 8px;
474
- height: 1px;
475
- background: var(--accent);
476
  }
477
 
478
- .tab-count {
479
- font-size: 9px;
480
- padding: 1px 5px;
481
- border-radius: 3px;
482
- background: var(--bg-void);
483
- color: var(--text-muted);
484
  }
485
 
486
- .tab-btn.active .tab-count {
487
- background: var(--accent-glow);
488
- color: var(--accent);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  }
490
 
491
- /* Tab content */
492
- .tab-content {
493
  flex: 1;
494
- min-height: 0;
495
- overflow: auto;
496
- display: none;
497
- padding: 12px 16px;
498
  }
499
 
500
- .tab-content.active { display: flex; }
 
 
 
 
 
 
 
501
 
502
- /* Scrollbar */
503
- .tab-content::-webkit-scrollbar { width: 4px; }
504
- .tab-content::-webkit-scrollbar-track { background: transparent; }
505
- .tab-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
 
 
 
 
 
 
506
 
507
- /* ── GENERATE TAB ───────────────────────────────── */
 
 
 
 
508
 
509
- #tab-generate {
510
- gap: 12px;
 
 
 
 
 
 
 
 
511
  }
512
 
513
- .input-area {
514
- flex: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  display: flex;
516
  flex-direction: column;
517
  gap: 8px;
518
- min-width: 0;
519
  }
520
 
521
- #prompt-input {
 
 
 
 
 
 
522
  flex: 1;
523
- min-height: 60px;
 
524
  background: var(--bg-input);
525
  border: 1px solid var(--border);
526
- border-radius: 4px;
527
- padding: 10px 12px;
528
  color: var(--text-primary);
529
  font-family: var(--font-body);
530
  font-size: 13px;
531
- line-height: 1.5;
532
  resize: none;
533
  outline: none;
534
  transition: border-color 0.2s;
535
  }
536
 
537
- #prompt-input::placeholder { color: var(--text-muted); }
538
- #prompt-input:focus { border-color: var(--accent-dim); }
539
 
540
- .action-row {
 
 
 
 
541
  display: flex;
542
- gap: 8px;
543
  align-items: center;
 
 
 
 
544
  }
545
 
546
- .btn-generate {
547
- all: unset;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  display: flex;
549
  align-items: center;
550
- justify-content: center;
551
- gap: 8px;
552
- flex: 1;
553
- padding: 8px 20px;
554
- background: linear-gradient(135deg, var(--accent-dim), var(--accent));
555
- border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  font-family: var(--font-mono);
 
 
557
  font-size: 12px;
558
- font-weight: 600;
559
- color: var(--bg-void);
560
- letter-spacing: 1px;
561
- cursor: pointer;
562
- transition: all 0.2s;
563
  }
564
 
565
- .btn-generate:hover { filter: brightness(1.15); }
566
-
567
- .btn-generate:disabled {
568
- opacity: 0.4;
569
- cursor: not-allowed;
570
- filter: none;
571
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
 
573
- .btn-image {
574
- all: unset;
575
- display: flex;
576
- align-items: center;
577
- justify-content: center;
578
- width: 36px; height: 36px;
579
- background: var(--bg-surface);
580
  border: 1px solid var(--border);
581
- border-radius: 4px;
582
- color: var(--text-muted);
583
- cursor: pointer;
584
- font-size: 16px;
585
- transition: all 0.2s;
 
586
  }
587
 
588
- .btn-image:hover {
589
- border-color: var(--accent-dim);
590
- color: var(--accent);
591
  }
592
 
593
- .examples-sidebar {
594
- flex: 0 0 220px;
595
  display: flex;
596
- flex-direction: column;
597
- gap: 4px;
598
- overflow-y: auto;
 
599
  }
600
 
601
- .examples-label {
602
  font-family: var(--font-mono);
603
- font-size: 9px;
604
- color: var(--text-muted);
605
- letter-spacing: 2px;
 
606
  text-transform: uppercase;
607
- margin-bottom: 2px;
608
  }
609
 
610
- .example-btn {
611
  all: unset;
 
612
  display: flex;
613
  align-items: center;
614
- gap: 8px;
615
- padding: 7px 10px;
616
- background: var(--bg-surface);
617
- border: 1px solid var(--border);
618
  border-radius: 4px;
619
- font-family: var(--font-mono);
620
- font-size: 10px;
621
- color: var(--text-secondary);
622
  cursor: pointer;
623
- transition: all 0.2s;
624
- white-space: nowrap;
625
- overflow: hidden;
626
- text-overflow: ellipsis;
627
- }
628
-
629
- .example-btn:hover {
630
- border-color: var(--accent-dim);
631
- color: var(--text-primary);
632
- background: var(--bg-input);
633
- }
634
-
635
- .example-arrow {
636
  color: var(--text-muted);
637
- font-size: 8px;
638
- flex-shrink: 0;
639
  }
640
 
641
- /* ── CODE TAB ───────────────────────────────────── */
642
-
643
- #tab-code {
644
- padding: 0;
645
  }
646
 
647
  #code-display {
648
- width: 100%;
649
- height: 100%;
650
  margin: 0;
651
- padding: 14px 16px;
652
  background: var(--bg-input);
653
- border: none;
654
  color: var(--machined-steel);
655
  font-family: var(--font-mono);
656
- font-size: 11.5px;
657
  line-height: 1.7;
658
  overflow: auto;
659
  white-space: pre;
660
  tab-size: 4;
661
  }
662
 
663
- .code-empty {
664
- color: var(--text-muted) !important;
665
- font-style: italic;
666
- }
667
-
668
- /* Syntax coloring via JS */
669
  .kw { color: #c792ea; }
670
  .fn { color: #82aaff; }
671
  .cm { color: #546e7a; }
@@ -673,102 +951,84 @@
673
  .nu { color: #f78c6c; }
674
  .op { color: #89ddff; }
675
 
676
- /* ── VALIDATION TAB ─────────────────────────────── */
677
 
678
- #tab-validation {
679
- flex-direction: column;
680
- gap: 10px;
681
- }
682
-
683
- .validation-empty {
684
- font-family: var(--font-mono);
685
- font-size: 11px;
686
- color: var(--text-muted);
687
- letter-spacing: 0.5px;
688
- margin: auto;
689
- }
690
-
691
- .validation-header {
692
- display: flex;
693
  align-items: center;
694
- gap: 12px;
695
- padding-bottom: 10px;
696
- border-bottom: 1px solid var(--border);
697
  }
698
 
699
- .validation-header .badge { font-size: 11px; }
700
-
701
- .validation-axis {
702
- font-family: var(--font-mono);
703
- font-size: 11px;
704
- color: var(--text-secondary);
705
- }
706
 
707
- .issue-list {
 
 
 
 
 
708
  display: flex;
709
  flex-direction: column;
710
- gap: 4px;
 
 
711
  }
712
 
713
- .issue-item {
714
  display: flex;
715
- align-items: flex-start;
716
- gap: 10px;
717
- padding: 6px 8px;
718
- border-radius: 3px;
719
- font-size: 12px;
720
- font-family: var(--font-mono);
721
- line-height: 1.4;
722
  }
723
 
724
- .issue-item.error { background: rgba(255, 82, 82, 0.05); }
725
- .issue-item.warning { background: rgba(255, 171, 64, 0.05); }
726
- .issue-item.info { background: rgba(0, 180, 216, 0.03); }
727
-
728
- .issue-severity {
729
- flex-shrink: 0;
730
- font-size: 10px;
731
  font-weight: 600;
 
 
732
  text-transform: uppercase;
733
- letter-spacing: 0.5px;
734
- width: 52px;
735
  }
736
 
737
- .issue-severity.error { color: var(--error); }
738
- .issue-severity.warning { color: var(--warning); }
739
- .issue-severity.info { color: var(--accent); }
740
-
741
- .issue-message { color: var(--text-secondary); }
742
-
743
- /* ── GALLERY TAB ────────────────────────────────── */
744
-
745
- #tab-gallery {
746
- gap: 8px;
747
  flex-wrap: wrap;
 
748
  align-content: flex-start;
749
  }
750
 
751
  .gallery-empty {
 
 
 
752
  font-family: var(--font-mono);
753
  font-size: 11px;
754
  color: var(--text-muted);
755
  letter-spacing: 0.5px;
756
- margin: auto;
757
  }
758
 
759
  .gallery-card {
760
  all: unset;
761
  flex: 0 0 auto;
762
- width: 160px;
763
  background: var(--bg-surface);
764
  border: 1px solid var(--border);
765
- border-radius: 4px;
766
- padding: 10px;
767
  cursor: pointer;
768
  transition: all 0.2s;
769
  display: flex;
770
  flex-direction: column;
771
- gap: 6px;
772
  }
773
 
774
  .gallery-card:hover {
@@ -794,7 +1054,7 @@
794
  gap: 8px;
795
  }
796
 
797
- /* ── ANIMATIONS ─────────────────────────────────── */
798
 
799
  @keyframes fade-in-up {
800
  from { opacity: 0; transform: translateY(8px); }
@@ -805,138 +1065,175 @@
805
  animation: fade-in-up 0.3s ease-out both;
806
  }
807
 
808
- /* ── RESPONSIVE ─────────────────────────────────── */
809
 
810
  @media (max-width: 768px) {
811
- .examples-sidebar { display: none; }
812
  .logo-sub { display: none; }
813
- #bottom-panel { height: 50vh; max-height: none; }
 
 
814
  }
815
  </style>
816
  </head>
817
- <body>
818
  <div id="app">
819
 
820
- <!-- ── TOP BAR ─────────────────────────────────── -->
821
  <div id="topbar">
822
  <div class="logo">
823
- <div class="logo-mark"></div>
824
  <span class="logo-text">NeuralCAD</span>
825
- <span class="logo-sub">Text-to-CNC Pipeline</span>
826
  </div>
827
  <div class="topbar-right">
828
  <div class="backend-toggle">
829
  <button id="btn-mock" class="active" onclick="setBackend('mock')">MOCK</button>
830
  <button id="btn-gemini" onclick="setBackend('gemini')">GEMINI</button>
831
- <button id="btn-api" onclick="setBackend('anthropic')">CLAUDE</button>
832
  </div>
833
- <div class="status-dot" id="status-dot" title="MCP Server Connected"></div>
834
- <div class="version-tag">v1.0.0</div>
 
 
 
835
  </div>
836
  </div>
837
 
838
- <!-- ── 3D VIEWER ───────────────────────────────── -->
839
- <div id="viewer-container">
840
- <canvas id="viewer-canvas"></canvas>
841
 
842
- <div class="corner-bracket tl"></div>
843
- <div class="corner-bracket tr"></div>
844
- <div class="corner-bracket bl"></div>
845
- <div class="corner-bracket br"></div>
846
 
847
- <div id="geo-stats">
848
- <div><span class="stat-label">VOL </span><span class="stat-value" id="stat-volume"></span></div>
849
- <div><span class="stat-label">BBOX </span><span class="stat-value" id="stat-bbox"></span></div>
850
- <div><span class="stat-label">FACES </span><span class="stat-value" id="stat-faces"></span><span class="stat-label"> EDGES </span><span class="stat-value" id="stat-edges"></span></div>
851
- </div>
852
 
853
- <div id="cnc-badge">
854
- <div class="badge badge-success" id="badge-cnc"></div>
855
- <div class="badge badge-info" id="badge-axis"></div>
856
- </div>
857
 
858
- <div id="download-btns">
859
- <a class="dl-btn" id="dl-step" download>STEP</a>
860
- <a class="dl-btn" id="dl-stl" download>STL</a>
861
- </div>
 
862
 
863
- <div id="viewer-hint">DRAG ROTATE &middot; SCROLL ZOOM &middot; RIGHT-DRAG PAN</div>
864
 
865
- <div id="viewer-loading">
866
- <div class="spinner"></div>
867
- <div class="loading-text" id="loading-msg">GENERATING MODEL...</div>
868
- </div>
869
 
870
- <div id="viewer-empty">
871
- <div class="empty-icon"><div class="empty-icon-inner"></div></div>
872
- <div class="empty-text">DESCRIBE A PART TO BEGIN</div>
 
873
  </div>
874
- </div>
875
 
876
- <!-- ── BOTTOM PANEL ────────────────────────────── -->
877
- <div id="bottom-panel">
878
- <div id="tabs">
879
- <button class="tab-btn active" data-tab="generate">GENERATE</button>
880
- <button class="tab-btn" data-tab="code">CODE</button>
881
- <button class="tab-btn" data-tab="validation">VALIDATION <span class="tab-count" id="issue-count" style="display:none">0</span></button>
882
- <button class="tab-btn" data-tab="gallery">GALLERY <span class="tab-count" id="gallery-count" style="display:none">0</span></button>
883
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
- <!-- Generate Tab -->
886
- <div class="tab-content active" id="tab-generate">
887
- <div class="input-area">
888
- <textarea id="prompt-input" placeholder="Describe a mechanical part...&#10;&#10;e.g. A mounting bracket with four M6 bolt holes, 80mm wide, with rounded corners"></textarea>
889
- <div class="action-row">
890
- <button class="btn-generate" id="btn-gen" onclick="doGenerate()">
891
- <span id="btn-gen-text">GENERATE MODEL</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
892
  </button>
893
- <button class="btn-image" title="Generate from image" onclick="document.getElementById('image-input').click()">
894
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
895
  </button>
896
- <input type="file" id="image-input" accept="image/*" style="display:none" onchange="doImageGenerate(this)">
897
  </div>
898
- </div>
899
- <div class="examples-sidebar">
900
- <div class="examples-label">Quick Examples</div>
901
- <button class="example-btn" onclick="runExample('A mounting bracket with four M6 bolt holes, 80mm wide')">
902
- <span class="example-arrow">&#9654;</span> Mounting bracket
903
- </button>
904
- <button class="example-btn" onclick="runExample('A spur gear with 20 teeth, module 2, 10mm thick')">
905
- <span class="example-arrow">&#9654;</span> Spur gear
906
- </button>
907
- <button class="example-btn" onclick="runExample('A 100mm pipe flange with 8 M8 bolt holes and center bore')">
908
- <span class="example-arrow">&#9654;</span> Pipe flange
909
- </button>
910
- <button class="example-btn" onclick="runExample('A 30mm cylinder with 12 cooling fins, heatsink')">
911
- <span class="example-arrow">&#9654;</span> Heatsink
912
- </button>
913
- <button class="example-btn" onclick="runExample('An L-bracket 60mm arms with M5 holes, 25mm wide')">
914
- <span class="example-arrow">&#9654;</span> L-bracket
915
- </button>
916
- <button class="example-btn" onclick="runExample('An enclosure 120x80x40mm with pocket, slots, and rounded corners')">
917
- <span class="example-arrow">&#9654;</span> Electronics enclosure
918
- </button>
919
- <button class="example-btn" onclick="runExample('A 50x50x10mm plate with central slot and two M6 holes')">
920
- <span class="example-arrow">&#9654;</span> Slotted plate
921
- </button>
922
- <button class="example-btn" onclick="runExample('A box 80x60x30mm with 4 mounting bosses and chamfered edges')">
923
- <span class="example-arrow">&#9654;</span> Boss mount
924
- </button>
925
  </div>
926
  </div>
927
 
928
- <!-- Code Tab -->
929
- <div class="tab-content" id="tab-code">
930
- <pre id="code-display" class="code-empty">No generated code yet. Run a generation to see the CadQuery output.</pre>
931
- </div>
 
 
 
 
 
 
 
 
 
 
932
 
933
- <!-- Validation Tab -->
934
- <div class="tab-content" id="tab-validation">
935
- <div class="validation-empty">No validation data. Generate a model first.</div>
 
 
 
936
  </div>
 
 
 
937
 
938
- <!-- Gallery Tab -->
939
- <div class="tab-content" id="tab-gallery">
 
 
 
 
 
 
940
  <div class="gallery-empty">No models generated yet.</div>
941
  </div>
942
  </div>
@@ -946,9 +1243,21 @@
946
  // ── STATE ─────────────────────────────────────────────
947
 
948
  let currentBackend = 'mock';
 
 
949
  let currentPartName = '';
950
- let scene, camera, renderer, controls, currentMesh;
 
951
  const galleryItems = [];
 
 
 
 
 
 
 
 
 
952
 
953
  // ── THREE.JS SETUP ────────────────────────────────────
954
 
@@ -986,6 +1295,11 @@ function initViewer() {
986
  rimLight.position.set(0, -50, 100);
987
  scene.add(rimLight);
988
 
 
 
 
 
 
989
  // Controls
990
  controls = new THREE.OrbitControls(camera, renderer.domElement);
991
  controls.enableDamping = true;
@@ -1017,14 +1331,12 @@ function loadSTL(url) {
1017
  return new Promise((resolve, reject) => {
1018
  const loader = new THREE.STLLoader();
1019
  loader.load(url, (geometry) => {
1020
- // Remove existing mesh
1021
  if (currentMesh) {
1022
  scene.remove(currentMesh);
1023
  currentMesh.geometry.dispose();
1024
  currentMesh.material.dispose();
1025
  }
1026
 
1027
- // Material: machined steel look
1028
  const material = new THREE.MeshPhongMaterial({
1029
  color: 0x7799aa,
1030
  specular: 0x445566,
@@ -1036,7 +1348,6 @@ function loadSTL(url) {
1036
  mesh.castShadow = true;
1037
  mesh.receiveShadow = true;
1038
 
1039
- // Center geometry
1040
  geometry.computeBoundingBox();
1041
  const center = new THREE.Vector3();
1042
  geometry.boundingBox.getCenter(center);
@@ -1046,15 +1357,19 @@ function loadSTL(url) {
1046
  currentMesh = mesh;
1047
 
1048
  // Fit camera
1049
- const box = geometry.boundingBox;
1050
  const size = new THREE.Vector3();
1051
- box.getSize(size);
1052
  const maxDim = Math.max(size.x, size.y, size.z);
1053
  const dist = maxDim * 2.5;
1054
  camera.position.set(dist * 0.7, dist * 0.5, dist * 0.7);
1055
  controls.target.set(0, 0, 0);
1056
  controls.update();
1057
 
 
 
 
 
 
1058
  document.getElementById('viewer-empty').style.display = 'none';
1059
  resolve();
1060
  }, undefined, reject);
@@ -1067,132 +1382,279 @@ function setBackend(name) {
1067
  currentBackend = name;
1068
  document.getElementById('btn-mock').classList.toggle('active', name === 'mock');
1069
  document.getElementById('btn-gemini').classList.toggle('active', name === 'gemini');
1070
- document.getElementById('btn-api').classList.toggle('active', name === 'anthropic');
1071
  }
1072
 
1073
- // ── TABS ──────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
 
1075
- document.getElementById('tabs').addEventListener('click', (e) => {
1076
- const btn = e.target.closest('.tab-btn');
1077
- if (!btn) return;
1078
- const tabName = btn.dataset.tab;
1079
 
1080
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1081
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1082
 
1083
- btn.classList.add('active');
1084
- document.getElementById('tab-' + tabName).classList.add('active');
1085
- });
 
 
 
 
 
1086
 
1087
- // ── GENERATION ────────────────────────────────────────
 
 
1088
 
1089
- async function doGenerate() {
1090
- const prompt = document.getElementById('prompt-input').value.trim();
1091
- if (!prompt) return;
1092
 
1093
- setLoading(true, 'GENERATING MODEL...');
 
1094
 
1095
  try {
1096
- const resp = await fetch('/api/generate', {
1097
  method: 'POST',
1098
  headers: { 'Content-Type': 'application/json' },
1099
- body: JSON.stringify({ prompt, backend: currentBackend }),
 
 
 
 
 
1100
  });
1101
  const data = await resp.json();
1102
- handleResult(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1103
  } catch (err) {
1104
- setLoading(false);
1105
- alert('Error: ' + err.message);
 
 
 
 
 
 
 
1106
  }
1107
  }
1108
 
1109
- async function doImageGenerate(input) {
1110
- const file = input.files[0];
1111
- if (!file) return;
 
 
 
 
 
 
 
 
 
 
1112
 
1113
- setLoading(true, 'ANALYZING IMAGE...');
 
 
 
 
1114
 
1115
- const form = new FormData();
1116
- form.append('image', file);
1117
- form.append('backend', currentBackend === 'mock' ? 'anthropic' : currentBackend);
1118
 
1119
- try {
1120
- const resp = await fetch('/api/generate-image', { method: 'POST', body: form });
1121
- const data = await resp.json();
1122
- handleResult(data);
1123
- } catch (err) {
1124
- setLoading(false);
1125
- alert('Error: ' + err.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126
  }
1127
 
1128
- input.value = '';
 
1129
  }
1130
 
1131
- function runExample(prompt) {
1132
- document.getElementById('prompt-input').value = prompt;
1133
- // Force mock for examples (reliability)
1134
- const prevBackend = currentBackend;
1135
- currentBackend = 'mock';
1136
- doGenerate().then(() => { currentBackend = prevBackend; });
 
 
1137
  }
1138
 
1139
- async function handleResult(data) {
1140
- if (!data.success) {
1141
- setLoading(false);
1142
- alert('Generation failed: ' + (data.execution?.error || data.error || 'Unknown error'));
1143
- return;
1144
- }
1145
 
1146
- currentPartName = data.part_name;
 
 
 
 
 
1147
 
1148
- // Update code tab
1149
- updateCodeTab(data.generated_code);
1150
 
1151
- // Update validation tab
1152
- updateValidationTab(data.validation, data.execution);
1153
 
1154
- // Update geometry stats
1155
- updateGeoStats(data.execution);
 
 
1156
 
1157
- // Update CNC badge
1158
- updateCNCBadge(data.validation);
 
1159
 
1160
- // Update downloads
1161
- updateDownloads(data.part_name, data.exported_files);
 
1162
 
1163
- // Load 3D model
1164
- setLoading(true, 'LOADING 3D MODEL...');
1165
- try {
1166
- await loadSTL('/api/models/' + data.part_name + '.stl');
1167
- } catch (e) {
1168
- console.warn('STL load failed:', e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
  }
 
1170
 
1171
- setLoading(false);
 
 
 
1172
 
1173
- // Add to gallery
1174
- addToGallery(data);
 
 
 
1175
 
1176
- // Switch to generate tab if not already there
1177
- loadGallery();
 
 
 
 
 
 
 
 
 
 
 
1178
  }
1179
 
1180
  // ── UI UPDATES ────────────────────────────────────────
1181
 
1182
- function setLoading(on, msg) {
1183
  const el = document.getElementById('viewer-loading');
1184
- const btn = document.getElementById('btn-gen');
1185
- const btnText = document.getElementById('btn-gen-text');
1186
-
1187
  if (on) {
1188
  el.classList.add('visible');
1189
  document.getElementById('loading-msg').textContent = msg || 'GENERATING...';
1190
- btn.disabled = true;
1191
- btnText.textContent = 'GENERATING...';
1192
  } else {
1193
  el.classList.remove('visible');
1194
- btn.disabled = false;
1195
- btnText.textContent = 'GENERATE MODEL';
1196
  }
1197
  }
1198
 
@@ -1211,8 +1673,8 @@ function updateGeoStats(exec) {
1211
  bbox.map(v => v.toFixed(1)).join(' \u00D7 ') + ' mm';
1212
  }
1213
 
1214
- document.getElementById('stat-faces').textContent = exec.face_count || '';
1215
- document.getElementById('stat-edges').textContent = exec.edge_count || '';
1216
  }
1217
 
1218
  function updateCNCBadge(validation) {
@@ -1233,125 +1695,83 @@ function updateCNCBadge(validation) {
1233
  axisBadge.textContent = (validation.axis_recommendation || '').toUpperCase();
1234
  }
1235
 
1236
- function updateDownloads(partName, files) {
1237
  const el = document.getElementById('download-btns');
1238
- if (!files) { el.classList.remove('visible'); return; }
1239
  el.classList.add('visible');
1240
 
1241
  document.getElementById('dl-step').href = '/api/models/' + partName + '.step';
1242
  document.getElementById('dl-stl').href = '/api/models/' + partName + '.stl';
 
1243
  }
1244
 
1245
- function updateCodeTab(code) {
1246
- const el = document.getElementById('code-display');
1247
- if (!code) {
1248
- el.textContent = 'No generated code yet.';
1249
- el.classList.add('code-empty');
1250
- return;
 
 
 
 
1251
  }
1252
- el.classList.remove('code-empty');
1253
- el.innerHTML = highlightPython(code);
 
 
 
 
1254
  }
1255
 
1256
  function highlightPython(code) {
1257
- // Simple Python syntax highlighting
1258
  let escaped = code
1259
  .replace(/&/g, '&amp;')
1260
  .replace(/</g, '&lt;')
1261
  .replace(/>/g, '&gt;');
1262
 
1263
- // Comments
1264
  escaped = escaped.replace(/(#.*$)/gm, '<span class="cm">$1</span>');
1265
- // Strings
1266
  escaped = escaped.replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"[^"\n]*"|'[^'\n]*')/g, '<span class="st">$1</span>');
1267
- // Keywords
1268
  const kw = /\b(import|from|as|def|class|return|if|else|elif|for|while|in|not|and|or|True|False|None|with|try|except|finally|raise|pass|break|continue|lambda|yield)\b/g;
1269
  escaped = escaped.replace(kw, '<span class="kw">$1</span>');
1270
- // Numbers
1271
  escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="nu">$1</span>');
1272
- // Function calls
1273
  escaped = escaped.replace(/\.([a-zA-Z_]\w*)\(/g, '.<span class="fn">$1</span>(');
1274
 
1275
  return escaped;
1276
  }
1277
 
1278
- function updateValidationTab(validation, exec) {
1279
- const el = document.getElementById('tab-validation');
1280
-
1281
- if (!validation) {
1282
- el.innerHTML = '<div class="validation-empty">No validation data. Generate a model first.</div>';
1283
- document.getElementById('issue-count').style.display = 'none';
1284
- return;
1285
- }
1286
-
1287
- let html = '<div class="validation-header">';
1288
-
1289
- if (validation.machinable) {
1290
- html += '<div class="badge badge-success">\u2713 MACHINABLE</div>';
1291
- } else {
1292
- html += '<div class="badge badge-error">\u2717 NOT MACHINABLE</div>';
1293
- }
1294
-
1295
- if (validation.axis_recommendation) {
1296
- html += '<div class="validation-axis">Recommended: ' + validation.axis_recommendation + '</div>';
1297
- }
1298
-
1299
- const errs = validation.error_count || 0;
1300
- const warns = validation.warning_count || 0;
1301
- html += '<div class="validation-axis" style="margin-left:auto">' + errs + ' errors, ' + warns + ' warnings</div>';
1302
- html += '</div>';
1303
-
1304
- if (validation.issues && validation.issues.length > 0) {
1305
- html += '<div class="issue-list">';
1306
- for (const issue of validation.issues) {
1307
- const sev = issue.severity || 'info';
1308
- html += '<div class="issue-item ' + sev + '">';
1309
- html += '<span class="issue-severity ' + sev + '">' + sev.toUpperCase() + '</span>';
1310
- html += '<span class="issue-message">' + escapeHtml(issue.message) + '</span>';
1311
- html += '</div>';
1312
- }
1313
- html += '</div>';
1314
- }
1315
-
1316
- el.innerHTML = html;
1317
-
1318
- const totalIssues = (validation.issues || []).length;
1319
- const countEl = document.getElementById('issue-count');
1320
- if (totalIssues > 0) {
1321
- countEl.textContent = totalIssues;
1322
- countEl.style.display = '';
1323
- } else {
1324
- countEl.style.display = 'none';
1325
- }
1326
- }
1327
 
1328
  function addToGallery(data) {
1329
  galleryItems.unshift({
1330
  name: data.part_name,
1331
- prompt: data.prompt,
1332
  volume: data.execution?.volume_mm3,
1333
  faces: data.execution?.face_count,
1334
  machinable: data.validation?.machinable,
1335
  });
1336
- loadGallery();
1337
  }
1338
 
1339
- function loadGallery() {
1340
- const el = document.getElementById('tab-gallery');
1341
- const countEl = document.getElementById('gallery-count');
 
 
 
 
 
 
 
 
1342
 
1343
  if (galleryItems.length === 0) {
1344
- el.innerHTML = '<div class="gallery-empty">No models generated yet.</div>';
1345
- countEl.style.display = 'none';
1346
  return;
1347
  }
1348
 
1349
- countEl.textContent = galleryItems.length;
1350
- countEl.style.display = '';
1351
-
1352
  let html = '';
1353
  for (const item of galleryItems) {
1354
- html += '<button class="gallery-card fade-in" onclick="loadGalleryItem(\'' + item.name + '\')">';
1355
  html += '<div class="gallery-card-name">' + escapeHtml(item.name) + '</div>';
1356
  html += '<div class="gallery-card-meta">';
1357
  if (item.faces) html += '<span>' + item.faces + ' faces</span>';
@@ -1362,19 +1782,22 @@ function loadGallery() {
1362
  html += '</div></button>';
1363
  }
1364
 
1365
- el.innerHTML = html;
1366
  }
1367
 
1368
  async function loadGalleryItem(name) {
1369
- setLoading(true, 'LOADING MODEL...');
 
1370
  try {
1371
  await loadSTL('/api/models/' + name + '.stl');
1372
  } catch (e) {
1373
  console.warn('Failed to load:', e);
1374
  }
1375
- setLoading(false);
1376
  }
1377
 
 
 
1378
  function escapeHtml(str) {
1379
  const div = document.createElement('div');
1380
  div.textContent = str;
@@ -1390,26 +1813,89 @@ async function checkServer() {
1390
  if (resp.ok) {
1391
  dot.style.background = 'var(--success)';
1392
  dot.style.boxShadow = '0 0 6px var(--success)';
1393
- dot.title = 'MCP Server Connected';
1394
  } else {
1395
  dot.style.background = 'var(--warning)';
1396
  dot.style.boxShadow = '0 0 6px var(--warning)';
1397
- dot.title = 'MCP Server Error';
1398
  }
1399
  } catch {
1400
  const dot = document.getElementById('status-dot');
1401
  dot.style.background = 'var(--error)';
1402
  dot.style.boxShadow = '0 0 6px var(--error)';
1403
- dot.title = 'MCP Server Offline';
1404
  }
1405
  }
1406
 
1407
- // ── KEYBOARD SHORTCUT ─────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1408
 
1409
- document.getElementById('prompt-input').addEventListener('keydown', (e) => {
1410
  if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
1411
  e.preventDefault();
1412
- doGenerate();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
  }
1414
  });
1415
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NeuralCAD — Multi-Agent Design</title>
7
 
8
  <!-- Three.js -->
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 
37
  --machined-steel: #8899aa;
38
  --font-mono: 'JetBrains Mono', 'Cascadia Code', monospace;
39
  --font-body: 'DM Sans', system-ui, sans-serif;
40
+ --agent-design: #7c3aed;
41
+ --agent-engineering: #00b4d8;
42
+ --agent-cnc: #00e676;
43
+ --agent-cad: #ffab40;
44
+ --chat-width: 340px;
45
  }
46
 
47
  html, body {
 
52
  font-family: var(--font-body);
53
  }
54
 
55
+ /* ---- Scrollbar ---- */
56
+ ::-webkit-scrollbar { width: 5px; }
57
+ ::-webkit-scrollbar-track { background: transparent; }
58
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
59
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
60
+
61
+ /* ---- LAYOUT ---- */
62
 
63
  #app {
64
  display: flex;
65
  flex-direction: column;
66
  height: 100vh;
67
+ width: 100vw;
68
+ overflow: hidden;
69
  }
70
 
71
+ /* ---- TOP BAR ---- */
72
 
73
  #topbar {
74
  flex: 0 0 44px;
 
78
  align-items: center;
79
  justify-content: space-between;
80
  padding: 0 16px;
81
+ z-index: 100;
82
  position: relative;
83
  }
84
 
 
98
  gap: 10px;
99
  }
100
 
101
+ .logo-diamond {
102
+ color: var(--accent);
103
+ font-size: 18px;
104
+ line-height: 1;
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
 
107
  .logo-text {
 
124
  border-left: 1px solid var(--border);
125
  }
126
 
127
+ .topbar-center {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 12px;
131
+ }
132
+
133
  .topbar-right {
134
  display: flex;
135
  align-items: center;
 
139
  .backend-toggle {
140
  display: flex;
141
  align-items: center;
142
+ gap: 0;
143
  background: var(--bg-void);
144
  border: 1px solid var(--border);
145
  border-radius: 4px;
 
154
  cursor: pointer;
155
  color: var(--text-muted);
156
  transition: all 0.2s;
157
+ border-right: 1px solid var(--border);
158
  }
159
 
160
+ .backend-toggle button:last-child { border-right: none; }
161
+
162
  .backend-toggle button.active {
163
  background: var(--accent-glow);
164
  color: var(--accent);
 
168
  color: var(--text-secondary);
169
  }
170
 
171
+ .gallery-btn {
172
+ all: unset;
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 6px;
176
+ padding: 4px 12px;
177
+ font-family: var(--font-mono);
178
+ font-size: 11px;
179
+ color: var(--text-secondary);
180
+ border: 1px solid var(--border);
181
+ border-radius: 4px;
182
+ cursor: pointer;
183
+ transition: all 0.2s;
184
+ }
185
+
186
+ .gallery-btn:hover {
187
+ border-color: var(--accent-dim);
188
+ color: var(--accent);
189
+ }
190
+
191
  .status-dot {
192
+ width: 7px; height: 7px;
193
  border-radius: 50%;
194
  background: var(--success);
195
  box-shadow: 0 0 6px var(--success);
 
201
  50% { opacity: 0.4; }
202
  }
203
 
204
+ /* ---- MAIN AREA ---- */
205
+
206
+ #main {
207
+ flex: 1;
208
+ display: flex;
209
+ position: relative;
210
+ min-height: 0;
211
+ overflow: hidden;
212
  }
213
 
214
+ /* ---- 3D VIEWER ---- */
215
 
216
  #viewer-container {
217
+ flex: 1;
218
  position: relative;
219
  background: var(--bg-void);
220
  overflow: hidden;
 
227
  display: block;
228
  }
229
 
230
+ /* Geo stats overlay - top left */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  #geo-stats {
232
  position: absolute;
233
  top: 14px;
234
+ left: 14px;
235
+ z-index: 10;
236
  background: rgba(6, 8, 12, 0.85);
237
  border: 1px solid var(--border);
238
  border-radius: 4px;
 
249
  .stat-label { color: var(--text-muted); }
250
  .stat-value { color: var(--accent); }
251
 
252
+ /* CNC badge - top right of viewer (NOT behind chat) */
253
  #cnc-badge {
254
  position: absolute;
255
  top: 14px;
256
+ right: 14px;
257
+ z-index: 10;
258
  display: none;
259
  gap: 6px;
260
+ transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
261
  }
262
 
263
  #cnc-badge.visible { display: flex; }
264
+ body.chat-open #cnc-badge { right: calc(var(--chat-width) + 14px); }
265
 
266
  .badge {
267
  font-family: var(--font-mono);
 
297
  color: var(--accent);
298
  }
299
 
300
+ /* Download buttons - bottom left */
301
  #download-btns {
302
  position: absolute;
303
  bottom: 14px;
304
+ left: 14px;
305
+ z-index: 10;
306
  display: none;
307
  gap: 6px;
308
  }
 
334
  #viewer-hint {
335
  position: absolute;
336
  bottom: 16px;
337
+ right: 16px;
338
+ z-index: 10;
339
  font-family: var(--font-mono);
340
  font-size: 10px;
341
  color: var(--text-muted);
342
  letter-spacing: 0.5px;
343
  pointer-events: none;
344
+ transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
345
  }
346
 
347
+ body.chat-open #viewer-hint { right: calc(var(--chat-width) + 16px); }
348
+
349
  /* Loading spinner */
350
  #viewer-loading {
351
  position: absolute;
352
  inset: 0;
353
+ z-index: 20;
354
  display: none;
355
  align-items: center;
356
  justify-content: center;
 
383
  #viewer-empty {
384
  position: absolute;
385
  inset: 0;
386
+ z-index: 5;
387
  display: flex;
388
  align-items: center;
389
  justify-content: center;
390
  flex-direction: column;
391
+ gap: 16px;
392
  pointer-events: none;
393
  }
394
 
 
400
  align-items: center;
401
  justify-content: center;
402
  transform: rotate(45deg);
403
+ opacity: 0.5;
404
  }
405
 
406
  .empty-icon-inner {
 
413
 
414
  .empty-text {
415
  font-family: var(--font-mono);
416
+ font-size: 12px;
417
  color: var(--text-muted);
418
  letter-spacing: 1px;
419
+ text-align: center;
420
+ line-height: 1.6;
421
  }
422
 
423
+ /* ---- CHAT PANEL ---- */
424
 
425
+ #chat-panel {
426
+ position: absolute;
427
+ top: 0;
428
+ right: 0;
429
+ width: var(--chat-width);
430
+ height: 100%;
431
+ background: rgba(10, 14, 20, 0.92);
432
+ backdrop-filter: blur(16px);
433
+ border-left: 1px solid var(--border);
434
  display: flex;
435
  flex-direction: column;
436
+ z-index: 50;
437
+ transform: translateX(0);
438
+ transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
439
  }
440
 
441
+ #chat-panel.collapsed {
442
+ transform: translateX(100%);
443
+ }
444
+
445
+ /* Collapse toggle */
446
+ #chat-toggle {
447
+ all: unset;
448
  position: absolute;
449
+ top: 50%;
450
+ left: -28px;
451
+ transform: translateY(-50%);
452
+ width: 28px;
453
+ height: 56px;
454
+ background: rgba(10, 14, 20, 0.92);
455
+ backdrop-filter: blur(16px);
456
+ border: 1px solid var(--border);
457
+ border-right: none;
458
+ border-radius: 6px 0 0 6px;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ cursor: pointer;
463
+ color: var(--text-secondary);
464
+ font-size: 14px;
465
+ transition: all 0.2s;
466
+ z-index: 51;
467
+ }
468
+
469
+ #chat-toggle:hover {
470
+ color: var(--accent);
471
+ background: rgba(10, 14, 20, 0.98);
472
+ }
473
+
474
+ /* Floating open pill */
475
+ #chat-open-pill {
476
+ position: fixed;
477
+ bottom: 20px;
478
+ left: 50%;
479
+ transform: translateX(-50%);
480
+ z-index: 60;
481
+ display: none;
482
+ align-items: center;
483
+ gap: 10px;
484
+ padding: 10px 20px;
485
+ background: rgba(10, 14, 20, 0.95);
486
+ backdrop-filter: blur(16px);
487
+ border: 1px solid var(--border);
488
+ border-radius: 24px;
489
+ cursor: pointer;
490
+ font-family: var(--font-mono);
491
+ font-size: 12px;
492
+ color: var(--text-primary);
493
+ letter-spacing: 0.5px;
494
+ transition: all 0.3s;
495
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
496
+ }
497
+
498
+ #chat-open-pill:hover {
499
+ border-color: var(--accent-dim);
500
+ box-shadow: 0 4px 32px rgba(0, 180, 216, 0.15);
501
+ }
502
+
503
+ #chat-open-pill.visible { display: flex; }
504
+
505
+ .pill-dots {
506
+ display: flex;
507
+ gap: 4px;
508
  }
509
 
510
+ .pill-dot {
511
+ width: 8px; height: 8px;
512
+ border-radius: 50%;
513
+ }
514
+
515
+ /* Chat header */
516
+ .chat-header {
517
+ flex: 0 0 48px;
518
  display: flex;
519
+ align-items: center;
520
+ justify-content: space-between;
521
+ padding: 0 16px;
522
  border-bottom: 1px solid var(--border);
 
 
523
  }
524
 
525
+ .chat-header-left {
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 10px;
529
+ }
530
+
531
+ .chat-header-title {
532
  font-family: var(--font-mono);
533
  font-size: 11px;
534
+ font-weight: 600;
535
+ letter-spacing: 2px;
536
+ color: var(--text-secondary);
537
+ text-transform: uppercase;
538
+ }
539
+
540
+ .agent-dots {
541
+ display: flex;
542
+ gap: 5px;
543
+ }
544
+
545
+ .agent-dot {
546
+ width: 8px; height: 8px;
547
+ border-radius: 50%;
548
+ opacity: 0.8;
549
+ }
550
+
551
+ /* Messages area */
552
+ #chat-messages {
553
+ flex: 1;
554
+ overflow-y: auto;
555
+ padding: 16px 12px;
556
  display: flex;
557
+ flex-direction: column;
558
+ gap: 12px;
559
+ min-height: 0;
560
+ }
561
+
562
+ /* Quick examples */
563
+ .quick-examples {
564
+ display: flex;
565
+ flex-direction: column;
566
  align-items: center;
567
+ gap: 12px;
568
+ padding: 40px 16px 20px;
569
+ }
570
+
571
+ .quick-examples-label {
572
+ font-family: var(--font-mono);
573
+ font-size: 10px;
574
+ color: var(--text-muted);
575
+ letter-spacing: 2px;
576
+ text-transform: uppercase;
577
+ }
578
+
579
+ .quick-chips {
580
+ display: flex;
581
+ flex-wrap: wrap;
582
  gap: 6px;
583
+ justify-content: center;
584
  }
585
 
586
+ .quick-chip {
587
+ all: unset;
588
+ padding: 6px 12px;
589
+ font-family: var(--font-mono);
590
+ font-size: 11px;
591
+ color: var(--text-secondary);
592
+ background: var(--bg-surface);
593
+ border: 1px solid var(--border);
594
+ border-radius: 16px;
595
+ cursor: pointer;
596
+ transition: all 0.2s;
597
+ white-space: nowrap;
598
+ }
599
 
600
+ .quick-chip:hover {
601
+ border-color: var(--accent-dim);
602
  color: var(--accent);
603
+ background: var(--accent-glow);
604
  }
605
 
606
+ /* Message bubbles */
607
+ .msg {
608
+ display: flex;
609
+ gap: 8px;
610
+ max-width: 100%;
611
+ animation: msg-in 0.25s ease-out both;
 
612
  }
613
 
614
+ @keyframes msg-in {
615
+ from { opacity: 0; transform: translateY(8px); }
616
+ to { opacity: 1; transform: translateY(0); }
 
 
 
617
  }
618
 
619
+ .msg-user {
620
+ justify-content: flex-end;
621
+ }
622
+
623
+ .msg-user .msg-bubble {
624
+ background: #1a2a3a;
625
+ border: 1px solid rgba(0, 180, 216, 0.15);
626
+ border-radius: 12px 12px 4px 12px;
627
+ padding: 8px 12px;
628
+ font-size: 13px;
629
+ line-height: 1.5;
630
+ color: var(--text-primary);
631
+ max-width: 85%;
632
+ word-wrap: break-word;
633
+ }
634
+
635
+ .msg-agent {
636
+ align-items: flex-start;
637
+ }
638
+
639
+ .msg-avatar {
640
+ flex-shrink: 0;
641
+ width: 24px; height: 24px;
642
+ border-radius: 50%;
643
+ display: flex;
644
+ align-items: center;
645
+ justify-content: center;
646
+ font-size: 11px;
647
+ font-weight: 700;
648
+ color: rgba(0, 0, 0, 0.7);
649
+ margin-top: 2px;
650
  }
651
 
652
+ .msg-agent-body {
 
653
  flex: 1;
654
+ min-width: 0;
 
 
 
655
  }
656
 
657
+ .msg-agent-name {
658
+ font-family: var(--font-mono);
659
+ font-size: 10px;
660
+ font-weight: 600;
661
+ letter-spacing: 0.5px;
662
+ margin-bottom: 3px;
663
+ text-transform: uppercase;
664
+ }
665
 
666
+ .msg-agent-bubble {
667
+ background: var(--bg-surface);
668
+ border: 1px solid var(--border);
669
+ border-radius: 4px 12px 12px 12px;
670
+ padding: 8px 12px;
671
+ font-size: 13px;
672
+ line-height: 1.5;
673
+ color: var(--text-primary);
674
+ word-wrap: break-word;
675
+ }
676
 
677
+ /* CAD Coder special styling */
678
+ .msg-agent-bubble.cad-bubble {
679
+ background: rgba(255, 171, 64, 0.08);
680
+ border-color: rgba(255, 171, 64, 0.2);
681
+ }
682
 
683
+ .msg-view-code {
684
+ display: inline-block;
685
+ margin-top: 6px;
686
+ font-family: var(--font-mono);
687
+ font-size: 10px;
688
+ color: var(--warning);
689
+ cursor: pointer;
690
+ text-decoration: none;
691
+ letter-spacing: 0.5px;
692
+ transition: opacity 0.2s;
693
  }
694
 
695
+ .msg-view-code:hover { opacity: 0.7; }
696
+
697
+ /* Typing indicator */
698
+ .typing-indicator {
699
+ display: flex;
700
+ align-items: center;
701
+ gap: 8px;
702
+ padding: 8px 12px;
703
+ }
704
+
705
+ .typing-dots {
706
+ display: flex;
707
+ gap: 4px;
708
+ }
709
+
710
+ .typing-dots span {
711
+ width: 6px; height: 6px;
712
+ border-radius: 50%;
713
+ background: var(--text-muted);
714
+ animation: typing-bounce 1.2s ease-in-out infinite;
715
+ }
716
+
717
+ .typing-dots span:nth-child(2) { animation-delay: 0.15s; }
718
+ .typing-dots span:nth-child(3) { animation-delay: 0.3s; }
719
+
720
+ @keyframes typing-bounce {
721
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
722
+ 30% { transform: translateY(-4px); opacity: 1; }
723
+ }
724
+
725
+ .typing-label {
726
+ font-family: var(--font-mono);
727
+ font-size: 10px;
728
+ color: var(--text-muted);
729
+ letter-spacing: 0.5px;
730
+ }
731
+
732
+ /* Chat input area */
733
+ .chat-input-area {
734
+ flex: 0 0 auto;
735
+ padding: 12px;
736
+ border-top: 1px solid var(--border);
737
  display: flex;
738
  flex-direction: column;
739
  gap: 8px;
 
740
  }
741
 
742
+ .chat-input-row {
743
+ display: flex;
744
+ gap: 6px;
745
+ align-items: flex-end;
746
+ }
747
+
748
+ #chat-input {
749
  flex: 1;
750
+ min-height: 38px;
751
+ max-height: 120px;
752
  background: var(--bg-input);
753
  border: 1px solid var(--border);
754
+ border-radius: 8px;
755
+ padding: 8px 12px;
756
  color: var(--text-primary);
757
  font-family: var(--font-body);
758
  font-size: 13px;
759
+ line-height: 1.4;
760
  resize: none;
761
  outline: none;
762
  transition: border-color 0.2s;
763
  }
764
 
765
+ #chat-input::placeholder { color: var(--text-muted); }
766
+ #chat-input:focus { border-color: var(--accent-dim); }
767
 
768
+ .chat-btn {
769
+ all: unset;
770
+ flex-shrink: 0;
771
+ width: 34px; height: 34px;
772
+ border-radius: 8px;
773
  display: flex;
 
774
  align-items: center;
775
+ justify-content: center;
776
+ cursor: pointer;
777
+ transition: all 0.2s;
778
+ font-size: 16px;
779
  }
780
 
781
+ .chat-btn-preview {
782
+ background: rgba(255, 171, 64, 0.1);
783
+ border: 1px solid rgba(255, 171, 64, 0.25);
784
+ color: var(--warning);
785
+ }
786
+
787
+ .chat-btn-preview:hover {
788
+ background: rgba(255, 171, 64, 0.2);
789
+ border-color: var(--warning);
790
+ }
791
+
792
+ .chat-btn-send {
793
+ background: var(--accent-glow);
794
+ border: 1px solid rgba(0, 180, 216, 0.3);
795
+ color: var(--accent);
796
+ }
797
+
798
+ .chat-btn-send:hover {
799
+ background: rgba(0, 180, 216, 0.25);
800
+ border-color: var(--accent);
801
+ }
802
+
803
+ .chat-shortcut-hint {
804
+ font-family: var(--font-mono);
805
+ font-size: 9px;
806
+ color: var(--text-muted);
807
+ text-align: right;
808
+ letter-spacing: 0.3px;
809
+ }
810
+
811
+ /* @mention autocomplete */
812
+ #mention-dropdown {
813
+ display: none;
814
+ position: absolute;
815
+ bottom: 100%;
816
+ left: 12px;
817
+ right: 12px;
818
+ margin-bottom: 4px;
819
+ background: var(--bg-panel);
820
+ border: 1px solid var(--border);
821
+ border-radius: 8px;
822
+ overflow: hidden;
823
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
824
+ z-index: 55;
825
+ }
826
+
827
+ #mention-dropdown.visible { display: block; }
828
+
829
+ .mention-option {
830
  display: flex;
831
  align-items: center;
832
+ gap: 10px;
833
+ padding: 8px 12px;
834
+ cursor: pointer;
835
+ transition: background 0.15s;
836
+ font-size: 12px;
837
+ }
838
+
839
+ .mention-option:hover,
840
+ .mention-option.active {
841
+ background: var(--bg-surface);
842
+ }
843
+
844
+ .mention-dot {
845
+ width: 10px; height: 10px;
846
+ border-radius: 50%;
847
+ flex-shrink: 0;
848
+ }
849
+
850
+ .mention-name {
851
  font-family: var(--font-mono);
852
+ font-weight: 500;
853
+ color: var(--text-primary);
854
  font-size: 12px;
 
 
 
 
 
855
  }
856
 
857
+ .mention-role {
858
+ font-family: var(--font-mono);
859
+ font-size: 10px;
860
+ color: var(--text-muted);
861
+ margin-left: auto;
862
+ }
863
+
864
+ /* ---- CODE VIEWER MODAL ---- */
865
+
866
+ #code-modal {
867
+ display: none;
868
+ position: fixed;
869
+ inset: 0;
870
+ z-index: 200;
871
+ align-items: center;
872
+ justify-content: center;
873
+ background: rgba(6, 8, 12, 0.85);
874
+ backdrop-filter: blur(8px);
875
+ }
876
+
877
+ #code-modal.visible { display: flex; }
878
 
879
+ .code-modal-inner {
880
+ width: min(720px, 90vw);
881
+ max-height: 80vh;
882
+ background: var(--bg-panel);
 
 
 
883
  border: 1px solid var(--border);
884
+ border-radius: 8px;
885
+ display: flex;
886
+ flex-direction: column;
887
+ overflow: hidden;
888
+ box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
889
+ animation: modal-in 0.25s ease-out;
890
  }
891
 
892
+ @keyframes modal-in {
893
+ from { opacity: 0; transform: scale(0.96) translateY(12px); }
894
+ to { opacity: 1; transform: scale(1) translateY(0); }
895
  }
896
 
897
+ .code-modal-header {
 
898
  display: flex;
899
+ align-items: center;
900
+ justify-content: space-between;
901
+ padding: 12px 16px;
902
+ border-bottom: 1px solid var(--border);
903
  }
904
 
905
+ .code-modal-title {
906
  font-family: var(--font-mono);
907
+ font-size: 11px;
908
+ font-weight: 600;
909
+ color: var(--text-secondary);
910
+ letter-spacing: 1px;
911
  text-transform: uppercase;
 
912
  }
913
 
914
+ .code-modal-close {
915
  all: unset;
916
+ width: 28px; height: 28px;
917
  display: flex;
918
  align-items: center;
919
+ justify-content: center;
 
 
 
920
  border-radius: 4px;
 
 
 
921
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  color: var(--text-muted);
923
+ font-size: 18px;
924
+ transition: all 0.15s;
925
  }
926
 
927
+ .code-modal-close:hover {
928
+ background: var(--bg-surface);
929
+ color: var(--text-primary);
 
930
  }
931
 
932
  #code-display {
933
+ flex: 1;
 
934
  margin: 0;
935
+ padding: 16px;
936
  background: var(--bg-input);
 
937
  color: var(--machined-steel);
938
  font-family: var(--font-mono);
939
+ font-size: 12px;
940
  line-height: 1.7;
941
  overflow: auto;
942
  white-space: pre;
943
  tab-size: 4;
944
  }
945
 
946
+ /* Syntax coloring */
 
 
 
 
 
947
  .kw { color: #c792ea; }
948
  .fn { color: #82aaff; }
949
  .cm { color: #546e7a; }
 
951
  .nu { color: #f78c6c; }
952
  .op { color: #89ddff; }
953
 
954
+ /* ---- GALLERY MODAL ---- */
955
 
956
+ #gallery-modal {
957
+ display: none;
958
+ position: fixed;
959
+ inset: 0;
960
+ z-index: 200;
 
 
 
 
 
 
 
 
 
 
961
  align-items: center;
962
+ justify-content: center;
963
+ background: rgba(6, 8, 12, 0.85);
964
+ backdrop-filter: blur(8px);
965
  }
966
 
967
+ #gallery-modal.visible { display: flex; }
 
 
 
 
 
 
968
 
969
+ .gallery-modal-inner {
970
+ width: min(800px, 90vw);
971
+ max-height: 80vh;
972
+ background: var(--bg-panel);
973
+ border: 1px solid var(--border);
974
+ border-radius: 8px;
975
  display: flex;
976
  flex-direction: column;
977
+ overflow: hidden;
978
+ box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
979
+ animation: modal-in 0.25s ease-out;
980
  }
981
 
982
+ .gallery-modal-header {
983
  display: flex;
984
+ align-items: center;
985
+ justify-content: space-between;
986
+ padding: 12px 16px;
987
+ border-bottom: 1px solid var(--border);
 
 
 
988
  }
989
 
990
+ .gallery-modal-title {
991
+ font-family: var(--font-mono);
992
+ font-size: 11px;
 
 
 
 
993
  font-weight: 600;
994
+ color: var(--text-secondary);
995
+ letter-spacing: 1px;
996
  text-transform: uppercase;
 
 
997
  }
998
 
999
+ .gallery-grid {
1000
+ flex: 1;
1001
+ overflow-y: auto;
1002
+ padding: 16px;
1003
+ display: flex;
 
 
 
 
 
1004
  flex-wrap: wrap;
1005
+ gap: 12px;
1006
  align-content: flex-start;
1007
  }
1008
 
1009
  .gallery-empty {
1010
+ width: 100%;
1011
+ text-align: center;
1012
+ padding: 40px;
1013
  font-family: var(--font-mono);
1014
  font-size: 11px;
1015
  color: var(--text-muted);
1016
  letter-spacing: 0.5px;
 
1017
  }
1018
 
1019
  .gallery-card {
1020
  all: unset;
1021
  flex: 0 0 auto;
1022
+ width: 180px;
1023
  background: var(--bg-surface);
1024
  border: 1px solid var(--border);
1025
+ border-radius: 6px;
1026
+ padding: 12px;
1027
  cursor: pointer;
1028
  transition: all 0.2s;
1029
  display: flex;
1030
  flex-direction: column;
1031
+ gap: 8px;
1032
  }
1033
 
1034
  .gallery-card:hover {
 
1054
  gap: 8px;
1055
  }
1056
 
1057
+ /* ---- ANIMATIONS ---- */
1058
 
1059
  @keyframes fade-in-up {
1060
  from { opacity: 0; transform: translateY(8px); }
 
1065
  animation: fade-in-up 0.3s ease-out both;
1066
  }
1067
 
1068
+ /* ---- RESPONSIVE ---- */
1069
 
1070
  @media (max-width: 768px) {
 
1071
  .logo-sub { display: none; }
1072
+ :root { --chat-width: 100vw; }
1073
+ #chat-toggle { display: none; }
1074
+ .gallery-btn span { display: none; }
1075
  }
1076
  </style>
1077
  </head>
1078
+ <body class="chat-open">
1079
  <div id="app">
1080
 
1081
+ <!-- ---- TOP BAR ---- -->
1082
  <div id="topbar">
1083
  <div class="logo">
1084
+ <span class="logo-diamond">&#9670;</span>
1085
  <span class="logo-text">NeuralCAD</span>
1086
+ <span class="logo-sub">Multi-Agent Design</span>
1087
  </div>
1088
  <div class="topbar-right">
1089
  <div class="backend-toggle">
1090
  <button id="btn-mock" class="active" onclick="setBackend('mock')">MOCK</button>
1091
  <button id="btn-gemini" onclick="setBackend('gemini')">GEMINI</button>
1092
+ <button id="btn-claude" onclick="setBackend('anthropic')">CLAUDE</button>
1093
  </div>
1094
+ <button class="gallery-btn" onclick="openGallery()">
1095
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
1096
+ <span>GALLERY</span>
1097
+ </button>
1098
+ <div class="status-dot" id="status-dot" title="Server Connected"></div>
1099
  </div>
1100
  </div>
1101
 
1102
+ <!-- ---- MAIN AREA ---- -->
1103
+ <div id="main">
 
1104
 
1105
+ <!-- 3D Viewer -->
1106
+ <div id="viewer-container">
1107
+ <canvas id="viewer-canvas"></canvas>
 
1108
 
1109
+ <div id="geo-stats">
1110
+ <div><span class="stat-label">VOL </span><span class="stat-value" id="stat-volume">&mdash;</span></div>
1111
+ <div><span class="stat-label">BBOX </span><span class="stat-value" id="stat-bbox">&mdash;</span></div>
1112
+ <div><span class="stat-label">FACES </span><span class="stat-value" id="stat-faces">&mdash;</span><span class="stat-label"> EDGES </span><span class="stat-value" id="stat-edges">&mdash;</span></div>
1113
+ </div>
1114
 
1115
+ <div id="cnc-badge">
1116
+ <div class="badge badge-success" id="badge-cnc"></div>
1117
+ <div class="badge badge-info" id="badge-axis"></div>
1118
+ </div>
1119
 
1120
+ <div id="download-btns">
1121
+ <a class="dl-btn" id="dl-step" download>STEP</a>
1122
+ <a class="dl-btn" id="dl-stl" download>STL</a>
1123
+ <a class="dl-btn" id="dl-report" download>REPORT</a>
1124
+ </div>
1125
 
1126
+ <div id="viewer-hint">DRAG ROTATE &middot; SCROLL ZOOM &middot; RIGHT-DRAG PAN</div>
1127
 
1128
+ <div id="viewer-loading">
1129
+ <div class="spinner"></div>
1130
+ <div class="loading-text" id="loading-msg">GENERATING MODEL...</div>
1131
+ </div>
1132
 
1133
+ <div id="viewer-empty">
1134
+ <div class="empty-icon"><div class="empty-icon-inner"></div></div>
1135
+ <div class="empty-text">Start a conversation to<br>design your part</div>
1136
+ </div>
1137
  </div>
 
1138
 
1139
+ <!-- Chat Panel -->
1140
+ <div id="chat-panel">
1141
+ <button id="chat-toggle" onclick="toggleChat()" title="Toggle chat panel">&#9664;</button>
1142
+
1143
+ <div class="chat-header">
1144
+ <div class="chat-header-left">
1145
+ <span class="chat-header-title">Design Chat</span>
1146
+ <div class="agent-dots">
1147
+ <div class="agent-dot" style="background: var(--agent-design);" title="Design Agent"></div>
1148
+ <div class="agent-dot" style="background: var(--agent-engineering);" title="Engineering Agent"></div>
1149
+ <div class="agent-dot" style="background: var(--agent-cnc);" title="CNC Agent"></div>
1150
+ <div class="agent-dot" style="background: var(--agent-cad);" title="CAD Coder Agent"></div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+
1155
+ <div id="chat-messages">
1156
+ <div class="quick-examples" id="quick-examples">
1157
+ <div class="quick-examples-label">Quick Start</div>
1158
+ <div class="quick-chips">
1159
+ <button class="quick-chip" onclick="quickSend('Design a servo bracket')">Design a servo bracket</button>
1160
+ <button class="quick-chip" onclick="quickSend('I need a spur gear')">I need a spur gear</button>
1161
+ <button class="quick-chip" onclick="quickSend('Create a heatsink')">Create a heatsink</button>
1162
+ <button class="quick-chip" onclick="quickSend('Design a pipe flange')">Design a pipe flange</button>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
 
1167
+ <div class="chat-input-area" style="position: relative;">
1168
+ <div id="mention-dropdown">
1169
+ <div class="mention-option" data-agent="design" onclick="insertMention('design')">
1170
+ <div class="mention-dot" style="background: var(--agent-design);"></div>
1171
+ <span class="mention-name">@design</span>
1172
+ <span class="mention-role">Design Agent</span>
1173
+ </div>
1174
+ <div class="mention-option" data-agent="engineering" onclick="insertMention('engineering')">
1175
+ <div class="mention-dot" style="background: var(--agent-engineering);"></div>
1176
+ <span class="mention-name">@engineering</span>
1177
+ <span class="mention-role">Engineering Agent</span>
1178
+ </div>
1179
+ <div class="mention-option" data-agent="cnc" onclick="insertMention('cnc')">
1180
+ <div class="mention-dot" style="background: var(--agent-cnc);"></div>
1181
+ <span class="mention-name">@cnc</span>
1182
+ <span class="mention-role">CNC Agent</span>
1183
+ </div>
1184
+ <div class="mention-option" data-agent="cad" onclick="insertMention('cad')">
1185
+ <div class="mention-dot" style="background: var(--agent-cad);"></div>
1186
+ <span class="mention-name">@cad</span>
1187
+ <span class="mention-role">CAD Coder</span>
1188
+ </div>
1189
+ </div>
1190
+ <div class="chat-input-row">
1191
+ <textarea id="chat-input" rows="1" placeholder="Type your message..."></textarea>
1192
+ <button class="chat-btn chat-btn-preview" onclick="sendPreview()" title="Generate 3D preview">
1193
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
1194
  </button>
1195
+ <button class="chat-btn chat-btn-send" onclick="sendFromInput()" title="Send message">
1196
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
1197
  </button>
 
1198
  </div>
1199
+ <div class="chat-shortcut-hint">Ctrl+Enter to send</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  </div>
1201
  </div>
1202
 
1203
+ </div>
1204
+ </div>
1205
+
1206
+ <!-- Floating open pill (when chat collapsed) -->
1207
+ <div id="chat-open-pill" onclick="toggleChat()">
1208
+ <span>Open Chat</span>
1209
+ <div class="pill-dots">
1210
+ <div class="pill-dot" style="background: var(--agent-design);"></div>
1211
+ <div class="pill-dot" style="background: var(--agent-engineering);"></div>
1212
+ <div class="pill-dot" style="background: var(--agent-cnc);"></div>
1213
+ <div class="pill-dot" style="background: var(--agent-cad);"></div>
1214
+ </div>
1215
+ <span>&#9654;</span>
1216
+ </div>
1217
 
1218
+ <!-- Code Viewer Modal -->
1219
+ <div id="code-modal">
1220
+ <div class="code-modal-inner">
1221
+ <div class="code-modal-header">
1222
+ <span class="code-modal-title">CadQuery Code</span>
1223
+ <button class="code-modal-close" onclick="closeCodeModal()">&times;</button>
1224
  </div>
1225
+ <pre id="code-display"></pre>
1226
+ </div>
1227
+ </div>
1228
 
1229
+ <!-- Gallery Modal -->
1230
+ <div id="gallery-modal">
1231
+ <div class="gallery-modal-inner">
1232
+ <div class="gallery-modal-header">
1233
+ <span class="gallery-modal-title">Model Gallery</span>
1234
+ <button class="code-modal-close" onclick="closeGallery()">&times;</button>
1235
+ </div>
1236
+ <div class="gallery-grid" id="gallery-grid">
1237
  <div class="gallery-empty">No models generated yet.</div>
1238
  </div>
1239
  </div>
 
1243
  // ── STATE ─────────────────────────────────────────────
1244
 
1245
  let currentBackend = 'mock';
1246
+ let chatHistory = [];
1247
+ let chatPanelOpen = true;
1248
  let currentPartName = '';
1249
+ let currentCode = '';
1250
+ let scene, camera, renderer, controls, currentMesh, gridHelper;
1251
  const galleryItems = [];
1252
+ let mentionActive = false;
1253
+ let mentionIndex = 0;
1254
+
1255
+ const AGENTS = {
1256
+ design: { name: 'Design', color: '#7c3aed', avatar: 'D' },
1257
+ engineering: { name: 'Engineering', color: '#00b4d8', avatar: 'E' },
1258
+ cnc: { name: 'CNC', color: '#00e676', avatar: 'C' },
1259
+ cad: { name: 'CAD Coder', color: '#ffab40', avatar: '{}' },
1260
+ };
1261
 
1262
  // ── THREE.JS SETUP ────────────────────────────────────
1263
 
 
1295
  rimLight.position.set(0, -50, 100);
1296
  scene.add(rimLight);
1297
 
1298
+ // Grid helper
1299
+ gridHelper = new THREE.GridHelper(400, 40, 0x1a2636, 0x111822);
1300
+ gridHelper.position.y = -0.5;
1301
+ scene.add(gridHelper);
1302
+
1303
  // Controls
1304
  controls = new THREE.OrbitControls(camera, renderer.domElement);
1305
  controls.enableDamping = true;
 
1331
  return new Promise((resolve, reject) => {
1332
  const loader = new THREE.STLLoader();
1333
  loader.load(url, (geometry) => {
 
1334
  if (currentMesh) {
1335
  scene.remove(currentMesh);
1336
  currentMesh.geometry.dispose();
1337
  currentMesh.material.dispose();
1338
  }
1339
 
 
1340
  const material = new THREE.MeshPhongMaterial({
1341
  color: 0x7799aa,
1342
  specular: 0x445566,
 
1348
  mesh.castShadow = true;
1349
  mesh.receiveShadow = true;
1350
 
 
1351
  geometry.computeBoundingBox();
1352
  const center = new THREE.Vector3();
1353
  geometry.boundingBox.getCenter(center);
 
1357
  currentMesh = mesh;
1358
 
1359
  // Fit camera
 
1360
  const size = new THREE.Vector3();
1361
+ geometry.boundingBox.getSize(size);
1362
  const maxDim = Math.max(size.x, size.y, size.z);
1363
  const dist = maxDim * 2.5;
1364
  camera.position.set(dist * 0.7, dist * 0.5, dist * 0.7);
1365
  controls.target.set(0, 0, 0);
1366
  controls.update();
1367
 
1368
+ // Update grid to match model scale
1369
+ if (gridHelper) {
1370
+ gridHelper.position.y = -size.y / 2 - 0.5;
1371
+ }
1372
+
1373
  document.getElementById('viewer-empty').style.display = 'none';
1374
  resolve();
1375
  }, undefined, reject);
 
1382
  currentBackend = name;
1383
  document.getElementById('btn-mock').classList.toggle('active', name === 'mock');
1384
  document.getElementById('btn-gemini').classList.toggle('active', name === 'gemini');
1385
+ document.getElementById('btn-claude').classList.toggle('active', name === 'anthropic');
1386
  }
1387
 
1388
+ // ── CHAT PANEL TOGGLE ─────────────────────────────────
1389
+
1390
+ function toggleChat() {
1391
+ chatPanelOpen = !chatPanelOpen;
1392
+ const panel = document.getElementById('chat-panel');
1393
+ const pill = document.getElementById('chat-open-pill');
1394
+ const toggle = document.getElementById('chat-toggle');
1395
+
1396
+ if (chatPanelOpen) {
1397
+ panel.classList.remove('collapsed');
1398
+ pill.classList.remove('visible');
1399
+ toggle.innerHTML = '&#9664;';
1400
+ document.body.classList.add('chat-open');
1401
+ } else {
1402
+ panel.classList.add('collapsed');
1403
+ pill.classList.add('visible');
1404
+ toggle.innerHTML = '&#9654;';
1405
+ document.body.classList.remove('chat-open');
1406
+ }
1407
+ }
1408
 
1409
+ // ── CHAT MESSAGING ────────────────────────────────────
 
 
 
1410
 
1411
+ async function sendMessage(text) {
1412
+ if (!text.trim()) return;
1413
 
1414
+ // Parse @mentions
1415
+ const mentions = [];
1416
+ const mentionRegex = /@(design|engineering|cnc|cad)\b/gi;
1417
+ let match;
1418
+ while ((match = mentionRegex.exec(text)) !== null) {
1419
+ mentions.push(match[1].toLowerCase());
1420
+ }
1421
+ const cleanedText = text.replace(mentionRegex, '').trim();
1422
 
1423
+ // Hide quick examples
1424
+ const examples = document.getElementById('quick-examples');
1425
+ if (examples) examples.style.display = 'none';
1426
 
1427
+ // Add user message
1428
+ addMessage({ role: 'user', content: text });
1429
+ chatHistory.push({ role: 'user', content: text });
1430
 
1431
+ // Show typing
1432
+ showTyping();
1433
 
1434
  try {
1435
+ const resp = await fetch('/api/chat', {
1436
  method: 'POST',
1437
  headers: { 'Content-Type': 'application/json' },
1438
+ body: JSON.stringify({
1439
+ message: cleanedText,
1440
+ history: chatHistory,
1441
+ mentions: mentions,
1442
+ backend: currentBackend,
1443
+ }),
1444
  });
1445
  const data = await resp.json();
1446
+
1447
+ hideTyping();
1448
+
1449
+ // Add agent responses
1450
+ for (const r of data.responses) {
1451
+ addMessage({
1452
+ role: 'agent',
1453
+ agent_id: r.agent_id,
1454
+ agent_name: r.agent_name,
1455
+ content: r.message,
1456
+ color: r.color,
1457
+ avatar: r.avatar,
1458
+ code: r.code,
1459
+ });
1460
+ chatHistory.push({ role: 'agent', agent_id: r.agent_id, content: r.message });
1461
+ }
1462
+
1463
+ // If preview available, load 3D model
1464
+ if (data.preview && data.preview.success) {
1465
+ setViewerLoading(true, 'LOADING 3D MODEL...');
1466
+ try {
1467
+ await loadSTL(data.preview.stl_url);
1468
+ } catch (e) {
1469
+ console.warn('STL load failed:', e);
1470
+ }
1471
+ setViewerLoading(false);
1472
+ updateGeoStats(data.preview.execution);
1473
+ updateCNCBadge(data.preview.validation);
1474
+ updateDownloads(data.preview.part_name);
1475
+
1476
+ if (data.preview.part_name) {
1477
+ currentPartName = data.preview.part_name;
1478
+ addToGallery(data.preview);
1479
+ }
1480
+ }
1481
  } catch (err) {
1482
+ hideTyping();
1483
+ addMessage({
1484
+ role: 'agent',
1485
+ agent_id: 'system',
1486
+ agent_name: 'System',
1487
+ content: 'Error: ' + err.message,
1488
+ color: '#ff5252',
1489
+ avatar: '!',
1490
+ });
1491
  }
1492
  }
1493
 
1494
+ function sendFromInput() {
1495
+ const input = document.getElementById('chat-input');
1496
+ const text = input.value.trim();
1497
+ if (!text) return;
1498
+ input.value = '';
1499
+ input.style.height = 'auto';
1500
+ closeMentionDropdown();
1501
+ sendMessage(text);
1502
+ }
1503
+
1504
+ function sendPreview() {
1505
+ sendMessage('@cad Generate a 3D preview based on our discussion');
1506
+ }
1507
 
1508
+ function quickSend(text) {
1509
+ const examples = document.getElementById('quick-examples');
1510
+ if (examples) examples.style.display = 'none';
1511
+ sendMessage(text);
1512
+ }
1513
 
1514
+ // ── MESSAGE RENDERING ─────────────────────────────────
 
 
1515
 
1516
+ function addMessage(msg) {
1517
+ const container = document.getElementById('chat-messages');
1518
+
1519
+ const el = document.createElement('div');
1520
+
1521
+ if (msg.role === 'user') {
1522
+ el.className = 'msg msg-user';
1523
+ el.innerHTML = '<div class="msg-bubble">' + escapeHtml(msg.content) + '</div>';
1524
+ } else {
1525
+ const agentId = msg.agent_id || 'system';
1526
+ const agentInfo = AGENTS[agentId] || { name: msg.agent_name || 'Agent', color: msg.color || '#5a7089', avatar: '?' };
1527
+ const color = msg.color || agentInfo.color;
1528
+ const avatar = msg.avatar || agentInfo.avatar;
1529
+ const name = msg.agent_name || agentInfo.name;
1530
+ const isCad = agentId === 'cad';
1531
+
1532
+ el.className = 'msg msg-agent';
1533
+
1534
+ let html = '<div class="msg-avatar" style="background: ' + color + ';">' + avatar + '</div>';
1535
+ html += '<div class="msg-agent-body">';
1536
+ html += '<div class="msg-agent-name" style="color: ' + color + ';">' + escapeHtml(name) + '</div>';
1537
+ html += '<div class="msg-agent-bubble' + (isCad ? ' cad-bubble' : '') + '">' + escapeHtml(msg.content);
1538
+
1539
+ if (msg.code) {
1540
+ currentCode = msg.code;
1541
+ html += '<br><a class="msg-view-code" onclick="openCodeModal()">&#9654; View code</a>';
1542
+ }
1543
+
1544
+ html += '</div></div>';
1545
+ el.innerHTML = html;
1546
  }
1547
 
1548
+ container.appendChild(el);
1549
+ scrollChatToBottom();
1550
  }
1551
 
1552
+ function showTyping() {
1553
+ const container = document.getElementById('chat-messages');
1554
+ const el = document.createElement('div');
1555
+ el.className = 'typing-indicator';
1556
+ el.id = 'typing-indicator';
1557
+ el.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div><span class="typing-label">Agents are thinking...</span>';
1558
+ container.appendChild(el);
1559
+ scrollChatToBottom();
1560
  }
1561
 
1562
+ function hideTyping() {
1563
+ const el = document.getElementById('typing-indicator');
1564
+ if (el) el.remove();
1565
+ }
 
 
1566
 
1567
+ function scrollChatToBottom() {
1568
+ const container = document.getElementById('chat-messages');
1569
+ requestAnimationFrame(() => {
1570
+ container.scrollTop = container.scrollHeight;
1571
+ });
1572
+ }
1573
 
1574
+ // ── @MENTION AUTOCOMPLETE ─────────────────────────────
 
1575
 
1576
+ const mentionAgents = ['design', 'engineering', 'cnc', 'cad'];
 
1577
 
1578
+ function handleInputForMention(e) {
1579
+ const input = document.getElementById('chat-input');
1580
+ const val = input.value;
1581
+ const pos = input.selectionStart;
1582
 
1583
+ // Find @ before cursor
1584
+ const before = val.substring(0, pos);
1585
+ const atMatch = before.match(/@(\w*)$/);
1586
 
1587
+ if (atMatch) {
1588
+ const query = atMatch[1].toLowerCase();
1589
+ const filtered = mentionAgents.filter(a => a.startsWith(query));
1590
 
1591
+ if (filtered.length > 0) {
1592
+ showMentionDropdown(filtered);
1593
+ mentionActive = true;
1594
+ return;
1595
+ }
1596
+ }
1597
+
1598
+ closeMentionDropdown();
1599
+ }
1600
+
1601
+ function showMentionDropdown(filtered) {
1602
+ const dropdown = document.getElementById('mention-dropdown');
1603
+ const options = dropdown.querySelectorAll('.mention-option');
1604
+ let visibleCount = 0;
1605
+
1606
+ options.forEach(opt => {
1607
+ const agent = opt.dataset.agent;
1608
+ if (filtered.includes(agent)) {
1609
+ opt.style.display = 'flex';
1610
+ visibleCount++;
1611
+ } else {
1612
+ opt.style.display = 'none';
1613
+ }
1614
+ });
1615
+
1616
+ if (visibleCount > 0) {
1617
+ dropdown.classList.add('visible');
1618
+ mentionIndex = 0;
1619
+ updateMentionHighlight();
1620
  }
1621
+ }
1622
 
1623
+ function closeMentionDropdown() {
1624
+ document.getElementById('mention-dropdown').classList.remove('visible');
1625
+ mentionActive = false;
1626
+ }
1627
 
1628
+ function updateMentionHighlight() {
1629
+ const options = Array.from(document.querySelectorAll('#mention-dropdown .mention-option'))
1630
+ .filter(o => o.style.display !== 'none');
1631
+ options.forEach((o, i) => o.classList.toggle('active', i === mentionIndex));
1632
+ }
1633
 
1634
+ function insertMention(agent) {
1635
+ const input = document.getElementById('chat-input');
1636
+ const val = input.value;
1637
+ const pos = input.selectionStart;
1638
+ const before = val.substring(0, pos);
1639
+ const after = val.substring(pos);
1640
+ const atPos = before.lastIndexOf('@');
1641
+
1642
+ input.value = before.substring(0, atPos) + '@' + agent + ' ' + after;
1643
+ input.focus();
1644
+ const newPos = atPos + agent.length + 2;
1645
+ input.setSelectionRange(newPos, newPos);
1646
+ closeMentionDropdown();
1647
  }
1648
 
1649
  // ── UI UPDATES ────────────────────────────────────────
1650
 
1651
+ function setViewerLoading(on, msg) {
1652
  const el = document.getElementById('viewer-loading');
 
 
 
1653
  if (on) {
1654
  el.classList.add('visible');
1655
  document.getElementById('loading-msg').textContent = msg || 'GENERATING...';
 
 
1656
  } else {
1657
  el.classList.remove('visible');
 
 
1658
  }
1659
  }
1660
 
 
1673
  bbox.map(v => v.toFixed(1)).join(' \u00D7 ') + ' mm';
1674
  }
1675
 
1676
+ document.getElementById('stat-faces').textContent = exec.face_count || '\u2014';
1677
+ document.getElementById('stat-edges').textContent = exec.edge_count || '\u2014';
1678
  }
1679
 
1680
  function updateCNCBadge(validation) {
 
1695
  axisBadge.textContent = (validation.axis_recommendation || '').toUpperCase();
1696
  }
1697
 
1698
+ function updateDownloads(partName) {
1699
  const el = document.getElementById('download-btns');
1700
+ if (!partName) { el.classList.remove('visible'); return; }
1701
  el.classList.add('visible');
1702
 
1703
  document.getElementById('dl-step').href = '/api/models/' + partName + '.step';
1704
  document.getElementById('dl-stl').href = '/api/models/' + partName + '.stl';
1705
+ document.getElementById('dl-report').href = '/api/models/' + partName + '_report.json';
1706
  }
1707
 
1708
+ // ── CODE MODAL ────────────────────────────────────────
1709
+
1710
+ function openCodeModal() {
1711
+ const modal = document.getElementById('code-modal');
1712
+ const display = document.getElementById('code-display');
1713
+
1714
+ if (currentCode) {
1715
+ display.innerHTML = highlightPython(currentCode);
1716
+ } else {
1717
+ display.textContent = 'No code available.';
1718
  }
1719
+
1720
+ modal.classList.add('visible');
1721
+ }
1722
+
1723
+ function closeCodeModal() {
1724
+ document.getElementById('code-modal').classList.remove('visible');
1725
  }
1726
 
1727
  function highlightPython(code) {
 
1728
  let escaped = code
1729
  .replace(/&/g, '&amp;')
1730
  .replace(/</g, '&lt;')
1731
  .replace(/>/g, '&gt;');
1732
 
 
1733
  escaped = escaped.replace(/(#.*$)/gm, '<span class="cm">$1</span>');
 
1734
  escaped = escaped.replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"[^"\n]*"|'[^'\n]*')/g, '<span class="st">$1</span>');
1735
+
1736
  const kw = /\b(import|from|as|def|class|return|if|else|elif|for|while|in|not|and|or|True|False|None|with|try|except|finally|raise|pass|break|continue|lambda|yield)\b/g;
1737
  escaped = escaped.replace(kw, '<span class="kw">$1</span>');
 
1738
  escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="nu">$1</span>');
 
1739
  escaped = escaped.replace(/\.([a-zA-Z_]\w*)\(/g, '.<span class="fn">$1</span>(');
1740
 
1741
  return escaped;
1742
  }
1743
 
1744
+ // ── GALLERY ───────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1745
 
1746
  function addToGallery(data) {
1747
  galleryItems.unshift({
1748
  name: data.part_name,
 
1749
  volume: data.execution?.volume_mm3,
1750
  faces: data.execution?.face_count,
1751
  machinable: data.validation?.machinable,
1752
  });
 
1753
  }
1754
 
1755
+ function openGallery() {
1756
+ renderGallery();
1757
+ document.getElementById('gallery-modal').classList.add('visible');
1758
+ }
1759
+
1760
+ function closeGallery() {
1761
+ document.getElementById('gallery-modal').classList.remove('visible');
1762
+ }
1763
+
1764
+ function renderGallery() {
1765
+ const grid = document.getElementById('gallery-grid');
1766
 
1767
  if (galleryItems.length === 0) {
1768
+ grid.innerHTML = '<div class="gallery-empty">No models generated yet.</div>';
 
1769
  return;
1770
  }
1771
 
 
 
 
1772
  let html = '';
1773
  for (const item of galleryItems) {
1774
+ html += '<button class="gallery-card fade-in" onclick="loadGalleryItem(\'' + escapeHtml(item.name) + '\')">';
1775
  html += '<div class="gallery-card-name">' + escapeHtml(item.name) + '</div>';
1776
  html += '<div class="gallery-card-meta">';
1777
  if (item.faces) html += '<span>' + item.faces + ' faces</span>';
 
1782
  html += '</div></button>';
1783
  }
1784
 
1785
+ grid.innerHTML = html;
1786
  }
1787
 
1788
  async function loadGalleryItem(name) {
1789
+ closeGallery();
1790
+ setViewerLoading(true, 'LOADING MODEL...');
1791
  try {
1792
  await loadSTL('/api/models/' + name + '.stl');
1793
  } catch (e) {
1794
  console.warn('Failed to load:', e);
1795
  }
1796
+ setViewerLoading(false);
1797
  }
1798
 
1799
+ // ── UTILS ─────────────────────────────────────────────
1800
+
1801
  function escapeHtml(str) {
1802
  const div = document.createElement('div');
1803
  div.textContent = str;
 
1813
  if (resp.ok) {
1814
  dot.style.background = 'var(--success)';
1815
  dot.style.boxShadow = '0 0 6px var(--success)';
1816
+ dot.title = 'Server Connected';
1817
  } else {
1818
  dot.style.background = 'var(--warning)';
1819
  dot.style.boxShadow = '0 0 6px var(--warning)';
1820
+ dot.title = 'Server Error';
1821
  }
1822
  } catch {
1823
  const dot = document.getElementById('status-dot');
1824
  dot.style.background = 'var(--error)';
1825
  dot.style.boxShadow = '0 0 6px var(--error)';
1826
+ dot.title = 'Server Offline';
1827
  }
1828
  }
1829
 
1830
+ // ── KEYBOARD / INPUT EVENTS ──────────────────────────
1831
+
1832
+ const chatInput = document.getElementById('chat-input');
1833
+
1834
+ chatInput.addEventListener('input', (e) => {
1835
+ // Auto-resize
1836
+ chatInput.style.height = 'auto';
1837
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
1838
+
1839
+ // Check for @mention
1840
+ handleInputForMention(e);
1841
+ });
1842
+
1843
+ chatInput.addEventListener('keydown', (e) => {
1844
+ if (mentionActive) {
1845
+ const dropdown = document.getElementById('mention-dropdown');
1846
+ const visibleOptions = Array.from(dropdown.querySelectorAll('.mention-option'))
1847
+ .filter(o => o.style.display !== 'none');
1848
+
1849
+ if (e.key === 'ArrowDown') {
1850
+ e.preventDefault();
1851
+ mentionIndex = (mentionIndex + 1) % visibleOptions.length;
1852
+ updateMentionHighlight();
1853
+ return;
1854
+ }
1855
+ if (e.key === 'ArrowUp') {
1856
+ e.preventDefault();
1857
+ mentionIndex = (mentionIndex - 1 + visibleOptions.length) % visibleOptions.length;
1858
+ updateMentionHighlight();
1859
+ return;
1860
+ }
1861
+ if (e.key === 'Enter' || e.key === 'Tab') {
1862
+ e.preventDefault();
1863
+ const agent = visibleOptions[mentionIndex]?.dataset.agent;
1864
+ if (agent) insertMention(agent);
1865
+ return;
1866
+ }
1867
+ if (e.key === 'Escape') {
1868
+ closeMentionDropdown();
1869
+ return;
1870
+ }
1871
+ }
1872
 
 
1873
  if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
1874
  e.preventDefault();
1875
+ sendFromInput();
1876
+ }
1877
+
1878
+ // Regular enter sends (without shift)
1879
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
1880
+ e.preventDefault();
1881
+ sendFromInput();
1882
+ }
1883
+ });
1884
+
1885
+ // Close modals on backdrop click
1886
+ document.getElementById('code-modal').addEventListener('click', (e) => {
1887
+ if (e.target === document.getElementById('code-modal')) closeCodeModal();
1888
+ });
1889
+
1890
+ document.getElementById('gallery-modal').addEventListener('click', (e) => {
1891
+ if (e.target === document.getElementById('gallery-modal')) closeGallery();
1892
+ });
1893
+
1894
+ // Escape to close modals
1895
+ document.addEventListener('keydown', (e) => {
1896
+ if (e.key === 'Escape') {
1897
+ closeCodeModal();
1898
+ closeGallery();
1899
  }
1900
  });
1901