@hale-bopp/valentino-engine 2.2.0 → 2.3.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.
Files changed (82) hide show
  1. package/README.md +61 -0
  2. package/dist/bin/commands/cockpit.d.ts +16 -0
  3. package/dist/bin/commands/cockpit.d.ts.map +1 -0
  4. package/dist/bin/commands/cockpit.js +205 -0
  5. package/dist/bin/commands/cockpit.js.map +1 -0
  6. package/dist/bin/commands/theme-audit.d.ts +7 -0
  7. package/dist/bin/commands/theme-audit.d.ts.map +1 -0
  8. package/dist/bin/commands/theme-audit.js +70 -0
  9. package/dist/bin/commands/theme-audit.js.map +1 -0
  10. package/dist/bin/commands/validate.d.ts.map +1 -1
  11. package/dist/bin/commands/validate.js +94 -1
  12. package/dist/bin/commands/validate.js.map +1 -1
  13. package/dist/bin/valentino.js +14 -0
  14. package/dist/bin/valentino.js.map +1 -1
  15. package/dist/cockpit-server.d.ts +17 -0
  16. package/dist/cockpit-server.d.ts.map +1 -0
  17. package/dist/cockpit-server.js +505 -0
  18. package/dist/cockpit-server.js.map +1 -0
  19. package/dist/cockpit-web/index.html +979 -0
  20. package/dist/core/cockpit-api.d.ts +99 -0
  21. package/dist/core/cockpit-api.d.ts.map +1 -0
  22. package/dist/core/cockpit-api.js +269 -0
  23. package/dist/core/cockpit-api.js.map +1 -0
  24. package/dist/core/cockpit-repl.d.ts +51 -0
  25. package/dist/core/cockpit-repl.d.ts.map +1 -0
  26. package/dist/core/cockpit-repl.js +217 -0
  27. package/dist/core/cockpit-repl.js.map +1 -0
  28. package/dist/core/contrast-usage-probe.d.ts +84 -0
  29. package/dist/core/contrast-usage-probe.d.ts.map +1 -0
  30. package/dist/core/contrast-usage-probe.js +244 -0
  31. package/dist/core/contrast-usage-probe.js.map +1 -0
  32. package/dist/core/contrast-usage-probe.test.d.ts +2 -0
  33. package/dist/core/contrast-usage-probe.test.d.ts.map +1 -0
  34. package/dist/core/contrast-usage-probe.test.js +186 -0
  35. package/dist/core/contrast-usage-probe.test.js.map +1 -0
  36. package/dist/core/intent-parser.d.ts +67 -0
  37. package/dist/core/intent-parser.d.ts.map +1 -0
  38. package/dist/core/intent-parser.js +485 -0
  39. package/dist/core/intent-parser.js.map +1 -0
  40. package/dist/core/openrouter-client.d.ts +38 -0
  41. package/dist/core/openrouter-client.d.ts.map +1 -0
  42. package/dist/core/openrouter-client.js +123 -0
  43. package/dist/core/openrouter-client.js.map +1 -0
  44. package/dist/core/page-status.d.ts +15 -1
  45. package/dist/core/page-status.d.ts.map +1 -1
  46. package/dist/core/page-status.js +21 -0
  47. package/dist/core/page-status.js.map +1 -1
  48. package/dist/core/project-adapter.d.ts +73 -0
  49. package/dist/core/project-adapter.d.ts.map +1 -0
  50. package/dist/core/project-adapter.js +364 -0
  51. package/dist/core/project-adapter.js.map +1 -0
  52. package/dist/core/schema-export.d.ts +32 -0
  53. package/dist/core/schema-export.d.ts.map +1 -0
  54. package/dist/core/schema-export.js +493 -0
  55. package/dist/core/schema-export.js.map +1 -0
  56. package/dist/core/theme-audit.d.ts +104 -0
  57. package/dist/core/theme-audit.d.ts.map +1 -0
  58. package/dist/core/theme-audit.js +127 -0
  59. package/dist/core/theme-audit.js.map +1 -0
  60. package/dist/core/theme-audit.test.d.ts +2 -0
  61. package/dist/core/theme-audit.test.d.ts.map +1 -0
  62. package/dist/core/theme-audit.test.js +198 -0
  63. package/dist/core/theme-audit.test.js.map +1 -0
  64. package/dist/core/url-import.d.ts +41 -0
  65. package/dist/core/url-import.d.ts.map +1 -0
  66. package/dist/core/url-import.js +207 -0
  67. package/dist/core/url-import.js.map +1 -0
  68. package/dist/core/video-import.d.ts +46 -0
  69. package/dist/core/video-import.d.ts.map +1 -0
  70. package/dist/core/video-import.js +192 -0
  71. package/dist/core/video-import.js.map +1 -0
  72. package/dist/core/visual-import.d.ts +42 -0
  73. package/dist/core/visual-import.d.ts.map +1 -0
  74. package/dist/core/visual-import.js +227 -0
  75. package/dist/core/visual-import.js.map +1 -0
  76. package/dist/index.d.ts +25 -2
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +25 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/mcp/index.js +32 -0
  81. package/dist/mcp/index.js.map +1 -1
  82. package/package.json +9 -3
