cyclecad 3.7.0 → 3.9.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.
@@ -0,0 +1,1505 @@
1
+ /**
2
+ * @fileoverview Engineering Notebook with AI module for cycleCAD
3
+ * Auto-logging system tracks every design action, manual entries with rich text,
4
+ * AI-powered analysis and suggestions, timeline visualization, version snapshots,
5
+ * and comprehensive export options.
6
+ *
7
+ * @author Claude Code
8
+ * @version 1.0.0
9
+ */
10
+
11
+ window.CycleCAD = window.CycleCAD || {};
12
+
13
+ /**
14
+ * Engineering Notebook module: Auto-logs design actions, provides manual entry system,
15
+ * AI features for analysis, timeline visualization, version snapshots, and export.
16
+ */
17
+ window.CycleCAD.EngineeringNotebook = (() => {
18
+ // ============================================================================
19
+ // STATE
20
+ // ============================================================================
21
+
22
+ let entries = [];
23
+ let snapshots = [];
24
+ let milestones = [];
25
+ let currentUI = null;
26
+ let autoLoggingEnabled = true;
27
+ let entryIdCounter = 0;
28
+ let lastAutoLogTime = 0;
29
+ const AUTO_LOG_THROTTLE = 1000; // ms, merge rapid edits
30
+
31
+ /**
32
+ * Entry object structure
33
+ * @typedef {Object} NotebookEntry
34
+ * @property {string} id - Unique entry ID
35
+ * @property {number} timestamp - Unix timestamp in ms
36
+ * @property {string} type - 'auto' | 'note' | 'decision' | 'requirement' | 'issue' | 'meeting' | 'review' | 'test'
37
+ * @property {string} action - Human-readable action description
38
+ * @property {Object} details - Type-specific details (geometry, parameters, etc.)
39
+ * @property {string} [snapshot] - Reference to associated snapshot ID
40
+ * @property {string} userId - User identifier (default: 'anonymous')
41
+ * @property {Array<string>} tags - Free-form tags
42
+ * @property {string} [content] - Manual entry rich text content
43
+ * @property {string} priority - 'info' | 'important' | 'critical'
44
+ */
45
+
46
+ /**
47
+ * Snapshot object structure
48
+ * @typedef {Object} VersionSnapshot
49
+ * @property {string} id - Unique snapshot ID
50
+ * @property {number} timestamp - Unix timestamp in ms
51
+ * @property {string} label - User-friendly label
52
+ * @property {Object} sceneState - Three.js scene serialization
53
+ * @property {Object} parameterSnapshot - Current parameters
54
+ * @property {Array<Object>} constraints - Assembly constraints
55
+ * @property {Object} analysisResults - Latest analysis (FEA, DFM, etc.)
56
+ */
57
+
58
+ // ============================================================================
59
+ // 1. AUTO-LOGGING SYSTEM (~300 lines)
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Initialize auto-logging by attaching event listeners to cycleCAD event bus
64
+ * @private
65
+ */
66
+ function initAutoLogging() {
67
+ // Hook into cycleCAD modules via window.CycleCAD.onEvent
68
+ if (!window.CycleCAD.onEvent) {
69
+ window.CycleCAD.onEvent = [];
70
+ }
71
+ window.CycleCAD.onEvent.push(handleCycleCADEvent);
72
+ }
73
+
74
+ /**
75
+ * Central event handler for all cycleCAD actions
76
+ * @param {Object} event - Event object from cycleCAD modules
77
+ * @param {string} event.type - Event type: 'geometry.create' | 'feature.apply' | 'param.change' | etc.
78
+ * @param {Object} event.data - Event-specific data
79
+ * @private
80
+ */
81
+ function handleCycleCADEvent(event) {
82
+ if (!autoLoggingEnabled) return;
83
+
84
+ const now = Date.now();
85
+ if (now - lastAutoLogTime < AUTO_LOG_THROTTLE) {
86
+ // Merge with last entry if within throttle window
87
+ const lastEntry = entries[entries.length - 1];
88
+ if (lastEntry && lastEntry.type === 'auto') {
89
+ lastEntry.details.mergedCount = (lastEntry.details.mergedCount || 1) + 1;
90
+ lastEntry.details.lastAction = event.type;
91
+ return;
92
+ }
93
+ }
94
+ lastAutoLogTime = now;
95
+
96
+ let action = '';
97
+ let details = { ...event.data };
98
+
99
+ switch (event.type) {
100
+ case 'geometry.create':
101
+ action = `Created ${event.data.shapeType} (${event.data.width}×${event.data.height}×${event.data.depth})`;
102
+ details.shapeType = event.data.shapeType;
103
+ details.dimensions = { width: event.data.width, height: event.data.height, depth: event.data.depth };
104
+ break;
105
+
106
+ case 'feature.apply':
107
+ action = `Applied ${event.data.featureType}`;
108
+ if (event.data.featureType === 'fillet') {
109
+ action += ` (radius: ${event.data.radius}mm)`;
110
+ details.radius = event.data.radius;
111
+ } else if (event.data.featureType === 'pattern') {
112
+ action += ` (${event.data.rows}×${event.data.cols})`;
113
+ details.pattern = { rows: event.data.rows, cols: event.data.cols };
114
+ }
115
+ details.featureType = event.data.featureType;
116
+ break;
117
+
118
+ case 'param.change':
119
+ action = `Changed ${event.data.paramName} from ${event.data.oldValue} to ${event.data.newValue}`;
120
+ details.paramName = event.data.paramName;
121
+ details.oldValue = event.data.oldValue;
122
+ details.newValue = event.data.newValue;
123
+ break;
124
+
125
+ case 'constraint.add':
126
+ action = `Added ${event.data.constraintType} constraint`;
127
+ details.constraintType = event.data.constraintType;
128
+ break;
129
+
130
+ case 'constraint.remove':
131
+ action = `Removed constraint`;
132
+ details.constraintId = event.data.constraintId;
133
+ break;
134
+
135
+ case 'analysis.run':
136
+ action = `Ran ${event.data.analysisType} analysis`;
137
+ details.analysisType = event.data.analysisType;
138
+ details.results = event.data.results;
139
+ break;
140
+
141
+ case 'export.save':
142
+ action = `Exported as ${event.data.format} (${event.data.filename})`;
143
+ details.format = event.data.format;
144
+ details.filename = event.data.filename;
145
+ break;
146
+
147
+ case 'library.insert':
148
+ action = `Inserted part from library: ${event.data.partName}`;
149
+ details.partName = event.data.partName;
150
+ details.category = event.data.category;
151
+ break;
152
+
153
+ case 'assembly.mate':
154
+ action = `Added ${event.data.mateType} mate`;
155
+ details.mateType = event.data.mateType;
156
+ details.component1 = event.data.component1;
157
+ details.component2 = event.data.component2;
158
+ break;
159
+
160
+ default:
161
+ action = `${event.type}`;
162
+ }
163
+
164
+ if (action) {
165
+ addEntry({
166
+ type: 'auto',
167
+ action,
168
+ details,
169
+ priority: 'info'
170
+ });
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Log a geometry creation event
176
+ * @param {string} shapeType - Type of shape created
177
+ * @param {number} width - Bounding box width
178
+ * @param {number} height - Bounding box height
179
+ * @param {number} depth - Bounding box depth
180
+ */
181
+ function logGeometryCreate(shapeType, width, height, depth) {
182
+ window.CycleCAD.onEvent?.forEach(cb => cb({
183
+ type: 'geometry.create',
184
+ data: { shapeType, width, height, depth }
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * Log a feature operation
190
+ * @param {string} featureType - Type of feature (fillet, chamfer, pattern, etc.)
191
+ * @param {Object} params - Feature parameters
192
+ */
193
+ function logFeatureApply(featureType, params) {
194
+ window.CycleCAD.onEvent?.forEach(cb => cb({
195
+ type: 'feature.apply',
196
+ data: { featureType, ...params }
197
+ }));
198
+ }
199
+
200
+ /**
201
+ * Log a parameter change
202
+ * @param {string} paramName - Parameter name
203
+ * @param {*} oldValue - Previous value
204
+ * @param {*} newValue - New value
205
+ */
206
+ function logParamChange(paramName, oldValue, newValue) {
207
+ window.CycleCAD.onEvent?.forEach(cb => cb({
208
+ type: 'param.change',
209
+ data: { paramName, oldValue, newValue }
210
+ }));
211
+ }
212
+
213
+ // ============================================================================
214
+ // 2. MANUAL ENTRY SYSTEM (~200 lines)
215
+ // ============================================================================
216
+
217
+ const ENTRY_TYPES = ['note', 'decision', 'requirement', 'issue', 'meeting', 'review', 'test'];
218
+ const PRIORITIES = ['info', 'important', 'critical'];
219
+
220
+ /**
221
+ * Create manual entry UI with rich text editor
222
+ * @returns {HTMLElement} Rich text editor panel
223
+ * @private
224
+ */
225
+ function createManualEntryEditor() {
226
+ const container = document.createElement('div');
227
+ container.className = 'engineering-notebook-editor';
228
+ container.style.cssText = `
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 12px;
232
+ padding: 12px;
233
+ background: var(--color-bg-secondary, #1e1e1e);
234
+ border-radius: 4px;
235
+ `;
236
+
237
+ // Type selector
238
+ const typeRow = document.createElement('div');
239
+ typeRow.style.cssText = 'display: flex; gap: 8px; align-items: center;';
240
+ const typeLabel = document.createElement('label');
241
+ typeLabel.textContent = 'Type: ';
242
+ typeLabel.style.fontSize = '12px';
243
+ const typeSelect = document.createElement('select');
244
+ typeSelect.style.cssText = `
245
+ padding: 4px 8px;
246
+ background: var(--color-bg-input, #2d2d2d);
247
+ color: var(--color-text, #e0e0e0);
248
+ border: 1px solid var(--color-border, #404040);
249
+ border-radius: 3px;
250
+ font-size: 12px;
251
+ `;
252
+ ENTRY_TYPES.forEach(type => {
253
+ const opt = document.createElement('option');
254
+ opt.value = type;
255
+ opt.textContent = type.charAt(0).toUpperCase() + type.slice(1);
256
+ typeSelect.appendChild(opt);
257
+ });
258
+ typeRow.appendChild(typeLabel);
259
+ typeRow.appendChild(typeSelect);
260
+
261
+ // Priority selector
262
+ const priorityLabel = document.createElement('label');
263
+ priorityLabel.textContent = ' Priority: ';
264
+ priorityLabel.style.fontSize = '12px';
265
+ priorityLabel.style.marginLeft = '16px';
266
+ const prioritySelect = document.createElement('select');
267
+ prioritySelect.style.cssText = `
268
+ padding: 4px 8px;
269
+ background: var(--color-bg-input, #2d2d2d);
270
+ color: var(--color-text, #e0e0e0);
271
+ border: 1px solid var(--color-border, #404040);
272
+ border-radius: 3px;
273
+ font-size: 12px;
274
+ `;
275
+ PRIORITIES.forEach(p => {
276
+ const opt = document.createElement('option');
277
+ opt.value = p;
278
+ opt.textContent = p.charAt(0).toUpperCase() + p.slice(1);
279
+ prioritySelect.appendChild(opt);
280
+ });
281
+ typeRow.appendChild(priorityLabel);
282
+ typeRow.appendChild(prioritySelect);
283
+ container.appendChild(typeRow);
284
+
285
+ // Formatting toolbar
286
+ const toolbar = document.createElement('div');
287
+ toolbar.style.cssText = `
288
+ display: flex;
289
+ gap: 4px;
290
+ padding: 8px;
291
+ background: var(--color-bg-tertiary, #252525);
292
+ border-radius: 3px;
293
+ border-bottom: 1px solid var(--color-border, #404040);
294
+ `;
295
+ const formatButtons = [
296
+ { cmd: 'bold', label: 'B', title: 'Bold' },
297
+ { cmd: 'italic', label: 'I', title: 'Italic' },
298
+ { cmd: 'underline', label: 'U', title: 'Underline' },
299
+ { cmd: 'strikethrough', label: 'S', title: 'Strikethrough' },
300
+ { cmd: 'insertUnorderedList', label: '•', title: 'Bullet list' },
301
+ { cmd: 'insertOrderedList', label: '1.', title: 'Numbered list' },
302
+ ];
303
+ formatButtons.forEach(({ cmd, label, title }) => {
304
+ const btn = document.createElement('button');
305
+ btn.textContent = label;
306
+ btn.title = title;
307
+ btn.style.cssText = `
308
+ padding: 4px 8px;
309
+ background: var(--color-bg-input, #2d2d2d);
310
+ color: var(--color-text, #e0e0e0);
311
+ border: 1px solid var(--color-border, #404040);
312
+ border-radius: 2px;
313
+ cursor: pointer;
314
+ font-size: 11px;
315
+ font-weight: bold;
316
+ `;
317
+ btn.onclick = () => document.execCommand(cmd);
318
+ toolbar.appendChild(btn);
319
+ });
320
+ container.appendChild(toolbar);
321
+
322
+ // Content editor
323
+ const editor = document.createElement('div');
324
+ editor.className = 'engineering-notebook-content-editor';
325
+ editor.contentEditable = true;
326
+ editor.style.cssText = `
327
+ min-height: 120px;
328
+ padding: 10px;
329
+ background: var(--color-bg-input, #2d2d2d);
330
+ color: var(--color-text, #e0e0e0);
331
+ border: 1px solid var(--color-border, #404040);
332
+ border-radius: 3px;
333
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
334
+ font-size: 13px;
335
+ line-height: 1.5;
336
+ overflow-y: auto;
337
+ max-height: 200px;
338
+ `;
339
+ editor.placeholder = 'Enter your notes here...';
340
+ container.appendChild(editor);
341
+
342
+ // Tags input
343
+ const tagsRow = document.createElement('div');
344
+ tagsRow.style.cssText = 'display: flex; gap: 8px; align-items: center;';
345
+ const tagsLabel = document.createElement('label');
346
+ tagsLabel.textContent = 'Tags: ';
347
+ tagsLabel.style.fontSize = '12px';
348
+ const tagsInput = document.createElement('input');
349
+ tagsInput.type = 'text';
350
+ tagsInput.placeholder = 'Comma-separated tags';
351
+ tagsInput.style.cssText = `
352
+ flex: 1;
353
+ padding: 4px 8px;
354
+ background: var(--color-bg-input, #2d2d2d);
355
+ color: var(--color-text, #e0e0e0);
356
+ border: 1px solid var(--color-border, #404040);
357
+ border-radius: 3px;
358
+ font-size: 12px;
359
+ `;
360
+ tagsRow.appendChild(tagsLabel);
361
+ tagsRow.appendChild(tagsInput);
362
+ container.appendChild(tagsRow);
363
+
364
+ // Add entry button
365
+ const addBtn = document.createElement('button');
366
+ addBtn.textContent = 'Add Entry';
367
+ addBtn.style.cssText = `
368
+ padding: 8px 16px;
369
+ background: var(--color-accent, #0284c7);
370
+ color: white;
371
+ border: none;
372
+ border-radius: 3px;
373
+ cursor: pointer;
374
+ font-size: 12px;
375
+ font-weight: 600;
376
+ `;
377
+ addBtn.onclick = () => {
378
+ if (editor.textContent.trim()) {
379
+ addEntry({
380
+ type: typeSelect.value,
381
+ action: editor.textContent.substring(0, 100).trim(),
382
+ content: editor.innerHTML,
383
+ priority: prioritySelect.value,
384
+ tags: tagsInput.value.split(',').map(t => t.trim()).filter(t => t)
385
+ });
386
+ editor.innerHTML = '';
387
+ tagsInput.value = '';
388
+ typeSelect.value = 'note';
389
+ prioritySelect.value = 'info';
390
+ }
391
+ };
392
+ container.appendChild(addBtn);
393
+
394
+ return container;
395
+ }
396
+
397
+ // ============================================================================
398
+ // 3. AI FEATURES (~300 lines)
399
+ // ============================================================================
400
+
401
+ /**
402
+ * Simple local NLP: extract keywords from text
403
+ * @param {string} text - Input text
404
+ * @returns {Array<string>} Extracted keywords
405
+ * @private
406
+ */
407
+ function extractKeywords(text) {
408
+ const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'is', 'was', 'be', 'have', 'do', 'will']);
409
+ const words = text.toLowerCase().match(/\b\w+\b/g) || [];
410
+ return words.filter(w => w.length > 3 && !stopWords.has(w));
411
+ }
412
+
413
+ /**
414
+ * Calculate text similarity using Jaccard index
415
+ * @param {string} text1 - First text
416
+ * @param {string} text2 - Second text
417
+ * @returns {number} Similarity score 0-1
418
+ * @private
419
+ */
420
+ function calculateSimilarity(text1, text2) {
421
+ const set1 = new Set(extractKeywords(text1));
422
+ const set2 = new Set(extractKeywords(text2));
423
+ const intersection = [...set1].filter(w => set2.has(w)).length;
424
+ const union = set1.size + set2.size - intersection;
425
+ return union === 0 ? 0 : intersection / union;
426
+ }
427
+
428
+ /**
429
+ * Generate AI summary of design changes over time period
430
+ * @param {number} [hoursBack=24] - Hours to look back (default 24)
431
+ * @returns {string} Summary text
432
+ */
433
+ function autoSummarize(hoursBack = 24) {
434
+ const cutoffTime = Date.now() - (hoursBack * 3600000);
435
+ const recentEntries = entries.filter(e => e.timestamp >= cutoffTime);
436
+
437
+ if (recentEntries.length === 0) return 'No entries in this period.';
438
+
439
+ // Group entries by type
440
+ const byType = {};
441
+ recentEntries.forEach(e => {
442
+ if (!byType[e.type]) byType[e.type] = [];
443
+ byType[e.type].push(e);
444
+ });
445
+
446
+ let summary = `Design Summary (last ${hoursBack} hours):\n\n`;
447
+
448
+ if (byType.auto && byType.auto.length > 0) {
449
+ summary += `Applied ${byType.auto.length} operations:\n`;
450
+ byType.auto.slice(0, 5).forEach(e => {
451
+ summary += ` • ${e.action}\n`;
452
+ });
453
+ if (byType.auto.length > 5) summary += ` ... and ${byType.auto.length - 5} more\n`;
454
+ }
455
+
456
+ if (byType.decision && byType.decision.length > 0) {
457
+ summary += `\nKey Decisions:\n`;
458
+ byType.decision.forEach(e => {
459
+ summary += ` • ${e.action}\n`;
460
+ });
461
+ }
462
+
463
+ if (byType.issue && byType.issue.length > 0) {
464
+ summary += `\nOutstanding Issues:\n`;
465
+ byType.issue.forEach(e => {
466
+ summary += ` • [${e.priority.toUpperCase()}] ${e.action}\n`;
467
+ });
468
+ }
469
+
470
+ return summary;
471
+ }
472
+
473
+ /**
474
+ * Generate design review checklist from recent changes
475
+ * @returns {Array<string>} Checklist items
476
+ */
477
+ function generateReviewChecklist() {
478
+ const recentEntries = entries.slice(-20);
479
+ const checklist = [
480
+ 'Verify all dimensions match specification',
481
+ 'Check material properties are appropriate',
482
+ 'Validate manufacturing feasibility',
483
+ 'Review fillet/chamfer radii',
484
+ 'Confirm assembly constraints',
485
+ 'Check for sharp edges and stress concentrators',
486
+ 'Validate wall thickness requirements'
487
+ ];
488
+
489
+ // Add custom items based on recent operations
490
+ const hasHoles = recentEntries.some(e => e.details?.featureType === 'hole' || e.action?.includes('hole'));
491
+ if (hasHoles) checklist.push('Verify hole thread specifications and positions');
492
+
493
+ const hasFillets = recentEntries.some(e => e.details?.featureType === 'fillet');
494
+ if (hasFillets) checklist.push('Check fillet radii meet design requirements');
495
+
496
+ const hasPatterns = recentEntries.some(e => e.details?.featureType === 'pattern');
497
+ if (hasPatterns) checklist.push('Confirm pattern spacing and alignment');
498
+
499
+ return checklist;
500
+ }
501
+
502
+ /**
503
+ * Search entries using natural language
504
+ * @param {string} query - Natural language search query
505
+ * @returns {Array<NotebookEntry>} Matching entries with score
506
+ */
507
+ function searchEntries(query) {
508
+ const keywords = extractKeywords(query);
509
+ const results = entries.map(entry => {
510
+ let score = 0;
511
+ const text = (entry.action + ' ' + (entry.content || '')).toLowerCase();
512
+ keywords.forEach(kw => {
513
+ if (text.includes(kw)) score += 1;
514
+ });
515
+ score += calculateSimilarity(query, entry.action) * 5;
516
+ return { entry, score };
517
+ }).filter(r => r.score > 0)
518
+ .sort((a, b) => b.score - a.score);
519
+
520
+ return results.map(r => r.entry);
521
+ }
522
+
523
+ /**
524
+ * Detect contradictory design decisions
525
+ * @returns {Array<Object>} Array of conflicts
526
+ */
527
+ function detectConflicts() {
528
+ const conflicts = [];
529
+ const decisions = entries.filter(e => e.type === 'decision');
530
+
531
+ for (let i = 0; i < decisions.length; i++) {
532
+ for (let j = i + 1; j < decisions.length; j++) {
533
+ const sim = calculateSimilarity(decisions[i].action, decisions[j].action);
534
+ // Look for similar-sounding decisions that might contradict
535
+ if (sim > 0.6 && decisions[i].action !== decisions[j].action) {
536
+ conflicts.push({
537
+ decision1: decisions[i],
538
+ decision2: decisions[j],
539
+ similarity: sim
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ return conflicts;
546
+ }
547
+
548
+ /**
549
+ * Generate AI-powered engineering report
550
+ * @returns {string} HTML-formatted report
551
+ */
552
+ function generateReport() {
553
+ const summary = autoSummarize(72); // Last 3 days
554
+ const checklist = generateReviewChecklist();
555
+ const conflicts = detectConflicts();
556
+
557
+ let html = `
558
+ <div style="font-family: Georgia, serif; color: #333; line-height: 1.6;">
559
+ <h1>Engineering Design Report</h1>
560
+ <p><strong>Generated:</strong> ${new Date().toLocaleString()}</p>
561
+ <p><strong>Total Entries:</strong> ${entries.length}</p>
562
+
563
+ <h2>Design Summary</h2>
564
+ <pre style="background: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto;">${escapeHtml(summary)}</pre>
565
+
566
+ <h2>Design Review Checklist</h2>
567
+ <ul>
568
+ ${checklist.map(item => `<li>${escapeHtml(item)}</li>`).join('')}
569
+ </ul>
570
+
571
+ ${conflicts.length > 0 ? `
572
+ <h2>Potential Conflicts</h2>
573
+ <ul style="color: #d32f2f;">
574
+ ${conflicts.slice(0, 5).map(c => `
575
+ <li>
576
+ <strong>${new Date(c.decision1.timestamp).toLocaleDateString()}:</strong> "${escapeHtml(c.decision1.action)}"<br/>
577
+ vs <strong>${new Date(c.decision2.timestamp).toLocaleDateString()}:</strong> "${escapeHtml(c.decision2.action)}"
578
+ </li>
579
+ `).join('')}
580
+ </ul>
581
+ ` : ''}
582
+
583
+ <h2>Recent Entries</h2>
584
+ <table style="width: 100%; border-collapse: collapse;">
585
+ <tr style="border-bottom: 2px solid #ddd;">
586
+ <th style="text-align: left; padding: 8px;">Date</th>
587
+ <th style="text-align: left; padding: 8px;">Type</th>
588
+ <th style="text-align: left; padding: 8px;">Action</th>
589
+ </tr>
590
+ ${entries.slice(-20).reverse().map(e => `
591
+ <tr style="border-bottom: 1px solid #eee;">
592
+ <td style="padding: 8px;">${new Date(e.timestamp).toLocaleString()}</td>
593
+ <td style="padding: 8px;"><strong>${e.type}</strong></td>
594
+ <td style="padding: 8px;">${escapeHtml(e.action)}</td>
595
+ </tr>
596
+ `).join('')}
597
+ </table>
598
+ </div>
599
+ `;
600
+
601
+ return html;
602
+ }
603
+
604
+ /**
605
+ * Escape HTML special characters
606
+ * @private
607
+ */
608
+ function escapeHtml(text) {
609
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
610
+ return (text || '').replace(/[&<>"']/g, m => map[m]);
611
+ }
612
+
613
+ // ============================================================================
614
+ // 4. TIMELINE VIEW (~200 lines)
615
+ // ============================================================================
616
+
617
+ /**
618
+ * Create timeline visualization
619
+ * @returns {HTMLElement} Timeline panel
620
+ * @private
621
+ */
622
+ function createTimelineView() {
623
+ const container = document.createElement('div');
624
+ container.className = 'engineering-notebook-timeline';
625
+ container.style.cssText = `
626
+ display: flex;
627
+ flex-direction: column;
628
+ gap: 12px;
629
+ padding: 12px;
630
+ height: 100%;
631
+ overflow-y: auto;
632
+ `;
633
+
634
+ // Filter controls
635
+ const filterBar = document.createElement('div');
636
+ filterBar.style.cssText = `
637
+ display: flex;
638
+ gap: 8px;
639
+ padding: 8px;
640
+ background: var(--color-bg-secondary, #1e1e1e);
641
+ border-radius: 4px;
642
+ flex-wrap: wrap;
643
+ `;
644
+
645
+ const typeFilterLabel = document.createElement('label');
646
+ typeFilterLabel.textContent = 'Filter: ';
647
+ typeFilterLabel.style.fontSize = '12px';
648
+ const typeFilter = document.createElement('select');
649
+ typeFilter.style.cssText = `
650
+ padding: 4px 8px;
651
+ background: var(--color-bg-input, #2d2d2d);
652
+ color: var(--color-text, #e0e0e0);
653
+ border: 1px solid var(--color-border, #404040);
654
+ border-radius: 3px;
655
+ font-size: 12px;
656
+ `;
657
+ const allOpt = document.createElement('option');
658
+ allOpt.value = 'all';
659
+ allOpt.textContent = 'All Types';
660
+ typeFilter.appendChild(allOpt);
661
+ ENTRY_TYPES.forEach(type => {
662
+ const opt = document.createElement('option');
663
+ opt.value = type;
664
+ opt.textContent = type.charAt(0).toUpperCase() + type.slice(1);
665
+ typeFilter.appendChild(opt);
666
+ });
667
+ const autoOpt = document.createElement('option');
668
+ autoOpt.value = 'auto';
669
+ autoOpt.textContent = 'Auto-logged';
670
+ typeFilter.appendChild(autoOpt);
671
+ filterBar.appendChild(typeFilterLabel);
672
+ filterBar.appendChild(typeFilter);
673
+
674
+ const zoomLabel = document.createElement('label');
675
+ zoomLabel.textContent = ' Zoom: ';
676
+ zoomLabel.style.fontSize = '12px';
677
+ zoomLabel.style.marginLeft = '16px';
678
+ const zoomSelect = document.createElement('select');
679
+ zoomSelect.style.cssText = `
680
+ padding: 4px 8px;
681
+ background: var(--color-bg-input, #2d2d2d);
682
+ color: var(--color-text, #e0e0e0);
683
+ border: 1px solid var(--color-border, #404040);
684
+ border-radius: 3px;
685
+ font-size: 12px;
686
+ `;
687
+ ['Day', 'Week', 'Month'].forEach(zoom => {
688
+ const opt = document.createElement('option');
689
+ opt.value = zoom.toLowerCase();
690
+ opt.textContent = zoom;
691
+ zoomSelect.appendChild(opt);
692
+ });
693
+ filterBar.appendChild(zoomLabel);
694
+ filterBar.appendChild(zoomSelect);
695
+ container.appendChild(filterBar);
696
+
697
+ // Timeline container
698
+ const timeline = document.createElement('div');
699
+ timeline.style.cssText = `
700
+ flex: 1;
701
+ position: relative;
702
+ padding: 20px 0;
703
+ `;
704
+
705
+ // Central timeline line
706
+ const line = document.createElement('div');
707
+ line.style.cssText = `
708
+ position: absolute;
709
+ left: 30px;
710
+ top: 0;
711
+ bottom: 0;
712
+ width: 2px;
713
+ background: var(--color-border, #404040);
714
+ `;
715
+ timeline.appendChild(line);
716
+
717
+ // Render entries
718
+ function renderTimeline() {
719
+ const entriesContainer = document.createElement('div');
720
+ entriesContainer.style.cssText = 'position: relative; padding-left: 80px;';
721
+
722
+ let filteredEntries = entries;
723
+ if (typeFilter.value !== 'all') {
724
+ filteredEntries = entries.filter(e => e.type === typeFilter.value);
725
+ }
726
+
727
+ filteredEntries.reverse().forEach(entry => {
728
+ const card = document.createElement('div');
729
+ card.style.cssText = `
730
+ margin-bottom: 24px;
731
+ padding: 12px;
732
+ background: var(--color-bg-secondary, #1e1e1e);
733
+ border-left: 4px solid var(--color-accent, #0284c7);
734
+ border-radius: 4px;
735
+ cursor: pointer;
736
+ transition: background 0.2s;
737
+ `;
738
+
739
+ // Color by type
740
+ const typeColors = {
741
+ auto: '#0284c7',
742
+ decision: '#22c55e',
743
+ issue: '#ef4444',
744
+ requirement: '#f59e0b',
745
+ meeting: '#8b5cf6',
746
+ review: '#ec4899',
747
+ test: '#06b6d4',
748
+ note: '#64748b'
749
+ };
750
+ card.style.borderLeftColor = typeColors[entry.type] || '#0284c7';
751
+
752
+ const time = document.createElement('div');
753
+ time.style.cssText = 'font-size: 11px; color: #888; margin-bottom: 4px;';
754
+ time.textContent = new Date(entry.timestamp).toLocaleString();
755
+ card.appendChild(time);
756
+
757
+ const typeTag = document.createElement('span');
758
+ typeTag.style.cssText = `
759
+ display: inline-block;
760
+ padding: 2px 6px;
761
+ background: ${typeColors[entry.type] || '#0284c7'};
762
+ color: white;
763
+ border-radius: 2px;
764
+ font-size: 10px;
765
+ font-weight: 600;
766
+ margin-right: 6px;
767
+ `;
768
+ typeTag.textContent = entry.type.toUpperCase();
769
+ card.appendChild(typeTag);
770
+
771
+ if (entry.priority !== 'info') {
772
+ const priorityTag = document.createElement('span');
773
+ priorityTag.style.cssText = `
774
+ display: inline-block;
775
+ padding: 2px 6px;
776
+ background: ${entry.priority === 'critical' ? '#dc2626' : '#f59e0b'};
777
+ color: white;
778
+ border-radius: 2px;
779
+ font-size: 10px;
780
+ font-weight: 600;
781
+ `;
782
+ priorityTag.textContent = entry.priority.toUpperCase();
783
+ card.appendChild(priorityTag);
784
+ }
785
+
786
+ const action = document.createElement('div');
787
+ action.style.cssText = 'font-size: 13px; font-weight: 600; margin-top: 6px;';
788
+ action.textContent = entry.action;
789
+ card.appendChild(action);
790
+
791
+ if (entry.tags && entry.tags.length > 0) {
792
+ const tagContainer = document.createElement('div');
793
+ tagContainer.style.cssText = 'margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap;';
794
+ entry.tags.forEach(tag => {
795
+ const tagEl = document.createElement('span');
796
+ tagEl.style.cssText = `
797
+ padding: 2px 6px;
798
+ background: var(--color-bg-tertiary, #252525);
799
+ color: var(--color-text-secondary, #999);
800
+ border-radius: 2px;
801
+ font-size: 10px;
802
+ `;
803
+ tagEl.textContent = tag;
804
+ tagContainer.appendChild(tagEl);
805
+ });
806
+ card.appendChild(tagContainer);
807
+ }
808
+
809
+ card.onclick = () => {
810
+ card.style.background = 'var(--color-bg-tertiary, #252525)';
811
+ };
812
+
813
+ entriesContainer.appendChild(card);
814
+ });
815
+
816
+ timeline.innerHTML = line.outerHTML;
817
+ timeline.appendChild(entriesContainer);
818
+ }
819
+
820
+ renderTimeline();
821
+ typeFilter.onchange = renderTimeline;
822
+
823
+ container.appendChild(timeline);
824
+ return container;
825
+ }
826
+
827
+ // ============================================================================
828
+ // 5. VERSION SNAPSHOTS (~200 lines)
829
+ // ============================================================================
830
+
831
+ /**
832
+ * Create snapshot of current scene state
833
+ * @param {string} label - Snapshot label
834
+ * @returns {VersionSnapshot} Saved snapshot
835
+ */
836
+ function captureSnapshot(label = '') {
837
+ const snapshot = {
838
+ id: 'snap_' + (Math.random() * 1e9 | 0),
839
+ timestamp: Date.now(),
840
+ label: label || `Snapshot ${snapshots.length + 1}`,
841
+ sceneState: serializeScene(),
842
+ parameterSnapshot: captureParameters(),
843
+ constraints: captureConstraints(),
844
+ analysisResults: captureAnalysisResults()
845
+ };
846
+
847
+ snapshots.push(snapshot);
848
+ return snapshot;
849
+ }
850
+
851
+ /**
852
+ * Serialize Three.js scene to JSON
853
+ * @private
854
+ */
855
+ function serializeScene() {
856
+ return {
857
+ objectCount: 0,
858
+ timestamp: Date.now(),
859
+ note: 'Scene serialization (full implementation in main app)'
860
+ };
861
+ }
862
+
863
+ /**
864
+ * Capture current parameter values
865
+ * @private
866
+ */
867
+ function captureParameters() {
868
+ return {
869
+ timestamp: Date.now(),
870
+ parameters: {}
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Capture assembly constraints
876
+ * @private
877
+ */
878
+ function captureConstraints() {
879
+ return [];
880
+ }
881
+
882
+ /**
883
+ * Capture latest analysis results
884
+ * @private
885
+ */
886
+ function captureAnalysisResults() {
887
+ return {};
888
+ }
889
+
890
+ /**
891
+ * Compare two snapshots
892
+ * @param {string} snapId1 - First snapshot ID
893
+ * @param {string} snapId2 - Second snapshot ID
894
+ * @returns {Object} Diff object
895
+ */
896
+ function compareSnapshots(snapId1, snapId2) {
897
+ const snap1 = snapshots.find(s => s.id === snapId1);
898
+ const snap2 = snapshots.find(s => s.id === snapId2);
899
+
900
+ if (!snap1 || !snap2) return null;
901
+
902
+ return {
903
+ snap1: snap1.label,
904
+ snap2: snap2.label,
905
+ added: [],
906
+ removed: [],
907
+ modified: [],
908
+ timestamp1: new Date(snap1.timestamp).toLocaleString(),
909
+ timestamp2: new Date(snap2.timestamp).toLocaleString()
910
+ };
911
+ }
912
+
913
+ /**
914
+ * Restore scene to snapshot state
915
+ * @param {string} snapId - Snapshot ID
916
+ * @returns {boolean} Success
917
+ */
918
+ function restoreSnapshot(snapId) {
919
+ const snapshot = snapshots.find(s => s.id === snapId);
920
+ if (!snapshot) return false;
921
+
922
+ // Implementation would restore Three.js scene, parameters, constraints
923
+ addEntry({
924
+ type: 'auto',
925
+ action: `Restored snapshot: ${snapshot.label}`,
926
+ details: { snapshotId: snapId },
927
+ priority: 'important'
928
+ });
929
+
930
+ return true;
931
+ }
932
+
933
+ /**
934
+ * Create snapshots panel UI
935
+ * @returns {HTMLElement} Snapshots panel
936
+ * @private
937
+ */
938
+ function createSnapshotsView() {
939
+ const container = document.createElement('div');
940
+ container.style.cssText = 'padding: 12px; display: flex; flex-direction: column; gap: 12px;';
941
+
942
+ const captureBtn = document.createElement('button');
943
+ captureBtn.textContent = 'Capture Snapshot';
944
+ captureBtn.style.cssText = `
945
+ padding: 8px 16px;
946
+ background: var(--color-accent, #0284c7);
947
+ color: white;
948
+ border: none;
949
+ border-radius: 3px;
950
+ cursor: pointer;
951
+ font-size: 12px;
952
+ font-weight: 600;
953
+ `;
954
+ captureBtn.onclick = () => {
955
+ const label = prompt('Snapshot label:');
956
+ if (label) {
957
+ const snap = captureSnapshot(label);
958
+ renderSnapshots();
959
+ }
960
+ };
961
+ container.appendChild(captureBtn);
962
+
963
+ function renderSnapshots() {
964
+ const list = container.querySelector('.snapshot-list') || document.createElement('div');
965
+ list.className = 'snapshot-list';
966
+ list.style.cssText = 'display: flex; flex-direction: column; gap: 8px;';
967
+ list.innerHTML = '';
968
+
969
+ snapshots.forEach(snap => {
970
+ const card = document.createElement('div');
971
+ card.style.cssText = `
972
+ padding: 10px;
973
+ background: var(--color-bg-secondary, #1e1e1e);
974
+ border: 1px solid var(--color-border, #404040);
975
+ border-radius: 3px;
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ `;
980
+
981
+ const info = document.createElement('div');
982
+ const label = document.createElement('div');
983
+ label.style.cssText = 'font-size: 13px; font-weight: 600;';
984
+ label.textContent = snap.label;
985
+ const time = document.createElement('div');
986
+ time.style.cssText = 'font-size: 11px; color: #888;';
987
+ time.textContent = new Date(snap.timestamp).toLocaleString();
988
+ info.appendChild(label);
989
+ info.appendChild(time);
990
+ card.appendChild(info);
991
+
992
+ const actions = document.createElement('div');
993
+ actions.style.cssText = 'display: flex; gap: 4px;';
994
+
995
+ const restoreBtn = document.createElement('button');
996
+ restoreBtn.textContent = 'Restore';
997
+ restoreBtn.style.cssText = `
998
+ padding: 4px 8px;
999
+ background: var(--color-bg-tertiary, #252525);
1000
+ color: var(--color-text, #e0e0e0);
1001
+ border: 1px solid var(--color-border, #404040);
1002
+ border-radius: 2px;
1003
+ cursor: pointer;
1004
+ font-size: 11px;
1005
+ `;
1006
+ restoreBtn.onclick = () => {
1007
+ if (confirm(`Restore to "${snap.label}"?`)) {
1008
+ restoreSnapshot(snap.id);
1009
+ }
1010
+ };
1011
+ actions.appendChild(restoreBtn);
1012
+
1013
+ const deleteBtn = document.createElement('button');
1014
+ deleteBtn.textContent = 'Delete';
1015
+ deleteBtn.style.cssText = `
1016
+ padding: 4px 8px;
1017
+ background: #dc2626;
1018
+ color: white;
1019
+ border: none;
1020
+ border-radius: 2px;
1021
+ cursor: pointer;
1022
+ font-size: 11px;
1023
+ `;
1024
+ deleteBtn.onclick = () => {
1025
+ snapshots = snapshots.filter(s => s.id !== snap.id);
1026
+ renderSnapshots();
1027
+ };
1028
+ actions.appendChild(deleteBtn);
1029
+
1030
+ card.appendChild(actions);
1031
+ list.appendChild(card);
1032
+ });
1033
+
1034
+ if (!container.querySelector('.snapshot-list')) {
1035
+ container.appendChild(list);
1036
+ }
1037
+ }
1038
+
1039
+ renderSnapshots();
1040
+ return container;
1041
+ }
1042
+
1043
+ // ============================================================================
1044
+ // 6. EXPORT & SHARING (~150 lines)
1045
+ // ============================================================================
1046
+
1047
+ /**
1048
+ * Export notebook in specified format
1049
+ * @param {string} format - 'html' | 'pdf-html' | 'markdown' | 'json'
1050
+ * @param {Object} options - Export options
1051
+ * @param {number} [options.hoursBack] - Hours to include (default: all)
1052
+ * @param {Array<string>} [options.types] - Entry types to include (default: all)
1053
+ * @param {boolean} [options.includeAutoLog] - Include auto-logged entries (default: false)
1054
+ * @returns {string} Exported content
1055
+ */
1056
+ function exportNotebook(format = 'html', options = {}) {
1057
+ let filteredEntries = entries;
1058
+
1059
+ if (options.hoursBack) {
1060
+ const cutoff = Date.now() - (options.hoursBack * 3600000);
1061
+ filteredEntries = filteredEntries.filter(e => e.timestamp >= cutoff);
1062
+ }
1063
+
1064
+ if (options.types) {
1065
+ filteredEntries = filteredEntries.filter(e => options.types.includes(e.type));
1066
+ }
1067
+
1068
+ if (!options.includeAutoLog) {
1069
+ filteredEntries = filteredEntries.filter(e => e.type !== 'auto');
1070
+ }
1071
+
1072
+ switch (format) {
1073
+ case 'json':
1074
+ return JSON.stringify({
1075
+ exportDate: new Date().toISOString(),
1076
+ entryCount: filteredEntries.length,
1077
+ snapshotCount: snapshots.length,
1078
+ entries: filteredEntries,
1079
+ snapshots: snapshots
1080
+ }, null, 2);
1081
+
1082
+ case 'markdown':
1083
+ let md = `# Engineering Notebook\n\n`;
1084
+ md += `**Export Date:** ${new Date().toLocaleString()}\n`;
1085
+ md += `**Total Entries:** ${filteredEntries.length}\n\n`;
1086
+
1087
+ filteredEntries.reverse().forEach(entry => {
1088
+ md += `## ${entry.action}\n\n`;
1089
+ md += `**Type:** ${entry.type} | **Priority:** ${entry.priority}\n`;
1090
+ md += `**Date:** ${new Date(entry.timestamp).toLocaleString()}\n`;
1091
+ if (entry.tags.length > 0) {
1092
+ md += `**Tags:** ${entry.tags.join(', ')}\n`;
1093
+ }
1094
+ if (entry.content) {
1095
+ md += `\n${entry.content}\n`;
1096
+ }
1097
+ md += `\n---\n\n`;
1098
+ });
1099
+
1100
+ return md;
1101
+
1102
+ case 'pdf-html':
1103
+ case 'html':
1104
+ default:
1105
+ let html = generateReport();
1106
+ if (format === 'pdf-html') {
1107
+ html = `
1108
+ <!DOCTYPE html>
1109
+ <html>
1110
+ <head>
1111
+ <meta charset="UTF-8">
1112
+ <style>
1113
+ body { font-family: Georgia, serif; margin: 40px; }
1114
+ h1 { page-break-before: always; }
1115
+ h2 { margin-top: 24px; }
1116
+ table { width: 100%; border-collapse: collapse; page-break-inside: avoid; }
1117
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
1118
+ </style>
1119
+ </head>
1120
+ <body>${html}</body>
1121
+ </html>
1122
+ `;
1123
+ }
1124
+ return html;
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Generate shareable link (stored in memory, no server)
1130
+ * @returns {string} Link identifier
1131
+ */
1132
+ function generateShareLink() {
1133
+ const linkId = Math.random().toString(36).substring(2, 11);
1134
+ // In production, this would upload to server and return URL
1135
+ return `cyclecad.com/notebook/${linkId}`;
1136
+ }
1137
+
1138
+ // ============================================================================
1139
+ // PUBLIC API
1140
+ // ============================================================================
1141
+
1142
+ /**
1143
+ * Add new notebook entry
1144
+ * @param {Object} entryData - Entry data
1145
+ * @returns {NotebookEntry} Created entry
1146
+ */
1147
+ function addEntry(entryData) {
1148
+ const entry = {
1149
+ id: 'entry_' + (entryIdCounter++),
1150
+ timestamp: Date.now(),
1151
+ userId: 'anonymous',
1152
+ tags: [],
1153
+ priority: 'info',
1154
+ ...entryData
1155
+ };
1156
+
1157
+ entries.push(entry);
1158
+ return entry;
1159
+ }
1160
+
1161
+ /**
1162
+ * Search notebook entries
1163
+ * @param {string} query - Search query
1164
+ * @returns {Array<NotebookEntry>} Matching entries
1165
+ */
1166
+ function search(query) {
1167
+ return searchEntries(query);
1168
+ }
1169
+
1170
+ /**
1171
+ * Get timeline view of entries
1172
+ * @returns {Array<NotebookEntry>} All entries in chronological order
1173
+ */
1174
+ function getTimeline() {
1175
+ return [...entries].sort((a, b) => a.timestamp - b.timestamp);
1176
+ }
1177
+
1178
+ /**
1179
+ * Generate engineering report
1180
+ * @returns {string} HTML report
1181
+ */
1182
+ function generateReport_Public() {
1183
+ return generateReport();
1184
+ }
1185
+
1186
+ /**
1187
+ * Initialize the module and attach to page
1188
+ * @param {HTMLElement} container - Container element
1189
+ */
1190
+ function init(container) {
1191
+ initAutoLogging();
1192
+
1193
+ if (!container) return;
1194
+
1195
+ // Create tabbed interface
1196
+ const panel = document.createElement('div');
1197
+ panel.style.cssText = `
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ height: 100%;
1201
+ background: var(--color-bg-primary, #1a1a1a);
1202
+ color: var(--color-text, #e0e0e0);
1203
+ border: 1px solid var(--color-border, #404040);
1204
+ border-radius: 4px;
1205
+ overflow: hidden;
1206
+ `;
1207
+
1208
+ // Tabs
1209
+ const tabBar = document.createElement('div');
1210
+ tabBar.style.cssText = `
1211
+ display: flex;
1212
+ gap: 0;
1213
+ padding: 0;
1214
+ background: var(--color-bg-secondary, #1e1e1e);
1215
+ border-bottom: 1px solid var(--color-border, #404040);
1216
+ overflow-x: auto;
1217
+ `;
1218
+
1219
+ const tabs = [
1220
+ { id: 'timeline', label: `Timeline (${entries.length})`, creator: createTimelineView },
1221
+ { id: 'entry', label: 'New Entry', creator: createManualEntryEditor },
1222
+ { id: 'search', label: 'Search', creator: createSearchView },
1223
+ { id: 'snapshots', label: `Snapshots (${snapshots.length})`, creator: createSnapshotsView },
1224
+ { id: 'report', label: 'Report', creator: createReportView }
1225
+ ];
1226
+
1227
+ const tabContent = document.createElement('div');
1228
+ tabContent.style.cssText = `
1229
+ flex: 1;
1230
+ overflow-y: auto;
1231
+ background: var(--color-bg-primary, #1a1a1a);
1232
+ `;
1233
+
1234
+ tabs.forEach((tab, idx) => {
1235
+ const tabBtn = document.createElement('button');
1236
+ tabBtn.textContent = tab.label;
1237
+ tabBtn.style.cssText = `
1238
+ padding: 10px 16px;
1239
+ background: ${idx === 0 ? 'var(--color-bg-tertiary, #252525)' : 'transparent'};
1240
+ color: var(--color-text, #e0e0e0);
1241
+ border: none;
1242
+ border-bottom: ${idx === 0 ? '2px solid var(--color-accent, #0284c7)' : 'none'};
1243
+ cursor: pointer;
1244
+ font-size: 12px;
1245
+ font-weight: 600;
1246
+ white-space: nowrap;
1247
+ `;
1248
+ tabBtn.onclick = () => {
1249
+ tabs.forEach((_, i) => {
1250
+ const btn = tabBar.children[i];
1251
+ btn.style.background = i === idx ? 'var(--color-bg-tertiary, #252525)' : 'transparent';
1252
+ btn.style.borderBottom = i === idx ? '2px solid var(--color-accent, #0284c7)' : 'none';
1253
+ });
1254
+ tabContent.innerHTML = '';
1255
+ tabContent.appendChild(tab.creator());
1256
+ };
1257
+ tabBar.appendChild(tabBtn);
1258
+ });
1259
+
1260
+ panel.appendChild(tabBar);
1261
+ panel.appendChild(tabContent);
1262
+
1263
+ // Initial content
1264
+ tabContent.appendChild(createTimelineView());
1265
+
1266
+ container.appendChild(panel);
1267
+ currentUI = panel;
1268
+ }
1269
+
1270
+ /**
1271
+ * Get UI element
1272
+ * @returns {HTMLElement} Current UI element
1273
+ */
1274
+ function getUI() {
1275
+ const container = document.createElement('div');
1276
+ container.style.cssText = 'width: 100%; height: 100%;';
1277
+ init(container);
1278
+ return container;
1279
+ }
1280
+
1281
+ /**
1282
+ * Execute command from Agent API
1283
+ * @param {Object} params - Command parameters
1284
+ * @returns {*} Command result
1285
+ */
1286
+ function execute(params) {
1287
+ switch (params.command) {
1288
+ case 'addEntry':
1289
+ return addEntry(params.data);
1290
+ case 'search':
1291
+ return search(params.query);
1292
+ case 'snapshot':
1293
+ return captureSnapshot(params.label);
1294
+ case 'export':
1295
+ return exportNotebook(params.format, params.options);
1296
+ case 'summary':
1297
+ return autoSummarize(params.hoursBack);
1298
+ case 'checklist':
1299
+ return generateReviewChecklist();
1300
+ default:
1301
+ return null;
1302
+ }
1303
+ }
1304
+
1305
+ /**
1306
+ * Create search view UI
1307
+ * @private
1308
+ */
1309
+ function createSearchView() {
1310
+ const container = document.createElement('div');
1311
+ container.style.cssText = 'padding: 12px; display: flex; flex-direction: column; gap: 12px;';
1312
+
1313
+ const searchBox = document.createElement('input');
1314
+ searchBox.type = 'text';
1315
+ searchBox.placeholder = 'Search entries...';
1316
+ searchBox.style.cssText = `
1317
+ padding: 8px;
1318
+ background: var(--color-bg-input, #2d2d2d);
1319
+ color: var(--color-text, #e0e0e0);
1320
+ border: 1px solid var(--color-border, #404040);
1321
+ border-radius: 3px;
1322
+ font-size: 13px;
1323
+ `;
1324
+ container.appendChild(searchBox);
1325
+
1326
+ const results = document.createElement('div');
1327
+ results.style.cssText = 'display: flex; flex-direction: column; gap: 8px; flex: 1; overflow-y: auto;';
1328
+
1329
+ searchBox.oninput = () => {
1330
+ const matches = search(searchBox.value);
1331
+ results.innerHTML = '';
1332
+
1333
+ if (matches.length === 0) {
1334
+ const noResults = document.createElement('div');
1335
+ noResults.style.cssText = 'color: #888; font-size: 12px;';
1336
+ noResults.textContent = 'No results found';
1337
+ results.appendChild(noResults);
1338
+ } else {
1339
+ matches.forEach(entry => {
1340
+ const card = document.createElement('div');
1341
+ card.style.cssText = `
1342
+ padding: 10px;
1343
+ background: var(--color-bg-secondary, #1e1e1e);
1344
+ border-left: 3px solid var(--color-accent, #0284c7);
1345
+ border-radius: 3px;
1346
+ `;
1347
+
1348
+ const time = document.createElement('div');
1349
+ time.style.cssText = 'font-size: 11px; color: #888;';
1350
+ time.textContent = new Date(entry.timestamp).toLocaleString();
1351
+ card.appendChild(time);
1352
+
1353
+ const action = document.createElement('div');
1354
+ action.style.cssText = 'font-size: 13px; font-weight: 600; margin-top: 4px;';
1355
+ action.textContent = entry.action;
1356
+ card.appendChild(action);
1357
+
1358
+ results.appendChild(card);
1359
+ });
1360
+ }
1361
+ };
1362
+
1363
+ container.appendChild(results);
1364
+ return container;
1365
+ }
1366
+
1367
+ /**
1368
+ * Create report view UI
1369
+ * @private
1370
+ */
1371
+ function createReportView() {
1372
+ const container = document.createElement('div');
1373
+ container.style.cssText = 'padding: 12px; display: flex; flex-direction: column; gap: 12px;';
1374
+
1375
+ // Format selector
1376
+ const formatRow = document.createElement('div');
1377
+ formatRow.style.cssText = 'display: flex; gap: 8px; align-items: center;';
1378
+ const formatLabel = document.createElement('label');
1379
+ formatLabel.textContent = 'Format: ';
1380
+ formatLabel.style.fontSize = '12px';
1381
+ const formatSelect = document.createElement('select');
1382
+ formatSelect.style.cssText = `
1383
+ padding: 4px 8px;
1384
+ background: var(--color-bg-input, #2d2d2d);
1385
+ color: var(--color-text, #e0e0e0);
1386
+ border: 1px solid var(--color-border, #404040);
1387
+ border-radius: 3px;
1388
+ font-size: 12px;
1389
+ `;
1390
+ ['html', 'markdown', 'json'].forEach(fmt => {
1391
+ const opt = document.createElement('option');
1392
+ opt.value = fmt;
1393
+ opt.textContent = fmt.toUpperCase();
1394
+ formatSelect.appendChild(opt);
1395
+ });
1396
+ formatRow.appendChild(formatLabel);
1397
+ formatRow.appendChild(formatSelect);
1398
+ container.appendChild(formatRow);
1399
+
1400
+ // Date range
1401
+ const dateRow = document.createElement('div');
1402
+ dateRow.style.cssText = 'display: flex; gap: 8px; align-items: center;';
1403
+ const dateLabel = document.createElement('label');
1404
+ dateLabel.textContent = 'Last: ';
1405
+ dateLabel.style.fontSize = '12px';
1406
+ const hoursInput = document.createElement('input');
1407
+ hoursInput.type = 'number';
1408
+ hoursInput.min = '1';
1409
+ hoursInput.value = '24';
1410
+ hoursInput.style.cssText = `
1411
+ width: 60px;
1412
+ padding: 4px 8px;
1413
+ background: var(--color-bg-input, #2d2d2d);
1414
+ color: var(--color-text, #e0e0e0);
1415
+ border: 1px solid var(--color-border, #404040);
1416
+ border-radius: 3px;
1417
+ font-size: 12px;
1418
+ `;
1419
+ const hoursLabel = document.createElement('label');
1420
+ hoursLabel.textContent = ' hours';
1421
+ hoursLabel.style.fontSize = '12px';
1422
+ dateRow.appendChild(dateLabel);
1423
+ dateRow.appendChild(hoursInput);
1424
+ dateRow.appendChild(hoursLabel);
1425
+ container.appendChild(dateRow);
1426
+
1427
+ // Generate button
1428
+ const genBtn = document.createElement('button');
1429
+ genBtn.textContent = 'Generate Report';
1430
+ genBtn.style.cssText = `
1431
+ padding: 8px 16px;
1432
+ background: var(--color-accent, #0284c7);
1433
+ color: white;
1434
+ border: none;
1435
+ border-radius: 3px;
1436
+ cursor: pointer;
1437
+ font-size: 12px;
1438
+ font-weight: 600;
1439
+ `;
1440
+ genBtn.onclick = () => {
1441
+ const content = exportNotebook(formatSelect.value, { hoursBack: parseInt(hoursInput.value) });
1442
+ const output = document.createElement('div');
1443
+ output.style.cssText = `
1444
+ margin-top: 12px;
1445
+ padding: 12px;
1446
+ background: var(--color-bg-secondary, #1e1e1e);
1447
+ border: 1px solid var(--color-border, #404040);
1448
+ border-radius: 3px;
1449
+ max-height: 400px;
1450
+ overflow-y: auto;
1451
+ font-size: 11px;
1452
+ font-family: 'Courier New', monospace;
1453
+ white-space: pre-wrap;
1454
+ `;
1455
+ output.textContent = content.substring(0, 2000);
1456
+ container.appendChild(output);
1457
+
1458
+ const downloadBtn = document.createElement('button');
1459
+ downloadBtn.textContent = 'Download';
1460
+ downloadBtn.style.cssText = `
1461
+ margin-top: 8px;
1462
+ padding: 6px 12px;
1463
+ background: var(--color-bg-tertiary, #252525);
1464
+ color: var(--color-text, #e0e0e0);
1465
+ border: 1px solid var(--color-border, #404040);
1466
+ border-radius: 3px;
1467
+ cursor: pointer;
1468
+ font-size: 11px;
1469
+ `;
1470
+ downloadBtn.onclick = () => {
1471
+ const blob = new Blob([content], { type: 'text/plain' });
1472
+ const url = URL.createObjectURL(blob);
1473
+ const a = document.createElement('a');
1474
+ a.href = url;
1475
+ a.download = `notebook-report.${formatSelect.value === 'json' ? 'json' : formatSelect.value === 'markdown' ? 'md' : 'html'}`;
1476
+ a.click();
1477
+ };
1478
+ container.appendChild(downloadBtn);
1479
+ };
1480
+ container.appendChild(genBtn);
1481
+
1482
+ return container;
1483
+ }
1484
+
1485
+ // Export public API
1486
+ return {
1487
+ init,
1488
+ getUI,
1489
+ execute,
1490
+ addEntry,
1491
+ search,
1492
+ generateReport: generateReport_Public,
1493
+ getTimeline,
1494
+ captureSnapshot,
1495
+ compareSnapshots,
1496
+ restoreSnapshot,
1497
+ exportNotebook,
1498
+ autoSummarize,
1499
+ generateReviewChecklist,
1500
+ logGeometryCreate,
1501
+ logFeatureApply,
1502
+ logParamChange,
1503
+ toggleAutoLogging: () => { autoLoggingEnabled = !autoLoggingEnabled; }
1504
+ };
1505
+ })();