cyclecad 0.1.0
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.
- package/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-chat.js - Natural Language to CAD Command Parser
|
|
3
|
+
* cycleCAD: Browser-based parametric 3D modeler
|
|
4
|
+
*
|
|
5
|
+
* Parses natural language descriptions into structured CAD commands.
|
|
6
|
+
* Supports primitives, mechanical parts, operations, and optional LLM enhancement.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CONFIGURATION & DICTIONARIES
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const PART_TYPE_SYNONYMS = {
|
|
14
|
+
// Primitives
|
|
15
|
+
cube: ['cube', 'box', 'block', 'square block', 'rectangular block'],
|
|
16
|
+
box: ['box', 'rectangular box', 'cuboid'],
|
|
17
|
+
cylinder: ['cylinder', 'rod', 'post', 'pin', 'shaft', 'tube'],
|
|
18
|
+
sphere: ['sphere', 'ball', 'round', 'globe'],
|
|
19
|
+
cone: ['cone', 'conical'],
|
|
20
|
+
|
|
21
|
+
// Plates & Flats
|
|
22
|
+
plate: ['plate', 'flat plate', 'mounting plate', 'base plate', 'flat base'],
|
|
23
|
+
washer: ['washer', 'flat washer', 'ring'],
|
|
24
|
+
spacer: ['spacer', 'shim', 'ring spacer'],
|
|
25
|
+
|
|
26
|
+
// Mechanical Parts
|
|
27
|
+
bracket: ['bracket', 'L-bracket', 'angle bracket', 'support bracket', 'corner bracket'],
|
|
28
|
+
flange: ['flange', 'flanged bearing', 'flanged housing', 'hub'],
|
|
29
|
+
bearing: ['bearing', 'ball bearing', 'roller bearing'],
|
|
30
|
+
gear: ['gear', 'spur gear', 'pinion', 'toothed wheel'],
|
|
31
|
+
pulley: ['pulley', 'wheel', 'sheave'],
|
|
32
|
+
fastener: ['bolt', 'screw', 'stud', 'pin', 'rivet'],
|
|
33
|
+
housing: ['housing', 'enclosure', 'case', 'body'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DIMENSION_KEYWORDS = {
|
|
37
|
+
radius: ['radius', 'r', 'rad'],
|
|
38
|
+
diameter: ['diameter', 'd', 'dia', 'od', 'outer diameter', 'id', 'inner diameter'],
|
|
39
|
+
height: ['height', 'h', 'tall', 'thickness'],
|
|
40
|
+
width: ['width', 'w', 'wide'],
|
|
41
|
+
depth: ['depth', 'dp'],
|
|
42
|
+
length: ['length', 'l', 'long'],
|
|
43
|
+
thickness: ['thickness', 'thick', 't'],
|
|
44
|
+
count: ['count', 'number of', 'qty', 'quantity'],
|
|
45
|
+
teeth: ['teeth', 'tooth', 'tooth count'],
|
|
46
|
+
angle: ['angle', 'degrees', 'deg'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const OPERATION_KEYWORDS = {
|
|
50
|
+
fillet: ['fillet', 'round', 'rounded edge'],
|
|
51
|
+
chamfer: ['chamfer', 'beveled edge', 'bevel'],
|
|
52
|
+
hole: ['hole', 'bore', 'drill', 'perforation'],
|
|
53
|
+
cut: ['cut', 'remove', 'subtract', 'pocket'],
|
|
54
|
+
extrude: ['extrude', 'extend', 'raise', 'pull'],
|
|
55
|
+
revolve: ['revolve', 'rotate', 'sweep', 'spin'],
|
|
56
|
+
pattern: ['pattern', 'array', 'repeat', 'duplicate'],
|
|
57
|
+
mirror: ['mirror', 'flip', 'symmetric'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const UNIT_FACTORS = {
|
|
61
|
+
mm: 1,
|
|
62
|
+
m: 1000,
|
|
63
|
+
cm: 10,
|
|
64
|
+
in: 25.4,
|
|
65
|
+
inch: 25.4,
|
|
66
|
+
ft: 304.8,
|
|
67
|
+
foot: 304.8,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// STATE MANAGEMENT
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
let chatState = {
|
|
75
|
+
messages: [],
|
|
76
|
+
messagesEl: null,
|
|
77
|
+
inputEl: null,
|
|
78
|
+
sendBtn: null,
|
|
79
|
+
onCommand: null,
|
|
80
|
+
apiKeys: {
|
|
81
|
+
gemini: null,
|
|
82
|
+
groq: null,
|
|
83
|
+
},
|
|
84
|
+
isLoading: false,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// INITIALIZATION
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initialize chat UI and wire up event handlers
|
|
93
|
+
* @param {HTMLElement} messagesEl - Container for chat messages
|
|
94
|
+
* @param {HTMLElement} inputEl - Input field for user messages
|
|
95
|
+
* @param {HTMLElement} sendBtn - Send button
|
|
96
|
+
* @param {Function} onCommand - Callback for parsed CAD commands
|
|
97
|
+
*/
|
|
98
|
+
export function initChat(messagesEl, inputEl, sendBtn, onCommand) {
|
|
99
|
+
chatState.messagesEl = messagesEl;
|
|
100
|
+
chatState.inputEl = inputEl;
|
|
101
|
+
chatState.sendBtn = sendBtn;
|
|
102
|
+
chatState.onCommand = onCommand;
|
|
103
|
+
|
|
104
|
+
// Load stored API keys
|
|
105
|
+
const stored = localStorage.getItem('cyclecad_api_keys');
|
|
106
|
+
if (stored) {
|
|
107
|
+
try {
|
|
108
|
+
chatState.apiKeys = JSON.parse(stored);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn('Failed to load stored API keys:', e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Wire up send button
|
|
115
|
+
if (sendBtn) {
|
|
116
|
+
sendBtn.addEventListener('click', () => handleSendMessage());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Wire up input field Enter key
|
|
120
|
+
if (inputEl) {
|
|
121
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
122
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
handleSendMessage();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
addMessage('ai', 'Hello! I\'m your CAD assistant. Describe the part you want to create, and I\'ll generate CAD commands. Try things like: "50mm cube", "cylinder 30mm radius 60mm tall", or "bracket 80x40x5".');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle sending a user message
|
|
134
|
+
*/
|
|
135
|
+
async function handleSendMessage() {
|
|
136
|
+
const text = chatState.inputEl?.value.trim();
|
|
137
|
+
if (!text) return;
|
|
138
|
+
|
|
139
|
+
// Clear input and add user message
|
|
140
|
+
if (chatState.inputEl) {
|
|
141
|
+
chatState.inputEl.value = '';
|
|
142
|
+
}
|
|
143
|
+
addMessage('user', text);
|
|
144
|
+
|
|
145
|
+
// Show loading state
|
|
146
|
+
chatState.isLoading = true;
|
|
147
|
+
if (chatState.sendBtn) {
|
|
148
|
+
chatState.sendBtn.disabled = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Parse CAD commands
|
|
153
|
+
const commands = await parseCADPrompt(text);
|
|
154
|
+
|
|
155
|
+
if (commands && commands.length > 0) {
|
|
156
|
+
// Generate AI response
|
|
157
|
+
const response = commands
|
|
158
|
+
.map((cmd) => generateDescription(cmd))
|
|
159
|
+
.join(', ');
|
|
160
|
+
|
|
161
|
+
addMessage('ai', `Got it! ${response}`);
|
|
162
|
+
|
|
163
|
+
// Call callback for each command
|
|
164
|
+
if (chatState.onCommand) {
|
|
165
|
+
commands.forEach((cmd) => chatState.onCommand(cmd));
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
addMessage('ai', 'I couldn\'t parse that description. Try being more specific: "100x60x20 box", "cylinder r30 h60", "add a 10mm fillet", etc.');
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('Chat error:', error);
|
|
172
|
+
addMessage('ai', 'Sorry, I encountered an error. Please try again.');
|
|
173
|
+
} finally {
|
|
174
|
+
chatState.isLoading = false;
|
|
175
|
+
if (chatState.sendBtn) {
|
|
176
|
+
chatState.sendBtn.disabled = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Add a message to the chat
|
|
183
|
+
* @param {string} role - 'user' or 'ai'
|
|
184
|
+
* @param {string} text - Message text
|
|
185
|
+
*/
|
|
186
|
+
export function addMessage(role, text) {
|
|
187
|
+
chatState.messages.push({ role, text });
|
|
188
|
+
|
|
189
|
+
if (!chatState.messagesEl) return;
|
|
190
|
+
|
|
191
|
+
const msgDiv = document.createElement('div');
|
|
192
|
+
msgDiv.className = `chat-message chat-message-${role}`;
|
|
193
|
+
msgDiv.textContent = text;
|
|
194
|
+
|
|
195
|
+
chatState.messagesEl.appendChild(msgDiv);
|
|
196
|
+
chatState.messagesEl.scrollTop = chatState.messagesEl.scrollHeight;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// MAIN PARSING ENGINE
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse natural language into CAD commands
|
|
205
|
+
* @param {string} text - Natural language description
|
|
206
|
+
* @returns {Promise<Array>} Array of CAD command objects
|
|
207
|
+
*/
|
|
208
|
+
export async function parseCADPrompt(text) {
|
|
209
|
+
text = text.toLowerCase().trim();
|
|
210
|
+
|
|
211
|
+
// Try LLM first if available
|
|
212
|
+
if (chatState.apiKeys.gemini || chatState.apiKeys.groq) {
|
|
213
|
+
try {
|
|
214
|
+
const llmCommands = await queryLLM(text);
|
|
215
|
+
if (llmCommands && llmCommands.length > 0) {
|
|
216
|
+
return llmCommands;
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.warn('LLM query failed, falling back to local parser:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fall back to local parsing
|
|
224
|
+
return localParseCADPrompt(text);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Local parsing fallback (no LLM required)
|
|
229
|
+
*/
|
|
230
|
+
function localParseCADPrompt(text) {
|
|
231
|
+
const commands = [];
|
|
232
|
+
|
|
233
|
+
// Detect primary part type
|
|
234
|
+
const partType = detectPartType(text);
|
|
235
|
+
const numbers = parseNumbers(text);
|
|
236
|
+
const dims = parseDimensions(text);
|
|
237
|
+
|
|
238
|
+
// Handle primitives
|
|
239
|
+
if (partType === 'cube' || partType === 'box') {
|
|
240
|
+
const cmd = parseBoxCommand(text, numbers, dims);
|
|
241
|
+
if (cmd) commands.push(cmd);
|
|
242
|
+
} else if (partType === 'cylinder') {
|
|
243
|
+
const cmd = parseCylinderCommand(text, numbers);
|
|
244
|
+
if (cmd) commands.push(cmd);
|
|
245
|
+
} else if (partType === 'sphere') {
|
|
246
|
+
const cmd = parseSphereCommand(text, numbers);
|
|
247
|
+
if (cmd) commands.push(cmd);
|
|
248
|
+
} else if (partType === 'cone') {
|
|
249
|
+
const cmd = parseConeCommand(text, numbers);
|
|
250
|
+
if (cmd) commands.push(cmd);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle mechanical parts
|
|
254
|
+
if (partType === 'plate') {
|
|
255
|
+
const cmd = parsePlateCommand(text, numbers, dims);
|
|
256
|
+
if (cmd) commands.push(cmd);
|
|
257
|
+
} else if (partType === 'bracket') {
|
|
258
|
+
const cmd = parseBracketCommand(text, numbers, dims);
|
|
259
|
+
if (cmd) commands.push(cmd);
|
|
260
|
+
} else if (partType === 'flange') {
|
|
261
|
+
const cmd = parseFlangeCommand(text, numbers);
|
|
262
|
+
if (cmd) commands.push(cmd);
|
|
263
|
+
} else if (partType === 'washer') {
|
|
264
|
+
const cmd = parseWasherCommand(text, numbers);
|
|
265
|
+
if (cmd) commands.push(cmd);
|
|
266
|
+
} else if (partType === 'spacer') {
|
|
267
|
+
const cmd = parseSpacerCommand(text, numbers);
|
|
268
|
+
if (cmd) commands.push(cmd);
|
|
269
|
+
} else if (partType === 'gear') {
|
|
270
|
+
const cmd = parseGearCommand(text, numbers);
|
|
271
|
+
if (cmd) commands.push(cmd);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle operations (holes, fillets, etc.)
|
|
275
|
+
parseOperations(text, numbers, commands);
|
|
276
|
+
|
|
277
|
+
return commands;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// PRIMITIVE PARSERS
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parse box/cube command
|
|
286
|
+
*/
|
|
287
|
+
function parseBoxCommand(text, numbers, dims) {
|
|
288
|
+
let width, height, depth;
|
|
289
|
+
|
|
290
|
+
if (dims.length === 3) {
|
|
291
|
+
[width, height, depth] = dims;
|
|
292
|
+
} else if (dims.length === 1) {
|
|
293
|
+
// Assume cube
|
|
294
|
+
width = height = depth = dims[0];
|
|
295
|
+
} else if (numbers.length >= 3) {
|
|
296
|
+
width = numbers[0];
|
|
297
|
+
height = numbers[1];
|
|
298
|
+
depth = numbers[2];
|
|
299
|
+
} else if (numbers.length === 1) {
|
|
300
|
+
width = height = depth = numbers[0];
|
|
301
|
+
} else {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
type: 'box',
|
|
307
|
+
width: Math.round(width * 10) / 10,
|
|
308
|
+
height: Math.round(height * 10) / 10,
|
|
309
|
+
depth: Math.round(depth * 10) / 10,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parse cylinder command
|
|
315
|
+
*/
|
|
316
|
+
function parseCylinderCommand(text, numbers) {
|
|
317
|
+
let radius, height;
|
|
318
|
+
|
|
319
|
+
// Try to extract radius and height from keywords
|
|
320
|
+
if (text.match(/radius|r\s*(\d+)|rod/i)) {
|
|
321
|
+
const rMatch = text.match(/radius\s*(\d+(?:\.\d+)?)|r\s*(\d+(?:\.\d+)?)/i);
|
|
322
|
+
if (rMatch) {
|
|
323
|
+
radius = parseFloat(rMatch[1] || rMatch[2]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (text.match(/height|h\s*(\d+)|tall/i)) {
|
|
328
|
+
const hMatch = text.match(/height\s*(\d+(?:\.\d+)?)|h\s*(\d+(?:\.\d+)?)/i);
|
|
329
|
+
if (hMatch) {
|
|
330
|
+
height = parseFloat(hMatch[1] || hMatch[2]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fall back to first two numbers
|
|
335
|
+
if (!radius && !height && numbers.length >= 2) {
|
|
336
|
+
radius = numbers[0];
|
|
337
|
+
height = numbers[1];
|
|
338
|
+
} else if (!radius && numbers.length >= 1) {
|
|
339
|
+
radius = numbers[0];
|
|
340
|
+
height = radius;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!radius || !height) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
type: 'cylinder',
|
|
349
|
+
radius: Math.round(radius * 10) / 10,
|
|
350
|
+
height: Math.round(height * 10) / 10,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Parse sphere command
|
|
356
|
+
*/
|
|
357
|
+
function parseSphereCommand(text, numbers) {
|
|
358
|
+
let radius;
|
|
359
|
+
|
|
360
|
+
if (numbers.length >= 1) {
|
|
361
|
+
const val = numbers[0];
|
|
362
|
+
// Check if it's diameter or radius
|
|
363
|
+
if (text.match(/diameter|dia|d\s*(\d+)/i)) {
|
|
364
|
+
radius = val / 2;
|
|
365
|
+
} else {
|
|
366
|
+
radius = val;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!radius) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
type: 'sphere',
|
|
376
|
+
radius: Math.round(radius * 10) / 10,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Parse cone command
|
|
382
|
+
*/
|
|
383
|
+
function parseConeCommand(text, numbers) {
|
|
384
|
+
let radius, height;
|
|
385
|
+
|
|
386
|
+
if (numbers.length >= 2) {
|
|
387
|
+
radius = numbers[0];
|
|
388
|
+
height = numbers[1];
|
|
389
|
+
} else if (numbers.length === 1) {
|
|
390
|
+
radius = numbers[0];
|
|
391
|
+
height = radius;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!radius || !height) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
type: 'cone',
|
|
400
|
+
radius: Math.round(radius * 10) / 10,
|
|
401
|
+
height: Math.round(height * 10) / 10,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// MECHANICAL PART PARSERS
|
|
407
|
+
// ============================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Parse plate command
|
|
411
|
+
*/
|
|
412
|
+
function parsePlateCommand(text, numbers, dims) {
|
|
413
|
+
let width, depth, thickness;
|
|
414
|
+
|
|
415
|
+
if (dims.length >= 3) {
|
|
416
|
+
width = dims[0];
|
|
417
|
+
depth = dims[1];
|
|
418
|
+
thickness = dims[2];
|
|
419
|
+
} else if (dims.length === 2) {
|
|
420
|
+
width = dims[0];
|
|
421
|
+
depth = dims[1];
|
|
422
|
+
thickness = 5; // default
|
|
423
|
+
} else if (numbers.length >= 3) {
|
|
424
|
+
width = numbers[0];
|
|
425
|
+
depth = numbers[1];
|
|
426
|
+
thickness = numbers[2];
|
|
427
|
+
} else if (numbers.length >= 2) {
|
|
428
|
+
width = numbers[0];
|
|
429
|
+
depth = numbers[1];
|
|
430
|
+
thickness = 5;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!width || !depth) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const cmd = {
|
|
438
|
+
type: 'box',
|
|
439
|
+
width: Math.round(width * 10) / 10,
|
|
440
|
+
height: Math.round(thickness * 10) / 10,
|
|
441
|
+
depth: Math.round(depth * 10) / 10,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Parse hole pattern
|
|
445
|
+
const holePattern = parseHolePattern(text);
|
|
446
|
+
if (holePattern) {
|
|
447
|
+
cmd.holes = holePattern;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return cmd;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Parse bracket command
|
|
455
|
+
*/
|
|
456
|
+
function parseBracketCommand(text, numbers, dims) {
|
|
457
|
+
let width, height, thickness;
|
|
458
|
+
|
|
459
|
+
if (dims.length >= 3) {
|
|
460
|
+
width = dims[0];
|
|
461
|
+
height = dims[1];
|
|
462
|
+
thickness = dims[2];
|
|
463
|
+
} else if (numbers.length >= 3) {
|
|
464
|
+
width = numbers[0];
|
|
465
|
+
height = numbers[1];
|
|
466
|
+
thickness = numbers[2];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!width || !height) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
type: 'bracket',
|
|
475
|
+
width: Math.round(width * 10) / 10,
|
|
476
|
+
height: Math.round(height * 10) / 10,
|
|
477
|
+
thickness: Math.round((thickness || 5) * 10) / 10,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Parse flange command
|
|
483
|
+
*/
|
|
484
|
+
function parseFlangeCommand(text, numbers) {
|
|
485
|
+
let outerDiameter, innerDiameter, height, boltCount;
|
|
486
|
+
|
|
487
|
+
// OD first
|
|
488
|
+
const odMatch = text.match(/od\s*(\d+(?:\.\d+)?)|outer\s*diameter\s*(\d+(?:\.\d+)?)/i);
|
|
489
|
+
if (odMatch) {
|
|
490
|
+
outerDiameter = parseFloat(odMatch[1] || odMatch[2]);
|
|
491
|
+
} else if (numbers.length >= 1) {
|
|
492
|
+
outerDiameter = numbers[0];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ID
|
|
496
|
+
const idMatch = text.match(/id\s*(\d+(?:\.\d+)?)|inner\s*diameter\s*(\d+(?:\.\d+)?)/i);
|
|
497
|
+
if (idMatch) {
|
|
498
|
+
innerDiameter = parseFloat(idMatch[1] || idMatch[2]);
|
|
499
|
+
} else if (numbers.length >= 2) {
|
|
500
|
+
innerDiameter = numbers[1];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Height
|
|
504
|
+
const hMatch = text.match(/height\s*(\d+(?:\.\d+)?)|h\s*(\d+(?:\.\d+)?)|tall\s*(\d+(?:\.\d+)?)/i);
|
|
505
|
+
if (hMatch) {
|
|
506
|
+
height = parseFloat(hMatch[1] || hMatch[2] || hMatch[3]);
|
|
507
|
+
} else if (numbers.length >= 3) {
|
|
508
|
+
height = numbers[2];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Bolt count
|
|
512
|
+
const boltMatch = text.match(/(\d+)\s*bolt/i);
|
|
513
|
+
if (boltMatch) {
|
|
514
|
+
boltCount = parseInt(boltMatch[1]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!outerDiameter) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
type: 'flange',
|
|
523
|
+
outerDiameter: Math.round(outerDiameter * 10) / 10,
|
|
524
|
+
innerDiameter: innerDiameter ? Math.round(innerDiameter * 10) / 10 : outerDiameter * 0.5,
|
|
525
|
+
height: height ? Math.round(height * 10) / 10 : 10,
|
|
526
|
+
boltCount: boltCount || 4,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Parse washer command
|
|
532
|
+
*/
|
|
533
|
+
function parseWasherCommand(text, numbers) {
|
|
534
|
+
let outerDiameter, innerDiameter, thickness;
|
|
535
|
+
|
|
536
|
+
// Try M-size matching (e.g., M10, M8)
|
|
537
|
+
const mMatch = text.match(/m(\d+)/i);
|
|
538
|
+
if (mMatch) {
|
|
539
|
+
const size = parseInt(mMatch[1]);
|
|
540
|
+
// Standard washer dimensions (approximate)
|
|
541
|
+
outerDiameter = size * 2.5;
|
|
542
|
+
innerDiameter = size + 0.5;
|
|
543
|
+
thickness = 2;
|
|
544
|
+
} else if (numbers.length >= 2) {
|
|
545
|
+
outerDiameter = numbers[0];
|
|
546
|
+
innerDiameter = numbers[1];
|
|
547
|
+
thickness = numbers[2] || 2;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!outerDiameter || !innerDiameter) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
type: 'washer',
|
|
556
|
+
outerDiameter: Math.round(outerDiameter * 10) / 10,
|
|
557
|
+
innerDiameter: Math.round(innerDiameter * 10) / 10,
|
|
558
|
+
thickness: Math.round(thickness * 10) / 10,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Parse spacer command
|
|
564
|
+
*/
|
|
565
|
+
function parseSpacerCommand(text, numbers) {
|
|
566
|
+
let outerDiameter, innerDiameter, height;
|
|
567
|
+
|
|
568
|
+
const odMatch = text.match(/od\s*(\d+(?:\.\d+)?)|outer\s*(\d+(?:\.\d+)?)/i);
|
|
569
|
+
if (odMatch) {
|
|
570
|
+
outerDiameter = parseFloat(odMatch[1] || odMatch[2]);
|
|
571
|
+
} else if (numbers.length >= 1) {
|
|
572
|
+
outerDiameter = numbers[0];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const idMatch = text.match(/id\s*(\d+(?:\.\d+)?)|inner\s*(\d+(?:\.\d+)?)/i);
|
|
576
|
+
if (idMatch) {
|
|
577
|
+
innerDiameter = parseFloat(idMatch[1] || idMatch[2]);
|
|
578
|
+
} else if (numbers.length >= 2) {
|
|
579
|
+
innerDiameter = numbers[1];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const hMatch = text.match(/height\s*(\d+(?:\.\d+)?)|thick\s*(\d+(?:\.\d+)?)/i);
|
|
583
|
+
if (hMatch) {
|
|
584
|
+
height = parseFloat(hMatch[1] || hMatch[2]);
|
|
585
|
+
} else if (numbers.length >= 3) {
|
|
586
|
+
height = numbers[2];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!outerDiameter || !innerDiameter) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
type: 'spacer',
|
|
595
|
+
outerDiameter: Math.round(outerDiameter * 10) / 10,
|
|
596
|
+
innerDiameter: Math.round(innerDiameter * 10) / 10,
|
|
597
|
+
height: height ? Math.round(height * 10) / 10 : 5,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Parse gear command
|
|
603
|
+
*/
|
|
604
|
+
function parseGearCommand(text, numbers) {
|
|
605
|
+
let diameter, toothCount, thickness;
|
|
606
|
+
|
|
607
|
+
const diamMatch = text.match(/diameter\s*(\d+(?:\.\d+)?)|dia\s*(\d+(?:\.\d+)?)/i);
|
|
608
|
+
if (diamMatch) {
|
|
609
|
+
diameter = parseFloat(diamMatch[1] || diamMatch[2]);
|
|
610
|
+
} else if (numbers.length >= 1) {
|
|
611
|
+
diameter = numbers[0];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const toothMatch = text.match(/(\d+)\s*teeth|tooth\s*(\d+)/i);
|
|
615
|
+
if (toothMatch) {
|
|
616
|
+
toothCount = parseInt(toothMatch[1] || toothMatch[2]);
|
|
617
|
+
} else if (numbers.length >= 2) {
|
|
618
|
+
toothCount = numbers[1];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const thickMatch = text.match(/thick\s*(\d+(?:\.\d+)?)|height\s*(\d+(?:\.\d+)?)/i);
|
|
622
|
+
if (thickMatch) {
|
|
623
|
+
thickness = parseFloat(thickMatch[1] || thickMatch[2]);
|
|
624
|
+
} else if (numbers.length >= 3) {
|
|
625
|
+
thickness = numbers[2];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!diameter || !toothCount) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
type: 'gear',
|
|
634
|
+
diameter: Math.round(diameter * 10) / 10,
|
|
635
|
+
teeth: toothCount,
|
|
636
|
+
thickness: thickness ? Math.round(thickness * 10) / 10 : 10,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ============================================================================
|
|
641
|
+
// OPERATION PARSERS
|
|
642
|
+
// ============================================================================
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Parse operations (holes, fillets, chamfers, etc.)
|
|
646
|
+
*/
|
|
647
|
+
function parseOperations(text, numbers, commands) {
|
|
648
|
+
// Fillet
|
|
649
|
+
if (text.match(/fillet|round\s*edge|rounded/i)) {
|
|
650
|
+
const match = text.match(/(\d+(?:\.\d+)?)\s*mm\s*fillet|fillet\s*(\d+(?:\.\d+)?)/i);
|
|
651
|
+
const radius = match
|
|
652
|
+
? parseFloat(match[1] || match[2])
|
|
653
|
+
: (numbers.length > 0 ? numbers[numbers.length - 1] : 2);
|
|
654
|
+
|
|
655
|
+
commands.push({
|
|
656
|
+
type: 'fillet',
|
|
657
|
+
radius: Math.round(radius * 10) / 10,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Chamfer
|
|
662
|
+
if (text.match(/chamfer|beveled|bevel/i)) {
|
|
663
|
+
const match = text.match(/(\d+(?:\.\d+)?)\s*mm\s*chamfer|chamfer\s*(\d+(?:\.\d+)?)/i);
|
|
664
|
+
const distance = match
|
|
665
|
+
? parseFloat(match[1] || match[2])
|
|
666
|
+
: (numbers.length > 0 ? numbers[numbers.length - 1] : 1);
|
|
667
|
+
|
|
668
|
+
commands.push({
|
|
669
|
+
type: 'chamfer',
|
|
670
|
+
distance: Math.round(distance * 10) / 10,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Extrude
|
|
675
|
+
if (text.match(/extrude|extend|pull|raise/i)) {
|
|
676
|
+
const match = text.match(/extrude\s*(\d+(?:\.\d+)?)|(\d+(?:\.\d+)?)\s*mm\s*extrude/i);
|
|
677
|
+
const height = match
|
|
678
|
+
? parseFloat(match[1] || match[2])
|
|
679
|
+
: (numbers.length > 0 ? numbers[numbers.length - 1] : 10);
|
|
680
|
+
|
|
681
|
+
commands.push({
|
|
682
|
+
type: 'extrude',
|
|
683
|
+
height: Math.round(height * 10) / 10,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Revolve
|
|
688
|
+
if (text.match(/revolve|rotate|sweep|spin/i)) {
|
|
689
|
+
const match = text.match(/(\d+(?:\.\d+)?)\s*degree|revolve\s*(\d+)/i);
|
|
690
|
+
const angle = match
|
|
691
|
+
? parseFloat(match[1] || match[2])
|
|
692
|
+
: 360;
|
|
693
|
+
|
|
694
|
+
commands.push({
|
|
695
|
+
type: 'revolve',
|
|
696
|
+
angle: Math.round(angle * 10) / 10,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Parse hole patterns (through holes, corner holes, bolt circles)
|
|
703
|
+
*/
|
|
704
|
+
function parseHolePattern(text) {
|
|
705
|
+
const pattern = {};
|
|
706
|
+
|
|
707
|
+
// Through hole
|
|
708
|
+
const throughMatch = text.match(/(\d+(?:\.\d+)?)\s*mm\s*hole\s*through|hole\s*through\s*(?:center)?/i);
|
|
709
|
+
if (throughMatch) {
|
|
710
|
+
const diameter = parseFloat(throughMatch[1]) || 10;
|
|
711
|
+
return {
|
|
712
|
+
type: 'through',
|
|
713
|
+
diameter,
|
|
714
|
+
centerX: 0,
|
|
715
|
+
centerY: 0,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Corner holes
|
|
720
|
+
const cornerMatch = text.match(/(\d+)\s*holes?(?:\s+at)?\s*corners?/i);
|
|
721
|
+
if (cornerMatch) {
|
|
722
|
+
const count = parseInt(cornerMatch[1]);
|
|
723
|
+
const diamMatch = text.match(/(\d+(?:\.\d+)?)\s*mm\s*(?:diameter|dia)/i);
|
|
724
|
+
const diameter = diamMatch ? parseFloat(diamMatch[1]) : 8;
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
type: 'corners',
|
|
728
|
+
count,
|
|
729
|
+
diameter,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Bolt circle
|
|
734
|
+
const boltMatch = text.match(/(\d+)\s*bolt\s*(?:hole)?s?|bolt\s*circle\s*(\d+)/i);
|
|
735
|
+
if (boltMatch) {
|
|
736
|
+
const count = parseInt(boltMatch[1] || boltMatch[2]);
|
|
737
|
+
const diamMatch = text.match(/(\d+(?:\.\d+)?)\s*mm\s*(?:diameter|dia|bolt)/i);
|
|
738
|
+
const diameter = diamMatch ? parseFloat(diamMatch[1]) : 8;
|
|
739
|
+
const circleMatch = text.match(/circle\s*(\d+(?:\.\d+)?)/i);
|
|
740
|
+
const circleDia = circleMatch ? parseFloat(circleMatch[1]) : 60;
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
type: 'boltcircle',
|
|
744
|
+
count,
|
|
745
|
+
diameter,
|
|
746
|
+
circleDiameter: circleDia,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ============================================================================
|
|
754
|
+
// UTILITY PARSERS
|
|
755
|
+
// ============================================================================
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Parse all numbers from text with unit conversion
|
|
759
|
+
* @returns {Array<number>} Array of converted numbers (in mm)
|
|
760
|
+
*/
|
|
761
|
+
export function parseNumbers(text) {
|
|
762
|
+
const numbers = [];
|
|
763
|
+
const numberRegex = /(\d+(?:\.\d+)?)\s*(mm|cm|m|in|inch|ft|foot)?/gi;
|
|
764
|
+
let match;
|
|
765
|
+
|
|
766
|
+
while ((match = numberRegex.exec(text)) !== null) {
|
|
767
|
+
const value = parseFloat(match[1]);
|
|
768
|
+
const unit = (match[2] || 'mm').toLowerCase();
|
|
769
|
+
const factor = UNIT_FACTORS[unit] || 1;
|
|
770
|
+
numbers.push(value * factor);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return numbers;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Parse dimension patterns like "100x60x20" or "100 by 60 by 20"
|
|
778
|
+
* @returns {Array<number>} Array of dimensions
|
|
779
|
+
*/
|
|
780
|
+
export function parseDimensions(text) {
|
|
781
|
+
const dimensions = [];
|
|
782
|
+
|
|
783
|
+
// Try X pattern: "100x60x20"
|
|
784
|
+
const xMatch = text.match(/(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)\s*(?:x\s*(\d+(?:\.\d+)?))?/i);
|
|
785
|
+
if (xMatch) {
|
|
786
|
+
dimensions.push(parseFloat(xMatch[1]));
|
|
787
|
+
dimensions.push(parseFloat(xMatch[2]));
|
|
788
|
+
if (xMatch[3]) {
|
|
789
|
+
dimensions.push(parseFloat(xMatch[3]));
|
|
790
|
+
}
|
|
791
|
+
return dimensions;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Try "by" pattern: "100 by 60 by 20"
|
|
795
|
+
const byMatch = text.match(/(\d+(?:\.\d+)?)\s+by\s+(\d+(?:\.\d+)?)\s+(?:by\s+(\d+(?:\.\d+)?))?/i);
|
|
796
|
+
if (byMatch) {
|
|
797
|
+
dimensions.push(parseFloat(byMatch[1]));
|
|
798
|
+
dimensions.push(parseFloat(byMatch[2]));
|
|
799
|
+
if (byMatch[3]) {
|
|
800
|
+
dimensions.push(parseFloat(byMatch[3]));
|
|
801
|
+
}
|
|
802
|
+
return dimensions;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return dimensions;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Detect primary part type from text
|
|
810
|
+
* @returns {string} Part type identifier
|
|
811
|
+
*/
|
|
812
|
+
export function detectPartType(text) {
|
|
813
|
+
text = text.toLowerCase();
|
|
814
|
+
|
|
815
|
+
for (const [type, synonyms] of Object.entries(PART_TYPE_SYNONYMS)) {
|
|
816
|
+
for (const synonym of synonyms) {
|
|
817
|
+
if (text.includes(synonym)) {
|
|
818
|
+
return type;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return 'box'; // default
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// DESCRIPTION GENERATION
|
|
828
|
+
// ============================================================================
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Convert CAD command back to human-readable description
|
|
832
|
+
*/
|
|
833
|
+
export function generateDescription(command) {
|
|
834
|
+
switch (command.type) {
|
|
835
|
+
case 'box':
|
|
836
|
+
return `${command.width}x${command.height}x${command.depth}mm box`;
|
|
837
|
+
case 'cylinder':
|
|
838
|
+
return `cylinder r${command.radius}mm h${command.height}mm`;
|
|
839
|
+
case 'sphere':
|
|
840
|
+
return `sphere r${command.radius}mm`;
|
|
841
|
+
case 'cone':
|
|
842
|
+
return `cone r${command.radius}mm h${command.height}mm`;
|
|
843
|
+
case 'bracket':
|
|
844
|
+
return `${command.width}x${command.height}x${command.thickness}mm bracket`;
|
|
845
|
+
case 'flange':
|
|
846
|
+
return `flange OD${command.outerDiameter}mm ID${command.innerDiameter}mm h${command.height}mm`;
|
|
847
|
+
case 'washer':
|
|
848
|
+
return `washer OD${command.outerDiameter}mm ID${command.innerDiameter}mm`;
|
|
849
|
+
case 'spacer':
|
|
850
|
+
return `spacer OD${command.outerDiameter}mm ID${command.innerDiameter}mm h${command.height}mm`;
|
|
851
|
+
case 'gear':
|
|
852
|
+
return `${command.teeth}-tooth gear d${command.diameter}mm`;
|
|
853
|
+
case 'fillet':
|
|
854
|
+
return `fillet r${command.radius}mm`;
|
|
855
|
+
case 'chamfer':
|
|
856
|
+
return `chamfer ${command.distance}mm`;
|
|
857
|
+
case 'extrude':
|
|
858
|
+
return `extrude ${command.height}mm`;
|
|
859
|
+
case 'revolve':
|
|
860
|
+
return `revolve ${command.angle}°`;
|
|
861
|
+
case 'cut':
|
|
862
|
+
return `cut ${command.shape} hole`;
|
|
863
|
+
default:
|
|
864
|
+
return JSON.stringify(command);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ============================================================================
|
|
869
|
+
// API KEY MANAGEMENT
|
|
870
|
+
// ============================================================================
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Set LLM API keys for enhanced parsing
|
|
874
|
+
*/
|
|
875
|
+
export function setAPIKeys(geminiKey, groqKey) {
|
|
876
|
+
chatState.apiKeys.gemini = geminiKey || null;
|
|
877
|
+
chatState.apiKeys.groq = groqKey || null;
|
|
878
|
+
|
|
879
|
+
localStorage.setItem('cyclecad_api_keys', JSON.stringify(chatState.apiKeys));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Get current API keys
|
|
884
|
+
*/
|
|
885
|
+
export function getAPIKeys() {
|
|
886
|
+
return { ...chatState.apiKeys };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ============================================================================
|
|
890
|
+
// LLM INTEGRATION
|
|
891
|
+
// ============================================================================
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Query LLM for complex CAD parsing
|
|
895
|
+
* Falls back to local parser on failure
|
|
896
|
+
*/
|
|
897
|
+
async function queryLLM(prompt) {
|
|
898
|
+
const systemPrompt = `You are a CAD command parser. Convert natural language descriptions into JSON CAD commands.
|
|
899
|
+
|
|
900
|
+
Return a JSON array of command objects. Each command has:
|
|
901
|
+
- type: 'box', 'cylinder', 'sphere', 'cone', 'bracket', 'flange', 'gear', 'washer', 'spacer', 'fillet', 'chamfer', 'extrude', 'revolve', 'cut'
|
|
902
|
+
- Relevant dimensions (width, height, depth, radius, diameter, teeth, etc.)
|
|
903
|
+
|
|
904
|
+
Examples:
|
|
905
|
+
"50mm cube" → [{"type":"box","width":50,"height":50,"depth":50}]
|
|
906
|
+
"cylinder r30 h60" → [{"type":"cylinder","radius":30,"height":60}]
|
|
907
|
+
"100x60x20 box with 10mm fillet" → [{"type":"box","width":100,"height":60,"depth":20},{"type":"fillet","radius":10}]
|
|
908
|
+
|
|
909
|
+
Return ONLY valid JSON, no other text.`;
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
// Try Gemini Flash first
|
|
913
|
+
if (chatState.apiKeys.gemini) {
|
|
914
|
+
return await queryGemini(prompt, systemPrompt);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Try Groq Llama
|
|
918
|
+
if (chatState.apiKeys.groq) {
|
|
919
|
+
return await queryGroq(prompt, systemPrompt);
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.warn('LLM query failed:', error);
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Query Google Gemini Flash API
|
|
929
|
+
*/
|
|
930
|
+
async function queryGemini(prompt, systemPrompt) {
|
|
931
|
+
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' + chatState.apiKeys.gemini, {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: { 'Content-Type': 'application/json' },
|
|
934
|
+
body: JSON.stringify({
|
|
935
|
+
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
936
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
937
|
+
}),
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
if (!response.ok) {
|
|
941
|
+
throw new Error(`Gemini API error: ${response.statusText}`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const data = await response.json();
|
|
945
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
946
|
+
return JSON.parse(text);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Query Groq Llama 3.1 API
|
|
951
|
+
*/
|
|
952
|
+
async function queryGroq(prompt, systemPrompt) {
|
|
953
|
+
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
|
954
|
+
method: 'POST',
|
|
955
|
+
headers: {
|
|
956
|
+
'Content-Type': 'application/json',
|
|
957
|
+
Authorization: `Bearer ${chatState.apiKeys.groq}`,
|
|
958
|
+
},
|
|
959
|
+
body: JSON.stringify({
|
|
960
|
+
model: 'llama-3.1-8b-instant',
|
|
961
|
+
messages: [
|
|
962
|
+
{ role: 'system', content: systemPrompt },
|
|
963
|
+
{ role: 'user', content: prompt },
|
|
964
|
+
],
|
|
965
|
+
temperature: 0,
|
|
966
|
+
}),
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
if (!response.ok) {
|
|
970
|
+
throw new Error(`Groq API error: ${response.statusText}`);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const data = await response.json();
|
|
974
|
+
const text = data.choices?.[0]?.message?.content || '';
|
|
975
|
+
return JSON.parse(text);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ============================================================================
|
|
979
|
+
// EXPORTS
|
|
980
|
+
// ============================================================================
|
|
981
|
+
|
|
982
|
+
export default {
|
|
983
|
+
initChat,
|
|
984
|
+
addMessage,
|
|
985
|
+
parseCADPrompt,
|
|
986
|
+
parseNumbers,
|
|
987
|
+
parseDimensions,
|
|
988
|
+
detectPartType,
|
|
989
|
+
generateDescription,
|
|
990
|
+
setAPIKeys,
|
|
991
|
+
getAPIKeys,
|
|
992
|
+
};
|