CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
f873b45
·
1 Parent(s): 737682f

feat: add Chat/Guided tab switcher, wizard, plan card, 3MF downloads, i18n

Browse files
Files changed (1) hide show
  1. web/index.html +506 -0
web/index.html CHANGED
@@ -1171,6 +1171,151 @@
1171
  animation: fade-in-up 0.3s ease-out both;
1172
  }
1173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1174
  /* ---- RESPONSIVE ---- */
1175
 
1176
  @media (max-width: 768px) {
@@ -1230,6 +1375,7 @@
1230
  <div id="download-btns">
1231
  <a class="dl-btn" id="dl-step" download>STEP</a>
1232
  <a class="dl-btn" id="dl-stl" download>STL</a>
 
1233
  <a id="dl-gcode" class="dl-btn" download style="display:none"><span class="dl-icon">&#8615;</span> G-CODE</a>
1234
  <a class="dl-btn" id="dl-report" download>REPORT</a>
1235
  </div>
@@ -1257,6 +1403,11 @@
1257
  <div id="chat-panel">
1258
  <button id="chat-toggle" onclick="toggleChat()" title="Toggle chat panel">&#9664;</button>
1259
 
 
 
 
 
 
1260
  <div class="chat-header">
1261
  <div class="chat-header-left">
1262
  <span class="chat-header-title" data-i18n="designChat">Design Chat</span>
@@ -1282,6 +1433,84 @@
1282
  </div>
1283
  </div>
1284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
  <div class="chat-input-area" style="position: relative;">
1286
  <div id="mention-dropdown">
1287
  <div class="mention-option" data-agent="design" onclick="insertMention('design')">
@@ -1372,6 +1601,255 @@ let mentionActive = false;
1372
  let mentionIndex = 0;
1373
  let currentLang = localStorage.getItem('neuralcad_lang') || 'en';
1374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1375
  // ── i18n ─────────────────────────────────────────────
1376
 
1377
  const I18N = {
@@ -1406,6 +1884,11 @@ const I18N = {
1406
  cadFailed: 'CAD execution failed: ',
1407
  errorPrefix: 'Error: ',
1408
  sendPreviewMsg: '@cad Generate a 3D preview based on our discussion',
 
 
 
 
 
1409
  },
1410
  'zh-TW': {
1411
  subtitle: '\u591a\u4ee3\u7406\u8a2d\u8a08',
@@ -1438,6 +1921,11 @@ const I18N = {
1438
  cadFailed: 'CAD \u57f7\u884c\u5931\u6557\uff1a',
1439
  errorPrefix: '\u932f\u8aa4\uff1a',
1440
  sendPreviewMsg: '@cad \u6839\u64da\u6211\u5011\u7684\u8a0e\u8ad6\u751f\u6210 3D \u9810\u89bd',
 
 
 
 
 
1441
  },
1442
  vi: {
1443
  subtitle: 'Thi\u1ebft K\u1ebf \u0110a T\u00e1c T\u1eed',
@@ -1470,6 +1958,11 @@ const I18N = {
1470
  cadFailed: 'Th\u1ef1c thi CAD th\u1ea5t b\u1ea1i: ',
1471
  errorPrefix: 'L\u1ed7i: ',
1472
  sendPreviewMsg: '@cad T\u1ea1o b\u1ea3n xem tr\u01b0\u1edbc 3D d\u1ef1a tr\u00ean cu\u1ed9c th\u1ea3o lu\u1eadn c\u1ee7a ch\u00fang ta',
 
 
 
 
 
1473
  },
1474
  };
1475
 
@@ -1959,6 +2452,17 @@ async function sendMessage(text) {
1959
  }
1960
  saveState();
1961
 
 
 
 
 
 
 
 
 
 
 
 
1962
  // If preview available, load 3D model
1963
  if (data.preview && data.preview.success) {
1964
  setViewerLoading(true, t('loadingModel'));
@@ -2261,6 +2765,7 @@ function updateDownloads(partName) {
2261
 
2262
  document.getElementById('dl-step').href = '/api/models/' + partName + '.step';
2263
  document.getElementById('dl-stl').href = '/api/models/' + partName + '.stl';
 
2264
  document.getElementById('dl-report').href = '/api/models/' + partName + '_report.json';
2265
 
2266
  const dlGcode = document.getElementById('dl-gcode');
@@ -2352,6 +2857,7 @@ function renderGallery() {
2352
  html += '<div class="gallery-card-downloads">';
2353
  html += '<a class="gallery-dl" href="/api/models/' + name + '.step" download>STEP</a>';
2354
  html += '<a class="gallery-dl" href="/api/models/' + name + '.stl" download>STL</a>';
 
2355
  html += '<a class="gallery-dl" href="/api/models/' + name + '.gcode" download>GCODE</a>';
2356
  html += '</div>';
2357
  html += '</div>';
 
1171
  animation: fade-in-up 0.3s ease-out both;
1172
  }
1173
 
1174
+ /* ---- TAB SWITCHER ---- */
1175
+ .chat-tabs {
1176
+ display: flex;
1177
+ border-bottom: 1px solid var(--border);
1178
+ background: var(--bg-panel);
1179
+ flex-shrink: 0;
1180
+ }
1181
+ .chat-tab {
1182
+ flex: 1;
1183
+ padding: 8px 0;
1184
+ text-align: center;
1185
+ font-family: var(--font-mono);
1186
+ font-size: 11px;
1187
+ font-weight: 500;
1188
+ color: var(--text-secondary);
1189
+ background: none;
1190
+ border: none;
1191
+ cursor: pointer;
1192
+ border-bottom: 2px solid transparent;
1193
+ transition: color 0.2s, border-color 0.2s;
1194
+ }
1195
+ .chat-tab:hover { color: var(--text-primary); }
1196
+ .chat-tab.active {
1197
+ color: var(--accent);
1198
+ border-bottom-color: var(--accent);
1199
+ }
1200
+ #guided-panel { display: none; overflow-y: auto; flex: 1; padding: 12px; }
1201
+ #guided-panel.active { display: flex; flex-direction: column; gap: 12px; }
1202
+ #chat-messages.hidden { display: none; }
1203
+
1204
+ /* ---- WIZARD STEPS ---- */
1205
+ .wizard-step {
1206
+ background: var(--bg-surface);
1207
+ border: 1px solid var(--border);
1208
+ border-radius: 8px;
1209
+ padding: 12px;
1210
+ }
1211
+ .wizard-step.completed { border-color: var(--success); }
1212
+ .wizard-step-header {
1213
+ display: flex; justify-content: space-between; align-items: center;
1214
+ margin-bottom: 8px;
1215
+ }
1216
+ .wizard-step-title {
1217
+ font-family: var(--font-mono); font-size: 11px;
1218
+ color: var(--text-secondary); font-weight: 600;
1219
+ }
1220
+ .wizard-step-check { color: var(--success); font-size: 14px; }
1221
+ .wizard-chips {
1222
+ display: flex; flex-wrap: wrap; gap: 6px;
1223
+ }
1224
+ .wizard-chip {
1225
+ padding: 5px 12px; border-radius: 14px;
1226
+ font-family: var(--font-mono); font-size: 11px;
1227
+ background: var(--bg-input); border: 1px solid var(--border);
1228
+ color: var(--text-primary); cursor: pointer;
1229
+ transition: border-color 0.15s, background 0.15s;
1230
+ }
1231
+ .wizard-chip:hover { border-color: var(--accent); }
1232
+ .wizard-chip.selected {
1233
+ border-color: var(--accent); background: var(--accent-glow);
1234
+ color: var(--accent);
1235
+ }
1236
+ .wizard-input {
1237
+ width: 100%; padding: 6px 10px; margin-top: 6px;
1238
+ background: var(--bg-input); border: 1px solid var(--border);
1239
+ border-radius: 6px; color: var(--text-primary);
1240
+ font-family: var(--font-mono); font-size: 12px;
1241
+ }
1242
+ .wizard-input:focus { outline: none; border-color: var(--accent); }
1243
+ .wizard-dim-row {
1244
+ display: flex; gap: 8px; align-items: center; margin-top: 6px;
1245
+ }
1246
+ .wizard-dim-label {
1247
+ font-family: var(--font-mono); font-size: 11px;
1248
+ color: var(--text-secondary); min-width: 50px;
1249
+ }
1250
+ .wizard-dim-input {
1251
+ width: 80px; padding: 4px 8px;
1252
+ background: var(--bg-input); border: 1px solid var(--border);
1253
+ border-radius: 4px; color: var(--text-primary);
1254
+ font-family: var(--font-mono); font-size: 12px;
1255
+ }
1256
+ .wizard-dim-unit {
1257
+ font-family: var(--font-mono); font-size: 10px;
1258
+ color: var(--text-muted);
1259
+ }
1260
+ .wizard-review-field {
1261
+ display: flex; justify-content: space-between; align-items: center;
1262
+ padding: 4px 0; border-bottom: 1px solid var(--border);
1263
+ }
1264
+ .wizard-review-label {
1265
+ font-family: var(--font-mono); font-size: 10px;
1266
+ color: var(--text-secondary);
1267
+ }
1268
+ .wizard-review-value {
1269
+ font-family: var(--font-mono); font-size: 11px;
1270
+ color: var(--text-primary);
1271
+ }
1272
+ .wizard-btn-row { display: flex; gap: 8px; margin-top: 10px; }
1273
+ .wizard-btn {
1274
+ flex: 1; padding: 8px; border-radius: 6px;
1275
+ font-family: var(--font-mono); font-size: 11px;
1276
+ font-weight: 600; cursor: pointer; border: 1px solid var(--border);
1277
+ transition: background 0.15s;
1278
+ }
1279
+ .wizard-btn-primary {
1280
+ background: var(--accent); color: var(--bg-void); border-color: var(--accent);
1281
+ }
1282
+ .wizard-btn-secondary {
1283
+ background: var(--bg-surface); color: var(--text-secondary);
1284
+ }
1285
+
1286
+ /* ---- PLAN CARD ---- */
1287
+ .plan-card {
1288
+ background: var(--bg-surface);
1289
+ border: 1px solid var(--accent);
1290
+ border-radius: 8px;
1291
+ padding: 14px;
1292
+ margin: 8px 0;
1293
+ }
1294
+ .plan-card-title {
1295
+ font-family: var(--font-mono);
1296
+ font-size: 11px;
1297
+ font-weight: 700;
1298
+ color: var(--accent);
1299
+ margin-bottom: 10px;
1300
+ }
1301
+ .plan-card .wizard-review-field { padding: 3px 0; }
1302
+ .plan-card-score {
1303
+ font-family: var(--font-mono); font-size: 11px;
1304
+ color: var(--text-secondary); margin-top: 8px;
1305
+ }
1306
+ .plan-card-actions { display: flex; gap: 8px; margin-top: 10px; }
1307
+ .plan-card-btn {
1308
+ flex: 1; padding: 7px; border-radius: 5px;
1309
+ font-family: var(--font-mono); font-size: 11px;
1310
+ font-weight: 600; cursor: pointer; border: 1px solid var(--border);
1311
+ }
1312
+ .plan-card-approve {
1313
+ background: var(--success); color: var(--bg-void); border-color: var(--success);
1314
+ }
1315
+ .plan-card-reject {
1316
+ background: var(--bg-surface); color: var(--text-secondary);
1317
+ }
1318
+
1319
  /* ---- RESPONSIVE ---- */
1320
 
1321
  @media (max-width: 768px) {
 
1375
  <div id="download-btns">
1376
  <a class="dl-btn" id="dl-step" download>STEP</a>
1377
  <a class="dl-btn" id="dl-stl" download>STL</a>
1378
+ <a class="dl-btn" id="dl-3mf" download>3MF</a>
1379
  <a id="dl-gcode" class="dl-btn" download style="display:none"><span class="dl-icon">&#8615;</span> G-CODE</a>
1380
  <a class="dl-btn" id="dl-report" download>REPORT</a>
1381
  </div>
 
1403
  <div id="chat-panel">
1404
  <button id="chat-toggle" onclick="toggleChat()" title="Toggle chat panel">&#9664;</button>
1405
 
1406
+ <div class="chat-tabs">
1407
+ <button class="chat-tab active" id="tab-chat" onclick="switchTab('chat')">Chat</button>
1408
+ <button class="chat-tab" id="tab-guided" onclick="switchTab('guided')">Guided</button>
1409
+ </div>
1410
+
1411
  <div class="chat-header">
1412
  <div class="chat-header-left">
1413
  <span class="chat-header-title" data-i18n="designChat">Design Chat</span>
 
1433
  </div>
1434
  </div>
1435
 
1436
+ <div id="guided-panel">
1437
+ <div class="wizard-step" id="wiz-step-1">
1438
+ <div class="wizard-step-header"><span class="wizard-step-title">1. PART TYPE</span><span class="wizard-step-check" id="wiz-check-1"></span></div>
1439
+ <div class="wizard-chips">
1440
+ <button class="wizard-chip" onclick="wizSetPart('bracket','Mounting bracket')">Bracket</button>
1441
+ <button class="wizard-chip" onclick="wizSetPart('enclosure','Enclosure housing')">Enclosure</button>
1442
+ <button class="wizard-chip" onclick="wizSetPart('plate','Flat plate')">Plate</button>
1443
+ <button class="wizard-chip" onclick="wizSetPart('shaft','Cylindrical shaft')">Shaft</button>
1444
+ <button class="wizard-chip" onclick="wizSetPart('gear','Spur gear')">Gear</button>
1445
+ <button class="wizard-chip" onclick="wizSetPart('flange','Pipe flange')">Flange</button>
1446
+ </div>
1447
+ <input class="wizard-input" placeholder="Or type custom part name..." onchange="wizSetPart(this.value, this.value)">
1448
+ </div>
1449
+ <div class="wizard-step" id="wiz-step-2">
1450
+ <div class="wizard-step-header"><span class="wizard-step-title">2. MATERIAL</span><span class="wizard-step-check" id="wiz-check-2"></span></div>
1451
+ <div class="wizard-chips">
1452
+ <button class="wizard-chip" onclick="wizSetMaterial('aluminum 6061')">Aluminum 6061</button>
1453
+ <button class="wizard-chip" onclick="wizSetMaterial('aluminum 7075')">Aluminum 7075</button>
1454
+ <button class="wizard-chip" onclick="wizSetMaterial('stainless steel 304')">Steel 304</button>
1455
+ <button class="wizard-chip" onclick="wizSetMaterial('stainless steel 316')">Steel 316</button>
1456
+ <button class="wizard-chip" onclick="wizSetMaterial('brass')">Brass</button>
1457
+ <button class="wizard-chip" onclick="wizSetMaterial('titanium')">Titanium</button>
1458
+ <button class="wizard-chip" onclick="wizSetMaterial('nylon')">Nylon</button>
1459
+ <button class="wizard-chip" onclick="wizSetMaterial('delrin')">Delrin</button>
1460
+ </div>
1461
+ <input class="wizard-input" placeholder="Or type custom material..." onchange="wizSetMaterial(this.value)">
1462
+ </div>
1463
+ <div class="wizard-step" id="wiz-step-3">
1464
+ <div class="wizard-step-header"><span class="wizard-step-title">3. DIMENSIONS (mm)</span><span class="wizard-step-check" id="wiz-check-3"></span></div>
1465
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Width</span><input class="wizard-dim-input" id="wiz-dim-width" type="number" onchange="wizSetDim('width', this.value)"><span class="wizard-dim-unit">mm</span></div>
1466
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Height</span><input class="wizard-dim-input" id="wiz-dim-height" type="number" onchange="wizSetDim('height', this.value)"><span class="wizard-dim-unit">mm</span></div>
1467
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Depth</span><input class="wizard-dim-input" id="wiz-dim-depth" type="number" onchange="wizSetDim('depth', this.value)"><span class="wizard-dim-unit">mm</span></div>
1468
+ </div>
1469
+ <div class="wizard-step" id="wiz-step-4">
1470
+ <div class="wizard-step-header"><span class="wizard-step-title">4. FEATURES</span><span class="wizard-step-check" id="wiz-check-4"></span></div>
1471
+ <div class="wizard-chips">
1472
+ <button class="wizard-chip" id="wiz-feat-holes" onclick="wizToggleFeature(this, 'holes')">Mounting Holes</button>
1473
+ <button class="wizard-chip" id="wiz-feat-fillets" onclick="wizToggleFeature(this, 'fillets')">Fillets</button>
1474
+ <button class="wizard-chip" id="wiz-feat-chamfers" onclick="wizToggleFeature(this, 'chamfers')">Chamfers</button>
1475
+ <button class="wizard-chip" id="wiz-feat-pockets" onclick="wizToggleFeature(this, 'pockets')">Pockets</button>
1476
+ <button class="wizard-chip" id="wiz-feat-slots" onclick="wizToggleFeature(this, 'slots')">Slots</button>
1477
+ </div>
1478
+ <div id="wiz-holes-config" style="display:none;margin-top:8px;">
1479
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Count</span><input class="wizard-dim-input" id="wiz-hole-count" type="number" value="4" min="1" onchange="wizUpdateHoles()"></div>
1480
+ <div class="wizard-chips" style="margin-top:6px;">
1481
+ <button class="wizard-chip" id="wiz-hole-m3" onclick="wizSetHoleSize('M3')">M3</button>
1482
+ <button class="wizard-chip" id="wiz-hole-m4" onclick="wizSetHoleSize('M4')">M4</button>
1483
+ <button class="wizard-chip selected" id="wiz-hole-m6" onclick="wizSetHoleSize('M6')">M6</button>
1484
+ <button class="wizard-chip" id="wiz-hole-m8" onclick="wizSetHoleSize('M8')">M8</button>
1485
+ </div>
1486
+ </div>
1487
+ <input class="wizard-input" placeholder="Or type custom feature..." onkeydown="if(event.key==='Enter'){wizAddCustomFeature(this.value);this.value='';}">
1488
+ </div>
1489
+ <div class="wizard-step" id="wiz-step-5">
1490
+ <div class="wizard-step-header"><span class="wizard-step-title">5. CONSTRAINTS</span><span class="wizard-step-check" id="wiz-check-5"></span></div>
1491
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Min wall</span><input class="wizard-dim-input" id="wiz-min-wall" type="number" value="3" step="0.5" onchange="wizUpdateConstraints()"><span class="wizard-dim-unit">mm</span></div>
1492
+ <div class="wizard-dim-row"><span class="wizard-dim-label">Max size</span><input class="wizard-dim-input" id="wiz-max-size" type="number" value="500" onchange="wizUpdateConstraints()"><span class="wizard-dim-unit">mm</span></div>
1493
+ </div>
1494
+ <div class="wizard-step" id="wiz-step-6">
1495
+ <div class="wizard-step-header"><span class="wizard-step-title">6. MACHINING</span><span class="wizard-step-check" id="wiz-check-6"></span></div>
1496
+ <div class="wizard-chips">
1497
+ <button class="wizard-chip" onclick="wizSetAxis('3-axis', this)">3-axis</button>
1498
+ <button class="wizard-chip" onclick="wizSetAxis('3+2-axis', this)">3+2-axis</button>
1499
+ <button class="wizard-chip" onclick="wizSetAxis('5-axis', this)">5-axis</button>
1500
+ <button class="wizard-chip" onclick="wizSetAxis('', this)">Auto</button>
1501
+ </div>
1502
+ </div>
1503
+ <div class="wizard-step" id="wiz-step-7">
1504
+ <div class="wizard-step-header"><span class="wizard-step-title">7. REVIEW</span><span class="wizard-step-check" id="wiz-check-7"></span></div>
1505
+ <div id="wiz-review-content"></div>
1506
+ <div id="wiz-score" style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);margin-top:8px;"></div>
1507
+ <div class="wizard-btn-row">
1508
+ <button class="wizard-btn wizard-btn-primary" onclick="wizApprove()">Approve &amp; Generate</button>
1509
+ <button class="wizard-btn wizard-btn-secondary" onclick="switchTab('chat')">Back to Chat</button>
1510
+ </div>
1511
+ </div>
1512
+ </div>
1513
+
1514
  <div class="chat-input-area" style="position: relative;">
1515
  <div id="mention-dropdown">
1516
  <div class="mention-option" data-agent="design" onclick="insertMention('design')">
 
1601
  let mentionIndex = 0;
1602
  let currentLang = localStorage.getItem('neuralcad_lang') || 'en';
1603
 
1604
+ // ── WIZARD STATE ──────────────────────────────────────
1605
+ let activeTab = 'chat';
1606
+ let wizHoleSize = 'M6';
1607
+ let wizFeatures = new Set();
1608
+
1609
+ function switchTab(tab) {
1610
+ activeTab = tab;
1611
+ document.getElementById('tab-chat').classList.toggle('active', tab === 'chat');
1612
+ document.getElementById('tab-guided').classList.toggle('active', tab === 'guided');
1613
+ document.getElementById('chat-messages').classList.toggle('hidden', tab === 'guided');
1614
+ document.querySelector('.chat-header').style.display = tab === 'chat' ? 'flex' : 'none';
1615
+ document.getElementById('guided-panel').classList.toggle('active', tab === 'guided');
1616
+ if (tab === 'guided') syncWizardFromState();
1617
+ }
1618
+
1619
+ function wizSetPart(name, desc) {
1620
+ designState.part_name = name;
1621
+ designState.description = desc;
1622
+ wizMarkStep(1); saveState();
1623
+ document.querySelectorAll('#wiz-step-1 .wizard-chip').forEach(c => c.classList.remove('selected'));
1624
+ if (event && event.target) event.target.classList.add('selected');
1625
+ }
1626
+
1627
+ function wizSetMaterial(mat) {
1628
+ designState.material = mat;
1629
+ wizMarkStep(2); saveState();
1630
+ document.querySelectorAll('#wiz-step-2 .wizard-chip').forEach(c => c.classList.remove('selected'));
1631
+ if (event && event.target) event.target.classList.add('selected');
1632
+ }
1633
+
1634
+ function wizSetDim(name, val) {
1635
+ if (!designState.dimensions) designState.dimensions = {};
1636
+ const v = parseFloat(val);
1637
+ if (v > 0) designState.dimensions[name] = v;
1638
+ else delete designState.dimensions[name];
1639
+ wizMarkStep(3); saveState();
1640
+ }
1641
+
1642
+ function wizToggleFeature(el, feat) {
1643
+ if (wizFeatures.has(feat)) {
1644
+ wizFeatures.delete(feat);
1645
+ el.classList.remove('selected');
1646
+ } else {
1647
+ wizFeatures.add(feat);
1648
+ el.classList.add('selected');
1649
+ }
1650
+ if (feat === 'holes') {
1651
+ document.getElementById('wiz-holes-config').style.display = wizFeatures.has('holes') ? 'block' : 'none';
1652
+ }
1653
+ wizRebuildFeatures();
1654
+ wizMarkStep(4); saveState();
1655
+ }
1656
+
1657
+ function wizSetHoleSize(size) {
1658
+ wizHoleSize = size;
1659
+ document.querySelectorAll('#wiz-holes-config .wizard-chip').forEach(c => c.classList.remove('selected'));
1660
+ document.getElementById('wiz-hole-' + size.toLowerCase()).classList.add('selected');
1661
+ wizRebuildFeatures();
1662
+ saveState();
1663
+ }
1664
+
1665
+ function wizUpdateHoles() { wizRebuildFeatures(); saveState(); }
1666
+
1667
+ function wizRebuildFeatures() {
1668
+ if (!designState.features) designState.features = [];
1669
+ const custom = designState.features.filter(f => !['fillets','chamfers','pockets','slots'].includes(f) && !/^\d+x M\d+ holes$/.test(f));
1670
+ designState.features = [];
1671
+ if (wizFeatures.has('holes')) {
1672
+ const count = document.getElementById('wiz-hole-count')?.value || 4;
1673
+ designState.features.push(count + 'x ' + wizHoleSize + ' holes');
1674
+ }
1675
+ if (wizFeatures.has('fillets')) designState.features.push('fillets');
1676
+ if (wizFeatures.has('chamfers')) designState.features.push('chamfers');
1677
+ if (wizFeatures.has('pockets')) designState.features.push('pockets');
1678
+ if (wizFeatures.has('slots')) designState.features.push('slots');
1679
+ designState.features.push(...custom);
1680
+ }
1681
+
1682
+ function wizAddCustomFeature(val) {
1683
+ if (!val.trim()) return;
1684
+ if (!designState.features) designState.features = [];
1685
+ designState.features.push(val.trim());
1686
+ wizMarkStep(4); saveState();
1687
+ }
1688
+
1689
+ function wizUpdateConstraints() {
1690
+ designState.constraints = [];
1691
+ const wall = document.getElementById('wiz-min-wall')?.value;
1692
+ const size = document.getElementById('wiz-max-size')?.value;
1693
+ if (wall) designState.constraints.push('min wall ' + wall + 'mm');
1694
+ if (size && parseFloat(size) < 500) designState.constraints.push('max size ' + size + 'mm');
1695
+ wizMarkStep(5); saveState();
1696
+ }
1697
+
1698
+ function wizSetAxis(axis, el) {
1699
+ designState.axis_recommendation = axis;
1700
+ document.querySelectorAll('#wiz-step-6 .wizard-chip').forEach(c => c.classList.remove('selected'));
1701
+ if (el) el.classList.add('selected');
1702
+ wizMarkStep(6); saveState();
1703
+ }
1704
+
1705
+ function wizMarkStep(n) {
1706
+ const check = document.getElementById('wiz-check-' + n);
1707
+ const step = document.getElementById('wiz-step-' + n);
1708
+ if (check) check.textContent = '\u2713';
1709
+ if (step) step.classList.add('completed');
1710
+ if (n <= 6) wizUpdateReview();
1711
+ }
1712
+
1713
+ function wizUpdateReview() {
1714
+ const el = document.getElementById('wiz-review-content');
1715
+ if (!el) return;
1716
+ const fields = [
1717
+ ['Part', designState.part_name || ''],
1718
+ ['Material', designState.material || ''],
1719
+ ['Dimensions', Object.entries(designState.dimensions || {}).map(([k,v]) => k + '=' + v + 'mm').join(', ') || ''],
1720
+ ['Features', (designState.features || []).join(', ') || ''],
1721
+ ['Constraints', (designState.constraints || []).join(', ') || ''],
1722
+ ['Machining', designState.axis_recommendation || 'Auto'],
1723
+ ];
1724
+ let html = '';
1725
+ for (const [label, value] of fields) {
1726
+ html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
1727
+ }
1728
+ el.innerHTML = html;
1729
+ const score = wizComputeScore();
1730
+ const scoreEl = document.getElementById('wiz-score');
1731
+ if (scoreEl) scoreEl.textContent = 'Score: ' + score.toFixed(0) + '/8 ' + (score >= 8 ? '\u2713 Ready' : '\u2717 Need more info');
1732
+ }
1733
+
1734
+ function wizComputeScore() {
1735
+ let s = 0;
1736
+ if (designState.material) s += 3;
1737
+ if (designState.part_name) s += 1;
1738
+ if (designState.description) s += 1;
1739
+ if (designState.axis_recommendation) s += 2;
1740
+ s += Math.min(Object.keys(designState.dimensions || {}).length, 4);
1741
+ s += Math.min((designState.features || []).length, 4);
1742
+ s += Math.min((designState.constraints || []).length, 2);
1743
+ return s;
1744
+ }
1745
+
1746
+ async function wizApprove() {
1747
+ const plan = {
1748
+ part_name: designState.part_name || '',
1749
+ description: designState.description || '',
1750
+ material: designState.material || '',
1751
+ dimensions: designState.dimensions || {},
1752
+ features: designState.features || [],
1753
+ constraints: designState.constraints || [],
1754
+ axis_recommendation: designState.axis_recommendation || '',
1755
+ machining_notes: [],
1756
+ confidence_score: wizComputeScore(),
1757
+ };
1758
+ try {
1759
+ const resp = await fetch('/api/plan/approve', {
1760
+ method: 'POST',
1761
+ headers: { 'Content-Type': 'application/json' },
1762
+ body: JSON.stringify({ plan: plan, design_state: designState }),
1763
+ });
1764
+ const data = await resp.json();
1765
+ designState = data.design_state;
1766
+ saveState();
1767
+ switchTab('chat');
1768
+ await sendMessage('Generate the approved design');
1769
+ } catch (err) {
1770
+ console.error('Plan approve failed:', err);
1771
+ }
1772
+ }
1773
+
1774
+ function syncWizardFromState() {
1775
+ const dims = designState.dimensions || {};
1776
+ for (const key of ['width', 'height', 'depth']) {
1777
+ const el = document.getElementById('wiz-dim-' + key);
1778
+ if (el && dims[key]) el.value = dims[key];
1779
+ }
1780
+ if (designState.part_name) wizMarkStep(1);
1781
+ if (designState.material) wizMarkStep(2);
1782
+ if (Object.keys(dims).length > 0) wizMarkStep(3);
1783
+ if ((designState.features || []).length > 0) wizMarkStep(4);
1784
+ if ((designState.constraints || []).length > 0) wizMarkStep(5);
1785
+ if (designState.axis_recommendation) wizMarkStep(6);
1786
+ wizUpdateReview();
1787
+ }
1788
+
1789
+ // ── PLAN CARD ─────────────────────────────────────────
1790
+
1791
+ function renderPlanCard(plan) {
1792
+ const fields = [
1793
+ ['Part', plan.part_name],
1794
+ ['Material', plan.material],
1795
+ ['Dimensions', Object.entries(plan.dimensions || {}).map(([k,v]) => k + '=' + v + 'mm').join(', ')],
1796
+ ['Features', (plan.features || []).join(', ')],
1797
+ ['Constraints', (plan.constraints || []).join(', ')],
1798
+ ['Axis', plan.axis_recommendation || 'Auto'],
1799
+ ];
1800
+ let html = '<div class="plan-card" id="active-plan-card">';
1801
+ html += '<div class="plan-card-title">\u25c6 PLAN READY FOR REVIEW</div>';
1802
+ for (const [label, value] of fields) {
1803
+ html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
1804
+ }
1805
+ if (plan.machining_notes && plan.machining_notes.length) {
1806
+ html += '<div class="wizard-review-field"><span class="wizard-review-label">Notes</span><span class="wizard-review-value">' + plan.machining_notes.join('; ') + '</span></div>';
1807
+ }
1808
+ html += '<div class="plan-card-score">Score: ' + (plan.confidence_score || 0).toFixed(0) + '/8</div>';
1809
+ html += '<div class="plan-card-actions">';
1810
+ html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">Approve</button>';
1811
+ html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">Reject</button>';
1812
+ html += '</div></div>';
1813
+ return html;
1814
+ }
1815
+
1816
+ async function approvePlanCard() {
1817
+ const plan = designState.plan;
1818
+ if (!plan) return;
1819
+ try {
1820
+ const resp = await fetch('/api/plan/approve', {
1821
+ method: 'POST',
1822
+ headers: { 'Content-Type': 'application/json' },
1823
+ body: JSON.stringify({ plan: plan, design_state: designState }),
1824
+ });
1825
+ const data = await resp.json();
1826
+ designState = data.design_state;
1827
+ saveState();
1828
+ const card = document.getElementById('active-plan-card');
1829
+ if (card) card.remove();
1830
+ await sendMessage('Generate the approved design');
1831
+ } catch (err) {
1832
+ console.error('Plan approve failed:', err);
1833
+ }
1834
+ }
1835
+
1836
+ async function rejectPlanCard() {
1837
+ try {
1838
+ const resp = await fetch('/api/plan/reject', {
1839
+ method: 'POST',
1840
+ headers: { 'Content-Type': 'application/json' },
1841
+ body: JSON.stringify({ design_state: designState }),
1842
+ });
1843
+ const data = await resp.json();
1844
+ designState = data.design_state;
1845
+ saveState();
1846
+ const card = document.getElementById('active-plan-card');
1847
+ if (card) card.remove();
1848
+ } catch (err) {
1849
+ console.error('Plan reject failed:', err);
1850
+ }
1851
+ }
1852
+
1853
  // ── i18n ─────────────────────────────────────────────
1854
 
1855
  const I18N = {
 
1884
  cadFailed: 'CAD execution failed: ',
1885
  errorPrefix: 'Error: ',
1886
  sendPreviewMsg: '@cad Generate a 3D preview based on our discussion',
1887
+ tabChat: 'Chat',
1888
+ tabGuided: 'Guided',
1889
+ planReady: 'PLAN READY FOR REVIEW',
1890
+ planApprove: 'Approve',
1891
+ planReject: 'Reject',
1892
  },
1893
  'zh-TW': {
1894
  subtitle: '\u591a\u4ee3\u7406\u8a2d\u8a08',
 
1921
  cadFailed: 'CAD \u57f7\u884c\u5931\u6557\uff1a',
1922
  errorPrefix: '\u932f\u8aa4\uff1a',
1923
  sendPreviewMsg: '@cad \u6839\u64da\u6211\u5011\u7684\u8a0e\u8ad6\u751f\u6210 3D \u9810\u89bd',
1924
+ tabChat: '\u5c0d\u8a71',
1925
+ tabGuided: '\u5f15\u5c0e',
1926
+ planReady: '\u8a08\u756b\u5df2\u6e96\u5099\u5be9\u67e5',
1927
+ planApprove: '\u6279\u51c6',
1928
+ planReject: '\u62d2\u7d55',
1929
  },
1930
  vi: {
1931
  subtitle: 'Thi\u1ebft K\u1ebf \u0110a T\u00e1c T\u1eed',
 
1958
  cadFailed: 'Th\u1ef1c thi CAD th\u1ea5t b\u1ea1i: ',
1959
  errorPrefix: 'L\u1ed7i: ',
1960
  sendPreviewMsg: '@cad T\u1ea1o b\u1ea3n xem tr\u01b0\u1edbc 3D d\u1ef1a tr\u00ean cu\u1ed9c th\u1ea3o lu\u1eadn c\u1ee7a ch\u00fang ta',
1961
+ tabChat: 'Tr\u00f2 Chuy\u1ec7n',
1962
+ tabGuided: 'H\u01b0\u1edbng D\u1eabn',
1963
+ planReady: 'K\u1ebe HO\u1ea0CH S\u1eb4N S\u00c0NG',
1964
+ planApprove: 'Duy\u1ec7t',
1965
+ planReject: 'T\u1eeb Ch\u1ed1i',
1966
  },
1967
  };
1968
 
 
2452
  }
2453
  saveState();
2454
 
2455
+ // If phase transitioned to planning, show plan card
2456
+ if (designState.phase === 'planning' && designState.plan) {
2457
+ const old = document.getElementById('active-plan-card');
2458
+ if (old) old.remove();
2459
+ const msgs = document.getElementById('chat-messages');
2460
+ const cardDiv = document.createElement('div');
2461
+ cardDiv.innerHTML = renderPlanCard(designState.plan);
2462
+ msgs.appendChild(cardDiv.firstChild);
2463
+ msgs.scrollTop = msgs.scrollHeight;
2464
+ }
2465
+
2466
  // If preview available, load 3D model
2467
  if (data.preview && data.preview.success) {
2468
  setViewerLoading(true, t('loadingModel'));
 
2765
 
2766
  document.getElementById('dl-step').href = '/api/models/' + partName + '.step';
2767
  document.getElementById('dl-stl').href = '/api/models/' + partName + '.stl';
2768
+ document.getElementById('dl-3mf').href = '/api/models/' + partName + '.3mf';
2769
  document.getElementById('dl-report').href = '/api/models/' + partName + '_report.json';
2770
 
2771
  const dlGcode = document.getElementById('dl-gcode');
 
2857
  html += '<div class="gallery-card-downloads">';
2858
  html += '<a class="gallery-dl" href="/api/models/' + name + '.step" download>STEP</a>';
2859
  html += '<a class="gallery-dl" href="/api/models/' + name + '.stl" download>STL</a>';
2860
+ html += '<a class="gallery-dl" href="/api/models/' + name + '.3mf" download>3MF</a>';
2861
  html += '<a class="gallery-dl" href="/api/models/' + name + '.gcode" download>GCODE</a>';
2862
  html += '</div>';
2863
  html += '</div>';