cyclecad 3.9.20 → 3.10.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/app/index.html CHANGED
@@ -1010,6 +1010,7 @@
1010
1010
  <button class="menu-item-link" data-action="tools-addins">Add-ins...</button>
1011
1011
  <button class="menu-item-link" data-action="tools-marketplace">Marketplace...</button>
1012
1012
  <div class="menu-separator"></div>
1013
+ <button class="menu-item-link" data-action="tools-ai-copilot">✨ AI Copilot (multi-step)</button>
1013
1014
  <button class="menu-item-link" data-action="tools-text-to-cad">Text-to-CAD (AI)</button>
1014
1015
  <button class="menu-item-link" data-action="tools-photo-to-cad">Photo-to-CAD</button>
1015
1016
  <button class="menu-item-link" data-action="tools-dfm">Manufacturability Check</button>
@@ -1533,6 +1534,7 @@ window._dismissSplash = function(action) {
1533
1534
  <script type="module" src="/app/js/marketplace.js"></script>
1534
1535
 
1535
1536
  <!-- Killer Feature Modules -->
1537
+ <script src="/app/js/modules/ai-copilot.js"></script>
1536
1538
  <script src="/app/js/modules/text-to-cad.js"></script>
1537
1539
  <script src="/app/js/modules/photo-to-cad.js"></script>
1538
1540
  <script src="/app/js/modules/manufacturability.js"></script>
@@ -1954,6 +1956,13 @@ window._dismissSplash = function(action) {
1954
1956
  case 'help-about':
1955
1957
  showDialog('About cycleCAD', 'cycleCAD v0.9.0 - Fusion 360 Clone<br>Open-source parametric 3D CAD modeler<br><br>Built with Three.js, supporting STEP/IGES import, full parametric modeling, and AI-powered design assistance.');
1956
1958
  break;
1959
+ case 'tools-ai-copilot':
1960
+ if (window.CycleCAD && window.CycleCAD.AICopilot) {
1961
+ showDialog('✨ AI Copilot — multi-step CAD from natural language', '');
1962
+ const dbc = document.getElementById('dialog-body');
1963
+ if (dbc) { dbc.innerHTML = ''; dbc.appendChild(window.CycleCAD.AICopilot.getUI()); dbc.style.maxHeight = '620px'; dbc.style.overflow = 'auto'; }
1964
+ } else { showToast('AI Copilot module not loaded', 'error'); }
1965
+ break;
1957
1966
  case 'tools-text-to-cad':
1958
1967
  if (window.CycleCAD && window.CycleCAD.TextToCAD) {
1959
1968
  showDialog('Text-to-CAD (AI)', '');
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AI Copilot — multi-step CAD generation from natural language.
3
+ * Uses the same API keys as ai-chat.js (localStorage 'cyclecad_api_keys').
4
+ * LLM returns JSON array of {method, params, note} that execute live via
5
+ * window.cycleCAD.execute(). On error, asks LLM for recovery plan (2 retries).
6
+ * @license MIT
7
+ */
8
+ (function(){'use strict';
9
+ const S={running:false,aborted:false,plan:[],results:[],errors:[],els:{}};
10
+ const MODELS={
11
+ 'claude-sonnet':{label:'Claude Sonnet 4.6',provider:'anthropic',model:'claude-sonnet-4-6',keyField:'anthropic'},
12
+ 'claude-haiku':{label:'Claude Haiku 4.5',provider:'anthropic',model:'claude-haiku-4-5-20251001',keyField:'anthropic'},
13
+ 'claude-opus':{label:'Claude Opus 4.6',provider:'anthropic',model:'claude-opus-4-6',keyField:'anthropic'},
14
+ 'gemini':{label:'Gemini 2.0 Flash (free)',provider:'gemini',model:'gemini-2.0-flash-exp',keyField:'gemini'},
15
+ 'groq':{label:'Groq Llama 3.1 70B (free)',provider:'groq',model:'llama-3.1-70b-versatile',keyField:'groq'},
16
+ };
17
+ const SYS=`You are a CAD design agent for cycleCAD. Return ONLY a JSON array of steps. No prose, no markdown. Each step: {"method":"<ns>.<cmd>","params":{...},"note":"<what>"}.
18
+ Commands: sketch.start{plane:"XY"|"XZ"|"YZ"}, sketch.line{x1,y1,x2,y2}, sketch.circle{cx,cy,radius}, sketch.rect{x,y,width,height}, sketch.end{}, ops.extrude{depth}, ops.revolve{angle}, ops.fillet{radius}, ops.chamfer{distance}, ops.shell{thickness}, ops.hole{x,y,diameter,depth}, ops.pattern{type,count,spacing}, view.set{view:"isometric"|"top"|"front"}, view.fit{}, query.features{}, query.bbox{}, validate.cost{}, validate.mass{}, meta.ping{}.
19
+ Dimensions in mm. Pi 4B: 85×56×1.4, holes at (3.5,3.5),(61.5,3.5),(3.5,52.5),(61.5,52.5) Ø2.7. Arduino Uno: 68.6×53.4×1.6.
20
+ Design process for an enclosure: sketch rect → extrude → shell → mounting posts → port cutouts → vents → fillet → view.set isometric → view.fit.
21
+ Example for "box 100x50x20 with 3mm fillet":
22
+ [{"method":"sketch.start","params":{"plane":"XY"},"note":"Start sketch"},{"method":"sketch.rect","params":{"x":0,"y":0,"width":100,"height":50},"note":"Rectangle"},{"method":"sketch.end","params":{},"note":"Finish"},{"method":"ops.extrude","params":{"depth":20},"note":"Extrude"},{"method":"ops.fillet","params":{"radius":3},"note":"Fillet edges"},{"method":"view.set","params":{"view":"isometric"},"note":"Isometric"},{"method":"view.fit","params":{},"note":"Fit"}]
23
+ Return valid JSON array only.`;
24
+ function keys(){try{return JSON.parse(localStorage.getItem('cyclecad_api_keys')||'{}')}catch{return{}}}
25
+ function saveKey(f,v){const k=keys();k[f]=v;localStorage.setItem('cyclecad_api_keys',JSON.stringify(k))}
26
+ async function callClaude(m,sys,u,k){const r=await fetch('https://api.anthropic.com/v1/messages',{method:'POST',headers:{'x-api-key':k,'anthropic-version':'2023-06-01','content-type':'application/json','anthropic-dangerous-direct-browser-access':'true'},body:JSON.stringify({model:m,max_tokens:4096,system:sys,messages:[{role:'user',content:u}]})});if(!r.ok)throw new Error(`Claude ${r.status}: ${(await r.text()).slice(0,200)}`);return(await r.json()).content?.[0]?.text||''}
27
+ async function callGemini(m,sys,u,k){const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m}:generateContent?key=${k}`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({systemInstruction:{parts:[{text:sys}]},contents:[{role:'user',parts:[{text:u}]}],generationConfig:{temperature:0.2,maxOutputTokens:4096}})});if(!r.ok)throw new Error(`Gemini ${r.status}: ${(await r.text()).slice(0,200)}`);return(await r.json()).candidates?.[0]?.content?.parts?.[0]?.text||''}
28
+ 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,200)}`);return(await r.json()).choices?.[0]?.message?.content||''}
29
+ 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 🔑`);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)}
30
+ function parseJson(t){let s=t.trim();const f=s.match(/```(?:json)?\s*([\s\S]*?)\s*```/);if(f)s=f[1];const a=s.indexOf('['),b=s.lastIndexOf(']');if(a<0||b<a)throw new Error('No JSON array');return JSON.parse(s.slice(a,b+1))}
31
+ async function runStep(st){if(!window.cycleCAD?.execute)throw new Error('window.cycleCAD.execute unavailable');const r=window.cycleCAD.execute({method:st.method,params:st.params||{}});if(r?.ok===false)throw new Error(r.error||'step failed');return r}
32
+ function log(kind,txt){if(!S.els.log)return;const d=document.createElement('div');d.className=`aic-log-${kind}`;d.textContent=({info:'ℹ',ok:'✓',warn:'⚠',err:'✗',run:'⋯',fail:'✗'})[kind]+' '+txt;S.els.log.appendChild(d);S.els.log.scrollTop=S.els.log.scrollHeight}
33
+ function progress(c,t){const p=t?Math.round(c/t*100):0;S.els.prog.style.width=p+'%';S.els.stat.textContent=`${c}/${t}`}
34
+ async function run(){if(S.running)return;const p=S.els.prompt.value.trim();if(!p){log('warn','Enter a prompt first.');return}S.running=true;S.aborted=false;S.els.go.disabled=true;S.els.stop.disabled=false;S.els.log.innerHTML='';log('info',`Planning with ${MODELS[S.els.model.value].label}...`);try{const t=await callLLM(S.els.model.value,SYS,p);let plan;try{plan=parseJson(t)}catch(e){log('err',`Parse: ${e.message}`);log('warn',t.slice(0,300));return}if(!Array.isArray(plan)||!plan.length){log('warn','Empty plan');return}log('ok',`Got ${plan.length}-step plan. Executing...`);S.plan=plan;S.results=[];S.errors=[];let retries=0;for(let i=0;i<S.plan.length;i++){if(S.aborted){log('warn','Aborted.');return}const st=S.plan[i];log('run',`step ${i+1}: ${st.method} — ${st.note||''}`);progress(i,S.plan.length);await new Promise(r=>setTimeout(r,150));try{const r=await runStep(st);S.results.push(r);log('ok',`step ${i+1} done`)}catch(e){log('fail',`step ${i+1}: ${e.message}`);S.errors.push({step:st,error:e.message});if(retries>=2){log('warn','Max retries — stopping.');return}retries++;log('info',`Asking for recovery plan (${retries}/2)...`);const rem=S.plan.slice(i+1);const rp=`Previous step failed:\nFAILED_STEP: ${JSON.stringify(st)}\nERROR: ${e.message}\nREMAINING: ${JSON.stringify(rem)}\nGoal: ${p}\nReturn replacement JSON array of steps to recover and finish.`;try{const rt=await callLLM(S.els.model.value,SYS,rp);const newPlan=parseJson(rt);S.plan=S.plan.slice(0,i+1).concat(newPlan);log('info',`Inserted ${newPlan.length} recovery steps`)}catch(e2){log('warn',`Recovery failed: ${e2.message}`);return}}}progress(S.plan.length,S.plan.length);log('ok',`Done — ${S.results.length}/${S.plan.length} succeeded`)}catch(e){log('err',e.message)}finally{S.running=false;S.els.go.disabled=false;S.els.stop.disabled=true}}
35
+ function abort(){if(S.running){S.aborted=true;log('warn','Abort signaled')}}
36
+ function askKey(){const f=MODELS[S.els.model.value].keyField;const c=keys()[f]||'';const n=prompt(`Enter ${f} API key (localStorage):`,c);if(n!==null&&n!==c){saveKey(f,n.trim());log('ok',`${f} key saved`)}}
37
+ function buildUI(){const p=document.createElement('div');p.innerHTML=`<style>.aic{display:flex;flex-direction:column;gap:10px;padding:12px;color:#e2e8f0;background:#1a202c;font:13px -apple-system,sans-serif;min-width:320px;max-width:420px}.aic h3{margin:0;font-size:15px;color:#38bdf8}.aic-row{display:flex;gap:6px;align-items:center}.aic-row select{flex:1;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:4px;padding:4px 6px;font-size:12px}.aic-row button{background:#334155;color:#e2e8f0;border:0;border-radius:4px;padding:4px 8px;cursor:pointer}.aic textarea{width:100%;min-height:80px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:4px;padding:8px;font:13px inherit;resize:vertical;box-sizing:border-box}.aic-btns{display:flex;gap:8px}.aic-btns button{flex:1;padding:8px 12px;border:0;border-radius:4px;font-weight:600;cursor:pointer}.aic-go{background:#38bdf8;color:#0f172a}.aic-go:disabled{background:#475569;color:#94a3b8;cursor:not-allowed}.aic-stop{background:#ef4444;color:#fff}.aic-stop:disabled{background:#334155;color:#64748b;cursor:not-allowed}.aic-bar{height:4px;background:#334155;border-radius:2px;overflow:hidden}.aic-prog{height:100%;width:0;background:linear-gradient(90deg,#38bdf8,#8b5cf6);transition:width .2s}.aic-stat{font-size:11px;color:#94a3b8;text-align:right}.aic-log{background:#0f172a;border:1px solid #1e293b;border-radius:4px;padding:8px;min-height:160px;max-height:320px;overflow-y:auto;font:11px 'SF Mono',monospace;line-height:1.5}.aic-log>div{white-space:pre-wrap}.aic-log-info{color:#94a3b8}.aic-log-ok{color:#10b981}.aic-log-warn{color:#f59e0b}.aic-log-err,.aic-log-fail{color:#ef4444}.aic-log-run{color:#38bdf8}.aic-hint{font-size:11px;color:#64748b}</style><div class="aic"><h3>🤖 AI Copilot</h3><div class="aic-row"><select class="aic-model"></select><button class="aic-key" title="Set API key">🔑</button></div><textarea class="aic-prompt" placeholder="Describe what to build. Examples:
38
+ • create a Raspberry Pi 4B case with port cutouts
39
+ • design a 50mm mounting bracket with 4 holes on 40mm PCD
40
+ • make a hex nut M10, 8mm thick"></textarea><div class="aic-btns"><button class="aic-go">⚡ Generate</button><button class="aic-stop" disabled>⏹ Stop</button></div><div class="aic-bar"><div class="aic-prog"></div></div><div class="aic-stat">0/0</div><div class="aic-log"></div><div class="aic-hint">Multi-step plan. LLM retries up to 2× on step failure.</div></div>`;S.els.prompt=p.querySelector('.aic-prompt');S.els.go=p.querySelector('.aic-go');S.els.stop=p.querySelector('.aic-stop');S.els.log=p.querySelector('.aic-log');S.els.stat=p.querySelector('.aic-stat');S.els.prog=p.querySelector('.aic-prog');S.els.model=p.querySelector('.aic-model');for(const[k,c]of Object.entries(MODELS)){const o=document.createElement('option');o.value=k;o.textContent=c.label;S.els.model.appendChild(o)}S.els.model.value=keys().anthropic?'claude-sonnet':'gemini';S.els.go.onclick=run;S.els.stop.onclick=abort;p.querySelector('.aic-key').onclick=askKey;S.els.prompt.onkeydown=e=>{if(e.key==='Enter'&&(e.metaKey||e.ctrlKey)){e.preventDefault();run()}};return p}
41
+ window.CycleCAD=window.CycleCAD||{};window.CycleCAD.AICopilot={init:()=>true,getUI:buildUI,execute:(c,a)=>c==='generate'&&a?.prompt?(S.els.prompt&&(S.els.prompt.value=a.prompt),run()):c==='stop'?abort():null,go:run,abort,getState:()=>({running:S.running,stepIndex:S.plan.length-1,results:S.results.length,errors:S.errors.length})};console.log('AI Copilot module loaded');
42
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "3.9.20",
3
+ "version": "3.10.0",
4
4
  "description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
5
5
  "main": "index.html",
6
6
  "bin": {