cyclecad 3.10.3 → 3.11.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.
@@ -1,4 +1,4 @@
1
- /* AI Copilot v1.1 — multi-step CAD generation from natural language */
1
+ /* AI Copilot v1.2 — multi-step CAD generation from natural language. 3.10.5 adds sketch.polyline support + templates for ball bearings (ISO 15/DIN 625), bearing housings, T-slot aluminum extrusions (2020–8080), and U-brackets. */
2
2
  (function(){
3
3
  'use strict';
4
4
  window.CycleCAD = window.CycleCAD || {};
@@ -93,6 +93,7 @@
93
93
  '- sketch.start {plane:"XY"}',
94
94
  '- sketch.rect {width, height} — rectangle centered at current origin',
95
95
  '- sketch.circle {radius} — OR diameter',
96
+ '- sketch.polyline {points:[[x,z],[x,z],...]} — closed polygon in XZ plane (for custom shapes, gear teeth, T-slot profiles, cams)',
96
97
  '- sketch.end',
97
98
  '- ops.extrude {depth, position:[x,y,z], subtract:bool} — create solid. subtract:true carves from last body.',
98
99
  '- ops.hole {position:[x,y,z], depth, radius OR width+height} — carves cylinder OR rectangular hole through last body.',
@@ -255,6 +256,14 @@
255
256
  if (method === 'sketch.start') { miniState.currentSketch = { plane: params.plane||'XY', origin: getPos(params) }; return {ok:true}; }
256
257
  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
258
  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}; }
259
+ if (method === 'sketch.polyline' || method === 'sketch.polygon') {
260
+ // Points arrive as [[x,z],[x,z],...] in world XZ plane. Min 3 pts for a valid polygon.
261
+ const raw = Array.isArray(params.points) ? params.points : [];
262
+ const pts = raw.filter(p => Array.isArray(p) && p.length >= 2).map(p => [Number(p[0])||0, Number(p[1])||0]);
263
+ if (pts.length < 3) return {ok:false, note:'polyline needs at least 3 points'};
264
+ miniState.currentSketch = Object.assign(miniState.currentSketch||{}, { shape:'polyline', points: pts, origin: getPos(params) });
265
+ return {ok:true};
266
+ }
258
267
  if (method === 'sketch.line' || method === 'sketch.end') return {ok:true};
259
268
  if (method === 'ops.extrude') {
260
269
  const d = params.depth||params.height||params.distance||20;
@@ -264,6 +273,20 @@
264
273
  let g;
265
274
  if (sk.shape==='rect') g = new THREE.BoxGeometry(sk.width, d, sk.height);
266
275
  else if (sk.shape==='circle') g = new THREE.CylinderGeometry(sk.radius, sk.radius, d, 48);
276
+ else if (sk.shape==='polyline' && Array.isArray(sk.points) && sk.points.length >= 3) {
277
+ // Build Shape from [x,z] points. RotateX(-PI/2) maps:
278
+ // Shape-X -> world X, Shape-Y -> world -Z, extrude Z -> world +Y.
279
+ // To preserve user's intent that points[i][1] is world Z, negate Y into Shape.
280
+ const shape = new THREE.Shape();
281
+ const pts = sk.points;
282
+ shape.moveTo(pts[0][0], -pts[0][1]);
283
+ for (let i = 1; i < pts.length; i++) shape.lineTo(pts[i][0], -pts[i][1]);
284
+ shape.closePath();
285
+ g = new THREE.ExtrudeGeometry(shape, { depth: d, bevelEnabled: false, curveSegments: 24 });
286
+ g.translate(0, 0, -d/2); // center along extrude axis BEFORE rotating
287
+ g.rotateX(-Math.PI/2);
288
+ // After rotation: geometry spans world Y:[-d/2, d/2] centered at origin. Matches BoxGeometry/CylinderGeometry convention.
289
+ }
267
290
  else g = new THREE.BoxGeometry(50, d, 30);
268
291
  const isSubtract = params.subtract === true || params.operation === 'cut' || params.operation === 'subtract';
269
292
  const mat = new THREE.MeshStandardMaterial({color: isSubtract?0x1a1a1a:0x4a90e2, metalness:0.35, roughness:0.45});
@@ -511,6 +534,256 @@
511
534
  plan.push({method:'view.fit', params:{}});
512
535
  return plan;
513
536
  }