@@ -0,0 +1,979 @@
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Valentino Cockpit — Il Sarto Parla</title>
7
+ <style>
8
+ :root {
9
+ --void: #050812;
10
+ --surface: #0a1125;
11
+ --surface-raised: #0f1a35;
12
+ --surface-hover: #142040;
13
+ --border: #1a2a50;
14
+ --gold: #f5d586;
15
+ --gold-dim: #b89e5c;
16
+ --cyan: #0cd6c7;
17
+ --text: #e8e8f0;
18
+ --text-dim: #8890a8;
19
+ --success: #34d399;
20
+ --error: #f87171;
21
+ --warning: #fbbf24;
22
+ --radius: 8px;
23
+ --font: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;
24
+ }
25
+
26
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27
+
28
+ body {
29
+ font-family: var(--font);
30
+ background: var(--void);
31
+ color: var(--text);
32
+ height: 100vh;
33
+ display: flex;
34
+ flex-direction: column;
35
+ overflow: hidden;
36
+ }
37
+
38
+ /* Header */
39
+ .header {
40
+ background: var(--surface);
41
+ border-bottom: 1px solid var(--border);
42
+ padding: 12px 20px;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 16px;
46
+ flex-shrink: 0;
47
+ }
48
+ .header h1 {
49
+ font-size: 14px;
50
+ color: var(--gold);
51
+ font-weight: 600;
52
+ letter-spacing: 0.5px;
53
+ }
54
+ .header .page-info {
55
+ font-size: 12px;
56
+ color: var(--text-dim);
57
+ }
58
+ .header .actions {
59
+ margin-left: auto;
60
+ display: flex;
61
+ gap: 8px;
62
+ }
63
+ .btn {
64
+ background: var(--surface-raised);
65
+ border: 1px solid var(--border);
66
+ color: var(--text-dim);
67
+ padding: 6px 12px;
68
+ border-radius: var(--radius);
69
+ font-family: var(--font);
70
+ font-size: 11px;
71
+ cursor: pointer;
72
+ transition: all 0.15s;
73
+ }
74
+ .btn:hover { background: var(--surface-hover); color: var(--text); border-color: var(--gold-dim); }
75
+ .btn.primary { background: var(--gold); color: var(--void); border-color: var(--gold); font-weight: 600; }
76
+ .btn.primary:hover { background: var(--gold-dim); }
77
+ .btn:disabled { opacity: 0.4; cursor: default; }
78
+
79
+ /* Main layout: chat + preview */
80
+ .main {
81
+ flex: 1;
82
+ display: flex;
83
+ overflow: hidden;
84
+ }
85
+
86
+ /* Chat panel */
87
+ .chat-panel {
88
+ flex: 1;
89
+ display: flex;
90
+ flex-direction: column;
91
+ border-right: 1px solid var(--border);
92
+ min-width: 0;
93
+ }
94
+
95
+ .messages {
96
+ flex: 1;
97
+ overflow-y: auto;
98
+ padding: 16px;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 12px;
102
+ }
103
+ .messages::-webkit-scrollbar { width: 6px; }
104
+ .messages::-webkit-scrollbar-track { background: transparent; }
105
+ .messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
106
+
107
+ .msg {
108
+ max-width: 85%;
109
+ padding: 10px 14px;
110
+ border-radius: var(--radius);
111
+ font-size: 13px;
112
+ line-height: 1.5;
113
+ word-break: break-word;
114
+ }
115
+ .msg.user {
116
+ align-self: flex-end;
117
+ background: var(--surface-raised);
118
+ border: 1px solid var(--border);
119
+ color: var(--text);
120
+ }
121
+ .msg.system {
122
+ align-self: flex-start;
123
+ background: var(--surface);
124
+ border: 1px solid var(--border);
125
+ }
126
+ .msg.success { border-left: 3px solid var(--success); }
127
+ .msg.error { border-left: 3px solid var(--error); }
128
+ .msg.info { border-left: 3px solid var(--cyan); }
129
+
130
+ .msg .label {
131
+ font-size: 10px;
132
+ text-transform: uppercase;
133
+ letter-spacing: 1px;
134
+ color: var(--text-dim);
135
+ margin-bottom: 4px;
136
+ }
137
+ .msg .description { color: var(--gold); font-weight: 500; }
138
+ .msg .confidence { font-size: 11px; color: var(--text-dim); }
139
+ .msg .warnings { margin-top: 6px; font-size: 12px; color: var(--warning); }
140
+ .msg .data { margin-top: 8px; font-size: 12px; }
141
+ .msg pre {
142
+ background: var(--void);
143
+ padding: 8px;
144
+ border-radius: 4px;
145
+ overflow-x: auto;
146
+ font-size: 11px;
147
+ margin-top: 6px;
148
+ }
149
+
150
+ /* Input */
151
+ .input-bar {
152
+ padding: 12px 16px;
153
+ background: var(--surface);
154
+ border-top: 1px solid var(--border);
155
+ display: flex;
156
+ gap: 8px;
157
+ flex-shrink: 0;
158
+ }
159
+ .input-bar input {
160
+ flex: 1;
161
+ background: var(--surface-raised);
162
+ border: 1px solid var(--border);
163
+ color: var(--text);
164
+ padding: 10px 14px;
165
+ border-radius: var(--radius);
166
+ font-family: var(--font);
167
+ font-size: 13px;
168
+ outline: none;
169
+ transition: border-color 0.15s;
170
+ }
171
+ .input-bar input:focus { border-color: var(--gold-dim); }
172
+ .input-bar input::placeholder { color: var(--text-dim); }
173
+
174
+ /* Preview panel */
175
+ .preview-panel {
176
+ width: 420px;
177
+ flex-shrink: 0;
178
+ display: flex;
179
+ flex-direction: column;
180
+ overflow: hidden;
181
+ }
182
+ .preview-header {
183
+ padding: 10px 16px;
184
+ background: var(--surface);
185
+ border-bottom: 1px solid var(--border);
186
+ font-size: 11px;
187
+ color: var(--text-dim);
188
+ text-transform: uppercase;
189
+ letter-spacing: 1px;
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ }
194
+ .preview-body {
195
+ flex: 1;
196
+ overflow-y: auto;
197
+ padding: 12px;
198
+ font-size: 12px;
199
+ }
200
+ .preview-body::-webkit-scrollbar { width: 6px; }
201
+ .preview-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
202
+
203
+ .section-card {
204
+ background: var(--surface);
205
+ border: 1px solid var(--border);
206
+ border-radius: var(--radius);
207
+ padding: 10px 12px;
208
+ margin-bottom: 8px;
209
+ transition: border-color 0.15s;
210
+ }
211
+ .section-card:hover { border-color: var(--gold-dim); }
212
+ .section-card .type {
213
+ font-size: 10px;
214
+ text-transform: uppercase;
215
+ letter-spacing: 1px;
216
+ color: var(--cyan);
217
+ margin-bottom: 4px;
218
+ }
219
+ .section-card .title { color: var(--text); font-size: 12px; }
220
+ .section-card .index { color: var(--text-dim); font-size: 10px; float: right; }
221
+
222
+ /* Welcome */
223
+ .welcome {
224
+ text-align: center;
225
+ padding: 40px 20px;
226
+ color: var(--text-dim);
227
+ }
228
+ .welcome h2 { color: var(--gold); font-size: 18px; margin-bottom: 12px; }
229
+ .welcome p { font-size: 13px; line-height: 1.6; margin-bottom: 8px; }
230
+ .welcome .hint { font-size: 11px; color: var(--text-dim); margin-top: 16px; }
231
+
232
+ /* Status bar */
233
+ .status-bar {
234
+ background: var(--surface);
235
+ border-top: 1px solid var(--border);
236
+ padding: 6px 16px;
237
+ font-size: 10px;
238
+ color: var(--text-dim);
239
+ display: flex;
240
+ gap: 16px;
241
+ flex-shrink: 0;
242
+ }
243
+ .status-bar .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
244
+ .status-bar .dot.green { background: var(--success); }
245
+ .status-bar .dot.yellow { background: var(--warning); }
246
+
247
+ /* Settings overlay */
248
+ .overlay {
249
+ display: none;
250
+ position: fixed;
251
+ inset: 0;
252
+ background: rgba(5,8,18,0.85);
253
+ z-index: 100;
254
+ align-items: center;
255
+ justify-content: center;
256
+ }
257
+ .overlay.open { display: flex; }
258
+ .settings-panel {
259
+ background: var(--surface);
260
+ border: 1px solid var(--border);
261
+ border-radius: 12px;
262
+ padding: 24px;
263
+ width: 480px;
264
+ max-width: 90vw;
265
+ }
266
+ .settings-panel h2 {
267
+ color: var(--gold);
268
+ font-size: 14px;
269
+ margin-bottom: 16px;
270
+ letter-spacing: 0.5px;
271
+ }
272
+ .field { margin-bottom: 14px; }
273
+ .field label {
274
+ display: block;
275
+ font-size: 11px;
276
+ color: var(--text-dim);
277
+ margin-bottom: 4px;
278
+ text-transform: uppercase;
279
+ letter-spacing: 0.5px;
280
+ }
281
+ .field input, .field select {
282
+ width: 100%;
283
+ background: var(--surface-raised);
284
+ border: 1px solid var(--border);
285
+ color: var(--text);
286
+ padding: 8px 12px;
287
+ border-radius: var(--radius);
288
+ font-family: var(--font);
289
+ font-size: 12px;
290
+ outline: none;
291
+ }
292
+ .field input:focus, .field select:focus { border-color: var(--gold-dim); }
293
+ .settings-actions {
294
+ display: flex;
295
+ gap: 8px;
296
+ justify-content: flex-end;
297
+ margin-top: 16px;
298
+ }
299
+ .llm-status {
300
+ font-size: 11px;
301
+ padding: 8px 12px;
302
+ border-radius: var(--radius);
303
+ margin-bottom: 14px;
304
+ }
305
+ .llm-status.connected { background: rgba(52,211,153,0.1); border: 1px solid var(--success); color: var(--success); }
306
+ .llm-status.disconnected { background: rgba(136,144,168,0.1); border: 1px solid var(--border); color: var(--text-dim); }
307
+ </style>
308
+ </head>
309
+ <body>
310
+
311
+ <div class="header">
312
+ <h1>VALENTINO COCKPIT</h1>
313
+ <span class="page-info" id="pageInfo">Loading...</span>
314
+ <div class="actions">
315
+ <button class="btn" id="btnUndo" disabled>Undo</button>
316
+ <button class="btn" id="btnImport">Import</button>
317
+ <button class="btn" id="btnSave">Save</button>
318
+ <button class="btn" id="btnSettings">Settings</button>
319
+ </div>
320
+ </div>
321
+
322
+ <div class="main">
323
+ <div class="chat-panel">
324
+ <div class="messages" id="messages">
325
+ <div class="welcome">
326
+ <h2>Il Sarto Parla</h2>
327
+ <p>Parla in linguaggio naturale per gestire la tua pagina.</p>
328
+ <p>Prova: "mostrami le sezioni" o "aggiungi una sezione stats"</p>
329
+ <div class="hint">IT + EN supportati. Scrivi "help" per i comandi.</div>
330
+ </div>
331
+ </div>
332
+ <div class="input-bar">
333
+ <input type="text" id="input" placeholder="Parla con Valentino..." autofocus>
334
+ <button class="btn primary" id="btnSend">Invia</button>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="preview-panel">
339
+ <div class="preview-header">
340
+ <span>Page Preview</span>
341
+ <span id="sectionCount">0 sections</span>
342
+ </div>
343
+ <div class="preview-body" id="preview"></div>
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Import overlay -->
348
+ <div class="overlay" id="importOverlay">
349
+ <div class="settings-panel">
350
+ <h2>VISUAL IMPORT — Il Sarto Copia</h2>
351
+ <div class="llm-status disconnected" id="importInfo">Carica uno screenshot o inserisci un URL.</div>
352
+ <div style="display:flex;gap:8px;margin-bottom:14px">
353
+ <button class="btn primary" id="tabScreenshot" style="flex:1">Screenshot</button>
354
+ <button class="btn" id="tabUrl" style="flex:1">URL</button>
355
+ <button class="btn" id="tabVideo" style="flex:1">Video</button>
356
+ <button class="btn" id="tabProject" style="flex:1">Project</button>
357
+ </div>
358
+ <!-- Screenshot tab -->
359
+ <div id="importTabScreenshot">
360
+ <div class="field">
361
+ <label>Screenshot</label>
362
+ <input type="file" id="importFile" accept="image/png,image/jpeg,image/webp" style="padding:6px">
363
+ </div>
364
+ <div class="field" id="importPreviewWrap" style="display:none">
365
+ <img id="importPreview" style="max-width:100%;border-radius:var(--radius);border:1px solid var(--border)">
366
+ </div>
367
+ </div>
368
+ <!-- URL tab -->
369
+ <div id="importTabUrl" style="display:none">
370
+ <div class="field">
371
+ <label>URL della pagina</label>
372
+ <input type="text" id="importUrl" placeholder="https://example.com">
373
+ </div>
374
+ </div>
375
+ <!-- Video tab -->
376
+ <div id="importTabVideo" style="display:none">
377
+ <div class="field">
378
+ <label>Video</label>
379
+ <input type="file" id="importVideoFile" accept="video/mp4,video/webm,video/mov" style="padding:6px">
380
+ </div>
381
+ <div class="field" id="videoFramesWrap" style="display:none">
382
+ <label>Frames estratti (<span id="frameCountLabel">0</span>)</label>
383
+ <div id="videoFrames" style="display:flex;gap:4px;overflow-x:auto;padding:4px 0"></div>
384
+ </div>
385
+ </div>
386
+ <!-- Project tab -->
387
+ <div id="importTabProject" style="display:none">
388
+ <div class="field">
389
+ <label>Project directory (local path)</label>
390
+ <input type="text" id="importProjectPath" placeholder="C:\projects\my-site">
391
+ </div>
392
+ <div class="field" id="projectScanWrap" style="display:none">
393
+ <div class="llm-status connected" id="projectScanInfo"></div>
394
+ </div>
395
+ </div>
396
+ <div class="field">
397
+ <label>Page ID</label>
398
+ <input type="text" id="importId" placeholder="my-new-page" value="">
399
+ </div>
400
+ <div class="field">
401
+ <label>Language</label>
402
+ <select id="importLang">
403
+ <option value="it">Italiano</option>
404
+ <option value="en">English</option>
405
+ </select>
406
+ </div>
407
+ <div class="settings-actions">
408
+ <button class="btn" id="importCancel">Cancel</button>
409
+ <button class="btn primary" id="importGo" disabled>Importa</button>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Settings overlay -->
415
+ <div class="overlay" id="settingsOverlay">
416
+ <div class="settings-panel">
417
+ <h2>LLM CONFIGURATION</h2>
418
+ <div class="llm-status disconnected" id="llmStatus">LLM not connected — local parser only</div>
419
+ <div class="field">
420
+ <label>OpenRouter API Key</label>
421
+ <input type="password" id="cfgApiKey" placeholder="sk-or-v1-...">
422
+ </div>
423
+ <div class="field">
424
+ <label>Model</label>
425
+ <select id="cfgModel">
426
+ <option value="anthropic/claude-haiku-4.5-20251001">Claude Haiku 4.5 (fast, cheap)</option>
427
+ <option value="anthropic/claude-sonnet-4-6">Claude Sonnet 4.6 (balanced)</option>
428
+ <option value="google/gemini-2.5-flash">Gemini 2.5 Flash</option>
429
+ <option value="meta-llama/llama-4-scout">Llama 4 Scout</option>
430
+ </select>
431
+ </div>
432
+ <div class="settings-actions">
433
+ <button class="btn" id="cfgDisconnect">Disconnect</button>
434
+ <button class="btn" id="cfgCancel">Cancel</button>
435
+ <button class="btn primary" id="cfgConnect">Connect</button>
436
+ </div>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="status-bar">
441
+ <span><span class="dot green"></span> Connected</span>
442
+ <span id="statusActions">0 actions</span>
443
+ <span id="statusUndo">0 undo</span>
444
+ <span id="statusLlm"><span class="dot yellow"></span> Local only</span>
445
+ </div>
446
+
447
+ <script>
448
+ const API = '';
449
+ const messagesEl = document.getElementById('messages');
450
+ const inputEl = document.getElementById('input');
451
+ const previewEl = document.getElementById('preview');
452
+ const pageInfoEl = document.getElementById('pageInfo');
453
+ const sectionCountEl = document.getElementById('sectionCount');
454
+ const btnUndo = document.getElementById('btnUndo');
455
+ const btnSave = document.getElementById('btnSave');
456
+ const btnSend = document.getElementById('btnSend');
457
+ const statusActions = document.getElementById('statusActions');
458
+ const statusUndo = document.getElementById('statusUndo');
459
+
460
+ // --- API calls ---
461
+
462
+ async function apiGet(path) {
463
+ const res = await fetch(API + path);
464
+ return res.json();
465
+ }
466
+
467
+ async function apiPost(path, body) {
468
+ const res = await fetch(API + path, {
469
+ method: 'POST',
470
+ headers: { 'Content-Type': 'application/json' },
471
+ body: JSON.stringify(body),
472
+ });
473
+ return res.json();
474
+ }
475
+
476
+ // --- Messages ---
477
+
478
+ function addMessage(content, type = 'system', extra = '') {
479
+ const welcome = messagesEl.querySelector('.welcome');
480
+ if (welcome) welcome.remove();
481
+
482
+ const msg = document.createElement('div');
483
+ msg.className = `msg ${type} ${extra}`;
484
+ msg.innerHTML = content;
485
+ messagesEl.appendChild(msg);
486
+ messagesEl.scrollTop = messagesEl.scrollHeight;
487
+ }
488
+
489
+ function formatData(data) {
490
+ if (data === undefined || data === null) return '';
491
+ if (Array.isArray(data)) {
492
+ return '<pre>' + data.map((item, i) => {
493
+ if (typeof item === 'object') {
494
+ return Object.entries(item).map(([k,v]) => `${k}: ${v}`).join(', ');
495
+ }
496
+ return String(item);
497
+ }).join('\n') + '</pre>';
498
+ }
499
+ if (typeof data === 'object') {
500
+ return '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
501
+ }
502
+ return String(data);
503
+ }
504
+
505
+ // --- Preview ---
506
+
507
+ function updatePreview(spec) {
508
+ if (!spec) return;
509
+ pageInfoEl.textContent = `${spec.id} (${spec.profile || 'no profile'})`;
510
+ sectionCountEl.textContent = `${spec.sections.length} sections`;
511
+
512
+ previewEl.innerHTML = spec.sections.map((s, i) => {
513
+ const titleKey = s.titleKey || s.contentPrefix || '';
514
+ const surface = s.presentation?.surface || 'default';
515
+ return `<div class="section-card">
516
+ <span class="index">#${i}</span>
517
+ <div class="type">${s.type}</div>
518
+ <div class="title">${titleKey}</div>
519
+ </div>`;
520
+ }).join('');
521
+ }
522
+
523
+ function updateStatus(actionCount, undoAvailable) {
524
+ statusActions.textContent = `${actionCount} actions`;
525
+ statusUndo.textContent = `${undoAvailable} undo`;
526
+ btnUndo.disabled = undoAvailable === 0;
527
+ }
528
+
529
+ // --- Commands ---
530
+
531
+ const HELP_TEXT = `<div class="label">Help</div>
532
+ <b>Queries:</b> mostrami le sezioni, descrivi la pagina, valida, tipi sezioni<br>
533
+ <b>Mutations:</b> aggiungi stats, rimuovi la cta, sposta sezione 3 a 1<br>
534
+ <b>Built-in:</b> help, undo, save`;
535
+
536
+ async function handleInput(text) {
537
+ if (!text.trim()) return;
538
+
539
+ addMessage(text, 'user');
540
+
541
+ const lower = text.trim().toLowerCase();
542
+
543
+ if (lower === 'help' || lower === '?') {
544
+ addMessage(HELP_TEXT, 'system', 'info');
545
+ return;
546
+ }
547
+
548
+ // Send to speak endpoint
549
+ try {
550
+ const result = await apiPost('/api/speak', { text });
551
+
552
+ if (!result.parsed) {
553
+ addMessage('Non ho capito. Prova "help" per vedere i comandi.', 'system', 'error');
554
+ return;
555
+ }
556
+
557
+ let html = `<div class="description">${result.description}</div>`;
558
+ if (result.confidence && result.confidence !== 'high') {
559
+ html += `<div class="confidence">Confidence: ${result.confidence}</div>`;
560
+ }
561
+
562
+ if (result.warnings && result.warnings.length > 0) {
563
+ html += '<div class="warnings">' + result.warnings.map(w =>
564
+ `[${w.source}] ${w.message}`
565
+ ).join('<br>') + '</div>';
566
+ }
567
+
568
+ if (result.data !== undefined) {
569
+ html += '<div class="data">' + formatData(result.data) + '</div>';
570
+ }
571
+
572
+ addMessage(html, 'system', result.success ? 'success' : 'error');
573
+
574
+ // Refresh spec + status
575
+ const specResult = await apiGet('/api/spec');
576
+ updatePreview(specResult.spec);
577
+ updateStatus(specResult.actionCount, specResult.undoAvailable);
578
+
579
+ } catch (err) {
580
+ addMessage(`Error: ${err.message}`, 'system', 'error');
581
+ }
582
+ }
583
+
584
+ // --- Event handlers ---
585
+
586
+ inputEl.addEventListener('keydown', (e) => {
587
+ if (e.key === 'Enter') {
588
+ const text = inputEl.value;
589
+ inputEl.value = '';
590
+ handleInput(text);
591
+ }
592
+ });
593
+
594
+ btnSend.addEventListener('click', () => {
595
+ const text = inputEl.value;
596
+ inputEl.value = '';
597
+ handleInput(text);
598
+ });
599
+
600
+ btnUndo.addEventListener('click', async () => {
601
+ const result = await apiPost('/api/undo', {});
602
+ if (result.success) {
603
+ addMessage('Undo successful.', 'system', 'success');
604
+ updatePreview(result.spec);
605
+ updateStatus(result.actionCount || 0, result.undoAvailable);
606
+ } else {
607
+ addMessage(result.message || 'Nothing to undo.', 'system', 'error');
608
+ }
609
+ });
610
+
611
+ btnSave.addEventListener('click', async () => {
612
+ const result = await apiPost('/api/save', {});
613
+ if (result.success) {
614
+ addMessage(`Saved to ${result.path}`, 'system', 'success');
615
+ } else {
616
+ addMessage(`Save failed: ${result.error}`, 'system', 'error');
617
+ }
618
+ });
619
+
620
+ // --- Import ---
621
+
622
+ const importOverlay = document.getElementById('importOverlay');
623
+ const btnImport = document.getElementById('btnImport');
624
+ const importFile = document.getElementById('importFile');
625
+ const importPreview = document.getElementById('importPreview');
626
+ const importPreviewWrap = document.getElementById('importPreviewWrap');
627
+ const importId = document.getElementById('importId');
628
+ const importLang = document.getElementById('importLang');
629
+ const importGo = document.getElementById('importGo');
630
+ const importCancel = document.getElementById('importCancel');
631
+ const importInfo = document.getElementById('importInfo');
632
+
633
+ let importBase64 = null;
634
+ let importMime = null;
635
+ let importMode = 'screenshot';
636
+
637
+ const tabScreenshot = document.getElementById('tabScreenshot');
638
+ const tabUrl = document.getElementById('tabUrl');
639
+ const tabVideo = document.getElementById('tabVideo');
640
+ const importTabScreenshot = document.getElementById('importTabScreenshot');
641
+ const importTabUrl = document.getElementById('importTabUrl');
642
+ const importTabVideo = document.getElementById('importTabVideo');
643
+ const tabProject = document.getElementById('tabProject');
644
+ const importTabProject = document.getElementById('importTabProject');
645
+ const importProjectPath = document.getElementById('importProjectPath');
646
+ const projectScanWrap = document.getElementById('projectScanWrap');
647
+ const projectScanInfo = document.getElementById('projectScanInfo');
648
+ const importUrlInput = document.getElementById('importUrl');
649
+ const importVideoFile = document.getElementById('importVideoFile');
650
+ const videoFramesWrap = document.getElementById('videoFramesWrap');
651
+ const videoFramesEl = document.getElementById('videoFrames');
652
+ const frameCountLabel = document.getElementById('frameCountLabel');
653
+
654
+ let videoFrames = []; // { base64, mimeType, timestamp }
655
+
656
+ function setImportTab(mode) {
657
+ importMode = mode;
658
+ const tabs = { screenshot: tabScreenshot, url: tabUrl, video: tabVideo, project: tabProject };
659
+ const panels = { screenshot: importTabScreenshot, url: importTabUrl, video: importTabVideo, project: importTabProject };
660
+ for (const [k, btn] of Object.entries(tabs)) {
661
+ btn.className = k === mode ? 'btn primary' : 'btn';
662
+ }
663
+ for (const [k, panel] of Object.entries(panels)) {
664
+ panel.style.display = k === mode ? '' : 'none';
665
+ }
666
+ if (mode === 'screenshot') importGo.disabled = !importBase64;
667
+ else if (mode === 'url') importGo.disabled = !importUrlInput.value.trim();
668
+ else if (mode === 'video') importGo.disabled = videoFrames.length === 0;
669
+ else if (mode === 'project') importGo.disabled = !importProjectPath.value.trim();
670
+ }
671
+
672
+ tabScreenshot.addEventListener('click', () => setImportTab('screenshot'));
673
+ tabUrl.addEventListener('click', () => setImportTab('url'));
674
+ tabVideo.addEventListener('click', () => setImportTab('video'));
675
+ tabProject.addEventListener('click', () => setImportTab('project'));
676
+
677
+ // Auto-scan project on path input
678
+ importProjectPath?.addEventListener('change', async () => {
679
+ const path = importProjectPath.value.trim();
680
+ if (!path) return;
681
+ try {
682
+ const scan = await apiPost('/api/import/project/scan', { path });
683
+ projectScanWrap.style.display = '';
684
+ projectScanInfo.textContent = `${scan.htmlFiles?.length || 0} HTML, ${scan.cssFiles?.length || 0} CSS${scan.framework ? `, framework: ${scan.framework}` : ''}`;
685
+ importGo.disabled = !(scan.htmlFiles?.length > 0);
686
+ } catch { projectScanWrap.style.display = 'none'; }
687
+ });
688
+
689
+ importUrlInput?.addEventListener('input', () => {
690
+ if (importMode === 'url') {
691
+ importGo.disabled = !importUrlInput.value.trim();
692
+ // Auto-generate ID from URL hostname
693
+ try {
694
+ const hostname = new URL(importUrlInput.value).hostname;
695
+ if (!importId.value) importId.value = hostname.replace(/\./g, '-');
696
+ } catch {}
697
+ }
698
+ });
699
+
700
+ // Video frame extraction (browser-side)
701
+ importVideoFile?.addEventListener('change', async (e) => {
702
+ const file = e.target.files[0];
703
+ if (!file) return;
704
+ videoFrames = [];
705
+ videoFramesEl.innerHTML = '';
706
+ videoFramesWrap.style.display = 'none';
707
+ importInfo.textContent = 'Extracting frames...';
708
+
709
+ const url = URL.createObjectURL(file);
710
+ const video = document.createElement('video');
711
+ video.src = url;
712
+ video.muted = true;
713
+ video.preload = 'auto';
714
+
715
+ await new Promise((resolve) => { video.onloadedmetadata = resolve; });
716
+ const duration = video.duration;
717
+ const numFrames = Math.min(5, Math.max(2, Math.floor(duration / 3)));
718
+ const interval = duration / (numFrames + 1);
719
+
720
+ const canvas = document.createElement('canvas');
721
+ const ctx = canvas.getContext('2d');
722
+ canvas.width = 1440;
723
+ canvas.height = 900;
724
+
725
+ for (let i = 1; i <= numFrames; i++) {
726
+ const time = interval * i;
727
+ video.currentTime = time;
728
+ await new Promise((resolve) => { video.onseeked = resolve; });
729
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
730
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
731
+ const base64 = dataUrl.split(',')[1];
732
+ videoFrames.push({ base64, mimeType: 'image/jpeg', timestamp: Math.round(time) });
733
+
734
+ // Show thumbnail
735
+ const thumb = document.createElement('img');
736
+ thumb.src = dataUrl;
737
+ thumb.style.cssText = 'height:60px;border-radius:4px;border:1px solid var(--border)';
738
+ videoFramesEl.appendChild(thumb);
739
+ }
740
+
741
+ URL.revokeObjectURL(url);
742
+ frameCountLabel.textContent = videoFrames.length;
743
+ videoFramesWrap.style.display = '';
744
+ importGo.disabled = false;
745
+ importInfo.textContent = `${videoFrames.length} frames extracted. Ready to import.`;
746
+
747
+ if (!importId.value) {
748
+ importId.value = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
749
+ }
750
+ });
751
+
752
+ btnImport.addEventListener('click', () => {
753
+ importOverlay.classList.add('open');
754
+ importFile.value = '';
755
+ importPreviewWrap.style.display = 'none';
756
+ importGo.disabled = true;
757
+ importBase64 = null;
758
+ videoFrames = [];
759
+ videoFramesWrap.style.display = 'none';
760
+ if (importVideoFile) importVideoFile.value = '';
761
+ setImportTab('screenshot');
762
+ importInfo.className = 'llm-status disconnected';
763
+ importInfo.textContent = 'Carica uno screenshot, inserisci un URL, o importa un video.';
764
+ });
765
+
766
+ importCancel.addEventListener('click', () => {
767
+ importOverlay.classList.remove('open');
768
+ });
769
+
770
+ importOverlay.addEventListener('click', (e) => {
771
+ if (e.target === importOverlay) importOverlay.classList.remove('open');
772
+ });
773
+
774
+ importFile.addEventListener('change', (e) => {
775
+ const file = e.target.files[0];
776
+ if (!file) return;
777
+
778
+ importMime = file.type;
779
+ const reader = new FileReader();
780
+ reader.onload = () => {
781
+ const dataUrl = reader.result;
782
+ importPreview.src = dataUrl;
783
+ importPreviewWrap.style.display = 'block';
784
+ // Extract base64 without data: prefix
785
+ importBase64 = dataUrl.split(',')[1];
786
+ importGo.disabled = false;
787
+
788
+ // Auto-generate ID from filename
789
+ if (!importId.value) {
790
+ importId.value = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
791
+ }
792
+ };
793
+ reader.readAsDataURL(file);
794
+ });
795
+
796
+ importGo.addEventListener('click', async () => {
797
+ importGo.disabled = true;
798
+ importGo.textContent = 'Analyzing...';
799
+ importInfo.className = 'llm-status disconnected';
800
+
801
+ let result;
802
+ try {
803
+ if (importMode === 'project') {
804
+ importInfo.textContent = 'Scanning and analyzing project...';
805
+ result = await apiPost('/api/import/project', {
806
+ path: importProjectPath.value.trim(),
807
+ language: importLang.value,
808
+ });
809
+ } else if (importMode === 'video') {
810
+ importInfo.textContent = `Analyzing ${videoFrames.length} frames with LLM vision...`;
811
+ result = await apiPost('/api/import/video', {
812
+ frames: videoFrames,
813
+ id: importId.value || 'imported-page',
814
+ language: importLang.value,
815
+ });
816
+ } else if (importMode === 'url') {
817
+ importInfo.textContent = 'Fetching and analyzing URL...';
818
+ result = await apiPost('/api/import/url', {
819
+ url: importUrlInput.value.trim(),
820
+ id: importId.value || 'imported-page',
821
+ language: importLang.value,
822
+ });
823
+ } else {
824
+ if (!importBase64) return;
825
+ importInfo.textContent = 'Analyzing screenshot with LLM vision...';
826
+ result = await apiPost('/api/import/image', {
827
+ image: importBase64,
828
+ mimeType: importMime,
829
+ id: importId.value || 'imported-page',
830
+ language: importLang.value,
831
+ });
832
+ }
833
+
834
+ if (result.success) {
835
+ importInfo.className = 'llm-status connected';
836
+ const modeLabel = result.mode === 'playwright' ? 'Screenshot' : result.mode === 'html-fallback' ? 'HTML' : 'Image';
837
+ importInfo.textContent = `Imported (${modeLabel})! ${result.detectedSections?.length || 0} sections detected.`;
838
+ addMessage(
839
+ `<div class="description">Import ${modeLabel}: ${result.detectedSections?.join(', ')}</div>` +
840
+ (result.pageTitle ? `<div class="confidence">Page: ${result.pageTitle}</div>` : '') +
841
+ (result.warnings?.length ? '<div class="warnings">' + result.warnings.join('<br>') + '</div>' : ''),
842
+ 'system', 'success'
843
+ );
844
+
845
+ // Refresh preview
846
+ const specResult = await apiGet('/api/spec');
847
+ updatePreview(specResult.spec);
848
+ updateStatus(specResult.actionCount, specResult.undoAvailable);
849
+
850
+ setTimeout(() => importOverlay.classList.remove('open'), 1500);
851
+ } else {
852
+ importInfo.className = 'llm-status disconnected';
853
+ const errMsg = result.error || result.warnings?.join(', ') || 'Import failed';
854
+ const needsKey = errMsg.includes('API key') || errMsg.includes('OpenRouter') || errMsg.includes('OPENROUTER');
855
+ if (needsKey) {
856
+ importInfo.innerHTML = 'Configura OpenRouter: clicca <b>Settings</b> in alto a destra e inserisci la API key.';
857
+ addMessage(
858
+ '<div class="description">API key mancante</div>' +
859
+ 'L\'import richiede un LLM. Clicca <b>Settings</b> in alto a destra, inserisci la tua API key OpenRouter e premi Connect. Poi riprova.',
860
+ 'system', 'error'
861
+ );
862
+ } else {
863
+ importInfo.textContent = errMsg;
864
+ addMessage(`Import failed: ${errMsg}`, 'system', 'error');
865
+ }
866
+ }
867
+ } catch (err) {
868
+ const errMsg = err.message || String(err);
869
+ const needsKey = errMsg.includes('API key') || errMsg.includes('OpenRouter') || errMsg.includes('Not found');
870
+ if (needsKey || errMsg === 'Not found') {
871
+ importInfo.innerHTML = 'Configura OpenRouter: clicca <b>Settings</b> in alto a destra.';
872
+ addMessage(
873
+ '<div class="description">Configurazione necessaria</div>' +
874
+ 'Per importare serve un LLM. Clicca <b>Settings</b> → inserisci API key OpenRouter → Connect. Poi riprova.',
875
+ 'system', 'error'
876
+ );
877
+ } else {
878
+ importInfo.textContent = `Error: ${errMsg}`;
879
+ addMessage(`Import error: ${errMsg}`, 'system', 'error');
880
+ }
881
+ }
882
+
883
+ importGo.disabled = false;
884
+ importGo.textContent = 'Importa';
885
+ });
886
+
887
+ // --- Settings ---
888
+
889
+ const settingsOverlay = document.getElementById('settingsOverlay');
890
+ const btnSettings = document.getElementById('btnSettings');
891
+ const cfgApiKey = document.getElementById('cfgApiKey');
892
+ const cfgModel = document.getElementById('cfgModel');
893
+ const cfgConnect = document.getElementById('cfgConnect');
894
+ const cfgCancel = document.getElementById('cfgCancel');
895
+ const cfgDisconnect = document.getElementById('cfgDisconnect');
896
+ const llmStatusEl = document.getElementById('llmStatus');
897
+ const statusLlm = document.getElementById('statusLlm');
898
+
899
+ btnSettings.addEventListener('click', async () => {
900
+ settingsOverlay.classList.add('open');
901
+ // Load current config
902
+ const config = await apiGet('/api/config');
903
+ updateLlmStatus(config);
904
+ });
905
+
906
+ cfgCancel.addEventListener('click', () => {
907
+ settingsOverlay.classList.remove('open');
908
+ });
909
+
910
+ settingsOverlay.addEventListener('click', (e) => {
911
+ if (e.target === settingsOverlay) settingsOverlay.classList.remove('open');
912
+ });
913
+
914
+ cfgConnect.addEventListener('click', async () => {
915
+ const apiKey = cfgApiKey.value.trim();
916
+ if (!apiKey) return;
917
+
918
+ cfgConnect.disabled = true;
919
+ cfgConnect.textContent = 'Connecting...';
920
+
921
+ try {
922
+ const result = await apiPost('/api/config', {
923
+ apiKey,
924
+ model: cfgModel.value,
925
+ });
926
+
927
+ if (result.success) {
928
+ updateLlmStatus({ llmConnected: true, llmConfig: { model: result.model, connected: true } });
929
+ addMessage(`LLM connected: ${result.model}`, 'system', 'success');
930
+ settingsOverlay.classList.remove('open');
931
+ } else {
932
+ updateLlmStatus({ llmConnected: false });
933
+ addMessage(`LLM connection failed: ${result.error}`, 'system', 'error');
934
+ }
935
+ } catch (err) {
936
+ addMessage(`Error: ${err.message}`, 'system', 'error');
937
+ }
938
+
939
+ cfgConnect.disabled = false;
940
+ cfgConnect.textContent = 'Connect';
941
+ });
942
+
943
+ cfgDisconnect.addEventListener('click', async () => {
944
+ await fetch(API + '/api/config', { method: 'DELETE' });
945
+ updateLlmStatus({ llmConnected: false });
946
+ addMessage('LLM disconnected. Using local parser.', 'system', 'info');
947
+ settingsOverlay.classList.remove('open');
948
+ });
949
+
950
+ function updateLlmStatus(config) {
951
+ if (config.llmConnected) {
952
+ llmStatusEl.className = 'llm-status connected';
953
+ llmStatusEl.textContent = `Connected: ${config.llmConfig?.model || 'unknown'}`;
954
+ statusLlm.innerHTML = '<span class="dot green"></span> LLM';
955
+ } else {
956
+ llmStatusEl.className = 'llm-status disconnected';
957
+ llmStatusEl.textContent = 'LLM not connected — local parser only';
958
+ statusLlm.innerHTML = '<span class="dot yellow"></span> Local only';
959
+ }
960
+ }
961
+
962
+ // --- Init ---
963
+
964
+ (async () => {
965
+ try {
966
+ const [specResult, configResult] = await Promise.all([
967
+ apiGet('/api/spec'),
968
+ apiGet('/api/config'),
969
+ ]);
970
+ updatePreview(specResult.spec);
971
+ updateStatus(specResult.actionCount, specResult.undoAvailable);
972
+ updateLlmStatus(configResult);
973
+ } catch (err) {
974
+ pageInfoEl.textContent = 'Connection failed';
975
+ }
976
+ })();
977
+ </script>
978
+ </body>
979
+ </html>