synthos 0.7.0 → 0.7.2
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/default-themes/nebula-dawn.css +682 -0
- package/default-themes/nebula-dawn.json +19 -0
- package/default-themes/nebula-dusk.css +674 -0
- package/default-themes/nebula-dusk.json +19 -0
- package/package.json +4 -1
- package/page-scripts/helpers-v2.js +121 -0
- package/page-scripts/page-v2.js +615 -0
- package/tests/README.md +12 -0
- package/tests/migrations.spec.ts +91 -0
- package/tests/pages.spec.ts +103 -0
- package/tests/transformPage.spec.ts +414 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
if (window.__synthOSChatPanel) return;
|
|
3
|
+
window.__synthOSChatPanel = true;
|
|
4
|
+
|
|
5
|
+
// 0. Themed tooltips for chat panel controls
|
|
6
|
+
(function() {
|
|
7
|
+
var style = document.createElement('style');
|
|
8
|
+
style.textContent =
|
|
9
|
+
'.synthos-tooltip {' +
|
|
10
|
+
'position: fixed;' +
|
|
11
|
+
'padding: 6px 10px;' +
|
|
12
|
+
'background: var(--bg-tertiary, #0f0f23);' +
|
|
13
|
+
'color: var(--text-secondary, #b794f6);' +
|
|
14
|
+
'border: 1px solid var(--border-color, rgba(138,43,226,0.3));' +
|
|
15
|
+
'border-radius: 6px;' +
|
|
16
|
+
'font-size: 12px;' +
|
|
17
|
+
'max-width: 150px;' +
|
|
18
|
+
'text-align: center;' +
|
|
19
|
+
'pointer-events: none;' +
|
|
20
|
+
'z-index: 10000;' +
|
|
21
|
+
'box-shadow: 0 2px 8px rgba(0,0,0,0.3);' +
|
|
22
|
+
'opacity: 0;' +
|
|
23
|
+
'transition: opacity 0.15s;' +
|
|
24
|
+
'}' +
|
|
25
|
+
'.synthos-tooltip.visible { opacity: 1; }';
|
|
26
|
+
document.head.appendChild(style);
|
|
27
|
+
|
|
28
|
+
var tip = document.createElement('div');
|
|
29
|
+
tip.className = 'synthos-tooltip';
|
|
30
|
+
document.body.appendChild(tip);
|
|
31
|
+
|
|
32
|
+
function show(el) {
|
|
33
|
+
tip.textContent = el.getAttribute('data-tooltip');
|
|
34
|
+
tip.style.display = 'block';
|
|
35
|
+
tip.classList.remove('visible');
|
|
36
|
+
var r = el.getBoundingClientRect();
|
|
37
|
+
var tw = tip.offsetWidth;
|
|
38
|
+
var left = r.left + (r.width / 2) - (tw / 2);
|
|
39
|
+
if (left < 4) left = 4;
|
|
40
|
+
if (left + tw > window.innerWidth - 4) left = window.innerWidth - tw - 4;
|
|
41
|
+
tip.style.left = left + 'px';
|
|
42
|
+
tip.style.top = (r.top - tip.offsetHeight - 6) + 'px';
|
|
43
|
+
void tip.offsetWidth;
|
|
44
|
+
tip.classList.add('visible');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hide() {
|
|
48
|
+
tip.classList.remove('visible');
|
|
49
|
+
tip.style.display = 'none';
|
|
50
|
+
}
|
|
51
|
+
hide();
|
|
52
|
+
|
|
53
|
+
function attach(el, text) {
|
|
54
|
+
el.setAttribute('data-tooltip', text);
|
|
55
|
+
el.addEventListener('mouseenter', function() { show(el); });
|
|
56
|
+
el.addEventListener('mouseleave', hide);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pages link is never renamed
|
|
60
|
+
var pagesLink = document.getElementById('pagesLink');
|
|
61
|
+
if (pagesLink) attach(pagesLink, 'Browse all pages');
|
|
62
|
+
|
|
63
|
+
// Save and Reset tooltips deferred — locked-mode renames them on DOMContentLoaded
|
|
64
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
65
|
+
setTimeout(function() {
|
|
66
|
+
var s = document.getElementById('saveLink');
|
|
67
|
+
if (s) attach(s, s.textContent.trim() === 'Copy' ? 'Copy page as a new name' : 'Save page as a new name');
|
|
68
|
+
var r = document.getElementById('resetLink');
|
|
69
|
+
if (r) attach(r, r.textContent.trim() === 'Reload' ? 'Reload this page' : 'Reset page to default');
|
|
70
|
+
}, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
window.__synthOSTooltip = attach;
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
// 1. Initial focus
|
|
77
|
+
var chatInput = document.getElementById('chatInput');
|
|
78
|
+
if (chatInput) chatInput.focus();
|
|
79
|
+
|
|
80
|
+
// 2. Form submit handler — show overlay + disable inputs
|
|
81
|
+
var chatForm = document.getElementById('chatForm');
|
|
82
|
+
if (chatForm) {
|
|
83
|
+
chatForm.addEventListener('submit', function() {
|
|
84
|
+
var overlay = document.getElementById('loadingOverlay');
|
|
85
|
+
if (overlay) overlay.style.display = 'flex';
|
|
86
|
+
chatForm.action = window.location.pathname;
|
|
87
|
+
setTimeout(function() {
|
|
88
|
+
var ci = document.getElementById('chatInput');
|
|
89
|
+
if (ci) ci.disabled = true;
|
|
90
|
+
var sb = document.querySelector('.chat-submit');
|
|
91
|
+
if (sb) sb.disabled = true;
|
|
92
|
+
document.querySelectorAll('.link-group a').forEach(function(a) {
|
|
93
|
+
a.style.pointerEvents = 'none';
|
|
94
|
+
a.style.opacity = '0.5';
|
|
95
|
+
});
|
|
96
|
+
}, 50);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Save link handler — themed modal with title, categories, greeting
|
|
101
|
+
(function() {
|
|
102
|
+
var saveLink = document.getElementById('saveLink');
|
|
103
|
+
if (!saveLink) return;
|
|
104
|
+
|
|
105
|
+
// Detect if current page is a Builders or System page (start with blank fields)
|
|
106
|
+
var isBuilder = window.pageInfo && Array.isArray(window.pageInfo.categories) &&
|
|
107
|
+
(window.pageInfo.categories.indexOf('Builders') !== -1 ||
|
|
108
|
+
window.pageInfo.categories.indexOf('System') !== -1);
|
|
109
|
+
|
|
110
|
+
// Original title for change detection
|
|
111
|
+
var originalTitle = (window.pageInfo && window.pageInfo.title) ? window.pageInfo.title : '';
|
|
112
|
+
|
|
113
|
+
// --- Create save modal ---
|
|
114
|
+
var modal = document.createElement('div');
|
|
115
|
+
modal.id = 'saveModal';
|
|
116
|
+
modal.className = 'modal-overlay';
|
|
117
|
+
modal.innerHTML =
|
|
118
|
+
'<div class="modal-content" style="max-width:480px;">' +
|
|
119
|
+
'<div class="modal-header">' +
|
|
120
|
+
'<span>Save Page</span>' +
|
|
121
|
+
'<button type="button" class="brainstorm-close-btn" id="saveCloseBtn">×</button>' +
|
|
122
|
+
'</div>' +
|
|
123
|
+
'<div class="modal-body" style="display:flex;flex-direction:column;gap:12px;padding:16px 20px;">' +
|
|
124
|
+
'<div>' +
|
|
125
|
+
'<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Display Title <span style="color:var(--accent-primary);">*</span></label>' +
|
|
126
|
+
'<input type="text" id="saveTitleInput" class="brainstorm-input" placeholder="Enter a display title..." style="width:100%;box-sizing:border-box;">' +
|
|
127
|
+
'<div id="saveTitleError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">Title is required</div>' +
|
|
128
|
+
'</div>' +
|
|
129
|
+
'<div>' +
|
|
130
|
+
'<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Categories <span style="color:var(--accent-primary);">*</span></label>' +
|
|
131
|
+
'<input type="text" id="saveCategoriesInput" class="brainstorm-input" placeholder="e.g. Tool, Game, Utility" style="width:100%;box-sizing:border-box;">' +
|
|
132
|
+
'<div id="saveCategoriesError" style="color:#ff6b6b;font-size:12px;margin-top:4px;display:none;">At least one category is required</div>' +
|
|
133
|
+
'</div>' +
|
|
134
|
+
'<div>' +
|
|
135
|
+
'<label style="display:block;margin-bottom:4px;font-size:13px;color:var(--text-secondary);">Greeting <span style="font-size:11px;opacity:0.7;">(optional)</span></label>' +
|
|
136
|
+
'<input type="text" id="saveGreetingInput" class="brainstorm-input" placeholder="Available when title changes" style="width:100%;box-sizing:border-box;" disabled>' +
|
|
137
|
+
'<div style="font-size:11px;color:var(--text-secondary);margin-top:4px;opacity:0.7;" id="saveGreetingHint">Change the title to enable a custom greeting.</div>' +
|
|
138
|
+
'</div>' +
|
|
139
|
+
'</div>' +
|
|
140
|
+
'<div class="modal-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;">' +
|
|
141
|
+
'<button type="button" class="brainstorm-send-btn" id="saveCancelBtn" style="background:transparent;border:1px solid var(--border-color);color:var(--text-secondary);">Cancel</button>' +
|
|
142
|
+
'<button type="button" class="brainstorm-send-btn" id="saveConfirmBtn">Save</button>' +
|
|
143
|
+
'</div>' +
|
|
144
|
+
'</div>';
|
|
145
|
+
document.body.appendChild(modal);
|
|
146
|
+
|
|
147
|
+
// --- Create error modal ---
|
|
148
|
+
var errorModal = document.createElement('div');
|
|
149
|
+
errorModal.id = 'errorModal';
|
|
150
|
+
errorModal.className = 'modal-overlay';
|
|
151
|
+
errorModal.innerHTML =
|
|
152
|
+
'<div class="modal-content" style="max-width:400px;">' +
|
|
153
|
+
'<div class="modal-header">' +
|
|
154
|
+
'<span>Error</span>' +
|
|
155
|
+
'<button type="button" class="brainstorm-close-btn" id="errorCloseBtn">×</button>' +
|
|
156
|
+
'</div>' +
|
|
157
|
+
'<div class="modal-body" style="padding:16px 20px;">' +
|
|
158
|
+
'<p id="errorMessage" style="margin:0;color:var(--text-primary);"></p>' +
|
|
159
|
+
'</div>' +
|
|
160
|
+
'<div class="modal-footer" style="display:flex;justify-content:flex-end;padding:12px 20px;">' +
|
|
161
|
+
'<button type="button" class="brainstorm-send-btn" id="errorOkBtn">OK</button>' +
|
|
162
|
+
'</div>' +
|
|
163
|
+
'</div>';
|
|
164
|
+
document.body.appendChild(errorModal);
|
|
165
|
+
|
|
166
|
+
// --- Element references ---
|
|
167
|
+
var titleInput = document.getElementById('saveTitleInput');
|
|
168
|
+
var categoriesInput = document.getElementById('saveCategoriesInput');
|
|
169
|
+
var greetingInput = document.getElementById('saveGreetingInput');
|
|
170
|
+
var greetingHint = document.getElementById('saveGreetingHint');
|
|
171
|
+
var titleError = document.getElementById('saveTitleError');
|
|
172
|
+
var categoriesError = document.getElementById('saveCategoriesError');
|
|
173
|
+
|
|
174
|
+
// --- Greeting enable/disable based on title change ---
|
|
175
|
+
titleInput.addEventListener('input', function() {
|
|
176
|
+
var changed = titleInput.value.trim() !== originalTitle;
|
|
177
|
+
greetingInput.disabled = !changed;
|
|
178
|
+
if (changed) {
|
|
179
|
+
greetingInput.placeholder = 'Enter a custom greeting...';
|
|
180
|
+
greetingHint.textContent = 'Replaces the initial Synthos greeting and removes chat history.';
|
|
181
|
+
} else {
|
|
182
|
+
greetingInput.placeholder = 'Available when title changes';
|
|
183
|
+
greetingInput.value = '';
|
|
184
|
+
greetingHint.textContent = 'Change the title to enable a custom greeting.';
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// --- Open modal ---
|
|
189
|
+
function openSaveModal() {
|
|
190
|
+
// Pre-fill fields (blank for Builder pages)
|
|
191
|
+
titleInput.value = isBuilder ? '' : originalTitle;
|
|
192
|
+
categoriesInput.value = isBuilder ? '' : (
|
|
193
|
+
(window.pageInfo && Array.isArray(window.pageInfo.categories))
|
|
194
|
+
? window.pageInfo.categories.join(', ')
|
|
195
|
+
: ''
|
|
196
|
+
);
|
|
197
|
+
greetingInput.value = '';
|
|
198
|
+
greetingInput.disabled = true;
|
|
199
|
+
greetingInput.placeholder = 'Available when title changes';
|
|
200
|
+
greetingHint.textContent = 'Change the title to enable a custom greeting.';
|
|
201
|
+
titleError.style.display = 'none';
|
|
202
|
+
categoriesError.style.display = 'none';
|
|
203
|
+
modal.classList.add('show');
|
|
204
|
+
titleInput.focus();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function closeSaveModal() {
|
|
208
|
+
modal.classList.remove('show');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function showError(msg) {
|
|
212
|
+
document.getElementById('errorMessage').textContent = msg;
|
|
213
|
+
errorModal.classList.add('show');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function closeError() {
|
|
217
|
+
errorModal.classList.remove('show');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Submit ---
|
|
221
|
+
function submitSave() {
|
|
222
|
+
var title = titleInput.value.trim();
|
|
223
|
+
var cats = categoriesInput.value.trim();
|
|
224
|
+
var greeting = greetingInput.value.trim();
|
|
225
|
+
var valid = true;
|
|
226
|
+
|
|
227
|
+
// Validate
|
|
228
|
+
if (!title) {
|
|
229
|
+
titleError.style.display = 'block';
|
|
230
|
+
valid = false;
|
|
231
|
+
} else {
|
|
232
|
+
titleError.style.display = 'none';
|
|
233
|
+
}
|
|
234
|
+
if (!cats) {
|
|
235
|
+
categoriesError.style.display = 'block';
|
|
236
|
+
valid = false;
|
|
237
|
+
} else {
|
|
238
|
+
categoriesError.style.display = 'none';
|
|
239
|
+
}
|
|
240
|
+
if (!valid) return;
|
|
241
|
+
|
|
242
|
+
// Parse categories
|
|
243
|
+
var categories = cats.split(',').map(function(c) { return c.trim(); }).filter(Boolean);
|
|
244
|
+
|
|
245
|
+
// Disable button during save
|
|
246
|
+
var confirmBtn = document.getElementById('saveConfirmBtn');
|
|
247
|
+
confirmBtn.disabled = true;
|
|
248
|
+
confirmBtn.textContent = 'Saving...';
|
|
249
|
+
|
|
250
|
+
var body = { title: title, categories: categories };
|
|
251
|
+
if (greeting && !greetingInput.disabled) {
|
|
252
|
+
body.greeting = greeting;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fetch(window.location.pathname + '/save', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
body: JSON.stringify(body)
|
|
259
|
+
})
|
|
260
|
+
.then(function(res) {
|
|
261
|
+
return res.json().then(function(data) {
|
|
262
|
+
return { ok: res.ok, data: data };
|
|
263
|
+
});
|
|
264
|
+
})
|
|
265
|
+
.then(function(result) {
|
|
266
|
+
if (result.ok && result.data.redirect) {
|
|
267
|
+
window.location.href = result.data.redirect;
|
|
268
|
+
} else {
|
|
269
|
+
closeSaveModal();
|
|
270
|
+
showError(result.data.error || 'An unknown error occurred');
|
|
271
|
+
confirmBtn.disabled = false;
|
|
272
|
+
confirmBtn.textContent = 'Save';
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
.catch(function(err) {
|
|
276
|
+
closeSaveModal();
|
|
277
|
+
showError('Network error: ' + err.message);
|
|
278
|
+
confirmBtn.disabled = false;
|
|
279
|
+
confirmBtn.textContent = 'Save';
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Event listeners ---
|
|
284
|
+
saveLink.addEventListener('click', openSaveModal);
|
|
285
|
+
document.getElementById('saveCloseBtn').addEventListener('click', closeSaveModal);
|
|
286
|
+
document.getElementById('saveCancelBtn').addEventListener('click', closeSaveModal);
|
|
287
|
+
document.getElementById('saveConfirmBtn').addEventListener('click', submitSave);
|
|
288
|
+
document.getElementById('errorCloseBtn').addEventListener('click', closeError);
|
|
289
|
+
document.getElementById('errorOkBtn').addEventListener('click', closeError);
|
|
290
|
+
|
|
291
|
+
modal.addEventListener('click', function(e) {
|
|
292
|
+
if (e.target === modal) closeSaveModal();
|
|
293
|
+
});
|
|
294
|
+
errorModal.addEventListener('click', function(e) {
|
|
295
|
+
if (e.target === errorModal) closeError();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
document.addEventListener('keydown', function(e) {
|
|
299
|
+
if (e.key === 'Escape') {
|
|
300
|
+
if (modal.classList.contains('show')) closeSaveModal();
|
|
301
|
+
if (errorModal.classList.contains('show')) closeError();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Enter key in title/categories inputs triggers save
|
|
306
|
+
titleInput.addEventListener('keydown', function(e) {
|
|
307
|
+
if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
|
|
308
|
+
});
|
|
309
|
+
categoriesInput.addEventListener('keydown', function(e) {
|
|
310
|
+
if (e.key === 'Enter') { e.preventDefault(); submitSave(); }
|
|
311
|
+
});
|
|
312
|
+
})();
|
|
313
|
+
|
|
314
|
+
// 4. Reset link handler
|
|
315
|
+
var resetLink = document.getElementById('resetLink');
|
|
316
|
+
if (resetLink) {
|
|
317
|
+
resetLink.addEventListener('click', function() {
|
|
318
|
+
window.location.href = window.location.pathname + '/reset';
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 5. Chat scroll to bottom
|
|
323
|
+
var chatMessages = document.getElementById('chatMessages');
|
|
324
|
+
if (chatMessages) {
|
|
325
|
+
chatMessages.scrollTo({
|
|
326
|
+
top: chatMessages.scrollHeight,
|
|
327
|
+
behavior: 'smooth'
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 6. Chat toggle button — create (if not already in markup), persist in localStorage
|
|
332
|
+
(function() {
|
|
333
|
+
var btn = document.querySelector('.chat-toggle');
|
|
334
|
+
if (!btn) {
|
|
335
|
+
btn = document.createElement('button');
|
|
336
|
+
btn.className = 'chat-toggle';
|
|
337
|
+
btn.setAttribute('aria-label', 'Toggle chat panel');
|
|
338
|
+
var dots = document.createElement('span');
|
|
339
|
+
dots.className = 'chat-toggle-dots';
|
|
340
|
+
for (var i = 0; i < 3; i++) {
|
|
341
|
+
var dot = document.createElement('span');
|
|
342
|
+
dot.className = 'chat-toggle-dot';
|
|
343
|
+
dots.appendChild(dot);
|
|
344
|
+
}
|
|
345
|
+
btn.appendChild(dots);
|
|
346
|
+
document.body.appendChild(btn);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var STORAGE_KEY = 'synthos-chat-collapsed';
|
|
350
|
+
|
|
351
|
+
if (localStorage.getItem(STORAGE_KEY) === 'true') {
|
|
352
|
+
document.body.classList.add('chat-collapsed');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
btn.addEventListener('click', function() {
|
|
356
|
+
document.body.classList.toggle('chat-collapsed');
|
|
357
|
+
localStorage.setItem(STORAGE_KEY, document.body.classList.contains('chat-collapsed'));
|
|
358
|
+
});
|
|
359
|
+
})();
|
|
360
|
+
|
|
361
|
+
// 7. Focus management — prevent viewer content from stealing keystrokes
|
|
362
|
+
(function() {
|
|
363
|
+
var ci = document.getElementById('chatInput');
|
|
364
|
+
var vp = document.getElementById('viewerPanel');
|
|
365
|
+
if (!ci || !vp) return;
|
|
366
|
+
|
|
367
|
+
ci.addEventListener('mousedown', function(e) {
|
|
368
|
+
e.stopPropagation();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
['keydown', 'keyup', 'keypress'].forEach(function(type) {
|
|
372
|
+
document.addEventListener(type, function(e) {
|
|
373
|
+
if (document.activeElement === ci) {
|
|
374
|
+
e.stopImmediatePropagation();
|
|
375
|
+
}
|
|
376
|
+
}, true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
vp.setAttribute('tabindex', '-1');
|
|
380
|
+
ci.addEventListener('blur', function() {
|
|
381
|
+
vp.focus();
|
|
382
|
+
});
|
|
383
|
+
})();
|
|
384
|
+
|
|
385
|
+
// 8. Brainstorm — dynamic brainstorming UI (available on every v2 page)
|
|
386
|
+
(function() {
|
|
387
|
+
var chatInput = document.getElementById('chatInput');
|
|
388
|
+
if (!chatInput) return;
|
|
389
|
+
|
|
390
|
+
// --- Wrap chatInput in .chat-input-wrapper if not already wrapped ---
|
|
391
|
+
if (!chatInput.parentElement || !chatInput.parentElement.classList.contains('chat-input-wrapper')) {
|
|
392
|
+
var wrapper = document.createElement('div');
|
|
393
|
+
wrapper.className = 'chat-input-wrapper';
|
|
394
|
+
chatInput.parentNode.insertBefore(wrapper, chatInput);
|
|
395
|
+
wrapper.appendChild(chatInput);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// --- Create brainstorm icon button ---
|
|
399
|
+
var brainstormBtn = document.createElement('button');
|
|
400
|
+
brainstormBtn.type = 'button';
|
|
401
|
+
brainstormBtn.className = 'brainstorm-icon-btn';
|
|
402
|
+
if (window.__synthOSTooltip) window.__synthOSTooltip(brainstormBtn, 'Brainstorm ideas');
|
|
403
|
+
brainstormBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
404
|
+
'<circle cx="12" cy="12" r="3"></circle>' +
|
|
405
|
+
'<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"></path>' +
|
|
406
|
+
'</svg>';
|
|
407
|
+
chatInput.parentElement.appendChild(brainstormBtn);
|
|
408
|
+
|
|
409
|
+
// --- Create brainstorm modal ---
|
|
410
|
+
var modal = document.createElement('div');
|
|
411
|
+
modal.id = 'brainstormModal';
|
|
412
|
+
modal.className = 'modal-overlay brainstorm-modal';
|
|
413
|
+
modal.innerHTML =
|
|
414
|
+
'<div class="modal-content">' +
|
|
415
|
+
'<div class="modal-header">' +
|
|
416
|
+
'<span>Brainstorm</span>' +
|
|
417
|
+
'<button type="button" class="brainstorm-close-btn" id="brainstormCloseBtn">×</button>' +
|
|
418
|
+
'</div>' +
|
|
419
|
+
'<div class="brainstorm-messages" id="brainstormMessages"></div>' +
|
|
420
|
+
'<div class="brainstorm-input-row">' +
|
|
421
|
+
'<input type="text" class="brainstorm-input" id="brainstormInput" placeholder="What\'s on your mind...">' +
|
|
422
|
+
'<button type="button" class="brainstorm-send-btn" id="brainstormSendBtn">Send</button>' +
|
|
423
|
+
'</div>' +
|
|
424
|
+
'</div>';
|
|
425
|
+
document.body.appendChild(modal);
|
|
426
|
+
|
|
427
|
+
// --- State ---
|
|
428
|
+
var brainstormHistory = [];
|
|
429
|
+
|
|
430
|
+
// --- Helpers ---
|
|
431
|
+
function openBrainstorm() {
|
|
432
|
+
modal.classList.add('show');
|
|
433
|
+
// Grab text from chat input as initial topic
|
|
434
|
+
var topic = chatInput.value.trim();
|
|
435
|
+
if (topic) {
|
|
436
|
+
chatInput.value = '';
|
|
437
|
+
sendBrainstormText(topic, true);
|
|
438
|
+
} else {
|
|
439
|
+
// No topic — send context-only opener so LLM starts the brainstorm
|
|
440
|
+
sendBrainstormText('', true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function closeBrainstorm() {
|
|
445
|
+
modal.classList.remove('show');
|
|
446
|
+
brainstormHistory = [];
|
|
447
|
+
document.getElementById('brainstormMessages').innerHTML = '';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function scrollBrainstormToBottom() {
|
|
451
|
+
var el = document.getElementById('brainstormMessages');
|
|
452
|
+
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function escapeHtml(str) {
|
|
456
|
+
var div = document.createElement('div');
|
|
457
|
+
div.appendChild(document.createTextNode(str));
|
|
458
|
+
return div.innerHTML;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function appendBrainstormMessage(role, text, prompt, suggestions, isOpener) {
|
|
462
|
+
var div = document.createElement('div');
|
|
463
|
+
div.className = 'brainstorm-message ' + (role === 'user' ? 'brainstorm-user' : 'brainstorm-assistant');
|
|
464
|
+
if (role === 'assistant') {
|
|
465
|
+
var html;
|
|
466
|
+
if (typeof marked !== 'undefined') {
|
|
467
|
+
html = marked.parse(text);
|
|
468
|
+
} else {
|
|
469
|
+
html = escapeHtml(text);
|
|
470
|
+
}
|
|
471
|
+
div.innerHTML = '<strong>SynthOS:</strong> ' + html;
|
|
472
|
+
// Clickable suggestion chips
|
|
473
|
+
if (suggestions && suggestions.length > 0) {
|
|
474
|
+
var chips = document.createElement('div');
|
|
475
|
+
chips.className = 'brainstorm-suggestions';
|
|
476
|
+
suggestions.forEach(function(s) {
|
|
477
|
+
var chip = document.createElement('button');
|
|
478
|
+
chip.type = 'button';
|
|
479
|
+
chip.className = 'brainstorm-suggestion-chip';
|
|
480
|
+
chip.textContent = s;
|
|
481
|
+
chip.addEventListener('click', function() {
|
|
482
|
+
submitSuggestion(s);
|
|
483
|
+
});
|
|
484
|
+
chips.appendChild(chip);
|
|
485
|
+
});
|
|
486
|
+
div.appendChild(chips);
|
|
487
|
+
}
|
|
488
|
+
// "Build It" button — skip on the opener response
|
|
489
|
+
if (prompt && !isOpener) {
|
|
490
|
+
var btnRow = document.createElement('div');
|
|
491
|
+
btnRow.className = 'brainstorm-build-row';
|
|
492
|
+
var buildBtn = document.createElement('button');
|
|
493
|
+
buildBtn.type = 'button';
|
|
494
|
+
buildBtn.className = 'brainstorm-build-btn';
|
|
495
|
+
buildBtn.textContent = 'Build It';
|
|
496
|
+
buildBtn.setAttribute('data-prompt', prompt);
|
|
497
|
+
buildBtn.addEventListener('click', function() {
|
|
498
|
+
chatInput.value = this.getAttribute('data-prompt');
|
|
499
|
+
closeBrainstorm();
|
|
500
|
+
chatInput.focus();
|
|
501
|
+
});
|
|
502
|
+
btnRow.appendChild(buildBtn);
|
|
503
|
+
div.appendChild(btnRow);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
div.textContent = text;
|
|
507
|
+
}
|
|
508
|
+
document.getElementById('brainstormMessages').appendChild(div);
|
|
509
|
+
scrollBrainstormToBottom();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function submitSuggestion(text) {
|
|
513
|
+
// Disable old suggestion chips so they can't be double-clicked
|
|
514
|
+
var oldChips = document.querySelectorAll('#brainstormMessages .brainstorm-suggestion-chip');
|
|
515
|
+
for (var i = 0; i < oldChips.length; i++) {
|
|
516
|
+
oldChips[i].disabled = true;
|
|
517
|
+
}
|
|
518
|
+
sendBrainstormText(text, false);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getBrainstormContext() {
|
|
522
|
+
var chatEl = document.getElementById('chatMessages');
|
|
523
|
+
var chatHistory = chatEl ? chatEl.innerText : '';
|
|
524
|
+
return '<CHAT_HISTORY>\n' + chatHistory;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Send from the input field
|
|
528
|
+
function sendBrainstormMessage() {
|
|
529
|
+
var input = document.getElementById('brainstormInput');
|
|
530
|
+
var text = input.value.trim();
|
|
531
|
+
if (!text) return;
|
|
532
|
+
input.value = '';
|
|
533
|
+
sendBrainstormText(text, false);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Core fetch — isOpener=true means this is the initial call when brainstorm opens
|
|
537
|
+
function sendBrainstormText(text, isOpener) {
|
|
538
|
+
var input = document.getElementById('brainstormInput');
|
|
539
|
+
var userMsg = text || (isOpener ? 'Look at the conversation so far and suggest what we could build or improve.' : '');
|
|
540
|
+
if (!userMsg) return;
|
|
541
|
+
|
|
542
|
+
// Show user message in chat (skip for auto-generated opener)
|
|
543
|
+
if (text) {
|
|
544
|
+
appendBrainstormMessage('user', text);
|
|
545
|
+
}
|
|
546
|
+
brainstormHistory.push({ role: 'user', content: userMsg });
|
|
547
|
+
|
|
548
|
+
var thinking = document.createElement('div');
|
|
549
|
+
thinking.className = 'brainstorm-thinking';
|
|
550
|
+
thinking.id = 'brainstormThinking';
|
|
551
|
+
thinking.textContent = 'Thinking...';
|
|
552
|
+
document.getElementById('brainstormMessages').appendChild(thinking);
|
|
553
|
+
scrollBrainstormToBottom();
|
|
554
|
+
|
|
555
|
+
input.disabled = true;
|
|
556
|
+
document.getElementById('brainstormSendBtn').disabled = true;
|
|
557
|
+
|
|
558
|
+
fetch('/api/brainstorm', {
|
|
559
|
+
method: 'POST',
|
|
560
|
+
headers: { 'Content-Type': 'application/json' },
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
context: getBrainstormContext(),
|
|
563
|
+
messages: brainstormHistory
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
.then(function(res) {
|
|
567
|
+
if (!res.ok) throw new Error('Brainstorm request failed');
|
|
568
|
+
return res.json();
|
|
569
|
+
})
|
|
570
|
+
.then(function(data) {
|
|
571
|
+
var thinkingEl = document.getElementById('brainstormThinking');
|
|
572
|
+
if (thinkingEl) thinkingEl.remove();
|
|
573
|
+
|
|
574
|
+
var response = data.response || 'Sorry, I didn\'t get a response.';
|
|
575
|
+
var prompt = data.prompt || '';
|
|
576
|
+
var suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
|
|
577
|
+
appendBrainstormMessage('assistant', response, prompt, suggestions, isOpener);
|
|
578
|
+
brainstormHistory.push({
|
|
579
|
+
role: 'assistant',
|
|
580
|
+
content: response + '\n\n[Suggested prompt: ' + prompt + ']'
|
|
581
|
+
});
|
|
582
|
+
})
|
|
583
|
+
.catch(function(err) {
|
|
584
|
+
var thinkingEl = document.getElementById('brainstormThinking');
|
|
585
|
+
if (thinkingEl) thinkingEl.remove();
|
|
586
|
+
appendBrainstormMessage('assistant', 'Something went wrong: ' + err.message);
|
|
587
|
+
})
|
|
588
|
+
.finally(function() {
|
|
589
|
+
input.disabled = false;
|
|
590
|
+
document.getElementById('brainstormSendBtn').disabled = false;
|
|
591
|
+
input.focus();
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// --- Event listeners ---
|
|
596
|
+
brainstormBtn.addEventListener('click', openBrainstorm);
|
|
597
|
+
document.getElementById('brainstormCloseBtn').addEventListener('click', closeBrainstorm);
|
|
598
|
+
|
|
599
|
+
modal.addEventListener('click', function(e) {
|
|
600
|
+
if (e.target === modal) closeBrainstorm();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
document.addEventListener('keydown', function(e) {
|
|
604
|
+
if (e.key === 'Escape' && modal.classList.contains('show')) closeBrainstorm();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
document.getElementById('brainstormSendBtn').addEventListener('click', sendBrainstormMessage);
|
|
608
|
+
document.getElementById('brainstormInput').addEventListener('keydown', function(e) {
|
|
609
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
sendBrainstormMessage();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
})();
|
|
615
|
+
})();
|
package/tests/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SynthOS Tests
|
|
2
|
+
|
|
3
|
+
## Tier 1 (implemented)
|
|
4
|
+
- `transformPage.spec.ts` — assignNodeIds, stripNodeIds, applyChangeList, parseChangeList, injectError
|
|
5
|
+
- `pages.spec.ts` — normalizePageName, parseMetadata
|
|
6
|
+
- `migrations.spec.ts` — postProcessV2
|
|
7
|
+
|
|
8
|
+
## Tier 2 (TODO)
|
|
9
|
+
- `scripts.spec.ts` — listScripts, loadScripts, saveScripts
|
|
10
|
+
- `modelInstructions.spec.ts` — getModelInstructions provider routing
|
|
11
|
+
- `debugLog.spec.ts` — log formatting and filtering
|
|
12
|
+
- `init.spec.ts` — folder creation and default file copying
|