537
+ // Spur gear (simplified — cylinder disc at OD with center bore)
538
+ const gearM = p.match(/\bspur\s*gear\b|\bgear\b/);
539
+ if (gearM) {
540
+ const modM = p.match(/module\s*(\d+(?:\.\d+)?)|\bm\s*=?\s*(\d+(?:\.\d+)?)\b/);
541
+ const teethM = p.match(/(\d+)\s*(?:teeth|tooth)|z\s*=?\s*(\d+)/);
542
+ const mod = modM ? parseFloat(modM[1]||modM[2]) : 2;
543
+ const teeth = teethM ? parseInt(teethM[1]||teethM[2]) : 20;
544
+ const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width|face)/);
545
+ const width = widthM ? parseInt(widthM[1]) : 10;
546
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
547
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 8;
548
+ const pitchDia = mod * teeth;
549
+ const outsideDia = mod * (teeth + 2);
550
+ return [
551
+ {method:'sketch.start', params:{plane:'XY'}},
552
+ {method:'sketch.circle', params:{radius: outsideDia/2}},
553
+ {method:'ops.extrude', params:{depth: width, position:[0,0,0]}, note:'spur gear blank — m='+mod+', Z='+teeth+', pitch Ø'+pitchDia+', OD Ø'+outsideDia+' (add involute teeth via Sketch tab polyline)'},
554
+ {method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
555
+ {method:'view.set', params:{view:'iso'}},
556
+ {method:'view.fit', params:{}}
557
+ ];
558
+ }
559
+ // Pulley (V-belt, simplified — disc with center bore, groove noted)
560
+ const pulleyM = p.match(/\bpulley\b|\bv-?belt\s*pulley\b|\btiming\s*pulley\b/);
561
+ if (pulleyM) {
562
+ const odM = p.match(/(\d+)\s*mm/);
563
+ const od = odM ? parseInt(odM[1]) : 80;
564
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s*(\d+)/);
565
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]) : 12;
566
+ const widthM = p.match(/(\d+)\s*mm\s*(?:wide|width)/);
567
+ const width = widthM ? parseInt(widthM[1]) : 20;
568
+ return [
569
+ {method:'sketch.start', params:{plane:'XY'}},
570
+ {method:'sketch.circle', params:{radius: od/2}},
571
+ {method:'ops.extrude', params:{depth: width, position:[0,0,0]}, note:'pulley blank Ø'+od+'x'+width+' (add V-groove via revolve in Solid tab)'},
572
+ {method:'ops.hole', params:{position:[0, width/2, 0], radius: bore/2, depth: width+2}, note:'center bore Ø'+bore},
573
+ {method:'view.set', params:{view:'iso'}},
574
+ {method:'view.fit', params:{}}
575
+ ];
576
+ }
577
+ // Shaft (simple cylinder or stepped if "stepped" keyword)
578
+ const shaftM = p.match(/\bshaft\b|\baxle\b|\bspindle\b/);
579
+ if (shaftM) {
580
+ // Explicit diameter keywords first
581
+ const diaM = p.match(/(\d+)\s*mm\s*(?:dia|diameter)|Ø\s*(\d+)|ø\s*(\d+)/);
582
+ // Explicit length keywords
583
+ const lenM = p.match(/(\d+)\s*mm\s*(?:long|length|tall)/);
584
+ // Fallback: if both missing, heuristic from bare Nmm values (smaller=dia, larger=length)
585
+ const allMm = (p.match(/(\d+)\s*mm/g) || []).map(s => parseInt(s)).filter(n => n > 0);
586
+ let dia, len;
587
+ if (diaM) dia = parseInt(diaM[1]||diaM[2]||diaM[3]);
588
+ if (lenM) len = parseInt(lenM[1]);
589
+ if (!dia && !len && allMm.length === 2) { dia = Math.min(allMm[0], allMm[1]); len = Math.max(allMm[0], allMm[1]); }
590
+ else if (!dia && allMm.length >= 1) dia = allMm[0];
591
+ else if (!len && allMm.length >= 2) len = allMm[1];
592
+ dia = dia || 20; len = len || 100;
593
+ const stepped = /\bstepped\b|\bstep\b|\b2[- ]?step\b/.test(p);
594
+ if (stepped) {
595
+ const dia2 = Math.max(6, dia - 4);
596
+ const len1 = Math.round(len * 0.6);
597
+ const len2 = len - len1;
598
+ return [
599
+ {method:'sketch.start', params:{plane:'XY'}},
600
+ {method:'sketch.circle', params:{radius: dia/2}},
601
+ {method:'ops.extrude', params:{depth: len1, position:[0,0,0]}, note:'stepped shaft main Ø'+dia+' x '+len1+'mm'},
602
+ {method:'sketch.start', params:{plane:'XY'}},
603
+ {method:'sketch.circle', params:{radius: dia2/2}},
604
+ {method:'ops.extrude', params:{depth: len2, position:[0,len1,0]}, note:'stepped shaft reduced Ø'+dia2+' x '+len2+'mm'},
605
+ {method:'view.set', params:{view:'iso'}},
606
+ {method:'view.fit', params:{}}
607
+ ];
608
+ }
609
+ return [
610
+ {method:'sketch.start', params:{plane:'XY'}},
611
+ {method:'sketch.circle', params:{radius: dia/2}},
612
+ {method:'ops.extrude', params:{depth: len, position:[0,0,0]}, note:'shaft Ø'+dia+' x '+len+'mm'},
613
+ {method:'view.set', params:{view:'iso'}},
614
+ {method:'view.fit', params:{}}
615
+ ];
616
+ }
617
+ // Ball bearing (ISO 15 / DIN 625 deep-groove) — simplified solid ring.
618
+ // Designations: 60x (light), 62x (medium), 63x (heavy), 6x (small single-digit), 625, 608 etc.
619
+ const bearingDesignations = {
620
+ '608': {d:8, D:22, B:7},
621
+ '625': {d:5, D:16, B:5},
622
+ '6000': {d:10, D:26, B:8},
623
+ '6001': {d:12, D:28, B:8},
624
+ '6002': {d:15, D:32, B:9},
625
+ '6003': {d:17, D:35, B:10},
626
+ '6004': {d:20, D:42, B:12},
627
+ '6005': {d:25, D:47, B:12},
628
+ '6006': {d:30, D:55, B:13},
629
+ '6200': {d:10, D:30, B:9},
630
+ '6201': {d:12, D:32, B:10},
631
+ '6202': {d:15, D:35, B:11},
632
+ '6203': {d:17, D:40, B:12},
633
+ '6204': {d:20, D:47, B:14},
634
+ '6205': {d:25, D:52, B:15},
635
+ '6206': {d:30, D:62, B:16},
636
+ '6300': {d:10, D:35, B:11},
637
+ '6301': {d:12, D:37, B:12},
638
+ '6302': {d:15, D:42, B:13},
639
+ '6303': {d:17, D:47, B:14},
640
+ '6304': {d:20, D:52, B:15}
641
+ };
642
+ const bearingCodeM = p.match(/\b(6[023]0[0-6]|608|625)\b/);
643
+ // Housing/pocket/seat keywords take priority — a "bearing pocket for 6204" is a housing, not a bearing body.
644
+ const isHousingIntent = /bearing\s*(?:housing|pocket|seat|recess|mount)/.test(p);
645
+ if (bearingCodeM && /bearing/.test(p) && !isHousingIntent) {
646
+ const spec = bearingDesignations[bearingCodeM[1]];
647
+ if (spec) {
648
+ const code = bearingCodeM[1];
649
+ return [
650
+ {method:'sketch.start', params:{plane:'XY'}},
651
+ {method:'sketch.circle', params:{radius: spec.D/2}},
652
+ {method:'ops.extrude', params:{depth: spec.B, position:[0,0,0]}, note:'bearing '+code+' OD Ø'+spec.D+' x width '+spec.B},
653
+ {method:'ops.hole', params:{position:[0,0,0], radius: spec.d/2, depth: spec.B + 2}, note:code+' bore Ø'+spec.d+' (ISO 15 — simplified ring, real bearing has balls + races)'},
654
+ {method:'view.set', params:{view:'iso'}},
655
+ {method:'view.fit', params:{}}
656
+ ];
657
+ }
658
+ }
659
+ // Generic ball bearing with explicit bore ("ball bearing 10mm bore")
660
+ const genBearingM = p.match(/(?:ball|deep[- ]?groove|roller)\s*bearing/);
661
+ if (genBearingM) {
662
+ const boreM = p.match(/(\d+)\s*mm\s*bore|bore\s+(\d+)|Ø\s*(\d+)/);
663
+ const bore = boreM ? parseInt(boreM[1]||boreM[2]||boreM[3]) : 10;
664
+ // Rough approximation matching 62xx series: OD ≈ 2.5*bore + 10, width ≈ 0.4*bore + 4
665
+ const OD = Math.round(bore * 2.5 + 10);
666
+ const B = Math.max(5, Math.round(bore * 0.4 + 4));
667
+ return [
668
+ {method:'sketch.start', params:{plane:'XY'}},
669
+ {method:'sketch.circle', params:{radius: OD/2}},
670
+ {method:'ops.extrude', params:{depth: B, position:[0,0,0]}, note:'ball bearing Ø'+OD+' x '+B+'mm (approx 62xx series)'},
671
+ {method:'ops.hole', params:{position:[0,0,0], radius: bore/2, depth: B+2}, note:'bore Ø'+bore+' (look up ISO 15 for exact OD)'},
672
+ {method:'view.set', params:{view:'iso'}},
673
+ {method:'view.fit', params:{}}
674
+ ];
675
+ }
676
+ // Bearing housing / pocket — block with recess and clearance through-hole
677
+ if (/bearing\s*(?:housing|pocket|seat|recess|mount)/.test(p)) {
678
+ const odM = p.match(/(\d+)\s*mm\s*(?:od|outer|outside)|for\s+(?:a\s+)?(\d+)\s*mm|bearing\s+(\d+)\s*mm/);
679
+ // If the user named a bearing designation, look up its OD
680
+ const housingBearingCode = p.match(/\b(6[023]0[0-6]|608|625)\b/);
681
+ const codeSpec = housingBearingCode ? bearingDesignations[housingBearingCode[1]] : null;
682
+ const OD = odM ? parseInt(odM[1]||odM[2]||odM[3]) : (codeSpec ? codeSpec.D : 32);
683
+ const pocketDepth = Math.max(6, Math.round(OD * 0.3));
684
+ const wall = 8;
685
+ const blockW = OD + 2*wall;
686
+ const blockH = OD + 2*wall;
687
+ const blockD = pocketDepth + 5;
688
+ const throughR = Math.max(3, OD/2 - 4);
689
+ return [
690
+ {method:'sketch.start', params:{plane:'XY'}},
691
+ {method:'sketch.rect', params:{width: blockW, height: blockH}},
692
+ {method:'ops.extrude', params:{depth: blockD, position:[0,0,0]}, note:'housing block '+blockW+'×'+blockH+'×'+blockD+' for Ø'+OD+' bearing'},
693
+ {method:'ops.hole', params:{position:[0, blockD - pocketDepth, 0], radius: OD/2, depth: pocketDepth + 0.5}, note:'bearing pocket Ø'+OD+' × '+pocketDepth+'mm deep'},
694
+ {method:'ops.hole', params:{position:[0,0,0], radius: throughR, depth: blockD + 2}, note:'shaft clearance bore Ø'+(throughR*2).toFixed(0)},
695
+ // 4 mounting holes at corners
696
+ {method:'ops.hole', params:{position:[-blockW/2 + 6, 0, -blockH/2 + 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 1/4'},
697
+ {method:'ops.hole', params:{position:[ blockW/2 - 6, 0, -blockH/2 + 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 2/4'},
698
+ {method:'ops.hole', params:{position:[-blockW/2 + 6, 0, blockH/2 - 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 3/4'},
699
+ {method:'ops.hole', params:{position:[ blockW/2 - 6, 0, blockH/2 - 6], radius: 2.5, depth: blockD + 2}, note:'mounting hole 4/4'},
700
+ {method:'view.set', params:{view:'iso'}},
701
+ {method:'view.fit', params:{}}
702
+ ];
703
+ }
704
+ // T-slot aluminum extrusion (2020/3030/4040/4080 profiles — Bosch/Item/80/20 style)
705
+ const tSlotKw = /\bt-?slot|aluminum\s*(?:extrusion|profile)|\b80[- ]?20\b|\bitem\s*profile|\bmisumi\s*extrusion|\bbosch\s*extrusion|structural\s*extrusion/.test(p);
706
+ const codeM = p.match(/\b(20|25|30|40|45|50|60|80|100)\s*(?:x|×)\s*(20|25|30|40|45|50|60|80|100)\b/);
707
+ const fourDigitM = p.match(/\b(2020|2040|2080|3030|3060|4040|4080|4545|6060|8080)\b/);
708
+ if (tSlotKw || codeM || (fourDigitM && /extrusion|profile|t-?slot/.test(p))) {
709
+ let W = 40, H = 40;
710
+ if (codeM) { W = parseInt(codeM[1]); H = parseInt(codeM[2]); }
711
+ else if (fourDigitM) {
712
+ const c = fourDigitM[1];
713
+ W = parseInt(c.slice(0, c.length/2));
714
+ H = parseInt(c.slice(c.length/2));
715
+ }
716
+ const lenM = p.match(/(\d{2,5})\s*mm\s*(?:long|length)|length\s+(\d{2,5})|\b(\d{3,4})\s*mm\s+(?:rail|bar|stick|piece)/);
717
+ let length = lenM ? parseInt(lenM[1]||lenM[2]||lenM[3]) : 0;
718
+ if (!length) {
719
+ // Fallback: take the largest \d+mm value that isn't the profile code itself (e.g. "4040 extrusion 1000mm")
720
+ const allMm = [...p.matchAll(/\b(\d{2,5})\s*mm\b/g)].map(m => parseInt(m[1]))
721
+ .filter(n => n !== W && n !== H && n !== W*100 + H && n !== W*10 + H);
722
+ length = allMm.length ? Math.max(...allMm) : 500;
723
+ }
724
+ // Slot geometry scales with profile size — approximation of real T-slot
725
+ const slotW = W <= 25 ? 6 : W <= 40 ? 8 : 10; // slot opening (across inward axis)
726
+ const slotD = Math.max(6, W/3); // slot depth
727
+ const yMargin = 2; // covers Y range [0, length]
728
+ const plan = [
729
+ {method:'sketch.start', params:{plane:'XY'}},
730
+ {method:'sketch.rect', params:{width: W, height: H}},
731
+ {method:'ops.extrude', params:{depth: length, position:[0,0,0]}, note:W+'×'+H+' T-slot extrusion, '+length+'mm long (simplified — real profile has T-grooves + hollow center)'},
732
+ // +X face slot
733
+ {method:'ops.hole', params:{position:[W/2 - slotD/2, 0, 0], width: slotD, height: slotW, depth: length + yMargin}, note:'+X T-slot'},
734
+ // -X face slot
735
+ {method:'ops.hole', params:{position:[-W/2 + slotD/2, 0, 0], width: slotD, height: slotW, depth: length + yMargin}, note:'-X T-slot'},
736
+ // +Z face slot
737
+ {method:'ops.hole', params:{position:[0, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot'},
738
+ // -Z face slot
739
+ {method:'ops.hole', params:{position:[0, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot'},
740
+ {method:'view.set', params:{view:'iso'}},
741
+ {method:'view.fit', params:{}}
742
+ ];
743
+ // For double-wide profiles (e.g. 4080), the longer face gets two T-slots per side
744
+ if (Math.max(W, H) >= 2 * Math.min(W, H)) {
745
+ const longSide = W > H ? 'x' : 'z';
746
+ const longDim = Math.max(W, H);
747
+ const offset = longDim / 4;
748
+ if (longSide === 'x') {
749
+ // Add extra slots on top/bottom (Z faces) since X-axis is longer
750
+ plan.splice(-2, 0,
751
+ {method:'ops.hole', params:{position:[ offset, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot (extra)'},
752
+ {method:'ops.hole', params:{position:[-offset, 0, H/2 - slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'+Z T-slot (extra)'},
753
+ {method:'ops.hole', params:{position:[ offset, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot (extra)'},
754
+ {method:'ops.hole', params:{position:[-offset, 0, -H/2 + slotD/2], width: slotW, height: slotD, depth: length + yMargin}, note:'-Z T-slot (extra)'}
755
+ );
756
+ }
757
+ }
758
+ return plan;
759
+ }
760
+ // U-bracket / channel bracket
761
+ if (/u-?bracket|u-?channel|channel\s*bracket|mounting\s*channel/.test(p)) {
762
+ const wM = p.match(/(\d+)\s*mm\s*(?:wide|width)|width\s+(\d+)/);
763
+ const width = wM ? parseInt(wM[1]||wM[2]) : 60;
764
+ const hM = p.match(/(\d+)\s*mm\s*(?:high|height|tall)|height\s+(\d+)/);
765
+ const height = hM ? parseInt(hM[1]||hM[2]) : 40;
766
+ const lM = p.match(/(\d+)\s*mm\s*(?:long|length)|length\s+(\d+)/);
767
+ const length = lM ? parseInt(lM[1]||lM[2]) : 100;
768
+ const wall = Math.max(3, Math.round(Math.min(width, height) / 12));
769
+ const holesM = p.match(/(\d+)\s*holes?/);
770
+ const nHoles = holesM ? parseInt(holesM[1]) : 2;
771
+ const plan = [
772
+ {method:'sketch.start', params:{plane:'XY'}},
773
+ {method:'sketch.rect', params:{width: width, height: length}},
774
+ {method:'ops.extrude', params:{depth: height, position:[0,0,0]}, note:'U-bracket blank '+width+'×'+height+'×'+length+'mm'},
775
+ // Pocket cut from top down to wall thickness above base. Final Y span: [wall-0.5, height+1.5], intersects body at [wall, height].
776
+ {method:'ops.hole', params:{position:[0, wall, 0], width: width - 2*wall, height: length + 2, depth: height - wall + 2}, note:'U-bracket pocket ('+(width - 2*wall)+'×'+(height - wall)+' leaves base + 2 side walls)'}
777
+ ];
778
+ // Mounting holes through the base plate
779
+ for (let i = 0; i < nHoles; i++) {
780
+ const y = (nHoles === 1) ? 0 : -length/2 + 12 + i * ((length - 24) / Math.max(1, nHoles - 1));
781
+ plan.push({method:'ops.hole', params:{position:[0, 0, y], radius: 3, depth: wall + 2}, note:'base mounting hole '+(i+1)+'/'+nHoles});
782
+ }
783
+ plan.push({method:'view.set', params:{view:'iso'}});
784
+ plan.push({method:'view.fit', params:{}});
785
+ return plan;
786
+ }
514
787
  return null;
515
788
  }
516
789
  async function run(){
@@ -643,5 +916,5 @@
643
916
  abort: () => abort(),
644
917
  getState: () => ({ running:S.running, stepIndex:S.stepIndex, results:S.results.length, errors:S.errors.length, model:S.els.model?.value })
645
918
  };
646
- console.log('AI Copilot v1.1 module loaded');
919
+ console.log('AI Copilot v1.2 module loaded');
647
920
  })();