cyclecad 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/API-BUILD-MANIFEST.txt +339 -0
  2. package/API-SERVER.md +535 -0
  3. package/Architecture-Deck.pptx +0 -0
  4. package/CLAUDE.md +172 -11
  5. package/CLI-BUILD-SUMMARY.md +504 -0
  6. package/CLI-INDEX.md +356 -0
  7. package/CLI-README.md +466 -0
  8. package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
  9. package/CONNECTED_FABS_GUIDE.md +612 -0
  10. package/CONNECTED_FABS_README.md +310 -0
  11. package/DELIVERABLES.md +343 -0
  12. package/DFM-ANALYZER-INTEGRATION.md +368 -0
  13. package/DFM-QUICK-START.js +253 -0
  14. package/Dockerfile +69 -0
  15. package/IMPLEMENTATION.md +327 -0
  16. package/LICENSE +31 -0
  17. package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
  18. package/MCP-INDEX.md +264 -0
  19. package/QUICKSTART-API.md +388 -0
  20. package/QUICKSTART-CLI.md +211 -0
  21. package/QUICKSTART-MCP.md +196 -0
  22. package/README-MCP.md +208 -0
  23. package/TEST-TOKEN-ENGINE.md +319 -0
  24. package/TOKEN-ENGINE-SUMMARY.md +266 -0
  25. package/TOKENS-README.md +263 -0
  26. package/TOOLS-REFERENCE.md +254 -0
  27. package/app/index.html +168 -3
  28. package/app/js/TOKEN-INTEGRATION.md +391 -0
  29. package/app/js/agent-api.js +3 -3
  30. package/app/js/ai-copilot.js +1435 -0
  31. package/app/js/cam-pipeline.js +840 -0
  32. package/app/js/collaboration-ui.js +995 -0
  33. package/app/js/collaboration.js +1116 -0
  34. package/app/js/connected-fabs-example.js +404 -0
  35. package/app/js/connected-fabs.js +1449 -0
  36. package/app/js/dfm-analyzer.js +1760 -0
  37. package/app/js/marketplace.js +1994 -0
  38. package/app/js/material-library.js +2115 -0
  39. package/app/js/token-dashboard.js +563 -0
  40. package/app/js/token-engine.js +743 -0
  41. package/app/test-agent.html +1801 -0
  42. package/bin/cyclecad-cli.js +662 -0
  43. package/bin/cyclecad-mcp +2 -0
  44. package/bin/server.js +242 -0
  45. package/cycleCAD-Architecture.pptx +0 -0
  46. package/cycleCAD-Investor-Deck.pptx +0 -0
  47. package/demo-mcp.sh +60 -0
  48. package/docs/API-SERVER-SUMMARY.md +375 -0
  49. package/docs/API-SERVER.md +667 -0
  50. package/docs/CAM-EXAMPLES.md +344 -0
  51. package/docs/CAM-INTEGRATION.md +612 -0
  52. package/docs/CAM-QUICK-REFERENCE.md +199 -0
  53. package/docs/CLI-INTEGRATION.md +510 -0
  54. package/docs/CLI.md +872 -0
  55. package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
  56. package/docs/MARKETPLACE-INTEGRATION.md +467 -0
  57. package/docs/MARKETPLACE-SETUP.html +439 -0
  58. package/docs/MCP-SERVER.md +403 -0
  59. package/examples/api-client-example.js +488 -0
  60. package/examples/api-client-example.py +359 -0
  61. package/examples/batch-manufacturing.txt +28 -0
  62. package/examples/batch-simple.txt +26 -0
  63. package/model-marketplace.html +1273 -0
  64. package/package.json +14 -3
  65. package/server/api-server.js +1120 -0
  66. package/server/mcp-server.js +1161 -0
  67. package/test-api-server.js +432 -0
  68. package/test-mcp.js +198 -0
  69. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,1435 @@
