cyclecad 0.2.2 → 0.2.3

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.
Files changed (69) hide show
  1. package/API-BUILD-MANIFEST.txt +339 -0
  2. package/API-SERVER.md +535 -0
  3. package/Architecture-Deck.pptx +0 -0
  4. package/CLAUDE.md +172 -11
  5. package/CLI-BUILD-SUMMARY.md +504 -0
  6. package/CLI-INDEX.md +356 -0
  7. package/CLI-README.md +466 -0
  8. package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
  9. package/CONNECTED_FABS_GUIDE.md +612 -0
  10. package/CONNECTED_FABS_README.md +310 -0
  11. package/DELIVERABLES.md +343 -0
  12. package/DFM-ANALYZER-INTEGRATION.md +368 -0
  13. package/DFM-QUICK-START.js +253 -0
  14. package/Dockerfile +69 -0
  15. package/IMPLEMENTATION.md +327 -0
  16. package/LICENSE +31 -0
  17. package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
  18. package/MCP-INDEX.md +264 -0
  19. package/QUICKSTART-API.md +388 -0
  20. package/QUICKSTART-CLI.md +211 -0
  21. package/QUICKSTART-MCP.md +196 -0
  22. package/README-MCP.md +208 -0
  23. package/TEST-TOKEN-ENGINE.md +319 -0
  24. package/TOKEN-ENGINE-SUMMARY.md +266 -0
  25. package/TOKENS-README.md +263 -0
  26. package/TOOLS-REFERENCE.md +254 -0
  27. package/app/index.html +168 -3
  28. package/app/js/TOKEN-INTEGRATION.md +391 -0
  29. package/app/js/agent-api.js +3 -3
  30. package/app/js/ai-copilot.js +1435 -0
  31. package/app/js/cam-pipeline.js +840 -0
  32. package/app/js/collaboration-ui.js +995 -0
  33. package/app/js/collaboration.js +1116 -0
  34. package/app/js/connected-fabs-example.js +404 -0
  35. package/app/js/connected-fabs.js +1449 -0
  36. package/app/js/dfm-analyzer.js +1760 -0
  37. package/app/js/marketplace.js +1994 -0
  38. package/app/js/material-library.js +2115 -0
  39. package/app/js/token-dashboard.js +563 -0
  40. package/app/js/token-engine.js +743 -0
  41. package/app/test-agent.html +1801 -0
  42. package/bin/cyclecad-cli.js +662 -0
  43. package/bin/cyclecad-mcp +2 -0
  44. package/bin/server.js +242 -0
  45. package/cycleCAD-Architecture.pptx +0 -0
  46. package/cycleCAD-Investor-Deck.pptx +0 -0
  47. package/demo-mcp.sh +60 -0
  48. package/docs/API-SERVER-SUMMARY.md +375 -0
  49. package/docs/API-SERVER.md +667 -0
  50. package/docs/CAM-EXAMPLES.md +344 -0
  51. package/docs/CAM-INTEGRATION.md +612 -0
  52. package/docs/CAM-QUICK-REFERENCE.md +199 -0
  53. package/docs/CLI-INTEGRATION.md +510 -0
  54. package/docs/CLI.md +872 -0
  55. package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
  56. package/docs/MARKETPLACE-INTEGRATION.md +467 -0
  57. package/docs/MARKETPLACE-SETUP.html +439 -0
  58. package/docs/MCP-SERVER.md +403 -0
  59. package/examples/api-client-example.js +488 -0
  60. package/examples/api-client-example.py +359 -0
  61. package/examples/batch-manufacturing.txt +28 -0
  62. package/examples/batch-simple.txt +26 -0
  63. package/model-marketplace.html +1273 -0
  64. package/package.json +14 -3
  65. package/server/api-server.js +1120 -0
  66. package/server/mcp-server.js +1161 -0
  67. package/test-api-server.js +432 -0
  68. package/test-mcp.js +198 -0
  69. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,563 @@
