cyclecad 3.10.1 → 3.10.2
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/app/index.html +75 -1
- package/app/js/killer-features.js +18 -6
- package/app/js/modules/ai-copilot.js +527 -252
- package/app/js/vendor/three-bvh-csg.js +3891 -0
- package/package.json +1 -1
|
@@ -1,274 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
return{kind:'other',hint:''};
|
|
7
|
-
}
|
|
1
|
+
/* AI Copilot v1.1 — multi-step CAD generation from natural language */
|
|
2
|
+
(function(){
|
|
3
|
+
'use strict';
|
|
4
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
lastErr=`Gemini ${r.status}: ${errTxt}`;
|
|
20
|
-
// Only fall through on 404; other errors (401, 429) should surface immediately
|
|
21
|
-
if(r.status!==404)throw new Error(lastErr);
|
|
22
|
-
}catch(e){
|
|
23
|
-
if(!e.message.startsWith('Gemini 404'))throw e;
|
|
24
|
-
lastErr=e.message;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
throw new Error(lastErr||'All Gemini fallbacks failed');
|
|
28
|
-
}
|
|
29
|
-
async function callGroq(m,sys,u,k){const r=await fetch('https://api.groq.com/openai/v1/chat/completions',{method:'POST',headers:{'authorization':`Bearer ${k}`,'content-type':'application/json'},body:JSON.stringify({model:m,messages:[{role:'system',content:sys},{role:'user',content:u}],temperature:0.2,max_tokens:4096})});if(!r.ok)throw new Error(`Groq ${r.status}: ${(await r.text()).slice(0,400)}`);return(await r.json()).choices?.[0]?.message?.content||''}
|
|
30
|
-
async function callLLM(mk,sys,u){const c=MODELS[mk];if(!c)throw new Error('Unknown model');const ak=keys()[c.keyField];if(!ak)throw new Error(`Missing ${c.keyField} key — click 🔑 to add one`);if(c.provider==='anthropic')return callClaude(c.model,sys,u,ak);if(c.provider==='gemini')return callGemini(c.model,sys,u,ak);if(c.provider==='groq')return callGroq(c.model,sys,u,ak)}
|
|
6
|
+
const MODELS = {
|
|
7
|
+
'claude-sonnet': {label:'Claude Sonnet 4.6', provider:'anthropic', model:'claude-sonnet-4-5-20250929', keyField:'anthropic', free:false},
|
|
8
|
+
'claude-haiku': {label:'Claude Haiku 4.5', provider:'anthropic', model:'claude-haiku-4-5-20251001', keyField:'anthropic', free:false},
|
|
9
|
+
'claude-opus': {label:'Claude Opus 4.6', provider:'anthropic', model:'claude-opus-4-5-20250929', keyField:'anthropic', free:false},
|
|
10
|
+
'gemini-flash': {label:'Gemini 2.0 Flash (free)', provider:'gemini', model:'gemini-2.0-flash', keyField:'gemini', free:true},
|
|
11
|
+
'groq-llama': {label:'Groq Llama 3.3 70B (free)', provider:'groq', model:'llama-3.3-70b-versatile', keyField:'groq', free:true}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const KEY_STORE = 'cyclecad_api_keys';
|
|
15
|
+
const LAST_MODEL = 'cyclecad_ai_last_model';
|
|
31
16
|
|
|
32
|
-
function
|
|
33
|
-
|
|
17
|
+
function getKeys(){ try { return JSON.parse(localStorage.getItem(KEY_STORE)||'{}'); } catch(e){ return {}; } }
|
|
18
|
+
function setKey(f,v){ const k=getKeys(); k[f]=v; localStorage.setItem(KEY_STORE, JSON.stringify(k)); }
|
|
19
|
+
function getLastWorkingModel(){ return localStorage.getItem(LAST_MODEL); }
|
|
20
|
+
function setLastWorkingModel(id){ localStorage.setItem(LAST_MODEL, id); }
|
|
21
|
+
|
|
22
|
+
function smartDefault(){
|
|
23
|
+
const k = getKeys();
|
|
24
|
+
const last = getLastWorkingModel();
|
|
25
|
+
if (last && MODELS[last] && k[MODELS[last].keyField]) return last;
|
|
26
|
+
if (k.anthropic) return 'claude-sonnet';
|
|
27
|
+
if (k.gemini) return 'gemini-flash';
|
|
28
|
+
if (k.groq) return 'groq-llama';
|
|
29
|
+
return 'gemini-flash';
|
|
30
|
+
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
if(!S.els.log)return;
|
|
37
|
-
const d=document.createElement('div');
|
|
38
|
-
d.className=`aic-log-${kind}`;
|
|
39
|
-
d.textContent=({info:'ℹ',ok:'✓',warn:'⚠',err:'✗',run:'⋯',fail:'✗'})[kind]+' '+txt;
|
|
40
|
-
S.els.log.appendChild(d);
|
|
41
|
-
S.els.log.scrollTop=S.els.log.scrollHeight;
|
|
42
|
-
}
|
|
32
|
+
const S = { running:false, abort:false, stepIndex:0, results:[], errors:[], els:{} };
|
|
43
33
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
d.className='aic-hint-line';
|
|
50
|
-
d.textContent='💡 '+cls.hint;
|
|
34
|
+
function log(msg, cls){
|
|
35
|
+
if (!S.els.log) return;
|
|
36
|
+
const d = document.createElement('div');
|
|
37
|
+
d.className = 'aic-entry ' + (cls||'info');
|
|
38
|
+
d.textContent = msg;
|
|
51
39
|
S.els.log.appendChild(d);
|
|
40
|
+
S.els.log.scrollTop = S.els.log.scrollHeight;
|
|
52
41
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
function progress(pct, text){
|
|
43
|
+
if (S.els.prog) S.els.prog.style.width = Math.max(0, Math.min(100, pct)) + '%';
|
|
44
|
+
if (S.els.progText && text) S.els.progText.textContent = text;
|
|
45
|
+
}
|
|
46
|
+
function updateBanner(){
|
|
47
|
+
if (!S.els.banner) return;
|
|
48
|
+
const m = MODELS[S.els.model?.value]; if (!m) return;
|
|
49
|
+
const hasKey = !!getKeys()[m.keyField];
|
|
50
|
+
if (hasKey) {
|
|
51
|
+
S.els.banner.textContent = 'Ready: ' + m.label;
|
|
52
|
+
S.els.banner.style.background = '#065f46';
|
|
53
|
+
S.els.banner.style.color = '#d1fae5';
|
|
54
|
+
} else {
|
|
55
|
+
S.els.banner.textContent = 'No ' + m.keyField + ' API key — click the key icon';
|
|
56
|
+
S.els.banner.style.background = '#7f1d1d';
|
|
57
|
+
S.els.banner.style.color = '#fecaca';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function classifyError(e, provider){
|
|
61
|
+
const msg = (e && e.message) || String(e);
|
|
62
|
+
if (/credit|balance/i.test(msg)) return {kind:'low_credit', hint:'Your '+provider+' account is out of credits. Switch to Gemini (free).'};
|
|
63
|
+
if (/401|403/.test(msg) || /invalid.*key/i.test(msg)) return {kind:'bad_key', hint:'Your '+provider+' key is invalid.'};
|
|
64
|
+
if (/429|rate/i.test(msg)) return {kind:'rate_limit', hint:'Rate-limited. Wait 60s or switch providers.'};
|
|
65
|
+
if (/404/.test(msg)) return {kind:'model_404', hint:'Model not available. Trying fallback...'};
|
|
66
|
+
return {kind:'unknown', hint:msg.slice(0,200)};
|
|
67
|
+
}
|
|
68
|
+
function showActionableError(e, provider){
|
|
69
|
+
const c = classifyError(e, provider);
|
|
70
|
+
log('x '+c.hint, 'fail');
|
|
71
|
+
if (c.kind === 'low_credit' || c.kind === 'bad_key') {
|
|
72
|
+
const btn = document.createElement('button');
|
|
73
|
+
btn.textContent = 'Switch to Gemini (free)';
|
|
74
|
+
btn.style.cssText = 'margin:6px 0;padding:4px 10px;background:#059669;color:#fff;border:0;border-radius:4px;cursor:pointer;font-size:11px';
|
|
75
|
+
btn.onclick = () => { S.els.model.value = 'gemini-flash'; updateBanner(); btn.remove(); };
|
|
76
|
+
S.els.log.appendChild(btn);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
S.els.log.scrollTop=S.els.log.scrollHeight;
|
|
73
|
-
}
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
const SYSTEM_PROMPT = [
|
|
81
|
+
'You are a CAD planning assistant.',
|
|
82
|
+
'Output ONLY a JSON array of steps — no prose, no markdown fences.',
|
|
83
|
+
'Each step: {"method":"<ns.cmd>","params":{...},"note":"<short>"}.',
|
|
84
|
+
'',
|
|
85
|
+
'COORDINATE SYSTEM:',
|
|
86
|
+
'- Everything is centered at world origin (0,0,0).',
|
|
87
|
+
'- X = left/right, Y = up, Z = front/back.',
|
|
88
|
+
'- The MAIN BODY sketch is centered, so a 100x50 rect spans X:[-50,50], Z:[-25,25].',
|
|
89
|
+
'- After ops.extrude with depth=20, it spans Y:[0,20].',
|
|
90
|
+
'- Place features at coordinates RELATIVE to this centered body.',
|
|
91
|
+
'',
|
|
92
|
+
'AVAILABLE METHODS:',
|
|
93
|
+
'- sketch.start {plane:"XY"}',
|
|
94
|
+
'- sketch.rect {width, height} — rectangle centered at current origin',
|
|
95
|
+
'- sketch.circle {radius} — OR diameter',
|
|
96
|
+
'- sketch.end',
|
|
97
|
+
'- ops.extrude {depth, position:[x,y,z], subtract:bool} — create solid. subtract:true carves from last body.',
|
|
98
|
+
'- ops.hole {position:[x,y,z], depth, radius OR width+height} — carves cylinder OR rectangular hole through last body.',
|
|
99
|
+
'- ops.fillet / ops.chamfer / ops.shell — visual approximation only',
|
|
100
|
+
'- ops.pattern {count, spacingX, spacingZ} — duplicate last mesh in a grid',
|
|
101
|
+
'- view.set {view:"iso"|"top"|"front"|"side"}',
|
|
102
|
+
'- view.fit',
|
|
103
|
+
'',
|
|
104
|
+
'RULES:',
|
|
105
|
+
'1. ALWAYS use ops.hole for material removal. NEVER use sketch+extrude to make a cutout.',
|
|
106
|
+
'2. For each feature NOT at origin, pass position:[x,y,z] explicitly.',
|
|
107
|
+
'3. Build in order: main body first, then add features (posts, bosses), then subtract features (holes, cutouts), then view.',
|
|
108
|
+
'4. Case bodies: typical is 12mm internal height on a footprint matching the board (+ wall thickness).',
|
|
109
|
+
'',
|
|
110
|
+
'EXAMPLE — Raspberry Pi 4B case (85x56 board, centered):',
|
|
111
|
+
'[',
|
|
112
|
+
' {"method":"sketch.start","params":{"plane":"XY"}},',
|
|
113
|
+
' {"method":"sketch.rect","params":{"width":89,"height":60}},',
|
|
114
|
+
' {"method":"ops.extrude","params":{"depth":14,"position":[0,0,0]},"note":"case body"},',
|
|
115
|
+
' {"method":"sketch.start","params":{"plane":"XY"}},',
|
|
116
|
+
' {"method":"sketch.circle","params":{"radius":2.5}},',
|
|
117
|
+
' {"method":"ops.extrude","params":{"depth":5,"position":[-40.75,14,-26]},"note":"mounting post NW"},',
|
|
118
|
+
' {"method":"ops.pattern","params":{"count":2,"spacingX":81.5,"spacingZ":0}},',
|
|
119
|
+
' {"method":"ops.hole","params":{"position":[44.5,7,-10],"width":15,"height":6,"depth":8},"note":"USB cutout"},',
|
|
120
|
+
' {"method":"ops.hole","params":{"position":[44.5,4,10],"width":12,"height":4,"depth":8},"note":"HDMI"},',
|
|
121
|
+
' {"method":"ops.hole","params":{"position":[-44.5,7,0],"width":17,"height":14,"depth":8},"note":"Ethernet"},',
|
|
122
|
+
' {"method":"view.set","params":{"view":"iso"}},',
|
|
123
|
+
' {"method":"view.fit","params":{}}',
|
|
124
|
+
']'
|
|
125
|
+
].join('\n');
|
|
76
126
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}else{
|
|
88
|
-
S.els.banner.className='aic-banner aic-banner-ok';
|
|
89
|
-
S.els.banner.innerHTML=` ✓ Ready — using <b>${cfg.label}</b>${cfg.free?' (free tier)':''}`;
|
|
90
|
-
S.els.banner.style.display='block';
|
|
127
|
+
async function callClaude(model, prompt){
|
|
128
|
+
const k = getKeys().anthropic; if (!k) throw new Error('Missing anthropic key');
|
|
129
|
+
const r = await fetch('https://api.anthropic.com/v1/messages', {
|
|
130
|
+
method:'POST',
|
|
131
|
+
headers:{'Content-Type':'application/json','x-api-key':k,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},
|
|
132
|
+
body: JSON.stringify({model, max_tokens:4096, system:SYSTEM_PROMPT, messages:[{role:'user',content:prompt}]})
|
|
133
|
+
});
|
|
134
|
+
if (!r.ok) throw new Error('Claude '+r.status+': '+await r.text());
|
|
135
|
+
const j = await r.json();
|
|
136
|
+
return j.content?.[0]?.text || '';
|
|
91
137
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
async function callGemini(model, prompt){
|
|
139
|
+
const k = getKeys().gemini; if (!k) throw new Error('Missing gemini key');
|
|
140
|
+
const tryModels = [model, 'gemini-1.5-flash', 'gemini-1.5-flash-latest'];
|
|
141
|
+
let lastErr;
|
|
142
|
+
for (const m of tryModels) {
|
|
143
|
+
try {
|
|
144
|
+
const url = 'https://generativelanguage.googleapis.com/v1beta/models/'+m+':generateContent?key='+k;
|
|
145
|
+
const r = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({contents:[{parts:[{text:SYSTEM_PROMPT+'\n\n'+prompt}]}]})});
|
|
146
|
+
if (!r.ok) {
|
|
147
|
+
const t = await r.text();
|
|
148
|
+
if (r.status === 404) { lastErr = new Error('Gemini 404: '+t); continue; }
|
|
149
|
+
throw new Error('Gemini '+r.status+': '+t);
|
|
150
|
+
}
|
|
151
|
+
const j = await r.json();
|
|
152
|
+
return j.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
153
|
+
} catch(e) { lastErr = e; if (!/404/.test(e.message)) throw e; }
|
|
154
|
+
}
|
|
155
|
+
throw lastErr || new Error('Gemini failed');
|
|
156
|
+
}
|
|
157
|
+
async function callGroq(model, prompt){
|
|
158
|
+
const k = getKeys().groq; if (!k) throw new Error('Missing groq key');
|
|
159
|
+
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
|
160
|
+
method:'POST',
|
|
161
|
+
headers:{'Content-Type':'application/json','Authorization':'Bearer '+k},
|
|
162
|
+
body: JSON.stringify({model, messages:[{role:'system',content:SYSTEM_PROMPT},{role:'user',content:prompt}], max_tokens:4096})
|
|
163
|
+
});
|
|
164
|
+
if (!r.ok) throw new Error('Groq '+r.status+': '+await r.text());
|
|
165
|
+
const j = await r.json();
|
|
166
|
+
return j.choices?.[0]?.message?.content || '';
|
|
167
|
+
}
|
|
168
|
+
async function callLLM(modelId, prompt){
|
|
169
|
+
const m = MODELS[modelId]; if (!m) throw new Error('Unknown model: '+modelId);
|
|
170
|
+
if (m.provider === 'anthropic') return callClaude(m.model, prompt);
|
|
171
|
+
if (m.provider === 'gemini') return callGemini(m.model, prompt);
|
|
172
|
+
if (m.provider === 'groq') return callGroq(m.model, prompt);
|
|
173
|
+
throw new Error('Unknown provider');
|
|
174
|
+
}
|
|
175
|
+
function parseJson(text){
|
|
176
|
+
let t = (text||'').trim();
|
|
177
|
+
t = t.replace(/```(?:json)?/gi, '').trim();
|
|
178
|
+
const start = t.indexOf('[');
|
|
179
|
+
if (start < 0) throw new Error('No JSON array found in: '+t.slice(0,120));
|
|
180
|
+
let depth = 0, inStr = false, esc = false, end = -1;
|
|
181
|
+
for (let i = start; i < t.length; i++) {
|
|
182
|
+
const ch = t[i];
|
|
183
|
+
if (esc) { esc = false; continue; }
|
|
184
|
+
if (ch === '\\') { esc = true; continue; }
|
|
185
|
+
if (ch === '"') { inStr = !inStr; continue; }
|
|
186
|
+
if (inStr) continue;
|
|
187
|
+
if (ch === '[') depth++;
|
|
188
|
+
else if (ch === ']') { depth--; if (depth === 0) { end = i; break; } }
|
|
189
|
+
}
|
|
190
|
+
if (end < 0) throw new Error('Unclosed JSON array');
|
|
191
|
+
t = t.slice(start, end+1);
|
|
192
|
+
try { return JSON.parse(t); }
|
|
193
|
+
catch(e) {
|
|
194
|
+
const t2 = t.replace(/,(\s*[\]}])/g, '$1');
|
|
195
|
+
return JSON.parse(t2);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const miniState = { currentSketch: null, lastMesh: null, group: null };
|
|
199
|
+
function miniReset(){
|
|
200
|
+
if (window._scene) {
|
|
201
|
+
[...window._scene.children].filter(c => c.name === 'AICopilotBuild').forEach(g => window._scene.remove(g));
|
|
202
|
+
}
|
|
203
|
+
miniState.currentSketch = null; miniState.lastMesh = null; miniState.group = null; miniState.body = null;
|
|
204
|
+
}
|
|
205
|
+
function miniEnsureGroup(){
|
|
206
|
+
if (!miniState.group || miniState.group.parent !== window._scene) {
|
|
207
|
+
miniState.group = new window.THREE.Group();
|
|
208
|
+
miniState.group.name = 'AICopilotBuild';
|
|
209
|
+
window._scene.add(miniState.group);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function getPos(p){
|
|
213
|
+
if (Array.isArray(p.position)) return p.position;
|
|
214
|
+
if (Array.isArray(p.center)) return p.center;
|
|
215
|
+
if (Array.isArray(p.at)) return p.at;
|
|
216
|
+
return [+p.x||0, +p.y||0, +p.z||0];
|
|
217
|
+
}
|
|
218
|
+
let _csgLib = null, _csgEv = null;
|
|
219
|
+
async function loadCSG(){
|
|
220
|
+
if (_csgLib) return _csgLib;
|
|
221
|
+
try {
|
|
222
|
+
const m = await import('/app/js/vendor/three-bvh-csg.js?v=1');
|
|
223
|
+
_csgLib = { Brush:m.Brush, Evaluator:m.Evaluator, SUBTRACTION:m.SUBTRACTION, ADDITION:m.ADDITION };
|
|
224
|
+
_csgEv = new m.Evaluator();
|
|
225
|
+
return _csgLib;
|
|
226
|
+
} catch(e) { console.warn('[Copilot] CSG load failed:', e.message); return null; }
|
|
227
|
+
}
|
|
228
|
+
async function subtractFromBody(cutMesh){
|
|
229
|
+
const target = miniState.body || miniState.lastMesh;
|
|
230
|
+
const csg = await loadCSG();
|
|
231
|
+
if (!csg || !target) { miniState.group.add(cutMesh); return; }
|
|
232
|
+
try {
|
|
233
|
+
target.updateMatrixWorld(true);
|
|
234
|
+
cutMesh.updateMatrixWorld(true);
|
|
235
|
+
const THREE = window.THREE;
|
|
236
|
+
const brA = new csg.Brush(target.geometry.clone(), target.material);
|
|
237
|
+
brA.position.copy(target.position); brA.quaternion.copy(target.quaternion); brA.scale.copy(target.scale); brA.updateMatrixWorld(true);
|
|
238
|
+
const brB = new csg.Brush(cutMesh.geometry.clone(), cutMesh.material);
|
|
239
|
+
brB.position.copy(cutMesh.position); brB.quaternion.copy(cutMesh.quaternion); brB.scale.copy(cutMesh.scale); brB.updateMatrixWorld(true);
|
|
240
|
+
const res = _csgEv.evaluate(brA, brB, csg.SUBTRACTION);
|
|
241
|
+
res.material = target.material;
|
|
242
|
+
miniState.group.remove(target);
|
|
243
|
+
miniState.group.add(res);
|
|
244
|
+
miniState.lastMesh = res;
|
|
245
|
+
miniState.body = res;
|
|
246
|
+
} catch(e) {
|
|
247
|
+
console.warn('[Copilot] CSG subtract failed, visual fallback:', e.message);
|
|
248
|
+
miniState.group.add(cutMesh);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function miniExecute(step){
|
|
252
|
+
const method = step.method, params = step.params || {};
|
|
253
|
+
if (!window._scene || !window.THREE) throw new Error('Scene not available');
|
|
254
|
+
const THREE = window.THREE;
|
|
255
|
+
if (method === 'sketch.start') { miniState.currentSketch = { plane: params.plane||'XY', origin: getPos(params) }; return {ok:true}; }
|
|
256
|
+
if (method === 'sketch.rect') { miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'rect', width: params.width||params.w||50, height: params.height||params.h||30, origin: getPos(params) }); return {ok:true}; }
|
|
257
|
+
if (method === 'sketch.circle'){ miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'circle', radius: params.radius||params.r||(params.diameter?params.diameter/2:25), origin: getPos(params) }); return {ok:true}; }
|
|
258
|
+
if (method === 'sketch.line' || method === 'sketch.end') return {ok:true};
|
|
259
|
+
if (method === 'ops.extrude') {
|
|
260
|
+
const d = params.depth||params.height||params.distance||20;
|
|
261
|
+
const sk = miniState.currentSketch || {};
|
|
262
|
+
const explicitPos = (Array.isArray(params.position)||params.x!==undefined) ? getPos(params) : null;
|
|
263
|
+
const pos = explicitPos || sk.origin || [0,0,0];
|
|
264
|
+
let g;
|
|
265
|
+
if (sk.shape==='rect') g = new THREE.BoxGeometry(sk.width, d, sk.height);
|
|
266
|
+
else if (sk.shape==='circle') g = new THREE.CylinderGeometry(sk.radius, sk.radius, d, 48);
|
|
267
|
+
else g = new THREE.BoxGeometry(50, d, 30);
|
|
268
|
+
const isSubtract = params.subtract === true || params.operation === 'cut' || params.operation === 'subtract';
|
|
269
|
+
const mat = new THREE.MeshStandardMaterial({color: isSubtract?0x1a1a1a:0x4a90e2, metalness:0.35, roughness:0.45});
|
|
270
|
+
const mesh = new THREE.Mesh(g, mat); mesh.castShadow = true;
|
|
271
|
+
mesh.position.set(pos[0]||0, (pos[1]||0) + d/2, pos[2]||0);
|
|
272
|
+
miniEnsureGroup();
|
|
273
|
+
if (isSubtract) { await subtractFromBody(mesh); }
|
|
274
|
+
else { miniState.group.add(mesh); miniState.lastMesh = mesh; if (!miniState.body) miniState.body = mesh; }
|
|
275
|
+
miniState.currentSketch = null;
|
|
276
|
+
return {ok:true};
|
|
277
|
+
}
|
|
278
|
+
if (method === 'ops.revolve') {
|
|
279
|
+
const r = params.radius||20;
|
|
280
|
+
const g = new THREE.TorusGeometry(r, Math.max(2, r/4), 24, 48);
|
|
281
|
+
const mat = new THREE.MeshStandardMaterial({color:0x4a90e2, metalness:0.35, roughness:0.45});
|
|
282
|
+
const mesh = new THREE.Mesh(g, mat);
|
|
283
|
+
miniEnsureGroup(); miniState.group.add(mesh); miniState.lastMesh = mesh;
|
|
284
|
+
return {ok:true};
|
|
111
285
|
}
|
|
112
|
-
if(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
286
|
+
if (method === 'ops.hole' || method === 'ops.subtract' || method === 'ops.cut') {
|
|
287
|
+
const pos = getPos(params);
|
|
288
|
+
const d = params.depth || 25;
|
|
289
|
+
let g;
|
|
290
|
+
if (params.width && params.height) {
|
|
291
|
+
g = new THREE.BoxGeometry(params.width, d, params.height);
|
|
292
|
+
} else {
|
|
293
|
+
const r = params.radius || (params.diameter?params.diameter/2:3);
|
|
294
|
+
g = new THREE.CylinderGeometry(r, r, d, 48);
|
|
295
|
+
}
|
|
296
|
+
const cutMesh = new THREE.Mesh(g, new THREE.MeshStandardMaterial({color:0x000000}));
|
|
297
|
+
cutMesh.position.set(pos[0]||0, (pos[1]||0) + d/2 - 0.5, pos[2]||0);
|
|
298
|
+
miniEnsureGroup();
|
|
299
|
+
await subtractFromBody(cutMesh);
|
|
300
|
+
return {ok:true};
|
|
301
|
+
}
|
|
302
|
+
if (method === 'ops.pattern') {
|
|
303
|
+
if (!miniState.lastMesh) return {ok:true, note:'no base mesh'};
|
|
304
|
+
const count = Math.max(2, Math.min(20, params.count||4));
|
|
305
|
+
const sx = params.spacingX || (params.direction==='x'?params.spacing:0) || 0;
|
|
306
|
+
const sz = params.spacingZ || (params.direction==='z'?params.spacing:0) || 0;
|
|
307
|
+
const sy = params.spacingY || 0;
|
|
308
|
+
for (let i = 1; i < count; i++) {
|
|
309
|
+
const c = miniState.lastMesh.clone();
|
|
310
|
+
c.position.x += sx*i; c.position.z += sz*i; c.position.y += sy*i;
|
|
311
|
+
miniState.group.add(c);
|
|
312
|
+
}
|
|
313
|
+
return {ok:true};
|
|
314
|
+
}
|
|
315
|
+
if (['ops.fillet','ops.chamfer','ops.shell'].includes(method)) return {ok:true, note:method+' (visual approximation)'};
|
|
316
|
+
if (method === 'view.fit') {
|
|
317
|
+
const tgt = miniState.group || window._scene;
|
|
318
|
+
if (tgt && window._camera) {
|
|
319
|
+
const box = new THREE.Box3().setFromObject(tgt);
|
|
320
|
+
if (!box.isEmpty()) {
|
|
321
|
+
const size = box.getSize(new THREE.Vector3());
|
|
322
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 100;
|
|
323
|
+
const fov = (window._camera.fov||50) * Math.PI/180;
|
|
324
|
+
const dist = maxDim / (2*Math.tan(fov/2)) * 2.2;
|
|
325
|
+
const c = box.getCenter(new THREE.Vector3());
|
|
326
|
+
window._camera.position.set(c.x+dist, c.y+dist, c.z+dist);
|
|
327
|
+
window._camera.lookAt(c);
|
|
328
|
+
if (window._controls) { window._controls.target.copy(c); window._controls.update(); }
|
|
142
329
|
}
|
|
143
330
|
}
|
|
331
|
+
return {ok:true};
|
|
144
332
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
333
|
+
if (method === 'view.set') {
|
|
334
|
+
if (!window._camera) return {ok:true};
|
|
335
|
+
const v = (params.view||params.orientation||'iso').toLowerCase();
|
|
336
|
+
const d2 = 250;
|
|
337
|
+
if (v === 'top') window._camera.position.set(0, d2, 0);
|
|
338
|
+
else if (v === 'front') window._camera.position.set(0, 0, d2);
|
|
339
|
+
else if (v === 'side' || v === 'right') window._camera.position.set(d2, 0, 0);
|
|
340
|
+
else window._camera.position.set(d2*0.7, d2*0.7, d2*0.7);
|
|
341
|
+
window._camera.lookAt(0,0,0);
|
|
342
|
+
if (window._controls) { window._controls.target.set(0,0,0); window._controls.update(); }
|
|
343
|
+
return {ok:true};
|
|
344
|
+
}
|
|
345
|
+
if (method.startsWith('query.')||method.startsWith('validate.')) return {ok:true, note:'stub'};
|
|
346
|
+
throw new Error('Unknown method: ' + method);
|
|
154
347
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const f=MODELS[S.els.model.value].keyField;
|
|
161
|
-
const c=keys()[f]||'';
|
|
162
|
-
const n=prompt(`Enter ${f} API key (stored in localStorage):`,c);
|
|
163
|
-
if(n!==null&&n!==c){
|
|
164
|
-
saveKey(f,n.trim());
|
|
165
|
-
log('ok',`${f} key saved`);
|
|
166
|
-
updateBanner();
|
|
348
|
+
async function runStep(step){
|
|
349
|
+
if (window.cycleCAD && typeof window.cycleCAD.execute === 'function') {
|
|
350
|
+
return window.cycleCAD.execute({method: step.method, params: step.params || {}});
|
|
351
|
+
}
|
|
352
|
+
return miniExecute(step);
|
|
167
353
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
.
|
|
181
|
-
.
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.
|
|
197
|
-
.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
for(const[k_c]of Object.entries(MODELS)){
|
|
233
|
-
const o=document.createElement('option');
|
|
234
|
-
o.value=k_;o.textContent=c.label;
|
|
235
|
-
S.els.model.appendChild(o);
|
|
354
|
+
function matchTemplate(prompt){
|
|
355
|
+
const p = (prompt||'').toLowerCase();
|
|
356
|
+
// Raspberry Pi 4B case
|
|
357
|
+
if (/raspberry\s*pi|\brpi\b|pi\s*4/.test(p) && /case|enclosure|housing|box/.test(p)) {
|
|
358
|
+
const hasUSB = /usb/.test(p), hasHDMI = /hdmi/.test(p), hasEth = /ethernet|lan|rj-?45/.test(p), hasPosts = /mount|post|stud|boss/.test(p);
|
|
359
|
+
const plan = [
|
|
360
|
+
{method:'sketch.start', params:{plane:'XY'}, note:'base sketch'},
|
|
361
|
+
{method:'sketch.rect', params:{width:89, height:60}, note:'case footprint 89x60'},
|
|
362
|
+
{method:'ops.extrude', params:{depth:14, position:[0,0,0]}, note:'case body'}
|
|
363
|
+
];
|
|
364
|
+
if (hasPosts) {
|
|
365
|
+
[[-38.75,14,-26],[38.75,14,-26],[-38.75,14,26],[38.75,14,26]].forEach((pos,i) => {
|
|
366
|
+
plan.push({method:'sketch.start', params:{plane:'XY'}});
|
|
367
|
+
plan.push({method:'sketch.circle', params:{radius:2.5}});
|
|
368
|
+
plan.push({method:'ops.extrude', params:{depth:4, position:pos}, note:'mounting post '+(i+1)+'/4'});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (hasUSB) plan.push({method:'ops.hole', params:{position:[44.5,7,-10], width:15, height:6, depth:10}, note:'USB cutout'});
|
|
372
|
+
if (hasHDMI) plan.push({method:'ops.hole', params:{position:[44.5,4,10], width:12, height:4, depth:10}, note:'HDMI cutout'});
|
|
373
|
+
if (hasEth) plan.push({method:'ops.hole', params:{position:[-44.5,7,0], width:17, height:14, depth:10}, note:'Ethernet cutout'});
|
|
374
|
+
plan.push({method:'view.set', params:{view:'iso'}});
|
|
375
|
+
plan.push({method:'view.fit', params:{}});
|
|
376
|
+
return plan;
|
|
377
|
+
}
|
|
378
|
+
// Hex nut
|
|
379
|
+
const nutM = p.match(/\bm(\d+)\b.*?(?:hex\s*)?nut|(?:hex\s*)?nut.*?\bm(\d+)\b/);
|
|
380
|
+
if (nutM) {
|
|
381
|
+
const size = parseInt(nutM[1]||nutM[2]);
|
|
382
|
+
const across = {3:5.5, 4:7, 5:8, 6:10, 8:13, 10:17, 12:19}[size] || size*1.7;
|
|
383
|
+
const thick = {3:2.4, 4:3.2, 5:4, 6:5, 8:6.8, 10:8.4, 12:10.8}[size] || size*0.85;
|
|
384
|
+
return [
|
|
385
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
386
|
+
{method:'sketch.circle', params:{radius: across/2}},
|
|
387
|
+
{method:'ops.extrude', params:{depth: thick, position:[0,0,0]}, note:'M'+size+' nut body ('+across+'mm across)'},
|
|
388
|
+
{method:'ops.hole', params:{position:[0, thick, 0], radius: size/2, depth: thick+2}, note:'threaded hole M'+size},
|
|
389
|
+
{method:'view.set', params:{view:'iso'}},
|
|
390
|
+
{method:'view.fit', params:{}}
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
// L-bracket with holes
|
|
394
|
+
if (/l-?bracket|mounting\s*bracket|angle\s*bracket/.test(p)) {
|
|
395
|
+
const lenM = p.match(/(\d+)\s*mm/);
|
|
396
|
+
const length = lenM ? parseInt(lenM[1]) : 100;
|
|
397
|
+
const countM = p.match(/(\d+)\s*holes?/);
|
|
398
|
+
const count = countM ? parseInt(countM[1]) : 4;
|
|
399
|
+
const spreadM = p.match(/(\d+)\s*mm\s*centers?|on\s*(\d+)/);
|
|
400
|
+
const spread = spreadM ? parseInt(spreadM[1]||spreadM[2]) : Math.max(20, length-20);
|
|
401
|
+
const plan = [
|
|
402
|
+
{method:'sketch.start', params:{plane:'XY'}},
|
|
403
|
+
{method:'sketch.rect', params:{width: length, height: 60}},
|
|
404
|
+
{method:'ops.extrude', params:{depth:5, position:[0,0,0]}, note:length+'mm L-bracket plate'}
|
|
405
|
+
];
|
|
406
|
+
const half = spread/2;
|
|
407
|
+
for (let i = 0; i < count; i++) {
|
|
408
|
+
const x = (count===4) ? (i%2===0?-half:half) : (i - (count-1)/2) * (spread/(count-1));
|
|
409
|
+
const z = (count===4) ? (i<2?-20:20) : 0;
|
|
410
|
+
plan.push({method:'ops.hole', params:{position:[x,3,z], radius:2.5, depth:6}, note:'hole '+(i+1)+'/'+count});
|
|
411
|
+
}
|
|
412
|
+
plan.push({method:'view.set', params:{view:'iso'}});
|
|
413
|
+
plan.push({method:'view.fit', params:{}});
|
|
414
|
+
return plan;
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
236
417
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
418
|
+
async function run(){
|
|
419
|
+
if (S.running) { log('Already running', 'fail'); return; }
|
|
420
|
+
const prompt = (S.els.prompt?.value || '').trim();
|
|
421
|
+
if (!prompt) { log('Enter a prompt first', 'fail'); return; }
|
|
422
|
+
const quotedMatches = prompt.match(/"([^"]{10,})"/g) || [];
|
|
423
|
+
let effectivePrompt = prompt;
|
|
424
|
+
if (quotedMatches.length >= 2) {
|
|
425
|
+
const first = quotedMatches[0].replace(/^"|"$/g, '');
|
|
426
|
+
log('Detected '+quotedMatches.length+' goals. Running only the first: \"'+first.slice(0,60)+'...\"', 'info');
|
|
427
|
+
log('To run the others, paste them one at a time.', 'info');
|
|
428
|
+
effectivePrompt = first;
|
|
429
|
+
}
|
|
430
|
+
const modelId = S.els.model?.value;
|
|
431
|
+
const m = MODELS[modelId];
|
|
432
|
+
const earlyTpl = matchTemplate(effectivePrompt);
|
|
433
|
+
if (!earlyTpl && !getKeys()[m.keyField]) { log('Missing '+m.keyField+' key — click the key icon', 'fail'); return; }
|
|
434
|
+
S.running = true; S.abort = false; S.stepIndex = 0; S.results = []; S.errors = []; miniReset();
|
|
435
|
+
progress(5, 'Planning...');
|
|
436
|
+
log('Planning with '+m.label+'...', 'info');
|
|
437
|
+
let plan;
|
|
438
|
+
if (earlyTpl) {
|
|
439
|
+
log('Matched built-in template ('+earlyTpl.length+' steps). Skipping LLM.', 'pass');
|
|
440
|
+
plan = earlyTpl;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
if (!plan) {
|
|
444
|
+
const raw = await callLLM(modelId, effectivePrompt);
|
|
445
|
+
plan = parseJson(raw);
|
|
446
|
+
if (!Array.isArray(plan)) throw new Error('Plan is not an array');
|
|
447
|
+
setLastWorkingModel(modelId);
|
|
448
|
+
log('Got '+plan.length+'-step plan. Executing...', 'pass');
|
|
449
|
+
}
|
|
450
|
+
} catch(e) {
|
|
451
|
+
showActionableError(e, m.provider);
|
|
452
|
+
S.running = false; progress(0, 'Failed'); return;
|
|
453
|
+
}
|
|
454
|
+
let recoveries = 0;
|
|
455
|
+
for (let i = 0; i < plan.length; i++) {
|
|
456
|
+
if (S.abort) { log('Aborted', 'fail'); break; }
|
|
457
|
+
S.stepIndex = i;
|
|
458
|
+
const step = plan[i];
|
|
459
|
+
progress(5 + (95*i/plan.length), 'Step '+(i+1)+'/'+plan.length);
|
|
460
|
+
log('step '+(i+1)+': '+step.method+' - '+(step.note||''), 'info');
|
|
461
|
+
try {
|
|
462
|
+
const res = await runStep(step);
|
|
463
|
+
S.results.push(res);
|
|
464
|
+
log('step '+(i+1)+' done', 'pass');
|
|
465
|
+
} catch(e) {
|
|
466
|
+
S.errors.push({step:i, error:e.message});
|
|
467
|
+
log('step '+(i+1)+': '+e.message, 'fail');
|
|
468
|
+
if (recoveries < 2) {
|
|
469
|
+
recoveries++;
|
|
470
|
+
log('Asking for recovery ('+recoveries+'/2)...', 'info');
|
|
471
|
+
try {
|
|
472
|
+
const recPrompt = 'Original goal: '+prompt+'\nFailed step: '+JSON.stringify(step)+'\nError: '+e.message+'\nRemaining: '+JSON.stringify(plan.slice(i+1))+'\nReturn a JSON array of replacement steps.';
|
|
473
|
+
const raw = await callLLM(modelId, recPrompt);
|
|
474
|
+
const fix = parseJson(raw);
|
|
475
|
+
if (Array.isArray(fix)) { plan.splice(i+1, 0, ...fix); log('Inserted '+fix.length+' recovery steps', 'info'); }
|
|
476
|
+
} catch(re) { log('Recovery failed: '+re.message, 'fail'); }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
progress(100, 'Done');
|
|
481
|
+
log('Done - '+S.results.length+'/'+plan.length+' succeeded', S.errors.length?'info':'pass');
|
|
482
|
+
S.running = false;
|
|
483
|
+
}
|
|
484
|
+
function abort(){ S.abort = true; S.running = false; log('Stop requested', 'info'); }
|
|
485
|
+
function askKey(){
|
|
486
|
+
const m = MODELS[S.els.model?.value]; if (!m) return;
|
|
487
|
+
const current = getKeys()[m.keyField] || '';
|
|
488
|
+
const v = window.prompt('Enter '+m.keyField+' API key:', current);
|
|
489
|
+
if (v !== null && v.trim()) { setKey(m.keyField, v.trim()); log(m.keyField+' key saved', 'pass'); updateBanner(); }
|
|
490
|
+
}
|
|
491
|
+
function buildUI(){
|
|
492
|
+
const wrap = document.createElement('div');
|
|
493
|
+
wrap.className = 'aic-panel';
|
|
494
|
+
wrap.style.cssText = 'display:flex;flex-direction:column;gap:8px;padding:12px;min-width:420px;max-width:640px;font-family:-apple-system,sans-serif';
|
|
495
|
+
wrap.innerHTML = ''+
|
|
496
|
+
'<div class="aic-banner" style="padding:6px 10px;border-radius:4px;font-size:12px;background:#374151;color:#e5e7eb">Loading...</div>'+
|
|
497
|
+
'<div style="display:flex;gap:6px;align-items:center">'+
|
|
498
|
+
'<select class="aic-model" style="flex:1;padding:6px;background:#1f2937;color:#e5e7eb;border:1px solid #374151;border-radius:4px;font-size:13px"></select>'+
|
|
499
|
+
'<button class="aic-key" title="Set API key" style="padding:6px 10px;background:#4b5563;color:white;border:0;border-radius:4px;cursor:pointer">Key</button>'+
|
|
500
|
+
'</div>'+
|
|
501
|
+
'<textarea class="aic-prompt" placeholder="e.g. box 100x50x20 with 3mm fillet" style="min-height:80px;padding:8px;background:#0f172a;color:#e2e8f0;border:1px solid #374151;border-radius:4px;font:13px inherit;resize:vertical"></textarea>'+
|
|
502
|
+
'<div style="display:flex;gap:6px">'+
|
|
503
|
+
'<button class="aic-go" style="flex:1;padding:8px;background:#38bdf8;color:#0f172a;border:0;border-radius:4px;font-weight:600;cursor:pointer">Generate</button>'+
|
|
504
|
+
'<button class="aic-stop" style="padding:8px 14px;background:#dc2626;color:white;border:0;border-radius:4px;cursor:pointer">Stop</button>'+
|
|
505
|
+
'</div>'+
|
|
506
|
+
'<div style="background:#1f2937;border-radius:4px;overflow:hidden;height:6px"><div class="aic-prog" style="height:100%;background:#38bdf8;width:0%;transition:width .2s"></div></div>'+
|
|
507
|
+
'<div class="aic-prog-text" style="font-size:11px;color:#94a3b8">Idle</div>'+
|
|
508
|
+
'<div class="aic-log" style="min-height:200px;max-height:400px;overflow-y:auto;padding:8px;background:#0f172a;border:1px solid #374151;border-radius:4px;font:11px/1.5 SF Mono,monospace;color:#e2e8f0"></div>';
|
|
509
|
+
const sel = wrap.querySelector('.aic-model');
|
|
510
|
+
Object.entries(MODELS).forEach(([id, m]) => {
|
|
511
|
+
const o = document.createElement('option');
|
|
512
|
+
o.value = id; o.textContent = m.label;
|
|
513
|
+
sel.appendChild(o);
|
|
514
|
+
});
|
|
515
|
+
sel.value = smartDefault();
|
|
516
|
+
S.els.banner = wrap.querySelector('.aic-banner');
|
|
517
|
+
S.els.model = sel;
|
|
518
|
+
S.els.key = wrap.querySelector('.aic-key');
|
|
519
|
+
S.els.prompt = wrap.querySelector('.aic-prompt');
|
|
520
|
+
S.els.go = wrap.querySelector('.aic-go');
|
|
521
|
+
S.els.stop = wrap.querySelector('.aic-stop');
|
|
522
|
+
S.els.prog = wrap.querySelector('.aic-prog');
|
|
523
|
+
S.els.progText = wrap.querySelector('.aic-prog-text');
|
|
524
|
+
S.els.log = wrap.querySelector('.aic-log');
|
|
525
|
+
S.els.model.addEventListener('change', updateBanner);
|
|
526
|
+
S.els.key.addEventListener('click', askKey);
|
|
527
|
+
S.els.go.addEventListener('click', run);
|
|
528
|
+
S.els.stop.addEventListener('click', abort);
|
|
529
|
+
S.els.prompt.addEventListener('keydown', (e) => {
|
|
530
|
+
if ((e.metaKey||e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); run(); }
|
|
531
|
+
});
|
|
532
|
+
updateBanner();
|
|
533
|
+
return wrap;
|
|
249
534
|
}
|
|
250
535
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
536
|
+
let uiEl = null;
|
|
537
|
+
window.CycleCAD.AICopilot = {
|
|
538
|
+
init: () => true,
|
|
539
|
+
getUI: () => { if (!uiEl) uiEl = buildUI(); return uiEl; },
|
|
540
|
+
execute: (cmd, params) => {
|
|
541
|
+
if (cmd === 'generate') { if (!uiEl) uiEl = buildUI(); if (params && params.prompt) S.els.prompt.value = params.prompt; return run(); }
|
|
542
|
+
if (cmd === 'stop') return abort();
|
|
543
|
+
},
|
|
544
|
+
go: () => { if (!uiEl) uiEl = buildUI(); return run(); },
|
|
545
|
+
abort: () => abort(),
|
|
546
|
+
getState: () => ({ running:S.running, stepIndex:S.stepIndex, results:S.results.length, errors:S.errors.length, model:S.els.model?.value })
|
|
257
547
|
};
|
|
258
|
-
|
|
259
|
-
// Initial banner
|
|
260
|
-
setTimeout(updateBanner,0);
|
|
261
|
-
return p;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
window.CycleCAD=window.CycleCAD||{};
|
|
265
|
-
window.CycleCAD.AICopilot={
|
|
266
|
-
init:()=>true,
|
|
267
|
-
getUI:buildUI,
|
|
268
|
-
execute:(c,a)=>c==='generate'&&a?.prompt?(S.els.prompt&&(S.els.prompt.value=a.prompt),run()):c==='stop'?abort():null,
|
|
269
|
-
go:run,
|
|
270
|
-
abort,
|
|
271
|
-
getState:()=>({running:S.running,stepIndex:S.plan.length-1,results:S.results.length,errors:S.errors.length,model:S.els.model?.value}),
|
|
272
|
-
};
|
|
273
|
-
console.log('AI Copilot v1.1 module loaded');
|
|
548
|
+
console.log('AI Copilot v1.1 module loaded');
|
|
274
549
|
})();
|