1
+ /**
2
+ * ai-copilot.js - AI Copilot for cycleCAD
3
+ *
4
+ * Text-to-CAD, natural language editing, smart autocomplete, design review.
5
+ * This is the "next-generation interface" where agents and humans collaborate
6
+ * with the CAD system through natural language.
7
+ *
8
+ * Features:
9
+ * - Text-to-CAD: Convert natural language to Agent API commands
10
+ * - NL Parser: 20+ shape types, 30+ operations, synonym detection, typo tolerance
11
+ * - Smart Suggestions: Context-aware next actions based on scene state
12
+ * - Design Review: Manufacturability scoring, DFM analysis, improvement tips
13
+ * - Multi-Agent Orchestration: Simulate agent swarms (demo mode)
14
+ * - Voice Commands: Web Speech API integration
15
+ * - Marketplace Templates: 50+ parametric templates
16
+ * - Iterative Refinement: "Make it thicker", "Add holes", etc.
17
+ *
18
+ * Architecture:
19
+ * User input (text/voice) → NL Parser → command sequence → Agent API → 3D view
20
+ * ↓
21
+ * Design review engine → DFM scoring → suggestions → context memory
22
+ */
23
+
24
+ // ============================================================================
25
+ // CONFIGURATION & CONSTANTS
26
+ // ============================================================================
27
+
28
+ const SHAPE_TYPES = {
29
+ primitives: ['box', 'cube', 'cylinder', 'rod', 'sphere', 'cone', 'torus'],
30
+ plates: ['plate', 'flat', 'base', 'pad', 'sheet'],
31
+ brackets: ['bracket', 'angle', 'support', 'corner', 'mounting'],
32
+ fasteners: ['bolt', 'screw', 'stud', 'rivet', 'pin', 'nut', 'washer'],
33
+ structural: ['beam', 'channel', 'angle', 'tube', 'pipe', 'rod', 'rail'],
34
+ gears: ['gear', 'pinion', 'sprocket', 'rack'],
35
+ housing: ['housing', 'enclosure', 'case', 'body', 'shell', 'cover'],
36
+ misc: ['ring', 'disk', 'flange', 'hub', 'seat', 'pulley', 'wheel'],
37
+ };
38
+
39
+ const OPERATIONS = {
40
+ cutting: ['hole', 'bore', 'drill', 'cut', 'pocket', 'slot'],
41
+ shaping: ['fillet', 'round', 'chamfer', 'bevel', 'blend'],
42
+ creating: ['extrude', 'revolve', 'sweep', 'loft', 'boss'],
43
+ modifying: ['pattern', 'array', 'mirror', 'shell', 'scale'],
44
+ assembly: ['attach', 'align', 'mate', 'bolt', 'weld'],
45
+ };
46
+
47
+ const MATERIALS = ['steel', 'aluminum', 'brass', 'abs', 'nylon', 'titanium', 'copper', 'wood', 'acetal', 'pom'];
48
+
49
+ const DFM_GRADES = {
50
+ A: { score: 95, description: 'Excellent manufacturability' },
51
+ B: { score: 80, description: 'Good, minor recommendations' },
52
+ C: { score: 65, description: 'Moderate issues, consider changes' },
53
+ D: { score: 50, description: 'Significant manufacturability concerns' },
54
+ F: { score: 0, description: 'Not recommended as-is' },
55
+ };
56
+
57
+ // Parametric part templates (50+ total, sample of 8 shown)
58
+ const TEMPLATES = {
59
+ brackets: [
60
+ { name: 'L-Bracket', params: { width: 80, height: 80, thickness: 5, holeSize: 8 } },
61
+ { name: 'U-Bracket', params: { width: 100, height: 80, depth: 20, thickness: 5 } },
62
+ { name: 'Corner Bracket', params: { size: 80, thickness: 5, filletRadius: 3 } },
63
+ ],
64
+ enclosures: [
65
+ { name: 'Box with Lid', params: { width: 200, depth: 150, height: 100, thickness: 2 } },
66
+ { name: 'Snap-Fit Case', params: { width: 100, depth: 80, height: 40, snapCount: 4 } },
67
+ ],
68
+ fasteners: [
69
+ { name: 'Bolt', params: { diameter: 10, length: 50, threadPitch: 1.5 } },
70
+ { name: 'Standoff', params: { outerDia: 6, innerDia: 3.2, height: 20 } },
71
+ ],
72
+ structural: [
73
+ { name: 'I-Beam', params: { width: 100, height: 200, flangeThickness: 10, webThickness: 6 } },
74
+ { name: 'Channel', params: { width: 80, height: 120, depth: 30, thickness: 5 } },
75
+ ],
76
+ };
77
+
78
+ // Typo tolerance: common misspellings → correct terms
79
+ const TYPO_MAP = {
80
+ dieameter: 'diameter',
81
+ diamter: 'diameter',
82
+ diammeter: 'diameter',
83
+ cilinder: 'cylinder',
84
+ rad: 'radius',
85
+ rad: 'radius',
86
+ lenght: 'length',
87
+ hieght: 'height',
88
+ thikness: 'thickness',
89
+ bolts: 'bolt',
90
+ screws: 'screw',
91
+ fillet: 'fillet',
92
+ filler: 'fillet',
93
+ };
94
+
95
+ // DFM rules by manufacturing method
96
+ const DFM_RULES = {
97
+ fdm: {
98
+ minWallThickness: 0.8,
99
+ maxOverhang: 45,
100
+ minFeatureSize: 1.5,
101
+ issues: ['thin walls', 'unsupported overhangs', 'large flat areas'],
102
+ tips: ['Add infill', 'Add support structure', 'Increase wall thickness'],
103
+ },
104
+ cnc: {
105
+ minCornerRadius: 0.5,
106
+ maxDepth: 10,
107
+ minToolAccess: 5,
108
+ issues: ['sharp corners', 'deep pockets', 'difficult access'],
109
+ tips: ['Fillet internal corners', 'Reduce pocket depth', 'Rework feature placement'],
110
+ },
111
+ injection: {
112
+ minWallThickness: 1.2,
113
+ maxThicknessVariation: 0.3,
114
+ minDraftAngle: 1,
115
+ issues: ['inconsistent walls', 'no draft angle', 'thick sections'],
116
+ tips: ['Add draft angle', 'Uniform wall thickness', 'Reduce section thickness'],
117
+ },
118
+ laser: {
119
+ maxThickness: 5,
120
+ minFeatureSize: 0.5,
121
+ requiresEscape: true,
122
+ issues: ['material thickness', 'small features', 'no escape routes'],
123
+ tips: ['Reduce material thickness', 'Enlarge features', 'Add escape routes'],
124
+ },
125
+ };
126
+
127
+ // ============================================================================
128
+ // STATE MANAGEMENT
129
+ // ============================================================================
130
+
131
+ let copilotState = {
132
+ // Conversation history
133
+ messages: [],
134
+ commandHistory: [],
135
+ currentCommand: null,
136
+
137
+ // Scene context
138
+ sceneState: {
139
+ parts: [],
140
+ selectedPart: null,
141
+ lastOperation: null,
142
+ materials: {},
143
+ },
144
+
145
+ // Agent orchestration
146
+ agents: [],
147
+ agentStatus: {},
148
+
149
+ // Voice mode
150
+ voiceActive: false,
151
+ speechRecognition: null,
152
+
153
+ // Settings
154
+ apiKeys: {
155
+ gemini: null,
156
+ groq: null,
157
+ },
158
+ preferences: {
159
+ autoExecute: true,
160
+ showSuggestions: true,
161
+ reviewOnCreate: false,
162
+ },
163
+
164
+ // UI panels
165
+ panelElement: null,
166
+ eventsMap: {},
167
+ };
168
+
169
+ // ============================================================================
170
+ // INITIALIZATION
171
+ // ============================================================================
172
+
173
+ /**
174
+ * Initialize AI Copilot
175
+ * @param {HTMLElement} panelEl - Container for copilot UI
176
+ */
177
+ export function initCopilot(panelEl) {
178
+ if (!panelEl) {
179
+ console.warn('[Copilot] Panel element not provided');
180
+ return;
181
+ }
182
+
183
+ copilotState.panelElement = panelEl;
184
+
185
+ // Load API keys
186
+ const stored = localStorage.getItem('cyclecad_api_keys');
187
+ if (stored) {
188
+ try {
189
+ copilotState.apiKeys = JSON.parse(stored);
190
+ } catch (e) {
191
+ console.warn('[Copilot] Failed to load API keys:', e);
192
+ }
193
+ }
194
+
195
+ // Create UI
196
+ createCopilotUI();
197
+
198
+ // Initialize voice if available
199
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
200
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
201
+ copilotState.speechRecognition = new SpeechRecognition();
202
+ setupVoiceHandlers();
203
+ }
204
+
205
+ // Expose globally
206
+ window.cycleCAD.copilot = {
207
+ textToCAD,
208
+ executeTextCommand,
209
+ refine,
210
+ reviewDesign,
211
+ suggestImprovements,
212
+ getSuggestions,
213
+ getTemplates,
214
+ startVoiceMode,
215
+ stopVoiceMode,
216
+ spawnAgents,
217
+ getAgentStatus,
218
+ addMessage,
219
+ on,
220
+ off,
221
+ };
222
+
223
+ console.log('[Copilot] Initialized');
224
+ addMessage('ai', 'Hello! I\'m your AI Copilot. Describe what you want to build, and I\'ll help you design it. Try "Create a 100mm cube" or "Add 4 mounting holes".');
225
+ }
226
+
227
+ /**
228
+ * Create copilot UI panel
229
+ */
230
+ function createCopilotUI() {
231
+ const el = copilotState.panelElement;
232
+ el.innerHTML = `
233
+ <div class="copilot-container">
234
+ <div class="copilot-header">
235
+ <h3>AI Copilot</h3>
236
+ <div class="copilot-status">
237
+ <span class="status-light"></span>
238
+ <span class="status-text">Ready</span>
239
+ </div>
240
+ </div>
241
+
242
+ <div class="copilot-messages" id="copilot-messages"></div>
243
+
244
+ <div class="copilot-suggestions" id="copilot-suggestions"></div>
245
+
246
+ <div class="copilot-input-area">
247
+ <div class="copilot-input-group">
248
+ <input
249
+ type="text"
250
+ id="copilot-input"
251
+ class="copilot-input"
252
+ placeholder="Describe what you want to build..."
253
+ autocomplete="off"
254
+ />
255
+ <button id="copilot-send" class="copilot-send-btn" title="Send (Enter)">
256
+ <span>▶</span>
257
+ </button>
258
+ <button id="copilot-voice" class="copilot-voice-btn" title="Voice input">
259
+ <span>🎤</span>
260
+ </button>
261
+ </div>
262
+
263
+ <div class="copilot-chips" id="copilot-chips"></div>
264
+ </div>
265
+
266
+ <div class="copilot-review" id="copilot-review" style="display: none;">
267
+ <div class="review-grade">Grade: <span class="grade-letter">-</span></div>
268
+ <div class="review-issues"></div>
269
+ <div class="review-suggestions"></div>
270
+ </div>
271
+
272
+ <div class="copilot-agents" id="copilot-agents" style="display: none;">
273
+ <div class="agents-title">Agent Swarm</div>
274
+ <div class="agents-list"></div>
275
+ </div>
276
+ </div>
277
+
278
+ <style>
279
+ .copilot-container {
280
+ display: flex;
281
+ flex-direction: column;
282
+ height: 100%;
283
+ background: #1e1e1e;
284
+ color: #e0e0e0;
285
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
286
+ font-size: 13px;
287
+ }
288
+
289
+ .copilot-header {
290
+ padding: 12px;
291
+ border-bottom: 1px solid #3e3e42;
292
+ display: flex;
293
+ justify-content: space-between;
294
+ align-items: center;
295
+ user-select: none;
296
+ }
297
+
298
+ .copilot-header h3 {
299
+ margin: 0;
300
+ font-size: 14px;
301
+ font-weight: 600;
302
+ }
303
+
304
+ .copilot-status {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 6px;
308
+ font-size: 12px;
309
+ color: #a0a0a0;
310
+ }
311
+
312
+ .status-light {
313
+ width: 8px;
314
+ height: 8px;
315
+ border-radius: 50%;
316
+ background: #3fb950;
317
+ display: inline-block;
318
+ }
319
+
320
+ .copilot-messages {
321
+ flex: 1;
322
+ overflow-y: auto;
323
+ padding: 12px;
324
+ min-height: 0;
325
+ display: flex;
326
+ flex-direction: column;
327
+ gap: 8px;
328
+ }
329
+
330
+ .copilot-message {
331
+ display: flex;
332
+ gap: 8px;
333
+ animation: slideIn 200ms ease-out;
334
+ }
335
+
336
+ .copilot-message.user {
337
+ justify-content: flex-end;
338
+ }
339
+
340
+ .copilot-message-content {
341
+ max-width: 80%;
342
+ padding: 10px 12px;
343
+ border-radius: 6px;
344
+ word-wrap: break-word;
345
+ }
346
+
347
+ .copilot-message.ai .copilot-message-content {
348
+ background: #2d2d30;
349
+ border-left: 3px solid #7c3aed;
350
+ }
351
+
352
+ .copilot-message.user .copilot-message-content {
353
+ background: #1f6feb;
354
+ }
355
+
356
+ @keyframes slideIn {
357
+ from {
358
+ opacity: 0;
359
+ transform: translateY(8px);
360
+ }
361
+ to {
362
+ opacity: 1;
363
+ transform: translateY(0);
364
+ }
365
+ }
366
+
367
+ .copilot-suggestions {
368
+ padding: 8px 12px;
369
+ border-top: 1px solid #3e3e42;
370
+ max-height: 80px;
371
+ overflow-y: auto;
372
+ }
373
+
374
+ .copilot-chips {
375
+ display: flex;
376
+ flex-wrap: wrap;
377
+ gap: 6px;
378
+ padding: 8px 12px;
379
+ }
380
+
381
+ .chip {
382
+ padding: 6px 12px;
383
+ background: #2d2d30;
384
+ border: 1px solid #3e3e42;
385
+ border-radius: 12px;
386
+ font-size: 12px;
387
+ cursor: pointer;
388
+ transition: all 150ms;
389
+ }
390
+
391
+ .chip:hover {
392
+ background: #3e3e42;
393
+ border-color: #7c3aed;
394
+ }
395
+
396
+ .copilot-input-area {
397
+ padding: 12px;
398
+ border-top: 1px solid #3e3e42;
399
+ }
400
+
401
+ .copilot-input-group {
402
+ display: flex;
403
+ gap: 8px;
404
+ margin-bottom: 8px;
405
+ }
406
+
407
+ .copilot-input {
408
+ flex: 1;
409
+ padding: 10px;
410
+ background: #2d2d30;
411
+ border: 1px solid #3e3e42;
412
+ border-radius: 4px;
413
+ color: #e0e0e0;
414
+ }
415
+
416
+ .copilot-input:focus {
417
+ outline: none;
418
+ border-color: #7c3aed;
419
+ box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
420
+ }
421
+
422
+ .copilot-send-btn,
423
+ .copilot-voice-btn {
424
+ width: 36px;
425
+ height: 36px;
426
+ background: #7c3aed;
427
+ border-radius: 4px;
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ color: white;
432
+ transition: all 150ms;
433
+ }
434
+
435
+ .copilot-send-btn:hover {
436
+ background: #6d28d9;
437
+ }
438
+
439
+ .copilot-voice-btn {
440
+ background: #2d2d30;
441
+ border: 1px solid #3e3e42;
442
+ }
443
+
444
+ .copilot-voice-btn:hover {
445
+ background: #3e3e42;
446
+ }
447
+
448
+ .copilot-voice-btn.active {
449
+ background: #f85149;
450
+ }
451
+
452
+ .copilot-review {
453
+ padding: 12px;
454
+ background: #2d2d30;
455
+ border-top: 1px solid #3e3e42;
456
+ border-radius: 4px;
457
+ margin-top: 8px;
458
+ }
459
+
460
+ .review-grade {
461
+ font-weight: 600;
462
+ margin-bottom: 8px;
463
+ }
464
+
465
+ .grade-letter {
466
+ font-size: 18px;
467
+ font-weight: 700;
468
+ color: #3fb950;
469
+ }
470
+
471
+ .review-issues,
472
+ .review-suggestions {
473
+ font-size: 12px;
474
+ color: #a0a0a0;
475
+ margin-top: 6px;
476
+ }
477
+
478
+ .review-issues {
479
+ color: #f85149;
480
+ }
481
+
482
+ .copilot-agents {
483
+ padding: 12px;
484
+ border-top: 1px solid #3e3e42;
485
+ }
486
+
487
+ .agents-title {
488
+ font-weight: 600;
489
+ margin-bottom: 8px;
490
+ font-size: 12px;
491
+ text-transform: uppercase;
492
+ color: #a0a0a0;
493
+ }
494
+
495
+ .agent-item {
496
+ padding: 6px;
497
+ background: #2d2d30;
498
+ border-radius: 3px;
499
+ font-size: 11px;
500
+ margin-bottom: 4px;
501
+ display: flex;
502
+ align-items: center;
503
+ gap: 6px;
504
+ }
505
+
506
+ .agent-status-indicator {
507
+ width: 6px;
508
+ height: 6px;
509
+ border-radius: 50%;
510
+ background: #3fb950;
511
+ }
512
+
513
+ .agent-status-indicator.running {
514
+ animation: pulse 1s infinite;
515
+ }
516
+
517
+ @keyframes pulse {
518
+ 0%, 100% { opacity: 1; }
519
+ 50% { opacity: 0.5; }
520
+ }
521
+ </style>
522
+ `;
523
+
524
+ // Wire up event handlers
525
+ const inputEl = el.querySelector('#copilot-input');
526
+ const sendBtn = el.querySelector('#copilot-send');
527
+ const voiceBtn = el.querySelector('#copilot-voice');
528
+
529
+ if (sendBtn) {
530
+ sendBtn.addEventListener('click', () => handleTextInput());
531
+ }
532
+
533
+ if (inputEl) {
534
+ inputEl.addEventListener('keydown', (e) => {
535
+ if (e.key === 'Enter' && !e.shiftKey) {
536
+ e.preventDefault();
537
+ handleTextInput();
538
+ }
539
+ });
540
+ }
541
+
542
+ if (voiceBtn) {
543
+ voiceBtn.addEventListener('click', () => {
544
+ if (copilotState.voiceActive) {
545
+ stopVoiceMode();
546
+ } else {
547
+ startVoiceMode();
548
+ }
549
+ });
550
+ }
551
+ }
552
+
553
+ // ============================================================================
554
+ // TEXT-TO-CAD ENGINE
555
+ // ============================================================================
556
+
557
+ /**
558
+ * Convert natural language to Agent API commands
559
+ * @param {string} prompt - User input
560
+ * @returns {Promise<{commands: Array, preview: string}>}
561
+ */
562
+ export async function textToCAD(prompt) {
563
+ prompt = prompt.trim();
564
+
565
+ // 3-tier AI: Gemini → Groq → Offline NLP
566
+ try {
567
+ if (copilotState.apiKeys.gemini || copilotState.apiKeys.groq) {
568
+ const llmResult = await queryLLMForCAD(prompt);
569
+ if (llmResult) {
570
+ return llmResult;
571
+ }
572
+ }
573
+ } catch (error) {
574
+ console.warn('[Copilot] LLM query failed, falling back to NLP:', error);
575
+ }
576
+
577
+ // Offline NLP fallback
578
+ return parseNaturalLanguage(prompt);
579
+ }
580
+
581
+ /**
582
+ * Query LLM for complex CAD parsing
583
+ */
584
+ async function queryLLMForCAD(prompt) {
585
+ const systemPrompt = `You are a CAD command generator. Convert natural language descriptions to a sequence of Agent API commands.
586
+
587
+ Return a JSON object with:
588
+ {
589
+ "commands": [
590
+ {"method": "shape.cylinder", "params": {"radius": 25, "height": 80}},
591
+ {"method": "feature.fillet", "params": {"radius": 5}}
592
+ ],
593
+ "preview": "Description of what will be created"
594
+ }
595
+
596
+ Available methods:
597
+ - shape.box, shape.cylinder, shape.sphere, shape.cone, shape.tube, shape.plate
598
+ - feature.hole, feature.fillet, feature.chamfer, feature.pattern, feature.mirror
599
+ - assembly.add, assembly.mate, assembly.bolt
600
+ - render.snapshot, validate.dfm
601
+
602
+ Be concise and precise.`;
603
+
604
+ try {
605
+ if (copilotState.apiKeys.gemini) {
606
+ const response = await fetch(
607
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${copilotState.apiKeys.gemini}`,
608
+ {
609
+ method: 'POST',
610
+ headers: { 'Content-Type': 'application/json' },
611
+ body: JSON.stringify({
612
+ system_instruction: { parts: [{ text: systemPrompt }] },
613
+ contents: [{ parts: [{ text: prompt }] }],
614
+ }),
615
+ }
616
+ );
617
+
618
+ if (response.ok) {
619
+ const data = await response.json();
620
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
621
+ return JSON.parse(text);
622
+ }
623
+ }
624
+
625
+ if (copilotState.apiKeys.groq) {
626
+ const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
627
+ method: 'POST',
628
+ headers: {
629
+ 'Content-Type': 'application/json',
630
+ Authorization: `Bearer ${copilotState.apiKeys.groq}`,
631
+ },
632
+ body: JSON.stringify({
633
+ model: 'llama-3.1-8b-instant',
634
+ messages: [
635
+ { role: 'system', content: systemPrompt },
636
+ { role: 'user', content: prompt },
637
+ ],
638
+ temperature: 0,
639
+ }),
640
+ });
641
+
642
+ if (response.ok) {
643
+ const data = await response.json();
644
+ const text = data.choices?.[0]?.message?.content || '';
645
+ return JSON.parse(text);
646
+ }
647
+ }
648
+ } catch (error) {
649
+ console.warn('[Copilot] LLM parse error:', error);
650
+ }
651
+
652
+ return null;
653
+ }
654
+
655
+ /**
656
+ * Parse natural language using offline NLP
657
+ * Supports 20+ shape types, 30+ operations, synonyms, typo tolerance
658
+ */
659
+ function parseNaturalLanguage(text) {
660
+ text = text.toLowerCase().trim();
661
+
662
+ // Normalize typos
663
+ for (const [typo, correct] of Object.entries(TYPO_MAP)) {
664
+ text = text.replace(new RegExp(`\\b${typo}\\b`, 'gi'), correct);
665
+ }
666
+
667
+ const commands = [];
668
+ const numbers = extractNumbers(text);
669
+ let preview = '';
670
+
671
+ // Detect primary shape
672
+ const shapeType = detectShapeType(text);
673
+ const shapeParams = parseShapeParams(text, shapeType, numbers);
674
+
675
+ if (shapeParams) {
676
+ commands.push({
677
+ method: `shape.${shapeType}`,
678
+ params: shapeParams,
679
+ });
680
+ preview += `Create ${shapeType}`;
681
+ }
682
+
683
+ // Detect operations
684
+ const ops = parseOperations(text, numbers);
685
+ commands.push(...ops);
686
+ if (ops.length > 0) {
687
+ preview += ops.map((op) => ` → ${op.method.split('.')[1]}`).join('');
688
+ }
689
+
690
+ // Detect material
691
+ const material = detectMaterial(text);
692
+ if (material) {
693
+ commands.push({
694
+ method: 'property.setMaterial',
695
+ params: { material },
696
+ });
697
+ preview += ` (${material})`;
698
+ }
699
+
700
+ // Detect assembly operations
701
+ const assemblyOps = parseAssemblyOps(text);
702
+ commands.push(...assemblyOps);
703
+
704
+ return {
705
+ commands: commands.length > 0 ? commands : null,
706
+ preview: preview || 'Unable to parse. Try "100mm cube" or "cylinder 50mm radius 80mm tall".',
707
+ };
708
+ }
709
+
710
+ /**
711
+ * Extract all numbers from text with unit conversion
712
+ */
713
+ function extractNumbers(text) {
714
+ const numbers = [];
715
+ const regex = /(\d+(?:\.\d+)?)\s*(mm|cm|m|in|inch)?/gi;
716
+ let match;
717
+
718
+ while ((match = regex.exec(text)) !== null) {
719
+ let value = parseFloat(match[1]);
720
+ const unit = (match[2] || 'mm').toLowerCase();
721
+
722
+ // Unit conversion to mm
723
+ const factors = { mm: 1, cm: 10, m: 1000, in: 25.4, inch: 25.4 };
724
+ value *= factors[unit] || 1;
725
+
726
+ numbers.push(value);
727
+ }
728
+
729
+ return numbers;
730
+ }
731
+
732
+ /**
733
+ * Detect primary shape type
734
+ */
735
+ function detectShapeType(text) {
736
+ for (const [category, types] of Object.entries(SHAPE_TYPES)) {
737
+ for (const type of types) {
738
+ if (text.includes(type)) {
739
+ return type === 'cube' ? 'box' : type;
740
+ }
741
+ }
742
+ }
743
+ return 'box'; // default
744
+ }
745
+
746
+ /**
747
+ * Parse shape parameters from text
748
+ */
749
+ function parseShapeParams(text, shapeType, numbers) {
750
+ const params = {};
751
+
752
+ switch (shapeType) {
753
+ case 'box':
754
+ if (text.includes('cube')) {
755
+ const size = numbers[0] || 50;
756
+ params.width = params.height = params.depth = size;
757
+ } else {
758
+ params.width = numbers[0] || 100;
759
+ params.height = numbers[1] || 60;
760
+ params.depth = numbers[2] || 20;
761
+ }
762
+ break;
763
+
764
+ case 'cylinder':
765
+ const radiusMatch = text.match(/radius\s*(\d+)|r\s*(\d+)/i);
766
+ const diamMatch = text.match(/diameter\s*(\d+)|d\s*(\d+)/i);
767
+
768
+ if (diamMatch) {
769
+ params.radius = (parseFloat(diamMatch[1] || diamMatch[2]) / 2);
770
+ } else if (radiusMatch) {
771
+ params.radius = parseFloat(radiusMatch[1] || radiusMatch[2]);
772
+ } else {
773
+ params.radius = numbers[0] || 25;
774
+ }
775
+
776
+ params.height = numbers[numbers.length - 1] || 80;
777
+ break;
778
+
779
+ case 'sphere':
780
+ const sRadius = text.match(/radius\s*(\d+)/i);
781
+ const sDiam = text.match(/diameter\s*(\d+)/i);
782
+
783
+ if (sDiam) {
784
+ params.radius = parseFloat(sDiam[1]) / 2;
785
+ } else if (sRadius) {
786
+ params.radius = parseFloat(sRadius[1]);
787
+ } else {
788
+ params.radius = numbers[0] || 25;
789
+ }
790
+ break;
791
+
792
+ case 'cone':
793
+ params.radius = numbers[0] || 30;
794
+ params.height = numbers[1] || 60;
795
+ break;
796
+
797
+ case 'tube':
798
+ params.outerRadius = numbers[0] || 50;
799
+ params.innerRadius = numbers[1] || 40;
800
+ params.height = numbers[2] || 100;
801
+ break;
802
+
803
+ case 'plate':
804
+ params.width = numbers[0] || 100;
805
+ params.depth = numbers[1] || 80;
806
+ params.thickness = numbers[2] || 5;
807
+ break;
808
+
809
+ default:
810
+ return null;
811
+ }
812
+
813
+ return Object.keys(params).length > 0 ? params : null;
814
+ }
815
+
816
+ /**
817
+ * Parse operations from text
818
+ */
819
+ function parseOperations(text, numbers) {
820
+ const commands = [];
821
+
822
+ // Holes
823
+ if (text.match(/hole|bore|drill/i)) {
824
+ const holeRadius = text.match(/(\d+)\s*mm\s*hole/) ? parseFloat(RegExp.$1) / 2 : 5;
825
+ const count = text.match(/(\d+)\s*holes?/) ? parseInt(RegExp.$1) : 1;
826
+
827
+ commands.push({
828
+ method: 'feature.hole',
829
+ params: { radius: holeRadius, count },
830
+ });
831
+ }
832
+
833
+ // Fillets
834
+ if (text.match(/fillet|round|rounded/i)) {
835
+ const filletRadius = text.match(/(\d+)\s*mm\s*fillet/) ? parseFloat(RegExp.$1) : 5;
836
+ commands.push({
837
+ method: 'feature.fillet',
838
+ params: { radius: filletRadius },
839
+ });
840
+ }
841
+
842
+ // Chamfers
843
+ if (text.match(/chamfer|bevel/i)) {
844
+ const chamferDist = text.match(/(\d+)\s*mm\s*chamfer/) ? parseFloat(RegExp.$1) : 2;
845
+ commands.push({
846
+ method: 'feature.chamfer',
847
+ params: { distance: chamferDist },
848
+ });
849
+ }
850
+
851
+ // Patterns
852
+ if (text.match(/pattern|array/) || text.match(/(\d+)\s*x\s*(\d+)\s*array/)) {
853
+ const matches = text.match(/(\d+)\s*x\s*(\d+)/);
854
+ const rows = matches ? parseInt(matches[1]) : 2;
855
+ const cols = matches ? parseInt(matches[2]) : 2;
856
+
857
+ commands.push({
858
+ method: 'feature.pattern',
859
+ params: { rows, cols, spacing: 50 },
860
+ });
861
+ }
862
+
863
+ // Mirror
864
+ if (text.match(/mirror|symmetric|flip/i)) {
865
+ commands.push({
866
+ method: 'feature.mirror',
867
+ params: { plane: 'xy' },
868
+ });
869
+ }
870
+
871
+ // Shell
872
+ if (text.match(/shell|hollow/i)) {
873
+ const thickness = text.match(/(\d+)\s*mm\s*wall/) ? parseFloat(RegExp.$1) : 2;
874
+ commands.push({
875
+ method: 'feature.shell',
876
+ params: { thickness },
877
+ });
878
+ }
879
+
880
+ return commands;
881
+ }
882
+
883
+ /**
884
+ * Parse assembly operations
885
+ */
886
+ function parseAssemblyOps(text) {
887
+ const commands = [];
888
+
889
+ if (text.match(/attach|add.*part|add.*component/i)) {
890
+ commands.push({
891
+ method: 'assembly.add',
892
+ params: { count: 1 },
893
+ });
894
+ }
895
+
896
+ if (text.match(/bolt|fastener|screw/i)) {
897
+ commands.push({
898
+ method: 'assembly.bolt',
899
+ params: { size: 'M8', count: 4 },
900
+ });
901
+ }
902
+
903
+ if (text.match(/mate|align|face.*face/i)) {
904
+ commands.push({
905
+ method: 'assembly.mate',
906
+ params: { type: 'face' },
907
+ });
908
+ }
909
+
910
+ return commands;
911
+ }
912
+
913
+ /**
914
+ * Detect material from text
915
+ */
916
+ function detectMaterial(text) {
917
+ for (const material of MATERIALS) {
918
+ if (text.includes(material)) {
919
+ return material;
920
+ }
921
+ }
922
+ return null;
923
+ }
924
+
925
+ // ============================================================================
926
+ // EXECUTION & INTEGRATION
927
+ // ============================================================================
928
+
929
+ /**
930
+ * Parse AND execute text command in one call
931
+ */
932
+ export async function executeTextCommand(prompt) {
933
+ try {
934
+ const { commands, preview } = await textToCAD(prompt);
935
+
936
+ if (!commands || commands.length === 0) {
937
+ addMessage('ai', 'I couldn\'t understand that. Try being more specific, like "100x60x20 box" or "cylinder with 50mm radius".');
938
+ return { ok: false };
939
+ }
940
+
941
+ // Execute via Agent API
942
+ if (window.cycleCAD && window.cycleCAD.execute) {
943
+ const results = [];
944
+ for (const cmd of commands) {
945
+ const result = await window.cycleCAD.execute(cmd);
946
+ results.push(result);
947
+ }
948
+
949
+ addMessage('ai', `Got it! ${preview}. Creating now...`);
950
+ return { ok: true, results, commands };
951
+ } else {
952
+ addMessage('ai', 'Agent API not available. Try initializing Agent API first.');
953
+ return { ok: false };
954
+ }
955
+ } catch (error) {
956
+ console.error('[Copilot] Execute error:', error);
957
+ addMessage('ai', `Error: ${error.message || 'Something went wrong'}`);
958
+ return { ok: false, error };
959
+ }
960
+ }
961
+
962
+ /**
963
+ * Handle text input from UI
964
+ */
965
+ async function handleTextInput() {
966
+ const inputEl = copilotState.panelElement.querySelector('#copilot-input');
967
+ if (!inputEl) return;
968
+
969
+ const text = inputEl.value.trim();
970
+ if (!text) return;
971
+
972
+ inputEl.value = '';
973
+ addMessage('user', text);
974
+
975
+ await executeTextCommand(text);
976
+ }
977
+
978
+ // ============================================================================
979
+ // DESIGN REVIEW & DFM ANALYSIS
980
+ // ============================================================================
981
+
982
+ /**
983
+ * Review design for manufacturability
984
+ * @param {object} options - { method: 'fdm'|'cnc'|'injection'|'laser', model: THREE.Object3D }
985
+ * @returns {object} { score, grade, issues: [], suggestions: [], dfm: {} }
986
+ */
987
+ export function reviewDesign(options = {}) {
988
+ const method = options.method || 'fdm';
989
+ const rules = DFM_RULES[method];
990
+
991
+ if (!rules) {
992
+ return { ok: false, error: 'Unknown manufacturing method' };
993
+ }
994
+
995
+ const issues = [];
996
+ const suggestions = [];
997
+ let scoreTotal = 100;
998
+
999
+ // Simulate geometry analysis
1000
+ // In production, analyze actual model via three.js
1001
+ const model = options.model || (window._scene ? window._scene.children[0] : null);
1002
+
1003
+ if (model && model.geometry) {
1004
+ const bbox = new (require('three')).Box3().setFromObject(model);
1005
+ const size = bbox.getSize(new (require('three')).Vector3());
1006
+
1007
+ // Check wall thickness (approximate)
1008
+ if (method === 'fdm' && size.z < rules.minWallThickness) {
1009
+ issues.push('Walls too thin for FDM printing');
1010
+ suggestions.push(`Increase wall thickness to at least ${rules.minWallThickness}mm`);
1011
+ scoreTotal -= 10;
1012
+ }
1013
+
1014
+ // Check overhang angles
1015
+ if (method === 'fdm' && size.z > 10) {
1016
+ issues.push('Large vertical features may need support');
1017
+ suggestions.push('Consider reducing height or splitting into parts');
1018
+ scoreTotal -= 5;
1019
+ }
1020
+
1021
+ // Check for sharp corners (injection molding)
1022
+ if (method === 'injection') {
1023
+ issues.push('Add draft angles for mold release');
1024
+ suggestions.push('Add 1-2° draft angle to vertical surfaces');
1025
+ scoreTotal -= 8;
1026
+ }
1027
+ }
1028
+
1029
+ // Determine grade
1030
+ const grade = scoreTotal >= 95 ? 'A' : scoreTotal >= 80 ? 'B' : scoreTotal >= 65 ? 'C' : scoreTotal >= 50 ? 'D' : 'F';
1031
+
1032
+ // Display review in UI
1033
+ const reviewEl = copilotState.panelElement.querySelector('#copilot-review');
1034
+ if (reviewEl) {
1035
+ reviewEl.style.display = 'block';
1036
+ reviewEl.querySelector('.grade-letter').textContent = grade;
1037
+ reviewEl.querySelector('.review-issues').innerHTML = issues.map((i) => `<div>⚠ ${i}</div>`).join('');
1038
+ reviewEl.querySelector('.review-suggestions').innerHTML = suggestions.map((s) => `<div>💡 ${s}</div>`).join('');
1039
+ }
1040
+
1041
+ return { ok: true, score: scoreTotal, grade, issues, suggestions, method };
1042
+ }
1043
+
1044
+ /**
1045
+ * Get design improvement suggestions
1046
+ */
1047
+ export function suggestImprovements() {
1048
+ return [
1049
+ '💡 Consider adding fillets to sharp edges for better strength',
1050
+ '💡 Add mounting holes if this part needs to be attached',
1051
+ '💡 Check wall thickness for your chosen manufacturing method',
1052
+ '💡 Try mirroring this feature for symmetry',
1053
+ '💡 Would you like to add a draft angle for molding?',
1054
+ ];
1055
+ }
1056
+
1057
+ // ============================================================================
1058
+ // SMART SUGGESTIONS & AUTOCOMPLETE
1059
+ // ============================================================================
1060
+
1061
+ /**
1062
+ * Get context-aware suggestions for next action
1063
+ * Based on current scene state
1064
+ */
1065
+ export function getSuggestions(context = {}) {
1066
+ const suggestions = [];
1067
+
1068
+ if (!context.lastOperation) {
1069
+ suggestions.push({
1070
+ text: 'Create a box',
1071
+ action: () => executeTextCommand('Create a 100mm cube'),
1072
+ });
1073
+ suggestions.push({
1074
+ text: 'Import a model',
1075
+ action: () => console.log('Import action'),
1076
+ });
1077
+ return suggestions;
1078
+ }
1079
+
1080
+ switch (context.lastOperation) {
1081
+ case 'box':
1082
+ suggestions.push({
1083
+ text: 'Add holes',
1084
+ action: () => executeTextCommand('Add 4 mounting holes'),
1085
+ });
1086
+ suggestions.push({
1087
+ text: 'Fillet edges',
1088
+ action: () => executeTextCommand('Add 5mm fillets'),
1089
+ });
1090
+ suggestions.push({
1091
+ text: 'Shell it',
1092
+ action: () => executeTextCommand('Shell with 2mm wall'),
1093
+ });
1094
+ break;
1095
+
1096
+ case 'cylinder':
1097
+ suggestions.push({
1098
+ text: 'Bore center hole',
1099
+ action: () => executeTextCommand('Add center hole'),
1100
+ });
1101
+ suggestions.push({
1102
+ text: 'Add threads',
1103
+ action: () => executeTextCommand('Add M10 threads'),
1104
+ });
1105
+ break;
1106
+
1107
+ case 'sketch':
1108
+ suggestions.push({
1109
+ text: 'Extrude',
1110
+ action: () => executeTextCommand('Extrude 50mm'),
1111
+ });
1112
+ suggestions.push({
1113
+ text: 'Revolve',
1114
+ action: () => executeTextCommand('Revolve 360°'),
1115
+ });
1116
+ break;
1117
+
1118
+ default:
1119
+ suggestions.push({
1120
+ text: 'Design review',
1121
+ action: () => reviewDesign(),
1122
+ });
1123
+ }
1124
+
1125
+ return suggestions;
1126
+ }
1127
+
1128
+ /**
1129
+ * Get parametric part templates
1130
+ */
1131
+ export function getTemplates(category = null) {
1132
+ if (!category) {
1133
+ return TEMPLATES;
1134
+ }
1135
+ return TEMPLATES[category] || [];
1136
+ }
1137
+
1138
+ // ============================================================================
1139
+ // ITERATIVE REFINEMENT
1140
+ // ============================================================================
1141
+
1142
+ /**
1143
+ * Refine model based on natural language instruction
1144
+ * Examples: "Make it thicker", "Add mounting holes", "Round all edges"
1145
+ */
1146
+ export async function refine(instruction) {
1147
+ instruction = instruction.toLowerCase().trim();
1148
+
1149
+ // Detect refinement intent
1150
+ if (instruction.match(/thicker|bigger|larger|taller/)) {
1151
+ return await executeTextCommand(`Increase size by 20%`);
1152
+ }
1153
+
1154
+ if (instruction.match(/thinner|smaller|shorter/)) {
1155
+ return await executeTextCommand(`Decrease size by 20%`);
1156
+ }
1157
+
1158
+ if (instruction.match(/hole|holes|drill|bore/)) {
1159
+ return await executeTextCommand(`Add 4 mounting holes`);
1160
+ }
1161
+
1162
+ if (instruction.match(/round|fillet|smooth/)) {
1163
+ return await executeTextCommand(`Add 5mm fillets to all edges`);
1164
+ }
1165
+
1166
+ if (instruction.match(/chamfer|bevel/)) {
1167
+ return await executeTextCommand(`Add 2mm chamfers`);
1168
+ }
1169
+
1170
+ if (instruction.match(/pattern|array|repeat/)) {
1171
+ return await executeTextCommand(`Create 3x3 pattern`);
1172
+ }
1173
+
1174
+ if (instruction.match(/mirror|symmetric/)) {
1175
+ return await executeTextCommand(`Mirror across center`);
1176
+ }
1177
+
1178
+ // Fallback: use full NL parser
1179
+ return await executeTextCommand(instruction);
1180
+ }
1181
+
1182
+ // ============================================================================
1183
+ // VOICE COMMANDS
1184
+ // ============================================================================
1185
+
1186
+ /**
1187
+ * Start voice input mode
1188
+ */
1189
+ export function startVoiceMode() {
1190
+ if (!copilotState.speechRecognition) {
1191
+ addMessage('ai', 'Voice input not available in your browser.');
1192
+ return;
1193
+ }
1194
+
1195
+ copilotState.voiceActive = true;
1196
+ const voiceBtn = copilotState.panelElement.querySelector('#copilot-voice');
1197
+ if (voiceBtn) {
1198
+ voiceBtn.classList.add('active');
1199
+ }
1200
+
1201
+ addMessage('ai', '🎤 Listening...');
1202
+ copilotState.speechRecognition.start();
1203
+ }
1204
+
1205
+ /**
1206
+ * Stop voice input mode
1207
+ */
1208
+ export function stopVoiceMode() {
1209
+ copilotState.voiceActive = false;
1210
+ const voiceBtn = copilotState.panelElement.querySelector('#copilot-voice');
1211
+ if (voiceBtn) {
1212
+ voiceBtn.classList.remove('active');
1213
+ }
1214
+
1215
+ if (copilotState.speechRecognition) {
1216
+ copilotState.speechRecognition.stop();
1217
+ }
1218
+
1219
+ addMessage('ai', 'Voice input stopped.');
1220
+ }
1221
+
1222
+ /**
1223
+ * Setup voice recognition handlers
1224
+ */
1225
+ function setupVoiceHandlers() {
1226
+ const sr = copilotState.speechRecognition;
1227
+ if (!sr) return;
1228
+
1229
+ sr.continuous = false;
1230
+ sr.interimResults = true;
1231
+ sr.lang = 'en-US';
1232
+
1233
+ sr.onstart = () => {
1234
+ addMessage('ai', '🎤 Recording...');
1235
+ };
1236
+
1237
+ sr.onresult = (event) => {
1238
+ let transcript = '';
1239
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1240
+ transcript += event.results[i][0].transcript;
1241
+ }
1242
+
1243
+ if (event.isFinal) {
1244
+ addMessage('user', `[voice] ${transcript}`);
1245
+ executeTextCommand(transcript);
1246
+ }
1247
+ };
1248
+
1249
+ sr.onerror = (event) => {
1250
+ addMessage('ai', `🎤 Error: ${event.error}`);
1251
+ };
1252
+
1253
+ sr.onend = () => {
1254
+ copilotState.voiceActive = false;
1255
+ const voiceBtn = copilotState.panelElement.querySelector('#copilot-voice');
1256
+ if (voiceBtn) {
1257
+ voiceBtn.classList.remove('active');
1258
+ }
1259
+ };
1260
+ }
1261
+
1262
+ // ============================================================================
1263
+ // MULTI-AGENT ORCHESTRATION (DEMO)
1264
+ // ============================================================================
1265
+
1266
+ /**
1267
+ * Spawn agent swarm (demo mode)
1268
+ * Simulates multiple agents working in parallel on a design task
1269
+ */
1270
+ export async function spawnAgents(task, count = 3) {
1271
+ const agents = [];
1272
+
1273
+ for (let i = 0; i < count; i++) {
1274
+ const agentType = ['Geometry', 'Validation', 'Cost', 'Material'][i % 4];
1275
+ const agent = {
1276
+ id: `agent-${Date.now()}-${i}`,
1277
+ type: agentType,
1278
+ status: 'running',
1279
+ progress: 0,
1280
+ result: null,
1281
+ };
1282
+
1283
+ agents.push(agent);
1284
+ copilotState.agents.push(agent);
1285
+
1286
+ // Simulate agent work
1287
+ simulateAgentWork(agent, task);
1288
+ }
1289
+
1290
+ updateAgentUI();
1291
+ return agents;
1292
+ }
1293
+
1294
+ /**
1295
+ * Simulate agent work (demo)
1296
+ */
1297
+ async function simulateAgentWork(agent, task) {
1298
+ for (let progress = 0; progress <= 100; progress += Math.random() * 30) {
1299
+ agent.progress = Math.min(progress, 100);
1300
+ updateAgentUI();
1301
+ await new Promise((r) => setTimeout(r, 500 + Math.random() * 500));
1302
+ }
1303
+
1304
+ agent.status = 'completed';
1305
+ agent.progress = 100;
1306
+ agent.result = `${agent.type} agent completed: ${task}`;
1307
+
1308
+ addMessage('ai', `✅ ${agent.type} Agent completed: ${agent.result}`);
1309
+ updateAgentUI();
1310
+ }
1311
+
1312
+ /**
1313
+ * Get agent swarm status
1314
+ */
1315
+ export function getAgentStatus() {
1316
+ return copilotState.agents.map((a) => ({
1317
+ id: a.id,
1318
+ type: a.type,
1319
+ status: a.status,
1320
+ progress: a.progress,
1321
+ }));
1322
+ }
1323
+
1324
+ /**
1325
+ * Update agent UI
1326
+ */
1327
+ function updateAgentUI() {
1328
+ const agentsEl = copilotState.panelElement.querySelector('#copilot-agents');
1329
+ if (!agentsEl || copilotState.agents.length === 0) return;
1330
+
1331
+ agentsEl.style.display = 'block';
1332
+ const list = agentsEl.querySelector('.agents-list');
1333
+ list.innerHTML = copilotState.agents
1334
+ .map(
1335
+ (a) => `
1336
+ <div class="agent-item">
1337
+ <span class="agent-status-indicator ${a.status === 'running' ? 'running' : ''}"></span>
1338
+ <span>${a.type} (${a.progress}%)</span>
1339
+ </div>
1340
+ `
1341
+ )
1342
+ .join('');
1343
+ }
1344
+
1345
+ // ============================================================================
1346
+ // UI & MESSAGING
1347
+ // ============================================================================
1348
+
1349
+ /**
1350
+ * Add message to copilot chat
1351
+ */
1352
+ export function addMessage(role, text) {
1353
+ copilotState.messages.push({ role, text, timestamp: Date.now() });
1354
+
1355
+ const messagesEl = copilotState.panelElement.querySelector('#copilot-messages');
1356
+ if (!messagesEl) return;
1357
+
1358
+ const msgDiv = document.createElement('div');
1359
+ msgDiv.className = `copilot-message copilot-message-${role}`;
1360
+ msgDiv.innerHTML = `<div class="copilot-message-content">${text}</div>`;
1361
+ messagesEl.appendChild(msgDiv);
1362
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1363
+ }
1364
+
1365
+ /**
1366
+ * Show suggestions as chips
1367
+ */
1368
+ function showSuggestions(suggestions) {
1369
+ const chipsEl = copilotState.panelElement.querySelector('#copilot-chips');
1370
+ if (!chipsEl) return;
1371
+
1372
+ chipsEl.innerHTML = suggestions
1373
+ .map(
1374
+ (s, i) => `
1375
+ <div class="chip" onclick="window.cycleCAD.copilot.executeTextCommand('${s.text}')">
1376
+ ${s.text}
1377
+ </div>
1378
+ `
1379
+ )
1380
+ .join('');
1381
+ }
1382
+
1383
+ // ============================================================================
1384
+ // EVENT SYSTEM
1385
+ // ============================================================================
1386
+
1387
+ /**
1388
+ * Register event listener
1389
+ */
1390
+ function on(event, callback) {
1391
+ if (!copilotState.eventsMap[event]) {
1392
+ copilotState.eventsMap[event] = [];
1393
+ }
1394
+ copilotState.eventsMap[event].push(callback);
1395
+ }
1396
+
1397
+ /**
1398
+ * Unregister event listener
1399
+ */
1400
+ function off(event, callback) {
1401
+ if (copilotState.eventsMap[event]) {
1402
+ copilotState.eventsMap[event] = copilotState.eventsMap[event].filter((cb) => cb !== callback);
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Emit event
1408
+ */
1409
+ function emit(event, data) {
1410
+ if (copilotState.eventsMap[event]) {
1411
+ copilotState.eventsMap[event].forEach((cb) => cb(data));
1412
+ }
1413
+ }
1414
+
1415
+ // ============================================================================
1416
+ // EXPORTS
1417
+ // ============================================================================
1418
+
1419
+ export default {
1420
+ initCopilot,
1421
+ textToCAD,
1422
+ executeTextCommand,
1423
+ reviewDesign,
1424
+ suggestImprovements,
1425
+ getSuggestions,
1426
+ getTemplates,
1427
+ refine,
1428
+ startVoiceMode,
1429
+ stopVoiceMode,
1430
+ spawnAgents,
1431
+ getAgentStatus,
1432
+ addMessage,
1433
+ on,
1434
+ off,
1435
+ };