1
+ /**
2
+ * token-dashboard.js — Token Engine UI Dashboard for cycleCAD
3
+ *
4
+ * Provides a rich UI for:
5
+ * - Balance display with tier badge
6
+ * - Recent transactions
7
+ * - Monthly usage chart
8
+ * - Tier information and upgrade buttons
9
+ * - Purchase tokens dialog
10
+ * - Operation price estimator
11
+ */
12
+
13
+ export function initTokenDashboard() {
14
+ // Create panel HTML
15
+ const panelHTML = `
16
+ <div id="tab-tokens" style="display: none; padding: 12px; overflow-y: auto;">
17
+ <!-- Balance Card -->
18
+ <div style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; padding: 12px; margin-bottom: 12px;">
19
+ <div style="font-size: 11px; color: var(--text-secondary); text-transform: uppercase; margin-bottom: 6px;">Balance</div>
20
+ <div style="display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px;">
21
+ <div id="token-balance-value" style="font-size: 28px; font-weight: bold; color: var(--accent-blue);">1,000</div>
22
+ <div style="color: var(--text-secondary); font-size: 13px;">tokens</div>
23
+ <div id="token-tier-badge" style="margin-left: auto; padding: 2px 8px; border-radius: 3px; background: rgba(152,152,152,0.2); color: #999; font-size: 10px; font-weight: 600; text-transform: uppercase;">FREE</div>
24
+ </div>
25
+ <div style="font-size: 11px; color: var(--text-secondary);">
26
+ <span id="token-monthly">0</span> / <span id="token-monthly-max">1,000</span> this month
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Quick Actions -->
31
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 12px;">
32
+ <button id="btn-estimate-op" style="
33
+ padding: 8px; border: 1px solid var(--border-color); background: var(--bg-tertiary);
34
+ border-radius: 4px; color: var(--text-primary); font-size: 11px; cursor: pointer;
35
+ transition: all 150ms; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
36
+ " onmouseover="this.style.background='var(--bg-primary)'" onmouseout="this.style.background='var(--bg-tertiary)'">
37
+ 📊 Estimate Price
38
+ </button>
39
+ <button id="btn-buy-tokens" style="
40
+ padding: 8px; border: 1px solid var(--accent-blue); background: rgba(88,166,255,0.1);
41
+ border-radius: 4px; color: var(--accent-blue); font-size: 11px; cursor: pointer;
42
+ transition: all 150ms; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500;
43
+ " onmouseover="this.style.background='rgba(88,166,255,0.2)'" onmouseout="this.style.background='rgba(88,166,255,0.1)'">
44
+ 💳 Buy Tokens
45
+ </button>
46
+ </div>
47
+
48
+ <!-- Tier Info -->
49
+ <div style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; padding: 10px; margin-bottom: 12px; font-size: 11px;">
50
+ <div style="color: var(--text-secondary); margin-bottom: 6px; font-weight: 500;">YOUR TIER</div>
51
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
52
+ <div>
53
+ <div style="color: var(--text-muted); font-size: 10px;">Monthly Allowance</div>
54
+ <div style="font-weight: 600; margin-top: 2px;"><span id="token-allowance">1,000</span> tokens</div>
55
+ </div>
56
+ <div>
57
+ <div style="color: var(--text-muted); font-size: 10px;">Creator Royalty</div>
58
+ <div style="font-weight: 600; margin-top: 2px;" id="token-royalty">70%</div>
59
+ </div>
60
+ </div>
61
+ <button id="btn-upgrade-tier" style="
62
+ margin-top: 8px; width: 100%; padding: 6px; border: 1px solid var(--accent-green);
63
+ background: rgba(63,185,80,0.1); border-radius: 4px; color: var(--accent-green);
64
+ font-size: 11px; font-weight: 500; cursor: pointer; transition: all 150ms;
65
+ " onmouseover="this.style.background='rgba(63,185,80,0.2)'" onmouseout="this.style.background='rgba(63,185,80,0.1)'">
66
+ Upgrade to PRO (€49/mo)
67
+ </button>
68
+ </div>
69
+
70
+ <!-- Recent Transactions -->
71
+ <div style="margin-bottom: 12px;">
72
+ <div style="font-size: 11px; color: var(--text-secondary); text-transform: uppercase; margin-bottom: 6px; font-weight: 500;">Recent Activity</div>
73
+ <div id="token-activity-list" style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; max-height: 120px; overflow-y: auto;">
74
+ <div style="padding: 12px; color: var(--text-secondary); text-align: center; font-size: 11px;">No transactions yet</div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Usage by Operation -->
79
+ <div style="margin-bottom: 12px;">
80
+ <div style="font-size: 11px; color: var(--text-secondary); text-transform: uppercase; margin-bottom: 6px; font-weight: 500;">Top Operations (This Month)</div>
81
+ <div id="token-usage-list" style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; max-height: 100px; overflow-y: auto;">
82
+ <div style="padding: 12px; color: var(--text-secondary); text-align: center; font-size: 11px;">No operations yet</div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Footer -->
87
+ <div style="border-top: 1px solid var(--border-color); padding-top: 8px; text-align: center;">
88
+ <button id="btn-token-history" style="
89
+ background: none; border: none; color: var(--accent-blue); cursor: pointer; font-size: 11px;
90
+ text-decoration: underline; opacity: 0.7; transition: opacity 150ms;
91
+ " onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">
92
+ View full history
93
+ </button>
94
+ </div>
95
+ </div>
96
+ `;
97
+
98
+ return {
99
+ html: panelHTML,
100
+ init: initTokenDashboardEvents,
101
+ update: updateTokenDashboard,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Initialize event handlers for token dashboard
107
+ */
108
+ function initTokenDashboardEvents() {
109
+ // Estimate price button
110
+ document.getElementById('btn-estimate-op')?.addEventListener('click', () => {
111
+ showEstimateDialog();
112
+ });
113
+
114
+ // Buy tokens button
115
+ document.getElementById('btn-buy-tokens')?.addEventListener('click', () => {
116
+ showPurchaseDialog();
117
+ });
118
+
119
+ // Upgrade tier button
120
+ document.getElementById('btn-upgrade-tier')?.addEventListener('click', () => {
121
+ showUpgradeDialog();
122
+ });
123
+
124
+ // History button
125
+ document.getElementById('btn-token-history')?.addEventListener('click', () => {
126
+ showHistoryModal();
127
+ });
128
+
129
+ // Subscribe to token engine events
130
+ if (window.cycleCAD?.tokens) {
131
+ window.cycleCAD.tokens.on('token-spent', () => updateTokenDashboard());
132
+ window.cycleCAD.tokens.on('token-added', () => updateTokenDashboard());
133
+ window.cycleCAD.tokens.on('month-reset', () => updateTokenDashboard());
134
+ window.cycleCAD.tokens.on('tier-changed', () => updateTokenDashboard());
135
+ }
136
+
137
+ updateTokenDashboard();
138
+ }
139
+
140
+ /**
141
+ * Update dashboard display
142
+ */
143
+ function updateTokenDashboard() {
144
+ if (!window.cycleCAD?.tokens) return;
145
+
146
+ const info = window.cycleCAD.tokens.getBalanceInfo();
147
+ const history = window.cycleCAD.tokens.getTransactionHistory({ limit: 5 });
148
+ const usage = window.cycleCAD.tokens.getUsageByOperation();
149
+
150
+ // Update balance
151
+ document.getElementById('token-balance-value').textContent = formatNumber(info.balance);
152
+ document.getElementById('token-tier-badge').textContent = info.tier;
153
+ document.getElementById('token-tier-badge').style.background = info.tierColor + '22';
154
+ document.getElementById('token-tier-badge').style.color = info.tierColor;
155
+
156
+ // Update monthly usage
157
+ document.getElementById('token-monthly').textContent = formatNumber(info.usedThisMonth);
158
+ document.getElementById('token-monthly-max').textContent = formatNumber(info.monthlyAllowance);
159
+
160
+ // Update tier info
161
+ document.getElementById('token-allowance').textContent = formatNumber(info.monthlyAllowance);
162
+ document.getElementById('token-royalty').textContent = info.creatorRoyalty;
163
+
164
+ // Update recent activity
165
+ const activityList = document.getElementById('token-activity-list');
166
+ if (history.length > 0) {
167
+ activityList.innerHTML = history.map(t => `
168
+ <div style="
169
+ padding: 6px 8px; border-bottom: 1px solid var(--border-color);
170
+ display: flex; justify-content: space-between; align-items: center; font-size: 10px;
171
+ ">
172
+ <div>
173
+ <div style="color: var(--text-primary);">${escapeHtml(t.operation)}</div>
174
+ <div style="color: var(--text-muted); margin-top: 2px;">${new Date(t.timestamp).toLocaleDateString()}</div>
175
+ </div>
176
+ <div style="text-align: right; font-weight: 500;">
177
+ <div style="color: ${t.type === 'debit' ? 'var(--accent-red)' : 'var(--accent-green)';};">
178
+ ${t.type === 'debit' ? '-' : '+'}${t.amount}
179
+ </div>
180
+ <div style="color: var(--text-secondary); margin-top: 2px;">Balance: ${formatNumber(t.balance_after)}</div>
181
+ </div>
182
+ </div>
183
+ `).join('');
184
+ } else {
185
+ activityList.innerHTML = '<div style="padding: 12px; color: var(--text-secondary); text-align: center; font-size: 11px;">No transactions yet</div>';
186
+ }
187
+
188
+ // Update top operations
189
+ const usageList = document.getElementById('token-usage-list');
190
+ const topOps = Object.entries(usage)
191
+ .sort(([, a], [, b]) => b.totalTokens - a.totalTokens)
192
+ .slice(0, 5);
193
+
194
+ if (topOps.length > 0) {
195
+ usageList.innerHTML = topOps.map(([op, stats]) => `
196
+ <div style="
197
+ padding: 6px 8px; border-bottom: 1px solid var(--border-color);
198
+ display: flex; justify-content: space-between; align-items: center; font-size: 10px;
199
+ ">
200
+ <div style="color: var(--text-primary);">${escapeHtml(op)}</div>
201
+ <div style="text-align: right;">
202
+ <div style="font-weight: 600;">${stats.totalTokens} tokens</div>
203
+ <div style="color: var(--text-secondary);">${stats.count}x</div>
204
+ </div>
205
+ </div>
206
+ `).join('');
207
+ } else {
208
+ usageList.innerHTML = '<div style="padding: 12px; color: var(--text-secondary); text-align: center; font-size: 11px;">No operations yet</div>';
209
+ }
210
+ }
211
+
212
+ // ============================================================================
213
+ // Dialog Functions
214
+ // ============================================================================
215
+
216
+ function showEstimateDialog() {
217
+ const backdrop = document.getElementById('dialog-backdrop');
218
+ if (!backdrop) return;
219
+
220
+ const operations = [
221
+ { id: 'model.export.stl', name: 'Export as STL', price: 2 },
222
+ { id: 'model.export.step', name: 'Export as STEP', price: 10 },
223
+ { id: 'model.export.gltf', name: 'Export as glTF', price: 3 },
224
+ { id: 'ai.identify.part', name: 'AI Part Identifier', price: 5 },
225
+ { id: 'ai.design_review', name: 'AI Design Review', price: 15 },
226
+ { id: 'cam.slice.model', name: 'Slice for 3D Print', price: 20 },
227
+ { id: 'analysis.weight', name: 'Weight Estimation', price: 2 },
228
+ { id: 'analysis.cost', name: 'Cost Analysis', price: 5 },
229
+ ];
230
+
231
+ const dialog = document.createElement('div');
232
+ dialog.className = 'operation-dialog';
233
+ dialog.style.zIndex = '10001';
234
+ dialog.innerHTML = `
235
+ <div class="dialog-header">
236
+ <div class="dialog-title">Estimate Operation Cost</div>
237
+ <div class="dialog-close-btn" onclick="this.closest('.operation-dialog').remove()">✕</div>
238
+ </div>
239
+ <div class="dialog-content">
240
+ <div class="dialog-form-group">
241
+ <label class="dialog-label">Operation</label>
242
+ <select class="dialog-select" id="estimate-operation" onchange="updateEstimatePreview()">
243
+ ${operations.map(op => `<option value="${op.id}">${op.name}</option>`).join('')}
244
+ </select>
245
+ </div>
246
+ <div class="dialog-form-group">
247
+ <label class="dialog-label">Batch Size</label>
248
+ <input type="number" class="dialog-input" id="estimate-batch" value="1" min="1" onchange="updateEstimatePreview()">
249
+ </div>
250
+ <div style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-top: 10px;">
251
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">ESTIMATED COST</div>
252
+ <div style="font-size: 20px; font-weight: bold; color: var(--accent-blue); margin-bottom: 4px;">
253
+ <span id="estimate-price">2</span> tokens
254
+ </div>
255
+ <div style="font-size: 10px; color: var(--text-secondary);">
256
+ €<span id="estimate-euros">0.02</span> · <span id="estimate-discount">no discount</span>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ <div class="dialog-footer">
261
+ <button class="dialog-button secondary" onclick="this.closest('.operation-dialog').remove()">Close</button>
262
+ </div>
263
+ `;
264
+
265
+ document.body.appendChild(dialog);
266
+ backdrop.style.display = 'block';
267
+
268
+ // Initial preview
269
+ window.updateEstimatePreview = () => {
270
+ const opId = document.getElementById('estimate-operation')?.value;
271
+ const batchSize = parseInt(document.getElementById('estimate-batch')?.value || 1);
272
+ if (!window.cycleCAD?.tokens || !opId) return;
273
+
274
+ const estimate = window.cycleCAD.tokens.estimateOperation(opId, { batchSize });
275
+ document.getElementById('estimate-price').textContent = estimate.finalPrice;
276
+ document.getElementById('estimate-euros').textContent = (estimate.finalPrice / 100).toFixed(2);
277
+ document.getElementById('estimate-discount').textContent =
278
+ estimate.batchDiscount > 0 ? `${estimate.batchDiscount}% batch discount` : 'no discount';
279
+ };
280
+
281
+ window.updateEstimatePreview();
282
+
283
+ // Close on backdrop click
284
+ backdrop.onclick = (e) => {
285
+ if (e.target === backdrop) {
286
+ backdrop.style.display = 'none';
287
+ dialog.remove();
288
+ }
289
+ };
290
+ }
291
+
292
+ function showPurchaseDialog() {
293
+ const backdrop = document.getElementById('dialog-backdrop');
294
+ if (!backdrop) return;
295
+
296
+ const presets = [
297
+ { tokens: 1000, euros: 10, label: '1,000 tokens' },
298
+ { tokens: 5000, euros: 50, label: '5,000 tokens (save €0.50)' },
299
+ { tokens: 10000, euros: 90, label: '10,000 tokens (save €10)' },
300
+ ];
301
+
302
+ const dialog = document.createElement('div');
303
+ dialog.className = 'operation-dialog';
304
+ dialog.style.zIndex = '10001';
305
+ dialog.innerHTML = `
306
+ <div class="dialog-header">
307
+ <div class="dialog-title">Purchase Tokens</div>
308
+ <div class="dialog-close-btn" onclick="this.closest('.operation-dialog').remove()">✕</div>
309
+ </div>
310
+ <div class="dialog-content">
311
+ <div class="dialog-form-group">
312
+ <label class="dialog-label">Choose Package</label>
313
+ <div style="display: grid; grid-template-columns: 1fr; gap: 6px;">
314
+ ${presets.map((p, i) => `
315
+ <button type="button" style="
316
+ padding: 10px; border: 1px solid var(--border-color); background: var(--bg-tertiary);
317
+ border-radius: 4px; color: var(--text-primary); cursor: pointer; text-align: left;
318
+ transition: all 150ms;
319
+ " onmouseover="this.style.background='rgba(88,166,255,0.1)'; this.style.borderColor='var(--accent-blue)'"
320
+ onmouseout="this.style.background='var(--bg-tertiary)'; this.style.borderColor='var(--border-color)'"
321
+ onclick="selectTokenPackage(${p.tokens})">
322
+ <div style="font-weight: 500; margin-bottom: 2px;">${p.label}</div>
323
+ <div style="font-size: 11px; color: var(--text-secondary);">€${p.euros.toFixed(2)}</div>
324
+ </button>
325
+ `).join('')}
326
+ </div>
327
+ </div>
328
+ <div class="dialog-form-group">
329
+ <label class="dialog-label">Custom Amount</label>
330
+ <input type="number" class="dialog-input" id="custom-tokens" placeholder="100" min="10" value="100">
331
+ </div>
332
+ <div style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-top: 10px;">
333
+ <div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
334
+ <span style="color: var(--text-secondary);">Total:</span>
335
+ <span id="purchase-total" style="font-weight: 600; font-size: 14px;">€1.00</span>
336
+ </div>
337
+ </div>
338
+ <div style="font-size: 10px; color: var(--text-secondary); margin-top: 10px; line-height: 1.6;">
339
+ Conversion: 1 token = €0.01 · Payment via Stripe
340
+ </div>
341
+ </div>
342
+ <div class="dialog-footer">
343
+ <button class="dialog-button secondary" onclick="this.closest('.operation-dialog').remove()">Cancel</button>
344
+ <button class="dialog-button primary" onclick="completePurchaseFlow()">Proceed to Checkout</button>
345
+ </div>
346
+ `;
347
+
348
+ document.body.appendChild(dialog);
349
+ backdrop.style.display = 'block';
350
+
351
+ window.selectTokenPackage = (tokens) => {
352
+ document.getElementById('custom-tokens').value = tokens;
353
+ updatePurchasePreview();
354
+ };
355
+
356
+ window.updatePurchasePreview = () => {
357
+ const tokens = parseInt(document.getElementById('custom-tokens')?.value || 100);
358
+ const euros = (tokens / 100).toFixed(2);
359
+ document.getElementById('purchase-total').textContent = `€${euros}`;
360
+ };
361
+
362
+ window.completePurchaseFlow = () => {
363
+ const tokens = parseInt(document.getElementById('custom-tokens')?.value || 100);
364
+ if (window.cycleCAD?.tokens) {
365
+ const session = window.cycleCAD.tokens.purchaseTokens(tokens, 'stripe');
366
+ alert(`Purchase initiated!\nTokens: ${tokens}\nURL: ${session.stripeCheckoutUrl}\n\n(Demo mode — would open Stripe checkout)`);
367
+ }
368
+ dialog.remove();
369
+ };
370
+
371
+ // Close on backdrop click
372
+ backdrop.onclick = (e) => {
373
+ if (e.target === backdrop) {
374
+ backdrop.style.display = 'none';
375
+ dialog.remove();
376
+ }
377
+ };
378
+
379
+ updatePurchasePreview();
380
+ }
381
+
382
+ function showUpgradeDialog() {
383
+ const backdrop = document.getElementById('dialog-backdrop');
384
+ if (!backdrop) return;
385
+
386
+ const tiers = [
387
+ {
388
+ name: 'FREE',
389
+ price: 'Free',
390
+ tokens: 1000,
391
+ royalty: '70%',
392
+ features: ['1,000 tokens/month', 'Basic exports', 'Community support'],
393
+ },
394
+ {
395
+ name: 'PRO',
396
+ price: '€49/mo',
397
+ tokens: 10000,
398
+ royalty: '80%',
399
+ features: ['10,000 tokens/month', 'STEP import/export', 'AI design review', 'Priority support'],
400
+ highlight: true,
401
+ },
402
+ {
403
+ name: 'ENTERPRISE',
404
+ price: '€299/mo',
405
+ tokens: 100000,
406
+ royalty: '90%',
407
+ features: ['100,000 tokens/month', 'Unlimited STEP', 'Advanced analytics', 'SLA & support', 'Custom integrations'],
408
+ },
409
+ ];
410
+
411
+ const dialog = document.createElement('div');
412
+ dialog.className = 'operation-dialog';
413
+ dialog.style.zIndex = '10001';
414
+ dialog.style.maxWidth = '600px';
415
+ dialog.innerHTML = `
416
+ <div class="dialog-header">
417
+ <div class="dialog-title">Upgrade to Higher Tier</div>
418
+ <div class="dialog-close-btn" onclick="this.closest('.operation-dialog').remove()">✕</div>
419
+ </div>
420
+ <div class="dialog-content" style="max-height: 400px; overflow-y: auto;">
421
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px;">
422
+ ${tiers.map(tier => `
423
+ <div style="
424
+ border: 2px solid ${tier.highlight ? 'var(--accent-blue)' : 'var(--border-color)'};
425
+ border-radius: 6px; padding: 12px; background: ${tier.highlight ? 'rgba(88,166,255,0.05)' : 'var(--bg-tertiary)'};
426
+ position: relative;
427
+ ">
428
+ ${tier.highlight ? '<div style="position: absolute; top: -10px; right: 12px; background: var(--accent-blue); color: white; padding: 2px 8px; font-size: 9px; border-radius: 3px; font-weight: 600;">POPULAR</div>' : ''}
429
+ <div style="font-weight: 600; margin-bottom: 4px; font-size: 12px;">${tier.name}</div>
430
+ <div style="font-size: 14px; font-weight: bold; color: var(--accent-blue); margin-bottom: 8px;">${tier.price}</div>
431
+ <div style="font-size: 10px; color: var(--text-secondary); margin-bottom: 6px;">
432
+ <div>${tier.tokens.toLocaleString()} tokens/month</div>
433
+ <div>${tier.royalty} creator royalty</div>
434
+ </div>
435
+ <div style="border-top: 1px solid var(--border-color); padding-top: 8px;">
436
+ ${tier.features.map(f => `<div style="font-size: 9px; color: var(--text-secondary); margin-bottom: 3px;">✓ ${f}</div>`).join('')}
437
+ </div>
438
+ <button style="
439
+ margin-top: 8px; width: 100%; padding: 6px; border: 1px solid ${tier.highlight ? 'var(--accent-blue)' : 'var(--border-color)'};
440
+ background: ${tier.highlight ? 'rgba(88,166,255,0.1)' : 'var(--bg-primary)'}; border-radius: 3px;
441
+ color: ${tier.highlight ? 'var(--accent-blue)' : 'var(--text-primary)'}; font-size: 10px; cursor: pointer;
442
+ transition: all 150ms;
443
+ " onclick="upgradeTo('${tier.name}')"
444
+ onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
445
+ ${tier.name === 'FREE' ? 'Current' : 'Upgrade'}
446
+ </button>
447
+ </div>
448
+ `).join('')}
449
+ </div>
450
+ </div>
451
+ <div class="dialog-footer">
452
+ <button class="dialog-button secondary" onclick="this.closest('.operation-dialog').remove()">Cancel</button>
453
+ </div>
454
+ `;
455
+
456
+ document.body.appendChild(dialog);
457
+ backdrop.style.display = 'block';
458
+
459
+ window.upgradeTo = (tier) => {
460
+ if (window.cycleCAD?.tokens) {
461
+ window.cycleCAD.tokens.setTier(tier);
462
+ alert(`Upgraded to ${tier} tier! Refreshing dashboard...`);
463
+ updateTokenDashboard();
464
+ dialog.remove();
465
+ }
466
+ };
467
+
468
+ backdrop.onclick = (e) => {
469
+ if (e.target === backdrop) {
470
+ backdrop.style.display = 'none';
471
+ dialog.remove();
472
+ }
473
+ };
474
+ }
475
+
476
+ function showHistoryModal() {
477
+ if (!window.cycleCAD?.tokens) return;
478
+
479
+ const history = window.cycleCAD.tokens.getTransactionHistory({ limit: 100 });
480
+ const backdrop = document.getElementById('dialog-backdrop');
481
+ if (!backdrop) return;
482
+
483
+ const dialog = document.createElement('div');
484
+ dialog.className = 'operation-dialog';
485
+ dialog.style.zIndex = '10001';
486
+ dialog.style.maxWidth = '500px';
487
+ dialog.innerHTML = `
488
+ <div class="dialog-header">
489
+ <div class="dialog-title">Token Transaction History</div>
490
+ <div class="dialog-close-btn" onclick="this.closest('.operation-dialog').remove()">✕</div>
491
+ </div>
492
+ <div class="dialog-content" style="max-height: 400px; overflow-y: auto;">
493
+ <table style="width: 100%; border-collapse: collapse; font-size: 11px;">
494
+ <thead>
495
+ <tr style="border-bottom: 1px solid var(--border-color);">
496
+ <th style="text-align: left; padding: 6px; color: var(--text-secondary); font-weight: 500;">Date</th>
497
+ <th style="text-align: left; padding: 6px; color: var(--text-secondary); font-weight: 500;">Operation</th>
498
+ <th style="text-align: right; padding: 6px; color: var(--text-secondary); font-weight: 500;">Amount</th>
499
+ <th style="text-align: right; padding: 6px; color: var(--text-secondary); font-weight: 500;">Balance</th>
500
+ </tr>
501
+ </thead>
502
+ <tbody>
503
+ ${history.map(t => `
504
+ <tr style="border-bottom: 1px solid var(--border-color);">
505
+ <td style="padding: 6px; color: var(--text-secondary);">${new Date(t.timestamp).toLocaleDateString()}</td>
506
+ <td style="padding: 6px; color: var(--text-primary);">${escapeHtml(t.operation)}</td>
507
+ <td style="text-align: right; padding: 6px; color: ${t.type === 'debit' ? 'var(--accent-red)' : 'var(--accent-green)'}; font-weight: 500;">${t.type === 'debit' ? '-' : '+'}${t.amount}</td>
508
+ <td style="text-align: right; padding: 6px; color: var(--text-secondary);">${formatNumber(t.balance_after)}</td>
509
+ </tr>
510
+ `).join('')}
511
+ </tbody>
512
+ </table>
513
+ </div>
514
+ <div class="dialog-footer">
515
+ <button class="dialog-button secondary" onclick="this.closest('.operation-dialog').remove()">Close</button>
516
+ <button class="dialog-button primary" onclick="exportTokenHistory()">Export CSV</button>
517
+ </div>
518
+ `;
519
+
520
+ document.body.appendChild(dialog);
521
+ backdrop.style.display = 'block';
522
+
523
+ window.exportTokenHistory = () => {
524
+ const csv = [
525
+ ['Date', 'Operation', 'Type', 'Amount', 'Balance'],
526
+ ...history.map(t => [
527
+ new Date(t.timestamp).toLocaleDateString(),
528
+ t.operation,
529
+ t.type,
530
+ t.amount,
531
+ t.balance_after,
532
+ ]),
533
+ ].map(row => row.join(',')).join('\n');
534
+
535
+ const blob = new Blob([csv], { type: 'text/csv' });
536
+ const url = URL.createObjectURL(blob);
537
+ const a = document.createElement('a');
538
+ a.href = url;
539
+ a.download = `cyclecad-tokens-${new Date().toISOString().slice(0, 10)}.csv`;
540
+ a.click();
541
+ URL.revokeObjectURL(url);
542
+ };
543
+
544
+ backdrop.onclick = (e) => {
545
+ if (e.target === backdrop) {
546
+ backdrop.style.display = 'none';
547
+ dialog.remove();
548
+ }
549
+ };
550
+ }
551
+
552
+ // ============================================================================
553
+ // Utility Functions
554
+ // ============================================================================
555
+
556
+ function formatNumber(num) {
557
+ return num.toLocaleString();
558
+ }
559
+
560
+ function escapeHtml(text) {
561
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
562
+ return text.replace(/[&<>"']/g, m => map[m]);
563
+ }