cyclecad 3.8.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.
- package/app/index.html +96 -0
- package/app/js/modules/auto-assembly.js +1146 -0
- package/app/js/modules/digital-twin.js +1225 -0
- package/app/js/modules/engineering-notebook.js +1505 -0
- package/app/js/modules/machine-control.js +1270 -0
- package/app/js/modules/parametric-from-example.js +900 -0
- package/app/js/modules/smart-assembly.js +1667 -0
- package/package.json +1 -1
|
@@ -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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
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
|
+
})();
|