phaibel 4.0.7 → 4.0.21

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 (150) hide show
  1. package/README.md +229 -245
  2. package/dist/commands/chat.d.ts +5 -2
  3. package/dist/commands/chat.d.ts.map +1 -1
  4. package/dist/commands/chat.js +168 -172
  5. package/dist/commands/chat.js.map +1 -1
  6. package/dist/commands/cron.d.ts.map +1 -1
  7. package/dist/commands/cron.js +1 -0
  8. package/dist/commands/cron.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +54 -52
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/config.js +2 -2
  13. package/dist/config.js.map +1 -1
  14. package/dist/entities/entity-summary.js +3 -3
  15. package/dist/entities/entity-type-config.d.ts +3 -0
  16. package/dist/entities/entity-type-config.d.ts.map +1 -1
  17. package/dist/entities/entity-type-config.js +67 -1
  18. package/dist/entities/entity-type-config.js.map +1 -1
  19. package/dist/entities/entity-types-defaults.d.ts.map +1 -1
  20. package/dist/entities/entity-types-defaults.js +2 -0
  21. package/dist/entities/entity-types-defaults.js.map +1 -1
  22. package/dist/entities/spawner.d.ts.map +1 -1
  23. package/dist/entities/spawner.js +4 -2
  24. package/dist/entities/spawner.js.map +1 -1
  25. package/dist/feral/catalog/entity-catalog-source.js +31 -31
  26. package/dist/feral/node-code/abstract-node-code.js +1 -1
  27. package/dist/feral/node-code/abstract-node-code.js.map +1 -1
  28. package/dist/feral/node-code/data/set-context-value-node-code.d.ts.map +1 -1
  29. package/dist/feral/node-code/data/set-context-value-node-code.js +10 -1
  30. package/dist/feral/node-code/data/set-context-value-node-code.js.map +1 -1
  31. package/dist/feral/node-code/entity/create-entity-type-node-code.js +20 -20
  32. package/dist/feral/node-code/entity/load-vault-context-node-code.d.ts.map +1 -1
  33. package/dist/feral/node-code/entity/load-vault-context-node-code.js +7 -15
  34. package/dist/feral/node-code/entity/load-vault-context-node-code.js.map +1 -1
  35. package/dist/feral/node-code/entity/update-entity-type-node-code.js +20 -20
  36. package/dist/feral/node-code/input/prompt-input-node-code.d.ts.map +1 -1
  37. package/dist/feral/node-code/input/prompt-input-node-code.js +6 -0
  38. package/dist/feral/node-code/input/prompt-input-node-code.js.map +1 -1
  39. package/dist/feral/node-code/input/prompt-select-node-code.d.ts.map +1 -1
  40. package/dist/feral/node-code/input/prompt-select-node-code.js +6 -0
  41. package/dist/feral/node-code/input/prompt-select-node-code.js.map +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -39
  44. package/dist/index.js.map +1 -1
  45. package/dist/llm/router.js +27 -27
  46. package/dist/personalities.js +4 -4
  47. package/dist/phaibel.txt +81 -81
  48. package/dist/service/api-router.d.ts.map +1 -1
  49. package/dist/service/api-router.js +42 -2
  50. package/dist/service/api-router.js.map +1 -1
  51. package/dist/service/cron/feedback-analysis.js +14 -14
  52. package/dist/service/cron/innovate.d.ts +2 -0
  53. package/dist/service/cron/innovate.d.ts.map +1 -0
  54. package/dist/service/cron/innovate.js +354 -0
  55. package/dist/service/cron/innovate.js.map +1 -0
  56. package/dist/service/cron/process-lifecycle.js +36 -36
  57. package/dist/service/cron/scheduler.d.ts.map +1 -1
  58. package/dist/service/cron/scheduler.js +8 -0
  59. package/dist/service/cron/scheduler.js.map +1 -1
  60. package/dist/service/index.d.ts.map +1 -1
  61. package/dist/service/index.js +27 -20
  62. package/dist/service/index.js.map +1 -1
  63. package/dist/service/web-client.html +1984 -1569
  64. package/dist/service/web-server.d.ts +1 -0
  65. package/dist/service/web-server.d.ts.map +1 -1
  66. package/dist/service/web-server.js +188 -6
  67. package/dist/service/web-server.js.map +1 -1
  68. package/dist/tools/daily.js +2 -2
  69. package/package.json +69 -66
  70. package/dist/agentary.txt +0 -82
  71. package/dist/commands/event.d.ts +0 -4
  72. package/dist/commands/event.d.ts.map +0 -1
  73. package/dist/commands/event.js +0 -515
  74. package/dist/commands/event.js.map +0 -1
  75. package/dist/commands/goal.d.ts +0 -4
  76. package/dist/commands/goal.d.ts.map +0 -1
  77. package/dist/commands/goal.js +0 -14
  78. package/dist/commands/goal.js.map +0 -1
  79. package/dist/commands/interview.d.ts +0 -14
  80. package/dist/commands/interview.d.ts.map +0 -1
  81. package/dist/commands/interview.js +0 -343
  82. package/dist/commands/interview.js.map +0 -1
  83. package/dist/commands/note.d.ts +0 -4
  84. package/dist/commands/note.d.ts.map +0 -1
  85. package/dist/commands/note.js +0 -548
  86. package/dist/commands/note.js.map +0 -1
  87. package/dist/commands/pamp.d.ts +0 -4
  88. package/dist/commands/pamp.d.ts.map +0 -1
  89. package/dist/commands/pamp.js +0 -323
  90. package/dist/commands/pamp.js.map +0 -1
  91. package/dist/commands/person.d.ts +0 -4
  92. package/dist/commands/person.d.ts.map +0 -1
  93. package/dist/commands/person.js +0 -185
  94. package/dist/commands/person.js.map +0 -1
  95. package/dist/commands/project.d.ts +0 -4
  96. package/dist/commands/project.d.ts.map +0 -1
  97. package/dist/commands/project.js +0 -70
  98. package/dist/commands/project.js.map +0 -1
  99. package/dist/commands/remember.d.ts +0 -4
  100. package/dist/commands/remember.d.ts.map +0 -1
  101. package/dist/commands/remember.js +0 -70
  102. package/dist/commands/remember.js.map +0 -1
  103. package/dist/commands/research.d.ts +0 -4
  104. package/dist/commands/research.d.ts.map +0 -1
  105. package/dist/commands/research.js +0 -14
  106. package/dist/commands/research.js.map +0 -1
  107. package/dist/commands/shell.d.ts +0 -3
  108. package/dist/commands/shell.d.ts.map +0 -1
  109. package/dist/commands/shell.js +0 -346
  110. package/dist/commands/shell.js.map +0 -1
  111. package/dist/commands/today.d.ts +0 -4
  112. package/dist/commands/today.d.ts.map +0 -1
  113. package/dist/commands/today.js +0 -112
  114. package/dist/commands/today.js.map +0 -1
  115. package/dist/commands/todo.d.ts +0 -4
  116. package/dist/commands/todo.d.ts.map +0 -1
  117. package/dist/commands/todo.js +0 -678
  118. package/dist/commands/todo.js.map +0 -1
  119. package/dist/commands/todont.d.ts +0 -19
  120. package/dist/commands/todont.d.ts.map +0 -1
  121. package/dist/commands/todont.js +0 -209
  122. package/dist/commands/todont.js.map +0 -1
  123. package/dist/commands/week.d.ts +0 -4
  124. package/dist/commands/week.d.ts.map +0 -1
  125. package/dist/commands/week.js +0 -267
  126. package/dist/commands/week.js.map +0 -1
  127. package/dist/dobbai.txt +0 -55
  128. package/dist/dobbi.txt +0 -55
  129. package/dist/dobbie-bundle.cjs +0 -73313
  130. package/dist/dobbie.txt +0 -55
  131. package/dist/feral/node-code/output/dobbai-speak-node-code.d.ts +0 -11
  132. package/dist/feral/node-code/output/dobbai-speak-node-code.d.ts.map +0 -1
  133. package/dist/feral/node-code/output/dobbai-speak-node-code.js +0 -56
  134. package/dist/feral/node-code/output/dobbai-speak-node-code.js.map +0 -1
  135. package/dist/feral/node-code/output/dobbi-speak-node-code.d.ts +0 -15
  136. package/dist/feral/node-code/output/dobbi-speak-node-code.d.ts.map +0 -1
  137. package/dist/feral/node-code/output/dobbi-speak-node-code.js +0 -60
  138. package/dist/feral/node-code/output/dobbi-speak-node-code.js.map +0 -1
  139. package/dist/feral/node-code/output/dobbie-speak-node-code.d.ts +0 -11
  140. package/dist/feral/node-code/output/dobbie-speak-node-code.d.ts.map +0 -1
  141. package/dist/feral/node-code/output/dobbie-speak-node-code.js +0 -56
  142. package/dist/feral/node-code/output/dobbie-speak-node-code.js.map +0 -1
  143. package/dist/service/cron/process-evaluator.d.ts +0 -6
  144. package/dist/service/cron/process-evaluator.d.ts.map +0 -1
  145. package/dist/service/cron/process-evaluator.js +0 -126
  146. package/dist/service/cron/process-evaluator.js.map +0 -1
  147. package/dist/tools/project.d.ts +0 -2
  148. package/dist/tools/project.d.ts.map +0 -1
  149. package/dist/tools/project.js +0 -112
  150. package/dist/tools/project.js.map +0 -1
@@ -1,1569 +1,1984 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Phaibel</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
8
- <style>
9
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
-
11
- :root {
12
- --bg: #1a1a2e;
13
- --panel: #16213e;
14
- --border: #0f3460;
15
- --text: #e0e0e0;
16
- --dim: #8888aa;
17
- --cyan: #00d4ff;
18
- --cyan-dim: #0099bb;
19
- --green: #00e676;
20
- --yellow: #ffd740;
21
- --red: #ff5252;
22
- --orange: #ff9100;
23
- }
24
-
25
- body {
26
- font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
27
- background: var(--bg);
28
- color: var(--text);
29
- height: 100vh;
30
- overflow: hidden;
31
- }
32
-
33
- .app {
34
- display: grid;
35
- grid-template-columns: 1fr 2fr;
36
- grid-template-rows: auto 1fr;
37
- height: 100vh;
38
- gap: 1px;
39
- background: var(--border);
40
- }
41
-
42
- .header {
43
- grid-column: 1 / -1;
44
- background: var(--panel);
45
- padding: 10px 20px;
46
- display: flex;
47
- align-items: center;
48
- gap: 20px;
49
- border-bottom: 1px solid var(--border);
50
- }
51
-
52
- .header h1 {
53
- font-size: 16px;
54
- font-weight: 600;
55
- color: var(--cyan);
56
- letter-spacing: 2px;
57
- text-transform: uppercase;
58
- flex-shrink: 0;
59
- }
60
-
61
- .header-stats {
62
- display: flex;
63
- gap: 16px;
64
- flex: 1;
65
- align-items: center;
66
- flex-wrap: wrap;
67
- }
68
-
69
- .stat {
70
- font-size: 10px;
71
- color: var(--dim);
72
- white-space: nowrap;
73
- }
74
-
75
- .stat-label {
76
- text-transform: uppercase;
77
- letter-spacing: 0.5px;
78
- margin-right: 4px;
79
- }
80
-
81
- .stat-value {
82
- color: var(--text);
83
- font-weight: 600;
84
- }
85
-
86
- .header-right {
87
- display: flex;
88
- align-items: center;
89
- gap: 14px;
90
- flex-shrink: 0;
91
- }
92
-
93
- .ws-status {
94
- font-size: 11px;
95
- color: var(--dim);
96
- white-space: nowrap;
97
- }
98
-
99
- .ws-status .dot {
100
- display: inline-block;
101
- width: 6px;
102
- height: 6px;
103
- border-radius: 50%;
104
- background: var(--green);
105
- margin-right: 6px;
106
- }
107
-
108
- .panel {
109
- background: var(--panel);
110
- display: flex;
111
- flex-direction: column;
112
- overflow: hidden;
113
- }
114
-
115
- .panel-title {
116
- padding: 12px 16px;
117
- font-size: 12px;
118
- font-weight: 600;
119
- text-transform: uppercase;
120
- letter-spacing: 1.5px;
121
- color: var(--cyan);
122
- border-bottom: 1px solid var(--border);
123
- flex-shrink: 0;
124
- }
125
-
126
- .panel-section-title {
127
- font-size: 12px;
128
- font-weight: 600;
129
- text-transform: uppercase;
130
- letter-spacing: 1.5px;
131
- color: var(--cyan);
132
- padding-bottom: 8px;
133
- border-bottom: 1px solid var(--border);
134
- margin-bottom: 8px;
135
- }
136
-
137
- .panel-body {
138
- flex: 1;
139
- overflow-y: auto;
140
- padding: 12px 16px;
141
- }
142
-
143
- .panel-body::-webkit-scrollbar { width: 4px; }
144
- .panel-body::-webkit-scrollbar-track { background: transparent; }
145
- .panel-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
146
-
147
- /* ── Today panel ─────────────────────────────────── */
148
-
149
- .task-item {
150
- display: flex;
151
- align-items: flex-start;
152
- gap: 10px;
153
- padding: 8px 0;
154
- border-bottom: 1px solid rgba(255,255,255,0.04);
155
- }
156
-
157
- .task-item:last-child { border-bottom: none; }
158
-
159
- .task-check {
160
- width: 16px;
161
- height: 16px;
162
- border: 1.5px solid var(--dim);
163
- border-radius: 3px;
164
- flex-shrink: 0;
165
- margin-top: 2px;
166
- cursor: pointer;
167
- transition: border-color 0.15s;
168
- }
169
-
170
- .task-check:hover { border-color: var(--cyan); }
171
-
172
- .task-info { flex: 1; min-width: 0; }
173
-
174
- .task-title {
175
- font-size: 13px;
176
- line-height: 1.4;
177
- word-break: break-word;
178
- }
179
-
180
- .task-meta {
181
- font-size: 10px;
182
- color: var(--dim);
183
- margin-top: 2px;
184
- }
185
-
186
- .badge {
187
- display: inline-block;
188
- font-size: 9px;
189
- padding: 1px 5px;
190
- border-radius: 3px;
191
- font-weight: 600;
192
- text-transform: uppercase;
193
- letter-spacing: 0.5px;
194
- }
195
-
196
- .badge-critical { background: var(--red); color: #fff; }
197
- .badge-high { background: var(--orange); color: #1a1a2e; }
198
- .badge-medium { background: var(--yellow); color: #1a1a2e; }
199
- .badge-low { background: var(--dim); color: #1a1a2e; }
200
-
201
- .empty-state {
202
- color: var(--dim);
203
- font-size: 12px;
204
- text-align: center;
205
- padding: 32px 16px;
206
- line-height: 1.6;
207
- }
208
-
209
- /* ── Calendar panel ──────────────────────────────── */
210
-
211
- .day-group { margin-bottom: 16px; }
212
-
213
- .day-header {
214
- font-size: 11px;
215
- font-weight: 600;
216
- text-transform: uppercase;
217
- letter-spacing: 1px;
218
- color: var(--cyan-dim);
219
- padding-bottom: 6px;
220
- border-bottom: 1px solid rgba(255,255,255,0.06);
221
- margin-bottom: 6px;
222
- }
223
-
224
- .day-header.today { color: var(--cyan); }
225
-
226
- .event-item {
227
- padding: 6px 0;
228
- font-size: 13px;
229
- }
230
-
231
- .event-time {
232
- font-size: 10px;
233
- color: var(--dim);
234
- }
235
-
236
- .event-location {
237
- font-size: 10px;
238
- color: var(--dim);
239
- font-style: italic;
240
- }
241
-
242
- .event-cal {
243
- display: inline-block;
244
- font-size: 9px;
245
- padding: 1px 5px;
246
- border-radius: 3px;
247
- background: rgba(0, 212, 255, 0.12);
248
- color: var(--cyan-dim);
249
- font-weight: 600;
250
- letter-spacing: 0.3px;
251
- margin-left: 6px;
252
- vertical-align: middle;
253
- }
254
-
255
- .day-empty {
256
- font-size: 11px;
257
- color: var(--dim);
258
- padding: 4px 0;
259
- font-style: italic;
260
- }
261
-
262
- /* ── Scheduler panel ────────────────────────────────── */
263
-
264
- .sched-item {
265
- display: flex;
266
- align-items: center;
267
- gap: 8px;
268
- padding: 5px 0;
269
- border-bottom: 1px solid rgba(255,255,255,0.04);
270
- font-size: 12px;
271
- }
272
-
273
- .sched-item:last-child { border-bottom: none; }
274
-
275
- .sched-dot {
276
- width: 7px;
277
- height: 7px;
278
- border-radius: 50%;
279
- flex-shrink: 0;
280
- }
281
-
282
- .sched-dot.on { background: var(--green); }
283
- .sched-dot.off { background: var(--dim); opacity: 0.4; }
284
-
285
- .sched-name {
286
- flex: 1;
287
- min-width: 0;
288
- white-space: nowrap;
289
- overflow: hidden;
290
- text-overflow: ellipsis;
291
- }
292
-
293
- .sched-detail {
294
- font-size: 10px;
295
- color: var(--dim);
296
- text-align: right;
297
- white-space: nowrap;
298
- }
299
-
300
- .sched-error {
301
- color: var(--red);
302
- }
303
-
304
- .sched-actions {
305
- display: flex;
306
- gap: 4px;
307
- flex-shrink: 0;
308
- }
309
-
310
- .sched-btn {
311
- background: rgba(255,255,255,0.06);
312
- border: 1px solid rgba(255,255,255,0.1);
313
- color: var(--dim);
314
- font-size: 10px;
315
- padding: 1px 6px;
316
- border-radius: 3px;
317
- cursor: pointer;
318
- white-space: nowrap;
319
- }
320
-
321
- .sched-btn:hover {
322
- background: rgba(255,255,255,0.12);
323
- color: var(--text);
324
- }
325
-
326
- /* ── Chat panel ──────────────────────────────────── */
327
-
328
- .chat-panel {
329
- display: flex;
330
- flex-direction: column;
331
- }
332
-
333
- .chat-messages {
334
- flex: 1;
335
- overflow-y: auto;
336
- padding: 12px 16px;
337
- }
338
-
339
- .chat-messages::-webkit-scrollbar { width: 4px; }
340
- .chat-messages::-webkit-scrollbar-track { background: transparent; }
341
- .chat-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
342
-
343
- .chat-msg {
344
- margin-bottom: 12px;
345
- line-height: 1.5;
346
- }
347
-
348
- .chat-msg.user {
349
- text-align: right;
350
- }
351
-
352
- .chat-msg.user .chat-bubble {
353
- background: var(--border);
354
- display: inline-block;
355
- padding: 8px 12px;
356
- border-radius: 12px 12px 2px 12px;
357
- max-width: 85%;
358
- text-align: left;
359
- font-size: 13px;
360
- }
361
-
362
- .chat-msg.bot .chat-bubble {
363
- background: rgba(0, 212, 255, 0.08);
364
- border: 1px solid rgba(0, 212, 255, 0.15);
365
- display: inline-block;
366
- padding: 8px 12px;
367
- border-radius: 12px 12px 12px 2px;
368
- max-width: 85%;
369
- font-size: 13px;
370
- }
371
-
372
- /* ── Markdown inside bot bubbles ──────────────────── */
373
-
374
- .chat-bubble p { margin: 0 0 8px 0; }
375
- .chat-bubble p:last-child { margin-bottom: 0; }
376
-
377
- .chat-bubble ul, .chat-bubble ol {
378
- margin: 4px 0 8px 0;
379
- padding-left: 20px;
380
- }
381
- .chat-bubble li { margin-bottom: 2px; }
382
-
383
- .chat-bubble h1, .chat-bubble h2, .chat-bubble h3,
384
- .chat-bubble h4, .chat-bubble h5, .chat-bubble h6 {
385
- margin: 8px 0 4px 0;
386
- color: var(--cyan);
387
- font-size: 13px;
388
- }
389
- .chat-bubble h1 { font-size: 15px; }
390
- .chat-bubble h2 { font-size: 14px; }
391
-
392
- .chat-bubble code {
393
- background: rgba(255,255,255,0.08);
394
- padding: 1px 4px;
395
- border-radius: 3px;
396
- font-size: 12px;
397
- }
398
-
399
- .chat-bubble pre {
400
- background: var(--bg);
401
- border: 1px solid var(--border);
402
- border-radius: 4px;
403
- padding: 8px 10px;
404
- margin: 6px 0;
405
- overflow-x: auto;
406
- font-size: 12px;
407
- }
408
- .chat-bubble pre code {
409
- background: none;
410
- padding: 0;
411
- border-radius: 0;
412
- }
413
-
414
- .chat-bubble blockquote {
415
- border-left: 3px solid var(--cyan-dim);
416
- padding-left: 10px;
417
- margin: 6px 0;
418
- color: var(--dim);
419
- }
420
-
421
- .chat-bubble strong { color: #fff; }
422
-
423
- .chat-bubble a {
424
- color: var(--cyan);
425
- text-decoration: none;
426
- }
427
- .chat-bubble a:hover { text-decoration: underline; }
428
-
429
- .chat-bubble hr {
430
- border: none;
431
- border-top: 1px solid var(--border);
432
- margin: 8px 0;
433
- }
434
-
435
- .chat-bubble table {
436
- border-collapse: collapse;
437
- margin: 6px 0;
438
- font-size: 12px;
439
- }
440
- .chat-bubble th, .chat-bubble td {
441
- border: 1px solid var(--border);
442
- padding: 3px 8px;
443
- }
444
- .chat-bubble th { background: rgba(255,255,255,0.05); }
445
-
446
- .chat-msg.thinking .chat-bubble {
447
- color: var(--dim);
448
- font-style: italic;
449
- font-size: 11px;
450
- border: none;
451
- background: none;
452
- }
453
-
454
- .chat-msg.error .chat-bubble {
455
- color: var(--red);
456
- font-size: 12px;
457
- border-color: rgba(255, 82, 82, 0.2);
458
- background: rgba(255, 82, 82, 0.05);
459
- }
460
-
461
- .chat-id-label {
462
- font-size: 9px;
463
- color: var(--dim);
464
- margin-top: 4px;
465
- letter-spacing: 0.5px;
466
- }
467
-
468
- .chat-msg.question .chat-bubble {
469
- background: rgba(0, 212, 255, 0.08);
470
- border: 1px solid rgba(0, 212, 255, 0.25);
471
- display: inline-block;
472
- padding: 10px 14px;
473
- border-radius: 12px 12px 12px 2px;
474
- max-width: 85%;
475
- font-size: 13px;
476
- }
477
-
478
- .chat-msg.question .question-text {
479
- margin-bottom: 10px;
480
- color: var(--cyan);
481
- font-weight: 600;
482
- }
483
-
484
- .chat-msg.question .question-options {
485
- display: flex;
486
- flex-wrap: wrap;
487
- gap: 6px;
488
- }
489
-
490
- .chat-msg.question .question-option-btn {
491
- background: var(--border);
492
- color: var(--text);
493
- border: 1px solid var(--cyan-dim);
494
- font-family: inherit;
495
- font-size: 12px;
496
- padding: 5px 12px;
497
- border-radius: 4px;
498
- cursor: pointer;
499
- transition: background 0.15s, border-color 0.15s;
500
- }
501
-
502
- .chat-msg.question .question-option-btn:hover {
503
- background: rgba(0, 212, 255, 0.15);
504
- border-color: var(--cyan);
505
- }
506
-
507
- .chat-msg.question .question-option-btn:disabled {
508
- opacity: 0.4;
509
- cursor: not-allowed;
510
- }
511
-
512
- .chat-msg.question .question-input-wrap {
513
- display: flex;
514
- gap: 6px;
515
- }
516
-
517
- .chat-msg.question .question-input {
518
- flex: 1;
519
- background: var(--bg);
520
- border: 1px solid var(--border);
521
- color: var(--text);
522
- font-family: inherit;
523
- font-size: 12px;
524
- padding: 6px 10px;
525
- border-radius: 4px;
526
- outline: none;
527
- }
528
-
529
- .chat-msg.question .question-input:focus { border-color: var(--cyan); }
530
-
531
- .chat-msg.question .question-submit-btn {
532
- background: var(--cyan);
533
- color: var(--bg);
534
- border: none;
535
- font-family: inherit;
536
- font-size: 11px;
537
- font-weight: 600;
538
- padding: 6px 12px;
539
- border-radius: 4px;
540
- cursor: pointer;
541
- }
542
-
543
- .chat-msg.question .question-submit-btn:disabled {
544
- opacity: 0.4;
545
- cursor: not-allowed;
546
- }
547
-
548
- .chat-process {
549
- margin-bottom: 12px;
550
- }
551
-
552
- .process-toggle {
553
- font-size: 11px;
554
- color: var(--cyan-dim);
555
- cursor: pointer;
556
- user-select: none;
557
- padding: 4px 0;
558
- }
559
-
560
- .process-toggle:hover { color: var(--cyan); }
561
-
562
- .process-diagram {
563
- background: var(--bg);
564
- border: 1px solid var(--border);
565
- border-radius: 6px;
566
- padding: 12px;
567
- margin-top: 6px;
568
- overflow-x: auto;
569
- }
570
-
571
- .process-diagram svg {
572
- max-width: 100%;
573
- height: auto;
574
- }
575
-
576
- .chat-input-wrap {
577
- padding: 12px 16px;
578
- border-top: 1px solid var(--border);
579
- flex-shrink: 0;
580
- }
581
-
582
- .chat-input-wrap form {
583
- display: flex;
584
- gap: 8px;
585
- }
586
-
587
- .chat-input {
588
- flex: 1;
589
- background: var(--bg);
590
- border: 1px solid var(--border);
591
- color: var(--text);
592
- font-family: inherit;
593
- font-size: 13px;
594
- padding: 8px 12px;
595
- border-radius: 6px;
596
- outline: none;
597
- transition: border-color 0.15s;
598
- }
599
-
600
- .chat-input:focus { border-color: var(--cyan); }
601
- .chat-input::placeholder { color: var(--dim); }
602
-
603
- .chat-send {
604
- background: var(--cyan);
605
- color: var(--bg);
606
- border: none;
607
- font-family: inherit;
608
- font-size: 12px;
609
- font-weight: 600;
610
- padding: 8px 16px;
611
- border-radius: 6px;
612
- cursor: pointer;
613
- transition: opacity 0.15s;
614
- }
615
-
616
- .chat-send:hover { opacity: 0.85; }
617
- .chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
618
-
619
- .chat-mic {
620
- background: transparent;
621
- border: 1px solid var(--border);
622
- color: var(--dim);
623
- font-size: 16px;
624
- width: 36px;
625
- height: 36px;
626
- border-radius: 6px;
627
- cursor: pointer;
628
- display: flex;
629
- align-items: center;
630
- justify-content: center;
631
- flex-shrink: 0;
632
- transition: border-color 0.15s, color 0.15s, background 0.15s;
633
- }
634
-
635
- .chat-mic:hover { border-color: var(--cyan); color: var(--text); }
636
-
637
- .chat-mic.listening {
638
- border-color: var(--red);
639
- color: var(--red);
640
- background: rgba(255, 82, 82, 0.1);
641
- animation: mic-pulse 1.2s ease-in-out infinite;
642
- }
643
-
644
- .chat-mic.unsupported { display: none; }
645
-
646
- @keyframes mic-pulse {
647
- 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.3); }
648
- 50% { box-shadow: 0 0 0 6px rgba(255, 82, 82, 0); }
649
- }
650
-
651
- /* ── Reaction buttons ───────────────────────────── */
652
-
653
- .chat-reactions {
654
- display: flex;
655
- gap: 4px;
656
- margin-top: 4px;
657
- }
658
-
659
- .reaction-btn {
660
- background: rgba(255,255,255,0.06);
661
- border: 1px solid rgba(255,255,255,0.1);
662
- color: var(--dim);
663
- font-size: 12px;
664
- width: 28px;
665
- height: 24px;
666
- border-radius: 4px;
667
- cursor: pointer;
668
- display: flex;
669
- align-items: center;
670
- justify-content: center;
671
- transition: background 0.15s, border-color 0.15s, color 0.15s;
672
- }
673
-
674
- .reaction-btn:hover { background: rgba(255,255,255,0.12); color: var(--text); }
675
-
676
- .reaction-btn.selected {
677
- border-color: var(--cyan);
678
- color: var(--cyan);
679
- background: rgba(0, 212, 255, 0.1);
680
- cursor: default;
681
- }
682
-
683
- .reaction-btn:disabled { opacity: 0.4; cursor: default; }
684
-
685
- .reaction-details-wrap {
686
- display: flex;
687
- gap: 4px;
688
- margin-top: 4px;
689
- }
690
-
691
- .reaction-details-input {
692
- flex: 1;
693
- background: var(--bg);
694
- border: 1px solid var(--border);
695
- color: var(--text);
696
- font-family: inherit;
697
- font-size: 11px;
698
- padding: 4px 8px;
699
- border-radius: 4px;
700
- outline: none;
701
- }
702
-
703
- .reaction-details-input:focus { border-color: var(--cyan); }
704
-
705
- .reaction-details-submit {
706
- background: var(--cyan);
707
- color: var(--bg);
708
- border: none;
709
- font-family: inherit;
710
- font-size: 10px;
711
- font-weight: 600;
712
- padding: 4px 8px;
713
- border-radius: 4px;
714
- cursor: pointer;
715
- }
716
-
717
- /* ── Responsive ──────────────────────────────────── */
718
-
719
- @media (max-width: 700px) {
720
- .app {
721
- grid-template-columns: 1fr;
722
- grid-template-rows: auto auto 1fr;
723
- }
724
- }
725
- </style>
726
- </head>
727
- <body>
728
-
729
- <div class="app">
730
- <div class="header">
731
- <h1 id="header-agent-name">Phaibel</h1>
732
- <div class="header-stats" id="header-stats">
733
- <div class="stat"><span class="stat-label">Vault</span><span class="stat-value" id="stat-vault">--</span></div>
734
- <div class="stat"><span class="stat-label">Queue</span><span class="stat-value" id="stat-queue">--</span></div>
735
- <div class="stat"><span class="stat-label">Graph</span><span class="stat-value" id="stat-graph">--</span></div>
736
- <div class="stat"><span class="stat-label">Mem</span><span class="stat-value" id="stat-mem">--</span></div>
737
- <div class="stat"><span class="stat-label">Uptime</span><span class="stat-value" id="stat-uptime">--</span></div>
738
- </div>
739
- <div class="header-right">
740
- <div class="ws-status" id="ws-status"><span class="dot"></span>Connected</div>
741
- </div>
742
- </div>
743
-
744
- <!-- Left: Timeline + Scheduler -->
745
- <div class="panel">
746
- <div class="panel-body" id="left-body">
747
- <div id="timeline-body">
748
- <div class="empty-state">Loading…</div>
749
- </div>
750
- <div class="panel-section-title" style="margin-top: 16px;">Scheduler</div>
751
- <div id="scheduler-body">
752
- <div class="empty-state">Loading scheduler…</div>
753
- </div>
754
- </div>
755
- </div>
756
-
757
- <!-- Chat -->
758
- <div class="panel chat-panel">
759
- <div class="panel-title">Chat</div>
760
- <div class="chat-messages" id="chat-messages">
761
- <div class="chat-msg bot">
762
- <div class="chat-bubble">Ready to serve.</div>
763
- </div>
764
- </div>
765
- <div class="chat-input-wrap">
766
- <form id="chat-form">
767
- <button class="chat-mic" id="chat-mic" type="button" title="Voice input">&#x1F3A4;</button>
768
- <input class="chat-input" id="chat-input" type="text"
769
- placeholder="Ask me anything…" autocomplete="off">
770
- <button class="chat-send" id="chat-send" type="submit">Send</button>
771
- </form>
772
- </div>
773
- </div>
774
- </div>
775
-
776
- <script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
777
- <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
778
- <script>mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#0f3460', primaryTextColor: '#e0e0e0', primaryBorderColor: '#00d4ff', lineColor: '#00d4ff', secondaryColor: '#16213e', tertiaryColor: '#1a1a2e', fontFamily: 'monospace' } });</script>
779
- <script>
780
- (function() {
781
- const timelineBody = document.getElementById('timeline-body');
782
- const schedulerBody = document.getElementById('scheduler-body');
783
- const chatMessages = document.getElementById('chat-messages');
784
- const chatForm = document.getElementById('chat-form');
785
- const chatInput = document.getElementById('chat-input');
786
- const chatSend = document.getElementById('chat-send');
787
- const wsStatusEl = document.getElementById('ws-status');
788
- const statVault = document.getElementById('stat-vault');
789
- const statQueue = document.getElementById('stat-queue');
790
- const statGraph = document.getElementById('stat-graph');
791
- const statMem = document.getElementById('stat-mem');
792
- const statUptime = document.getElementById('stat-uptime');
793
-
794
- const chatMic = document.getElementById('chat-mic');
795
-
796
- let ws = null;
797
- let thinkingEl = null;
798
- let currentChatId = null;
799
- let calendarNames = {}; // id → name
800
-
801
- // ── Speech-to-Text ──────────────────────────────────────────
802
-
803
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
804
- let recognition = null;
805
- let micListening = false;
806
-
807
- if (SpeechRecognition) {
808
- recognition = new SpeechRecognition();
809
- recognition.lang = 'en-US';
810
- recognition.continuous = false;
811
- recognition.interimResults = true;
812
-
813
- let finalTranscript = '';
814
-
815
- recognition.onstart = () => {
816
- micListening = true;
817
- chatMic.classList.add('listening');
818
- chatMic.title = 'Listening… click to stop';
819
- };
820
-
821
- recognition.onend = () => {
822
- micListening = false;
823
- chatMic.classList.remove('listening');
824
- chatMic.title = 'Voice input';
825
- // If we got a final transcript and the input has text, auto-submit
826
- if (finalTranscript && chatInput.value.trim()) {
827
- chatForm.dispatchEvent(new Event('submit'));
828
- }
829
- finalTranscript = '';
830
- };
831
-
832
- recognition.onresult = (event) => {
833
- let interim = '';
834
- finalTranscript = '';
835
- for (let i = event.resultIndex; i < event.results.length; i++) {
836
- const t = event.results[i][0].transcript;
837
- if (event.results[i].isFinal) {
838
- finalTranscript += t;
839
- } else {
840
- interim += t;
841
- }
842
- }
843
- // Show interim results in the input as they come
844
- if (finalTranscript) {
845
- chatInput.value = finalTranscript;
846
- } else {
847
- chatInput.value = interim;
848
- }
849
- };
850
-
851
- recognition.onerror = (event) => {
852
- micListening = false;
853
- chatMic.classList.remove('listening');
854
- chatMic.title = 'Voice input';
855
- if (event.error !== 'aborted' && event.error !== 'no-speech') {
856
- console.warn('Speech recognition error:', event.error);
857
- }
858
- };
859
-
860
- chatMic.addEventListener('click', () => {
861
- if (chatInput.disabled) return;
862
- if (micListening) {
863
- recognition.stop();
864
- } else {
865
- chatInput.value = '';
866
- recognition.start();
867
- }
868
- });
869
- } else {
870
- chatMic.classList.add('unsupported');
871
- }
872
-
873
- // ── Data fetching ──────────────────────────────────────────
874
-
875
- async function fetchCalendarNames() {
876
- try {
877
- const res = await fetch('/api/calendars');
878
- const cals = await res.json();
879
- calendarNames = {};
880
- for (const c of cals) calendarNames[c.id] = c.name;
881
- } catch { /* ignore */ }
882
- }
883
-
884
- function getValidDate(due) {
885
- if (!due || typeof due !== 'string') return null;
886
- if (due.startsWith('{')) return null;
887
- if (!/^\d{4}-\d{2}-\d{2}/.test(due)) return null;
888
- return due.slice(0, 10);
889
- }
890
-
891
- async function fetchTimeline() {
892
- try {
893
- const [taskRes, eventRes] = await Promise.all([
894
- fetch('/api/entities/task'),
895
- fetch('/api/entities/event'),
896
- ]);
897
- const tasks = await taskRes.json();
898
- const events = await eventRes.json();
899
-
900
- const now = new Date();
901
- const dates = [];
902
- for (let i = 0; i < 3; i++) {
903
- const d = new Date(now);
904
- d.setDate(d.getDate() + i);
905
- dates.push(d.toISOString().split('T')[0]);
906
- }
907
- const todayStr = dates[0];
908
-
909
- // Buckets: overdue, today, tomorrow, day-after
910
- const overdue = [];
911
- const buckets = { [dates[0]]: [], [dates[1]]: [], [dates[2]]: [] };
912
-
913
- // Assign tasks to buckets
914
- for (const e of tasks) {
915
- if (e.meta.status === 'done') continue;
916
- const due = getValidDate(e.meta.dueDate);
917
- const item = {
918
- kind: 'task',
919
- id: e.meta.id,
920
- title: e.meta.title,
921
- status: e.meta.status || 'open',
922
- priority: e.meta.priority || 'medium',
923
- dueDate: due,
924
- };
925
- if (!due) {
926
- // No date → today bucket
927
- buckets[dates[0]].push(item);
928
- } else if (due < todayStr) {
929
- overdue.push(item);
930
- } else if (buckets[due]) {
931
- buckets[due].push(item);
932
- }
933
- // Future beyond 2 days → skip
934
- }
935
-
936
- // Assign events to buckets
937
- for (const e of events) {
938
- const eventDate = ((e.meta.startDate || '') + '').split('T')[0];
939
- if (!buckets[eventDate]) continue;
940
- buckets[eventDate].push({
941
- kind: 'event',
942
- title: e.meta.title,
943
- startDate: e.meta.startDate,
944
- endDate: e.meta.endDate,
945
- location: e.meta.location || null,
946
- calendarId: e.meta.calendarId || null,
947
- });
948
- }
949
-
950
- renderTimeline(overdue, buckets, dates);
951
- } catch {
952
- timelineBody.innerHTML = '<div class="empty-state">Failed to load timeline.</div>';
953
- }
954
- }
955
-
956
- async function fetchScheduler() {
957
- try {
958
- const res = await fetch('/api/scheduler');
959
- const data = await res.json();
960
- renderScheduler(data);
961
- } catch {
962
- schedulerBody.innerHTML = '<div class="empty-state">Failed to load scheduler.</div>';
963
- }
964
- }
965
-
966
- window.schedToggle = async function(jobName) {
967
- try {
968
- const res = await fetch('/api/scheduler/' + encodeURIComponent(jobName) + '/toggle', { method: 'POST' });
969
- const data = await res.json();
970
- if (data.jobs) renderScheduler(data);
971
- else await fetchScheduler();
972
- } catch { await fetchScheduler(); }
973
- };
974
-
975
- window.schedRun = async function(jobName) {
976
- try {
977
- const res = await fetch('/api/scheduler/' + encodeURIComponent(jobName) + '/run', { method: 'POST' });
978
- const data = await res.json();
979
- if (data.jobs) renderScheduler(data);
980
- else await fetchScheduler();
981
- } catch { await fetchScheduler(); }
982
- };
983
-
984
- async function fetchStatus() {
985
- try {
986
- const res = await fetch('/api/status');
987
- const data = await res.json();
988
- statVault.textContent = data.vaultRoot || '--';
989
- statQueue.textContent = String(data.queueSize ?? 0);
990
- statGraph.textContent = (data.graph ? data.graph.nodes + 'n / ' + data.graph.edges + 'e' : '--');
991
- statMem.textContent = (data.memory ? data.memory.rss + ' MB' : '--');
992
- statUptime.textContent = formatUptime(data.uptime || 0);
993
- // Update agent name in header if available
994
- if (data.agentName) {
995
- document.getElementById('header-agent-name').textContent = data.agentName;
996
- }
997
- } catch { /* ignore */ }
998
- }
999
-
1000
- function formatUptime(seconds) {
1001
- if (seconds < 60) return seconds + 's';
1002
- if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
1003
- const h = Math.floor(seconds / 3600);
1004
- const m = Math.floor((seconds % 3600) / 60);
1005
- return h + 'h ' + m + 'm';
1006
- }
1007
-
1008
- // ── Rendering ──────────────────────────────────────────────
1009
-
1010
- function renderTimeline(overdue, buckets, dates) {
1011
- const dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
1012
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1013
- const sections = [];
1014
-
1015
- // A) Overdue
1016
- if (overdue.length > 0) {
1017
- overdue.sort((a, b) => (a.dueDate || '').localeCompare(b.dueDate || ''));
1018
- sections.push(renderSection('Overdue', null, overdue, true));
1019
- }
1020
-
1021
- // B) Today, Tomorrow, Day After
1022
- const labels = ['Today', null, null];
1023
- for (let i = 0; i < dates.length; i++) {
1024
- const date = dates[i];
1025
- const d = new Date(date + 'T12:00:00');
1026
- const label = labels[i] || dayNames[d.getDay()];
1027
- const items = buckets[date] || [];
1028
-
1029
- // Sort: events by time first, then tasks by priority
1030
- items.sort((a, b) => {
1031
- // Events before tasks
1032
- if (a.kind === 'event' && b.kind !== 'event') return -1;
1033
- if (a.kind !== 'event' && b.kind === 'event') return 1;
1034
- // Events: by start time
1035
- if (a.kind === 'event' && b.kind === 'event') {
1036
- return (a.startDate || '').localeCompare(b.startDate || '');
1037
- }
1038
- // Tasks: by priority
1039
- const pa = priorityOrder[a.priority] ?? 2;
1040
- const pb = priorityOrder[b.priority] ?? 2;
1041
- return pa - pb;
1042
- });
1043
-
1044
- sections.push(renderSection(label, date, items, i === 0));
1045
- }
1046
-
1047
- const html = sections.join('');
1048
- timelineBody.innerHTML = html || '<div class="empty-state">Nothing on the horizon. All clear!</div>';
1049
-
1050
- // Wire up task checkboxes
1051
- timelineBody.querySelectorAll('.task-check').forEach(el => {
1052
- el.addEventListener('click', async () => {
1053
- const item = el.closest('.task-item');
1054
- const id = item.dataset.taskId;
1055
- if (!id) return;
1056
- item.style.opacity = '0.4';
1057
- try {
1058
- const res = await fetch('/api/entities/task/' + encodeURIComponent(id), {
1059
- method: 'PATCH',
1060
- headers: { 'Content-Type': 'application/json' },
1061
- body: JSON.stringify({ status: 'done' }),
1062
- });
1063
- if (res.ok) {
1064
- item.remove();
1065
- } else {
1066
- item.style.opacity = '1';
1067
- }
1068
- } catch {
1069
- item.style.opacity = '1';
1070
- }
1071
- });
1072
- });
1073
- }
1074
-
1075
- function renderSection(label, date, items, isHighlight) {
1076
- const headerClass = isHighlight ? ' today' : '';
1077
- const dateStr = date ? ' · ' + date : '';
1078
- let body;
1079
-
1080
- if (items.length === 0) {
1081
- body = '<div class="day-empty">Nothing scheduled</div>';
1082
- } else {
1083
- body = items.map(item => {
1084
- if (item.kind === 'event') {
1085
- const time = item.startDate ? formatTime(item.startDate) : '';
1086
- const calName = item.calendarId && calendarNames[item.calendarId] ? calendarNames[item.calendarId] : '';
1087
- return `<div class="event-item">
1088
- ${time ? '<span class="event-time">' + time + '</span> ' : ''}
1089
- ${esc(item.title)}${calName ? '<span class="event-cal">' + esc(calName) + '</span>' : ''}
1090
- ${item.location ? '<div class="event-location">' + esc(item.location) + '</div>' : ''}
1091
- </div>`;
1092
- } else {
1093
- return `<div class="task-item" data-task-id="${esc(item.id)}">
1094
- <div class="task-check" title="Mark done"></div>
1095
- <div class="task-info">
1096
- <div class="task-title">${esc(item.title)}</div>
1097
- <div class="task-meta">
1098
- <span class="badge badge-${item.priority}">${item.priority}</span>
1099
- ${item.dueDate ? ' · due ' + item.dueDate : ''}
1100
- </div>
1101
- </div>
1102
- </div>`;
1103
- }
1104
- }).join('');
1105
- }
1106
-
1107
- return `<div class="day-group">
1108
- <div class="day-header${headerClass}">${label}${dateStr}</div>
1109
- ${body}
1110
- </div>`;
1111
- }
1112
-
1113
- function renderScheduler(data) {
1114
- if (!data.jobs || data.jobs.length === 0) {
1115
- schedulerBody.innerHTML = '<div class="empty-state">No scheduler jobs configured.</div>';
1116
- return;
1117
- }
1118
-
1119
- schedulerBody.innerHTML = data.jobs.map(j => {
1120
- const dot = j.enabled ? 'on' : 'off';
1121
- let detail;
1122
- if (j.running) {
1123
- detail = 'running…';
1124
- } else if (!j.enabled) {
1125
- detail = 'disabled';
1126
- } else if (j.lastRunAt) {
1127
- detail = timeAgo(j.lastRunAt);
1128
- if (j.lastError) {
1129
- detail += ' · <span class="sched-error">' + esc(j.lastError.slice(0, 40)) + '</span>';
1130
- } else if (j.lastResult) {
1131
- detail += ' · ' + esc(j.lastResult.slice(0, 40));
1132
- }
1133
- } else if (j.nextRunAt) {
1134
- detail = 'next ' + timeAgo(j.nextRunAt).replace(' ago', '');
1135
- } else {
1136
- detail = 'every ' + formatInterval(j.intervalMinutes);
1137
- }
1138
- const toggleLabel = j.enabled ? 'disable' : 'enable';
1139
- const runBtn = j.enabled
1140
- ? `<button class="sched-btn" onclick="schedRun('${esc(j.name)}')"${j.running ? ' disabled' : ''}>run</button>`
1141
- : '';
1142
- return `<div class="sched-item">
1143
- <span class="sched-dot ${dot}"></span>
1144
- <span class="sched-name">${esc(j.name)}</span>
1145
- <span class="sched-detail">${detail}</span>
1146
- <span class="sched-actions">
1147
- <button class="sched-btn" onclick="schedToggle('${esc(j.name)}')">${toggleLabel}</button>
1148
- ${runBtn}
1149
- </span>
1150
- </div>`;
1151
- }).join('');
1152
- }
1153
-
1154
- function formatInterval(minutes) {
1155
- if (minutes < 60) return minutes + 'm';
1156
- if (minutes < 1440) return Math.floor(minutes / 60) + 'h';
1157
- return Math.floor(minutes / 1440) + 'd';
1158
- }
1159
-
1160
- function timeAgo(isoString) {
1161
- try {
1162
- const diff = Date.now() - new Date(isoString).getTime();
1163
- if (diff < 0) return 'just now';
1164
- const secs = Math.floor(diff / 1000);
1165
- if (secs < 60) return secs + 's ago';
1166
- const mins = Math.floor(secs / 60);
1167
- if (mins < 60) return mins + 'm ago';
1168
- const hrs = Math.floor(mins / 60);
1169
- if (hrs < 24) return hrs + 'h ago';
1170
- const days = Math.floor(hrs / 24);
1171
- return days + 'd ago';
1172
- } catch { return 'unknown'; }
1173
- }
1174
-
1175
- // ── WebSocket ──────────────────────────────────────────────
1176
-
1177
- function connectWs() {
1178
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1179
- ws = new WebSocket(proto + '//' + location.host);
1180
-
1181
- ws.onopen = () => {
1182
- wsStatusEl.innerHTML = '<span class="dot"></span>Connected';
1183
- };
1184
-
1185
- ws.onclose = () => {
1186
- wsStatusEl.innerHTML = '<span class="dot" style="background:var(--red)"></span>Disconnected';
1187
- // Reconnect after 3s
1188
- setTimeout(connectWs, 3000);
1189
- };
1190
-
1191
- ws.onmessage = (evt) => {
1192
- let msg;
1193
- try { msg = JSON.parse(evt.data); } catch { return; }
1194
-
1195
- try {
1196
- switch (msg.type) {
1197
- case 'chat.start':
1198
- currentChatId = msg.chatId || null;
1199
- break;
1200
- case 'chat.thinking':
1201
- showThinking(msg.status);
1202
- break;
1203
- case 'chat.question':
1204
- clearThinking();
1205
- showQuestion(msg.question, msg.options);
1206
- break;
1207
- case 'chat.process':
1208
- clearThinking();
1209
- showProcessDiagram(msg.process);
1210
- break;
1211
- case 'chat.response':
1212
- clearThinking();
1213
- if (msg.chatId) currentChatId = msg.chatId;
1214
- addMessage('bot', msg.message, currentChatId);
1215
- currentChatId = null;
1216
- unlockChat();
1217
- break;
1218
- case 'chat.error':
1219
- clearThinking();
1220
- addMessage('error', msg.error);
1221
- currentChatId = null;
1222
- unlockChat();
1223
- break;
1224
- case 'refresh':
1225
- if (msg.panel === 'today' || msg.panel === 'calendar') fetchTimeline();
1226
- break;
1227
- }
1228
- } catch (err) {
1229
- console.error('WS message handler error:', err);
1230
- clearThinking();
1231
- addMessage('error', 'Client error: ' + (err.message || err));
1232
- unlockChat();
1233
- }
1234
- };
1235
- }
1236
-
1237
- // ── Chat UI ────────────────────────────────────────────────
1238
-
1239
- chatForm.addEventListener('submit', (e) => {
1240
- e.preventDefault();
1241
- const text = chatInput.value.trim();
1242
- if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
1243
-
1244
- addMessage('user', text);
1245
- ws.send(JSON.stringify({ type: 'chat', message: text }));
1246
- chatInput.value = '';
1247
- chatSend.disabled = true;
1248
- chatInput.disabled = true;
1249
- });
1250
-
1251
- function addMessage(role, text, chatId) {
1252
- const div = document.createElement('div');
1253
- div.className = 'chat-msg ' + role;
1254
- const bubble = document.createElement('div');
1255
- bubble.className = 'chat-bubble';
1256
- if (role === 'bot') {
1257
- bubble.innerHTML = renderMarkdown(text || '');
1258
- } else {
1259
- bubble.textContent = text || '';
1260
- }
1261
- div.appendChild(bubble);
1262
- if (chatId && role === 'bot') {
1263
- const idLabel = document.createElement('div');
1264
- idLabel.className = 'chat-id-label';
1265
- idLabel.textContent = chatId;
1266
- div.appendChild(idLabel);
1267
-
1268
- // Reaction buttons
1269
- const reactions = document.createElement('div');
1270
- reactions.className = 'chat-reactions';
1271
-
1272
- const upBtn = document.createElement('button');
1273
- upBtn.className = 'reaction-btn';
1274
- upBtn.innerHTML = '+';
1275
- upBtn.title = 'Helpful';
1276
-
1277
- const downBtn = document.createElement('button');
1278
- downBtn.className = 'reaction-btn';
1279
- downBtn.innerHTML = '&minus;';
1280
- downBtn.title = 'Not helpful';
1281
-
1282
- function sendReaction(reaction, details) {
1283
- if (ws && ws.readyState === WebSocket.OPEN) {
1284
- ws.send(JSON.stringify({
1285
- type: 'chat.reaction',
1286
- chatId: chatId,
1287
- reaction: reaction,
1288
- details: details || undefined,
1289
- }));
1290
- }
1291
- }
1292
-
1293
- upBtn.addEventListener('click', function() {
1294
- upBtn.classList.add('selected');
1295
- upBtn.disabled = true;
1296
- downBtn.disabled = true;
1297
- sendReaction('positive');
1298
- });
1299
-
1300
- downBtn.addEventListener('click', function() {
1301
- downBtn.classList.add('selected');
1302
- upBtn.disabled = true;
1303
- downBtn.disabled = true;
1304
-
1305
- var detailsWrap = document.createElement('div');
1306
- detailsWrap.className = 'reaction-details-wrap';
1307
- var detailsInput = document.createElement('input');
1308
- detailsInput.className = 'reaction-details-input';
1309
- detailsInput.type = 'text';
1310
- detailsInput.placeholder = 'What went wrong? (optional, Enter to send)';
1311
- var detailsSubmit = document.createElement('button');
1312
- detailsSubmit.className = 'reaction-details-submit';
1313
- detailsSubmit.textContent = 'Send';
1314
-
1315
- function submitDetails() {
1316
- sendReaction('negative', detailsInput.value.trim());
1317
- detailsWrap.remove();
1318
- }
1319
-
1320
- detailsSubmit.addEventListener('click', submitDetails);
1321
- detailsInput.addEventListener('keydown', function(e) {
1322
- if (e.key === 'Enter') { e.preventDefault(); submitDetails(); }
1323
- if (e.key === 'Escape') { sendReaction('negative'); detailsWrap.remove(); }
1324
- });
1325
-
1326
- detailsWrap.appendChild(detailsInput);
1327
- detailsWrap.appendChild(detailsSubmit);
1328
- div.appendChild(detailsWrap);
1329
- setTimeout(function() { detailsInput.focus(); }, 50);
1330
- });
1331
-
1332
- reactions.appendChild(upBtn);
1333
- reactions.appendChild(downBtn);
1334
- div.appendChild(reactions);
1335
- }
1336
- chatMessages.appendChild(div);
1337
- chatMessages.scrollTop = chatMessages.scrollHeight;
1338
- }
1339
-
1340
- function unlockChat() {
1341
- chatSend.disabled = false;
1342
- chatInput.disabled = false;
1343
- chatInput.focus();
1344
- }
1345
-
1346
- function renderMarkdown(text) {
1347
- try {
1348
- if (typeof marked !== 'undefined' && marked.parse) {
1349
- return marked.parse(text, { breaks: true, gfm: true });
1350
- }
1351
- } catch (err) {
1352
- console.warn('marked.parse failed:', err);
1353
- }
1354
- // Fallback: escape HTML and convert newlines to <br>
1355
- return esc(text).replace(/\n/g, '<br>');
1356
- }
1357
-
1358
- function showThinking(status) {
1359
- if (!thinkingEl) {
1360
- thinkingEl = document.createElement('div');
1361
- thinkingEl.className = 'chat-msg thinking';
1362
- thinkingEl.innerHTML = '<div class="chat-bubble"></div>';
1363
- chatMessages.appendChild(thinkingEl);
1364
- }
1365
- thinkingEl.querySelector('.chat-bubble').textContent = status;
1366
- chatMessages.scrollTop = chatMessages.scrollHeight;
1367
- }
1368
-
1369
- function clearThinking() {
1370
- if (thinkingEl) {
1371
- thinkingEl.remove();
1372
- thinkingEl = null;
1373
- }
1374
- }
1375
-
1376
- // ── Question UI ────────────────────────────────────────────
1377
-
1378
- function showQuestion(question, options) {
1379
- const div = document.createElement('div');
1380
- div.className = 'chat-msg question';
1381
- const bubble = document.createElement('div');
1382
- bubble.className = 'chat-bubble';
1383
-
1384
- const qText = document.createElement('div');
1385
- qText.className = 'question-text';
1386
- qText.textContent = question || 'A question for you:';
1387
- bubble.appendChild(qText);
1388
-
1389
- function sendAnswer(answer) {
1390
- // Show user's answer as a message
1391
- addMessage('user', answer);
1392
- // Send to server
1393
- if (ws && ws.readyState === WebSocket.OPEN) {
1394
- ws.send(JSON.stringify({ type: 'chat.answer', answer: answer }));
1395
- }
1396
- // Show thinking again while process resumes
1397
- showThinking('Resuming…');
1398
- }
1399
-
1400
- if (options && options.length > 0) {
1401
- const optWrap = document.createElement('div');
1402
- optWrap.className = 'question-options';
1403
- options.forEach(function(opt) {
1404
- const btn = document.createElement('button');
1405
- btn.className = 'question-option-btn';
1406
- btn.textContent = opt;
1407
- btn.addEventListener('click', function() {
1408
- // Disable all buttons
1409
- optWrap.querySelectorAll('button').forEach(function(b) { b.disabled = true; });
1410
- sendAnswer(opt);
1411
- });
1412
- optWrap.appendChild(btn);
1413
- });
1414
- bubble.appendChild(optWrap);
1415
- } else {
1416
- const inputWrap = document.createElement('div');
1417
- inputWrap.className = 'question-input-wrap';
1418
- const input = document.createElement('input');
1419
- input.className = 'question-input';
1420
- input.type = 'text';
1421
- input.placeholder = 'Type your answer…';
1422
- const submitBtn = document.createElement('button');
1423
- submitBtn.className = 'question-submit-btn';
1424
- submitBtn.textContent = 'Send';
1425
-
1426
- function submitAnswer() {
1427
- const val = input.value.trim();
1428
- if (!val) return;
1429
- input.disabled = true;
1430
- submitBtn.disabled = true;
1431
- sendAnswer(val);
1432
- }
1433
-
1434
- submitBtn.addEventListener('click', submitAnswer);
1435
- input.addEventListener('keydown', function(e) {
1436
- if (e.key === 'Enter') { e.preventDefault(); submitAnswer(); }
1437
- });
1438
-
1439
- inputWrap.appendChild(input);
1440
- inputWrap.appendChild(submitBtn);
1441
- bubble.appendChild(inputWrap);
1442
-
1443
- // Auto-focus the question input
1444
- setTimeout(function() { input.focus(); }, 50);
1445
- }
1446
-
1447
- div.appendChild(bubble);
1448
- chatMessages.appendChild(div);
1449
- chatMessages.scrollTop = chatMessages.scrollHeight;
1450
- }
1451
-
1452
- // ── Process diagram ────────────────────────────────────────
1453
-
1454
- let mermaidCounter = 0;
1455
-
1456
- function processToMermaid(proc) {
1457
- const nodes = proc.nodes || [];
1458
- if (nodes.length === 0) return null;
1459
-
1460
- const lines = ['graph LR'];
1461
-
1462
- // Define node shapes
1463
- for (const n of nodes) {
1464
- const label = n.catalog_node_key || n.key;
1465
- if (label === 'start') {
1466
- lines.push(' ' + n.key + '([start])');
1467
- } else if (label === 'stop') {
1468
- lines.push(' ' + n.key + '([stop])');
1469
- } else {
1470
- lines.push(' ' + n.key + '[' + label + ']');
1471
- }
1472
- }
1473
-
1474
- // Define edges
1475
- for (const n of nodes) {
1476
- if (!n.edges) continue;
1477
- for (const [status, target] of Object.entries(n.edges)) {
1478
- if (status === 'ok') {
1479
- lines.push(' ' + n.key + ' --> ' + target);
1480
- } else {
1481
- lines.push(' ' + n.key + ' -->|' + status + '| ' + target);
1482
- }
1483
- }
1484
- }
1485
-
1486
- return lines.join('\n');
1487
- }
1488
-
1489
- async function showProcessDiagram(proc) {
1490
- if (typeof mermaid === 'undefined') return;
1491
-
1492
- const mermaidSrc = processToMermaid(proc);
1493
- if (!mermaidSrc) return;
1494
-
1495
- const id = 'mermaid-' + (++mermaidCounter);
1496
- const wrapper = document.createElement('div');
1497
- wrapper.className = 'chat-process';
1498
-
1499
- const toggle = document.createElement('div');
1500
- toggle.className = 'process-toggle';
1501
- toggle.textContent = ' Process diagram';
1502
-
1503
- const diagram = document.createElement('div');
1504
- diagram.className = 'process-diagram';
1505
- diagram.style.display = 'none';
1506
- diagram.id = id;
1507
-
1508
- const pre = document.createElement('pre');
1509
- pre.className = 'mermaid';
1510
- pre.textContent = mermaidSrc;
1511
- diagram.appendChild(pre);
1512
-
1513
- toggle.addEventListener('click', async () => {
1514
- const showing = diagram.style.display !== 'none';
1515
- if (showing) {
1516
- diagram.style.display = 'none';
1517
- toggle.textContent = '▶ Process diagram';
1518
- } else {
1519
- diagram.style.display = 'block';
1520
- toggle.textContent = '▼ Process diagram';
1521
- // Render mermaid if not yet rendered
1522
- if (!diagram.dataset.rendered) {
1523
- try {
1524
- await mermaid.run({ nodes: [pre] });
1525
- diagram.dataset.rendered = '1';
1526
- } catch (err) {
1527
- console.warn('Mermaid render failed:', err);
1528
- pre.textContent = mermaidSrc;
1529
- }
1530
- }
1531
- }
1532
- chatMessages.scrollTop = chatMessages.scrollHeight;
1533
- });
1534
-
1535
- wrapper.appendChild(toggle);
1536
- wrapper.appendChild(diagram);
1537
- chatMessages.appendChild(wrapper);
1538
- chatMessages.scrollTop = chatMessages.scrollHeight;
1539
- }
1540
-
1541
- // ── Helpers ────────────────────────────────────────────────
1542
-
1543
- function esc(s) {
1544
- const d = document.createElement('div');
1545
- d.textContent = s || '';
1546
- return d.innerHTML;
1547
- }
1548
-
1549
- function formatTime(iso) {
1550
- try {
1551
- const d = new Date(iso);
1552
- return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1553
- } catch { return ''; }
1554
- }
1555
-
1556
- // ── Init ───────────────────────────────────────────────────
1557
-
1558
- fetchCalendarNames().then(() => fetchTimeline());
1559
- fetchStatus();
1560
- fetchScheduler();
1561
- connectWs();
1562
-
1563
- // Poll status and scheduler every 30s
1564
- setInterval(fetchStatus, 30000);
1565
- setInterval(fetchScheduler, 30000);
1566
- })();
1567
- </script>
1568
- </body>
1569
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Phaibel</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --bg: #1a1a2e;
13
+ --panel: #16213e;
14
+ --border: #0f3460;
15
+ --text: #e0e0e0;
16
+ --dim: #8888aa;
17
+ --cyan: #00d4ff;
18
+ --cyan-dim: #0099bb;
19
+ --green: #00e676;
20
+ --yellow: #ffd740;
21
+ --red: #ff5252;
22
+ --orange: #ff9100;
23
+ }
24
+
25
+ body {
26
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ height: 100vh;
30
+ overflow: hidden;
31
+ }
32
+
33
+ .app {
34
+ display: grid;
35
+ grid-template-columns: 1fr 2fr;
36
+ grid-template-rows: auto 1fr;
37
+ height: 100vh;
38
+ gap: 1px;
39
+ background: var(--border);
40
+ }
41
+
42
+ .header {
43
+ grid-column: 1 / -1;
44
+ background: var(--panel);
45
+ padding: 10px 20px;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 20px;
49
+ border-bottom: 1px solid var(--border);
50
+ }
51
+
52
+ .header h1 {
53
+ font-size: 16px;
54
+ font-weight: 600;
55
+ color: var(--cyan);
56
+ letter-spacing: 2px;
57
+ text-transform: uppercase;
58
+ flex-shrink: 0;
59
+ }
60
+
61
+ .header-stats {
62
+ display: flex;
63
+ gap: 16px;
64
+ flex: 1;
65
+ align-items: center;
66
+ flex-wrap: wrap;
67
+ }
68
+
69
+ .stat {
70
+ font-size: 10px;
71
+ color: var(--dim);
72
+ white-space: nowrap;
73
+ }
74
+
75
+ .stat-label {
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.5px;
78
+ margin-right: 4px;
79
+ }
80
+
81
+ .stat-value {
82
+ color: var(--text);
83
+ font-weight: 600;
84
+ }
85
+
86
+ .header-right {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 14px;
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ .ws-status {
94
+ font-size: 11px;
95
+ color: var(--dim);
96
+ white-space: nowrap;
97
+ }
98
+
99
+ .ws-status .dot {
100
+ display: inline-block;
101
+ width: 6px;
102
+ height: 6px;
103
+ border-radius: 50%;
104
+ background: var(--green);
105
+ margin-right: 6px;
106
+ }
107
+
108
+ .panel {
109
+ background: var(--panel);
110
+ display: flex;
111
+ flex-direction: column;
112
+ overflow: hidden;
113
+ }
114
+
115
+ .panel-title {
116
+ padding: 12px 16px;
117
+ font-size: 12px;
118
+ font-weight: 600;
119
+ text-transform: uppercase;
120
+ letter-spacing: 1.5px;
121
+ color: var(--cyan);
122
+ border-bottom: 1px solid var(--border);
123
+ flex-shrink: 0;
124
+ }
125
+
126
+ .panel-section-title {
127
+ font-size: 12px;
128
+ font-weight: 600;
129
+ text-transform: uppercase;
130
+ letter-spacing: 1.5px;
131
+ color: var(--cyan);
132
+ padding-bottom: 8px;
133
+ border-bottom: 1px solid var(--border);
134
+ margin-bottom: 8px;
135
+ }
136
+
137
+ .panel-body {
138
+ flex: 1;
139
+ overflow-y: auto;
140
+ padding: 12px 16px;
141
+ }
142
+
143
+ .panel-body::-webkit-scrollbar { width: 4px; }
144
+ .panel-body::-webkit-scrollbar-track { background: transparent; }
145
+ .panel-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
146
+
147
+ /* ── Today panel ─────────────────────────────────── */
148
+
149
+ .task-item {
150
+ display: flex;
151
+ align-items: flex-start;
152
+ gap: 10px;
153
+ padding: 8px 0;
154
+ border-bottom: 1px solid rgba(255,255,255,0.04);
155
+ }
156
+
157
+ .task-item:last-child { border-bottom: none; }
158
+
159
+ .task-check {
160
+ width: 16px;
161
+ height: 16px;
162
+ border: 1.5px solid var(--dim);
163
+ border-radius: 3px;
164
+ flex-shrink: 0;
165
+ margin-top: 2px;
166
+ cursor: pointer;
167
+ transition: border-color 0.15s;
168
+ }
169
+
170
+ .task-check:hover { border-color: var(--cyan); }
171
+
172
+ .task-info { flex: 1; min-width: 0; }
173
+
174
+ .task-title {
175
+ font-size: 13px;
176
+ line-height: 1.4;
177
+ word-break: break-word;
178
+ }
179
+
180
+ .task-meta {
181
+ font-size: 10px;
182
+ color: var(--dim);
183
+ margin-top: 2px;
184
+ }
185
+
186
+ .badge {
187
+ display: inline-block;
188
+ font-size: 9px;
189
+ padding: 1px 5px;
190
+ border-radius: 3px;
191
+ font-weight: 600;
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.5px;
194
+ }
195
+
196
+ .badge-critical { background: var(--red); color: #fff; }
197
+ .badge-high { background: var(--orange); color: #1a1a2e; }
198
+ .badge-medium { background: var(--yellow); color: #1a1a2e; }
199
+ .badge-low { background: var(--dim); color: #1a1a2e; }
200
+
201
+ .empty-state {
202
+ color: var(--dim);
203
+ font-size: 12px;
204
+ text-align: center;
205
+ padding: 32px 16px;
206
+ line-height: 1.6;
207
+ }
208
+
209
+ /* ── Calendar panel ──────────────────────────────── */
210
+
211
+ .day-group { margin-bottom: 16px; }
212
+
213
+ .day-header {
214
+ font-size: 11px;
215
+ font-weight: 600;
216
+ text-transform: uppercase;
217
+ letter-spacing: 1px;
218
+ color: var(--cyan-dim);
219
+ padding-bottom: 6px;
220
+ border-bottom: 1px solid rgba(255,255,255,0.06);
221
+ margin-bottom: 6px;
222
+ }
223
+
224
+ .day-header.today { color: var(--cyan); }
225
+
226
+ .event-item {
227
+ padding: 6px 0;
228
+ font-size: 13px;
229
+ }
230
+
231
+ .event-time {
232
+ font-size: 10px;
233
+ color: var(--dim);
234
+ }
235
+
236
+ .event-location {
237
+ font-size: 10px;
238
+ color: var(--dim);
239
+ font-style: italic;
240
+ }
241
+
242
+ .event-cal {
243
+ display: inline-block;
244
+ font-size: 9px;
245
+ padding: 1px 5px;
246
+ border-radius: 3px;
247
+ background: rgba(0, 212, 255, 0.12);
248
+ color: var(--cyan-dim);
249
+ font-weight: 600;
250
+ letter-spacing: 0.3px;
251
+ margin-left: 6px;
252
+ vertical-align: middle;
253
+ }
254
+
255
+ .day-empty {
256
+ font-size: 11px;
257
+ color: var(--dim);
258
+ padding: 4px 0;
259
+ font-style: italic;
260
+ }
261
+
262
+ .entity-item {
263
+ padding: 6px 0;
264
+ font-size: 13px;
265
+ border-bottom: 1px solid var(--border);
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 8px;
269
+ }
270
+ .entity-item:last-child { border-bottom: none; }
271
+
272
+ .badge-entity {
273
+ display: inline-block;
274
+ font-size: 9px;
275
+ padding: 1px 5px;
276
+ border-radius: 3px;
277
+ background: var(--cyan-dim, #1a3a3a);
278
+ color: var(--cyan, #5fdfdf);
279
+ font-weight: 600;
280
+ letter-spacing: 0.3px;
281
+ text-transform: uppercase;
282
+ flex-shrink: 0;
283
+ }
284
+
285
+ .entity-date {
286
+ font-size: 10px;
287
+ color: var(--dim);
288
+ margin-left: auto;
289
+ flex-shrink: 0;
290
+ }
291
+
292
+ /* ── Scheduler panel ────────────────────────────────── */
293
+
294
+ .sched-item {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 8px;
298
+ padding: 5px 0;
299
+ border-bottom: 1px solid rgba(255,255,255,0.04);
300
+ font-size: 12px;
301
+ }
302
+
303
+ .sched-item:last-child { border-bottom: none; }
304
+
305
+ .sched-dot {
306
+ width: 7px;
307
+ height: 7px;
308
+ border-radius: 50%;
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .sched-dot.on { background: var(--green); }
313
+ .sched-dot.off { background: var(--dim); opacity: 0.4; }
314
+
315
+ .sched-name {
316
+ flex: 1;
317
+ min-width: 0;
318
+ white-space: nowrap;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ }
322
+
323
+ .sched-detail {
324
+ font-size: 10px;
325
+ color: var(--dim);
326
+ text-align: right;
327
+ white-space: nowrap;
328
+ }
329
+
330
+ .sched-error {
331
+ color: var(--red);
332
+ }
333
+
334
+ .sched-actions {
335
+ display: flex;
336
+ gap: 4px;
337
+ flex-shrink: 0;
338
+ }
339
+
340
+ .sched-btn {
341
+ background: rgba(255,255,255,0.06);
342
+ border: 1px solid rgba(255,255,255,0.1);
343
+ color: var(--dim);
344
+ font-size: 10px;
345
+ padding: 1px 6px;
346
+ border-radius: 3px;
347
+ cursor: pointer;
348
+ white-space: nowrap;
349
+ }
350
+
351
+ .sched-btn:hover {
352
+ background: rgba(255,255,255,0.12);
353
+ color: var(--text);
354
+ }
355
+
356
+ /* ── Chat panel ──────────────────────────────────── */
357
+
358
+ .chat-panel {
359
+ display: flex;
360
+ flex-direction: column;
361
+ }
362
+
363
+ .chat-messages {
364
+ flex: 1;
365
+ overflow-y: auto;
366
+ padding: 12px 16px;
367
+ }
368
+
369
+ .chat-messages::-webkit-scrollbar { width: 4px; }
370
+ .chat-messages::-webkit-scrollbar-track { background: transparent; }
371
+ .chat-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
372
+
373
+ .chat-msg {
374
+ margin-bottom: 12px;
375
+ line-height: 1.5;
376
+ }
377
+
378
+ .chat-msg.user {
379
+ text-align: right;
380
+ }
381
+
382
+ .chat-msg.user .chat-bubble {
383
+ background: var(--border);
384
+ display: inline-block;
385
+ padding: 8px 12px;
386
+ border-radius: 12px 12px 2px 12px;
387
+ max-width: 85%;
388
+ text-align: left;
389
+ font-size: 13px;
390
+ }
391
+
392
+ .chat-msg.bot .chat-bubble {
393
+ background: rgba(0, 212, 255, 0.08);
394
+ border: 1px solid rgba(0, 212, 255, 0.15);
395
+ display: inline-block;
396
+ padding: 8px 12px;
397
+ border-radius: 12px 12px 12px 2px;
398
+ max-width: 85%;
399
+ font-size: 13px;
400
+ }
401
+
402
+ /* ── Markdown inside bot bubbles ──────────────────── */
403
+
404
+ .chat-bubble p { margin: 0 0 8px 0; }
405
+ .chat-bubble p:last-child { margin-bottom: 0; }
406
+
407
+ .chat-bubble ul, .chat-bubble ol {
408
+ margin: 4px 0 8px 0;
409
+ padding-left: 20px;
410
+ }
411
+ .chat-bubble li { margin-bottom: 2px; }
412
+
413
+ .chat-bubble h1, .chat-bubble h2, .chat-bubble h3,
414
+ .chat-bubble h4, .chat-bubble h5, .chat-bubble h6 {
415
+ margin: 8px 0 4px 0;
416
+ color: var(--cyan);
417
+ font-size: 13px;
418
+ }
419
+ .chat-bubble h1 { font-size: 15px; }
420
+ .chat-bubble h2 { font-size: 14px; }
421
+
422
+ .chat-bubble code {
423
+ background: rgba(255,255,255,0.08);
424
+ padding: 1px 4px;
425
+ border-radius: 3px;
426
+ font-size: 12px;
427
+ }
428
+
429
+ .chat-bubble pre {
430
+ background: var(--bg);
431
+ border: 1px solid var(--border);
432
+ border-radius: 4px;
433
+ padding: 8px 10px;
434
+ margin: 6px 0;
435
+ overflow-x: auto;
436
+ font-size: 12px;
437
+ }
438
+ .chat-bubble pre code {
439
+ background: none;
440
+ padding: 0;
441
+ border-radius: 0;
442
+ }
443
+
444
+ .chat-bubble blockquote {
445
+ border-left: 3px solid var(--cyan-dim);
446
+ padding-left: 10px;
447
+ margin: 6px 0;
448
+ color: var(--dim);
449
+ }
450
+
451
+ .chat-bubble strong { color: #fff; }
452
+
453
+ .chat-bubble a {
454
+ color: var(--cyan);
455
+ text-decoration: none;
456
+ }
457
+ .chat-bubble a:hover { text-decoration: underline; }
458
+
459
+ .chat-bubble hr {
460
+ border: none;
461
+ border-top: 1px solid var(--border);
462
+ margin: 8px 0;
463
+ }
464
+
465
+ .chat-bubble table {
466
+ border-collapse: collapse;
467
+ margin: 6px 0;
468
+ font-size: 12px;
469
+ }
470
+ .chat-bubble th, .chat-bubble td {
471
+ border: 1px solid var(--border);
472
+ padding: 3px 8px;
473
+ }
474
+ .chat-bubble th { background: rgba(255,255,255,0.05); }
475
+
476
+ .chat-msg.thinking .chat-bubble {
477
+ color: var(--dim);
478
+ font-style: italic;
479
+ font-size: 11px;
480
+ border: none;
481
+ background: none;
482
+ }
483
+
484
+ .chat-msg.error .chat-bubble {
485
+ color: var(--red);
486
+ font-size: 12px;
487
+ border-color: rgba(255, 82, 82, 0.2);
488
+ background: rgba(255, 82, 82, 0.05);
489
+ }
490
+
491
+ .chat-id-label {
492
+ font-size: 9px;
493
+ color: var(--dim);
494
+ margin-top: 4px;
495
+ letter-spacing: 0.5px;
496
+ }
497
+
498
+ .chat-msg.question .chat-bubble {
499
+ background: rgba(0, 212, 255, 0.08);
500
+ border: 1px solid rgba(0, 212, 255, 0.25);
501
+ display: inline-block;
502
+ padding: 10px 14px;
503
+ border-radius: 12px 12px 12px 2px;
504
+ max-width: 85%;
505
+ font-size: 13px;
506
+ }
507
+
508
+ .chat-msg.question .question-text {
509
+ margin-bottom: 10px;
510
+ color: var(--cyan);
511
+ font-weight: 600;
512
+ }
513
+
514
+ .chat-msg.question .question-options {
515
+ display: flex;
516
+ flex-wrap: wrap;
517
+ gap: 6px;
518
+ }
519
+
520
+ .chat-msg.question .question-option-btn {
521
+ background: var(--border);
522
+ color: var(--text);
523
+ border: 1px solid var(--cyan-dim);
524
+ font-family: inherit;
525
+ font-size: 12px;
526
+ padding: 5px 12px;
527
+ border-radius: 4px;
528
+ cursor: pointer;
529
+ transition: background 0.15s, border-color 0.15s;
530
+ }
531
+
532
+ .chat-msg.question .question-option-btn:hover {
533
+ background: rgba(0, 212, 255, 0.15);
534
+ border-color: var(--cyan);
535
+ }
536
+
537
+ .chat-msg.question .question-option-btn:disabled {
538
+ opacity: 0.4;
539
+ cursor: not-allowed;
540
+ }
541
+
542
+ .chat-msg.question .question-input-wrap {
543
+ display: flex;
544
+ gap: 6px;
545
+ }
546
+
547
+ .chat-msg.question .question-input {
548
+ flex: 1;
549
+ background: var(--bg);
550
+ border: 1px solid var(--border);
551
+ color: var(--text);
552
+ font-family: inherit;
553
+ font-size: 12px;
554
+ padding: 6px 10px;
555
+ border-radius: 4px;
556
+ outline: none;
557
+ }
558
+
559
+ .chat-msg.question .question-input:focus { border-color: var(--cyan); }
560
+
561
+ .chat-msg.question .question-submit-btn {
562
+ background: var(--cyan);
563
+ color: var(--bg);
564
+ border: none;
565
+ font-family: inherit;
566
+ font-size: 11px;
567
+ font-weight: 600;
568
+ padding: 6px 12px;
569
+ border-radius: 4px;
570
+ cursor: pointer;
571
+ }
572
+
573
+ .chat-msg.question .question-submit-btn:disabled {
574
+ opacity: 0.4;
575
+ cursor: not-allowed;
576
+ }
577
+
578
+ .chat-msg.question .question-cancel-btn {
579
+ background: transparent;
580
+ color: var(--dim);
581
+ border: 1px solid var(--border);
582
+ font-family: inherit;
583
+ font-size: 11px;
584
+ font-weight: 600;
585
+ padding: 6px 12px;
586
+ border-radius: 4px;
587
+ cursor: pointer;
588
+ transition: color 0.15s, border-color 0.15s;
589
+ }
590
+
591
+ .chat-msg.question .question-cancel-btn:hover {
592
+ color: var(--text);
593
+ border-color: var(--dim);
594
+ }
595
+
596
+ .chat-msg.question .question-cancel-btn:disabled {
597
+ opacity: 0.4;
598
+ cursor: not-allowed;
599
+ }
600
+
601
+ .chat-process {
602
+ margin-bottom: 12px;
603
+ }
604
+
605
+ .process-toggle {
606
+ font-size: 11px;
607
+ color: var(--cyan-dim);
608
+ cursor: pointer;
609
+ user-select: none;
610
+ padding: 4px 0;
611
+ }
612
+
613
+ .process-toggle:hover { color: var(--cyan); }
614
+
615
+ .process-diagram {
616
+ background: var(--bg);
617
+ border: 1px solid var(--border);
618
+ border-radius: 6px;
619
+ padding: 12px;
620
+ margin-top: 6px;
621
+ overflow-x: auto;
622
+ }
623
+
624
+ .process-diagram svg {
625
+ max-width: 100%;
626
+ height: auto;
627
+ }
628
+
629
+ .chat-input-wrap {
630
+ padding: 12px 16px;
631
+ border-top: 1px solid var(--border);
632
+ flex-shrink: 0;
633
+ }
634
+
635
+ .chat-input-wrap form {
636
+ display: flex;
637
+ gap: 8px;
638
+ }
639
+
640
+ .chat-input {
641
+ flex: 1;
642
+ background: var(--bg);
643
+ border: 1px solid var(--border);
644
+ color: var(--text);
645
+ font-family: inherit;
646
+ font-size: 13px;
647
+ padding: 8px 12px;
648
+ border-radius: 6px;
649
+ outline: none;
650
+ transition: border-color 0.15s;
651
+ }
652
+
653
+ .chat-input:focus { border-color: var(--cyan); }
654
+ .chat-input::placeholder { color: var(--dim); }
655
+
656
+ .chat-send {
657
+ background: var(--cyan);
658
+ color: var(--bg);
659
+ border: none;
660
+ font-family: inherit;
661
+ font-size: 12px;
662
+ font-weight: 600;
663
+ padding: 8px 16px;
664
+ border-radius: 6px;
665
+ cursor: pointer;
666
+ transition: opacity 0.15s;
667
+ }
668
+
669
+ .chat-send:hover { opacity: 0.85; }
670
+ .chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
671
+
672
+ .chat-mic {
673
+ background: transparent;
674
+ border: 1px solid var(--border);
675
+ color: var(--dim);
676
+ font-size: 16px;
677
+ width: 36px;
678
+ height: 36px;
679
+ border-radius: 6px;
680
+ cursor: pointer;
681
+ display: flex;
682
+ align-items: center;
683
+ justify-content: center;
684
+ flex-shrink: 0;
685
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
686
+ }
687
+
688
+ .chat-mic:hover { border-color: var(--cyan); color: var(--text); }
689
+
690
+ .chat-mic.listening {
691
+ border-color: var(--red);
692
+ color: var(--red);
693
+ background: rgba(255, 82, 82, 0.1);
694
+ animation: mic-pulse 1.2s ease-in-out infinite;
695
+ }
696
+
697
+ .chat-mic.unsupported { display: none; }
698
+
699
+ @keyframes mic-pulse {
700
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.3); }
701
+ 50% { box-shadow: 0 0 0 6px rgba(255, 82, 82, 0); }
702
+ }
703
+
704
+ /* ── Reaction buttons ───────────────────────────── */
705
+
706
+ .chat-reactions {
707
+ display: flex;
708
+ gap: 4px;
709
+ margin-top: 4px;
710
+ }
711
+
712
+ .reaction-btn {
713
+ background: rgba(255,255,255,0.06);
714
+ border: 1px solid rgba(255,255,255,0.1);
715
+ color: var(--dim);
716
+ font-size: 12px;
717
+ width: 28px;
718
+ height: 24px;
719
+ border-radius: 4px;
720
+ cursor: pointer;
721
+ display: flex;
722
+ align-items: center;
723
+ justify-content: center;
724
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
725
+ }
726
+
727
+ .reaction-btn:hover { background: rgba(255,255,255,0.12); color: var(--text); }
728
+
729
+ .reaction-btn.selected {
730
+ border-color: var(--cyan);
731
+ color: var(--cyan);
732
+ background: rgba(0, 212, 255, 0.1);
733
+ cursor: default;
734
+ }
735
+
736
+ .reaction-btn:disabled { opacity: 0.4; cursor: default; }
737
+
738
+ .reaction-details-wrap {
739
+ display: flex;
740
+ gap: 4px;
741
+ margin-top: 4px;
742
+ }
743
+
744
+ .reaction-details-input {
745
+ flex: 1;
746
+ background: var(--bg);
747
+ border: 1px solid var(--border);
748
+ color: var(--text);
749
+ font-family: inherit;
750
+ font-size: 11px;
751
+ padding: 4px 8px;
752
+ border-radius: 4px;
753
+ outline: none;
754
+ }
755
+
756
+ .reaction-details-input:focus { border-color: var(--cyan); }
757
+
758
+ .reaction-details-submit {
759
+ background: var(--cyan);
760
+ color: var(--bg);
761
+ border: none;
762
+ font-family: inherit;
763
+ font-size: 10px;
764
+ font-weight: 600;
765
+ padding: 4px 8px;
766
+ border-radius: 4px;
767
+ cursor: pointer;
768
+ }
769
+
770
+ /* ── Onboarding overlay ───────────────────────────── */
771
+
772
+ .onboarding-overlay {
773
+ position: fixed;
774
+ inset: 0;
775
+ background: rgba(10, 10, 20, 0.95);
776
+ z-index: 1000;
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: center;
780
+ overflow-y: auto;
781
+ }
782
+
783
+ .onboarding-overlay.hidden { display: none; }
784
+
785
+ .onboarding-card {
786
+ background: var(--panel);
787
+ border: 1px solid var(--border);
788
+ border-radius: 12px;
789
+ max-width: 560px;
790
+ width: 90%;
791
+ padding: 32px;
792
+ margin: 24px;
793
+ }
794
+
795
+ .onboarding-card h2 {
796
+ color: var(--cyan);
797
+ font-size: 18px;
798
+ margin-bottom: 4px;
799
+ text-align: center;
800
+ }
801
+
802
+ .onboarding-card .subtitle {
803
+ color: var(--dim);
804
+ font-size: 12px;
805
+ text-align: center;
806
+ margin-bottom: 24px;
807
+ }
808
+
809
+ .onboarding-step { display: none; }
810
+ .onboarding-step.active { display: block; }
811
+
812
+ .personality-grid {
813
+ display: grid;
814
+ grid-template-columns: 1fr 1fr;
815
+ gap: 10px;
816
+ margin-bottom: 20px;
817
+ }
818
+
819
+ .personality-option {
820
+ background: var(--bg);
821
+ border: 2px solid var(--border);
822
+ border-radius: 8px;
823
+ padding: 14px;
824
+ cursor: pointer;
825
+ transition: border-color 0.2s;
826
+ }
827
+
828
+ .personality-option:hover { border-color: var(--cyan-dim); }
829
+ .personality-option.selected { border-color: var(--cyan); }
830
+
831
+ .personality-option .p-label {
832
+ font-size: 13px;
833
+ font-weight: 600;
834
+ color: var(--text);
835
+ margin-bottom: 4px;
836
+ }
837
+
838
+ .personality-option .p-desc {
839
+ font-size: 11px;
840
+ color: var(--dim);
841
+ line-height: 1.4;
842
+ }
843
+
844
+ .ob-field {
845
+ margin-bottom: 14px;
846
+ }
847
+
848
+ .ob-field label {
849
+ display: block;
850
+ font-size: 11px;
851
+ color: var(--dim);
852
+ text-transform: uppercase;
853
+ letter-spacing: 0.5px;
854
+ margin-bottom: 4px;
855
+ }
856
+
857
+ .ob-field input, .ob-field select {
858
+ width: 100%;
859
+ background: var(--bg);
860
+ border: 1px solid var(--border);
861
+ color: var(--text);
862
+ font-family: inherit;
863
+ font-size: 13px;
864
+ padding: 8px 10px;
865
+ border-radius: 6px;
866
+ outline: none;
867
+ }
868
+
869
+ .ob-field input:focus, .ob-field select:focus { border-color: var(--cyan); }
870
+
871
+ .ob-field .hint {
872
+ font-size: 10px;
873
+ color: var(--dim);
874
+ margin-top: 2px;
875
+ }
876
+
877
+ .ob-buttons {
878
+ display: flex;
879
+ gap: 10px;
880
+ justify-content: flex-end;
881
+ margin-top: 20px;
882
+ }
883
+
884
+ .ob-btn {
885
+ background: var(--border);
886
+ color: var(--text);
887
+ border: none;
888
+ font-family: inherit;
889
+ font-size: 13px;
890
+ font-weight: 600;
891
+ padding: 8px 20px;
892
+ border-radius: 6px;
893
+ cursor: pointer;
894
+ }
895
+
896
+ .ob-btn.primary {
897
+ background: var(--cyan);
898
+ color: var(--bg);
899
+ }
900
+
901
+ .ob-btn:disabled {
902
+ opacity: 0.4;
903
+ cursor: not-allowed;
904
+ }
905
+
906
+ /* ── Responsive ──────────────────────────────────── */
907
+
908
+ @media (max-width: 700px) {
909
+ .app {
910
+ grid-template-columns: 1fr;
911
+ grid-template-rows: auto auto 1fr;
912
+ }
913
+ .personality-grid { grid-template-columns: 1fr; }
914
+ }
915
+ </style>
916
+ </head>
917
+ <body>
918
+
919
+ <!-- Onboarding overlay (shown when no vault or interview incomplete) -->
920
+ <div class="onboarding-overlay hidden" id="onboarding">
921
+ <div class="onboarding-card">
922
+ <h2>Welcome to Phaibel</h2>
923
+ <p class="subtitle">Let's set up your personal AI assistant</p>
924
+
925
+ <!-- Step 1: Personality -->
926
+ <div class="onboarding-step active" id="ob-step-1">
927
+ <div class="personality-grid" id="personality-grid"></div>
928
+ <div class="ob-buttons">
929
+ <button class="ob-btn primary" id="ob-next-1" disabled>Next</button>
930
+ </div>
931
+ </div>
932
+
933
+ <!-- Step 2: Identity -->
934
+ <div class="onboarding-step" id="ob-step-2">
935
+ <div class="ob-field">
936
+ <label>Agent Name</label>
937
+ <input type="text" id="ob-agent-name" placeholder="e.g. Jarvis, Friday, Alfred" value="Agent">
938
+ </div>
939
+ <div class="ob-field">
940
+ <label>Your Name</label>
941
+ <input type="text" id="ob-user-name" placeholder="Your name">
942
+ </div>
943
+ <div class="ob-field">
944
+ <label>How should your agent address you?</label>
945
+ <select id="ob-gender">
946
+ <option value="male">Sir / Boss / Captain</option>
947
+ <option value="female">Ma'am / Miss / Madam</option>
948
+ <option value="other">Boss / Chief / Friend</option>
949
+ </select>
950
+ </div>
951
+ <div class="ob-buttons">
952
+ <button class="ob-btn" id="ob-back-2">Back</button>
953
+ <button class="ob-btn primary" id="ob-next-2">Next</button>
954
+ </div>
955
+ </div>
956
+
957
+ <!-- Step 3: Profile (optional) -->
958
+ <div class="onboarding-step" id="ob-step-3">
959
+ <p class="subtitle" style="margin-bottom:16px">Optional skip any you like</p>
960
+ <div class="ob-field">
961
+ <label>Location</label>
962
+ <input type="text" id="ob-location" placeholder="City / region">
963
+ </div>
964
+ <div class="ob-field">
965
+ <label>Work</label>
966
+ <input type="text" id="ob-work" placeholder="Role, industry, student, retired…">
967
+ </div>
968
+ <div class="ob-field">
969
+ <label>Goals</label>
970
+ <input type="text" id="ob-goals" placeholder="What are you working towards?">
971
+ </div>
972
+ <div class="ob-field">
973
+ <label>Relationships</label>
974
+ <input type="text" id="ob-relationships" placeholder="Partner, kids, family, friends">
975
+ </div>
976
+ <div class="ob-field">
977
+ <label>Health &amp; Fitness</label>
978
+ <input type="text" id="ob-health" placeholder="Exercise, diet, health goals">
979
+ </div>
980
+ <div class="ob-field">
981
+ <label>Growth</label>
982
+ <input type="text" id="ob-growth" placeholder="Learning, hobbies, habits">
983
+ </div>
984
+ <div class="ob-buttons">
985
+ <button class="ob-btn" id="ob-back-3">Back</button>
986
+ <button class="ob-btn primary" id="ob-submit">Get Started</button>
987
+ </div>
988
+ </div>
989
+ </div>
990
+ </div>
991
+
992
+ <div class="app">
993
+ <div class="header">
994
+ <h1 id="header-agent-name">Phaibel</h1>
995
+ <div class="header-stats" id="header-stats">
996
+ <div class="stat"><span class="stat-label">Vault</span><span class="stat-value" id="stat-vault">--</span></div>
997
+ <div class="stat"><span class="stat-label">Queue</span><span class="stat-value" id="stat-queue">--</span></div>
998
+ <div class="stat"><span class="stat-label">Graph</span><span class="stat-value" id="stat-graph">--</span></div>
999
+ <div class="stat"><span class="stat-label">Mem</span><span class="stat-value" id="stat-mem">--</span></div>
1000
+ <div class="stat"><span class="stat-label">Uptime</span><span class="stat-value" id="stat-uptime">--</span></div>
1001
+ </div>
1002
+ <div class="header-right">
1003
+ <div class="ws-status" id="ws-status"><span class="dot"></span>Connected</div>
1004
+ </div>
1005
+ </div>
1006
+
1007
+ <!-- Left: Timeline + Scheduler -->
1008
+ <div class="panel">
1009
+ <div class="panel-body" id="left-body">
1010
+ <div id="timeline-body">
1011
+ <div class="empty-state">Loading…</div>
1012
+ </div>
1013
+ <div class="panel-section-title" style="margin-top: 16px;">Scheduler</div>
1014
+ <div id="scheduler-body">
1015
+ <div class="empty-state">Loading scheduler…</div>
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+
1020
+ <!-- Chat -->
1021
+ <div class="panel chat-panel">
1022
+ <div class="panel-title">Chat</div>
1023
+ <div class="chat-messages" id="chat-messages">
1024
+ <div class="chat-msg bot">
1025
+ <div class="chat-bubble">Ready to serve.</div>
1026
+ </div>
1027
+ </div>
1028
+ <div class="chat-input-wrap">
1029
+ <form id="chat-form">
1030
+ <button class="chat-mic" id="chat-mic" type="button" title="Voice input">&#x1F3A4;</button>
1031
+ <input class="chat-input" id="chat-input" type="text"
1032
+ placeholder="Ask me anything…" autocomplete="off">
1033
+ <button class="chat-send" id="chat-send" type="submit">Send</button>
1034
+ </form>
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ <script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
1040
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1041
+ <script>mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#0f3460', primaryTextColor: '#e0e0e0', primaryBorderColor: '#00d4ff', lineColor: '#00d4ff', secondaryColor: '#16213e', tertiaryColor: '#1a1a2e', fontFamily: 'monospace' } });</script>
1042
+ <script>
1043
+ // Global handler for task checkboxes (inline onclick avoids delegation issues)
1044
+ async function markTaskDone(checkEl) {
1045
+ const item = checkEl.closest('.task-item');
1046
+ if (!item) return;
1047
+ const id = item.dataset.taskId;
1048
+ if (!id) return;
1049
+ const entityType = item.dataset.entityType || 'task';
1050
+ checkEl.style.background = 'var(--cyan)';
1051
+ item.style.opacity = '0.4';
1052
+ try {
1053
+ const res = await fetch('/api/entities/' + encodeURIComponent(entityType) + '/' + encodeURIComponent(id), {
1054
+ method: 'PATCH',
1055
+ headers: { 'Content-Type': 'application/json' },
1056
+ body: JSON.stringify({ status: 'done' }),
1057
+ });
1058
+ if (res.ok) {
1059
+ item.remove();
1060
+ } else {
1061
+ const err = await res.json().catch(() => ({}));
1062
+ console.error('task-check: PATCH failed', res.status, err);
1063
+ checkEl.style.background = 'red';
1064
+ item.style.opacity = '1';
1065
+ }
1066
+ } catch (err) {
1067
+ console.error('task-check: fetch error', err);
1068
+ checkEl.style.background = 'red';
1069
+ item.style.opacity = '1';
1070
+ }
1071
+ }
1072
+ (function() {
1073
+ const timelineBody = document.getElementById('timeline-body');
1074
+ const schedulerBody = document.getElementById('scheduler-body');
1075
+ const chatMessages = document.getElementById('chat-messages');
1076
+ const chatForm = document.getElementById('chat-form');
1077
+ const chatInput = document.getElementById('chat-input');
1078
+ const chatSend = document.getElementById('chat-send');
1079
+ const wsStatusEl = document.getElementById('ws-status');
1080
+ const statVault = document.getElementById('stat-vault');
1081
+ const statQueue = document.getElementById('stat-queue');
1082
+ const statGraph = document.getElementById('stat-graph');
1083
+ const statMem = document.getElementById('stat-mem');
1084
+ const statUptime = document.getElementById('stat-uptime');
1085
+
1086
+ const chatMic = document.getElementById('chat-mic');
1087
+
1088
+ let ws = null;
1089
+ let thinkingEl = null;
1090
+ let currentChatId = null;
1091
+ let calendarNames = {}; // id → name
1092
+
1093
+ // ── Speech-to-Text ──────────────────────────────────────────
1094
+
1095
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1096
+ let recognition = null;
1097
+ let micListening = false;
1098
+
1099
+ if (SpeechRecognition) {
1100
+ recognition = new SpeechRecognition();
1101
+ recognition.lang = 'en-US';
1102
+ recognition.continuous = false;
1103
+ recognition.interimResults = true;
1104
+
1105
+ let finalTranscript = '';
1106
+
1107
+ recognition.onstart = () => {
1108
+ micListening = true;
1109
+ chatMic.classList.add('listening');
1110
+ chatMic.title = 'Listening… click to stop';
1111
+ };
1112
+
1113
+ recognition.onend = () => {
1114
+ micListening = false;
1115
+ chatMic.classList.remove('listening');
1116
+ chatMic.title = 'Voice input';
1117
+ // If we got a final transcript and the input has text, auto-submit
1118
+ if (finalTranscript && chatInput.value.trim()) {
1119
+ chatForm.dispatchEvent(new Event('submit'));
1120
+ }
1121
+ finalTranscript = '';
1122
+ };
1123
+
1124
+ recognition.onresult = (event) => {
1125
+ let interim = '';
1126
+ finalTranscript = '';
1127
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1128
+ const t = event.results[i][0].transcript;
1129
+ if (event.results[i].isFinal) {
1130
+ finalTranscript += t;
1131
+ } else {
1132
+ interim += t;
1133
+ }
1134
+ }
1135
+ // Show interim results in the input as they come
1136
+ if (finalTranscript) {
1137
+ chatInput.value = finalTranscript;
1138
+ } else {
1139
+ chatInput.value = interim;
1140
+ }
1141
+ };
1142
+
1143
+ recognition.onerror = (event) => {
1144
+ micListening = false;
1145
+ chatMic.classList.remove('listening');
1146
+ chatMic.title = 'Voice input';
1147
+ if (event.error !== 'aborted' && event.error !== 'no-speech') {
1148
+ console.warn('Speech recognition error:', event.error);
1149
+ }
1150
+ };
1151
+
1152
+ chatMic.addEventListener('click', () => {
1153
+ if (chatInput.disabled) return;
1154
+ if (micListening) {
1155
+ recognition.stop();
1156
+ } else {
1157
+ chatInput.value = '';
1158
+ recognition.start();
1159
+ }
1160
+ });
1161
+ } else {
1162
+ chatMic.classList.add('unsupported');
1163
+ }
1164
+
1165
+ // ── Data fetching ──────────────────────────────────────────
1166
+
1167
+ async function fetchCalendarNames() {
1168
+ try {
1169
+ const res = await fetch('/api/calendars');
1170
+ const cals = await res.json();
1171
+ calendarNames = {};
1172
+ for (const c of cals) calendarNames[c.id] = c.name;
1173
+ } catch { /* ignore */ }
1174
+ }
1175
+
1176
+ function getValidDate(due) {
1177
+ if (!due || typeof due !== 'string') return null;
1178
+ if (due.startsWith('{')) return null;
1179
+ if (!/^\d{4}-\d{2}-\d{2}/.test(due)) return null;
1180
+ return due.slice(0, 10);
1181
+ }
1182
+
1183
+ async function fetchTimeline() {
1184
+ try {
1185
+ const res = await fetch('/api/calendar');
1186
+ const calItems = await res.json();
1187
+
1188
+ const now = new Date();
1189
+ const dates = [];
1190
+ for (let i = 0; i < 3; i++) {
1191
+ const d = new Date(now);
1192
+ d.setDate(d.getDate() + i);
1193
+ dates.push(d.toISOString().split('T')[0]);
1194
+ }
1195
+ const todayStr = dates[0];
1196
+
1197
+ // Buckets: overdue, today, tomorrow, day-after
1198
+ const overdue = [];
1199
+ const buckets = { [dates[0]]: [], [dates[1]]: [], [dates[2]]: [] };
1200
+
1201
+ for (const ci of calItems) {
1202
+ const date = getValidDate(ci.date);
1203
+
1204
+ if (ci.entityType === 'task') {
1205
+ const item = {
1206
+ kind: 'task',
1207
+ id: ci.id,
1208
+ title: ci.title,
1209
+ status: ci.meta.status || 'open',
1210
+ priority: ci.meta.priority || 'medium',
1211
+ dueDate: date,
1212
+ };
1213
+ if (!date) {
1214
+ buckets[dates[0]].push(item);
1215
+ } else if (date < todayStr) {
1216
+ overdue.push(item);
1217
+ } else if (buckets[date]) {
1218
+ buckets[date].push(item);
1219
+ }
1220
+ } else if (ci.entityType === 'event') {
1221
+ if (!date || !buckets[date]) continue;
1222
+ buckets[date].push({
1223
+ kind: 'event',
1224
+ title: ci.title,
1225
+ startDate: ci.meta.startDate,
1226
+ endDate: ci.meta.endDate,
1227
+ location: ci.meta.location || null,
1228
+ calendarId: ci.meta.calendarId || null,
1229
+ });
1230
+ } else {
1231
+ // Generic dated entity (custom types)
1232
+ if (!date) continue;
1233
+ if (date < todayStr) {
1234
+ overdue.push({ kind: ci.entityType, id: ci.id, title: ci.title, date, entityType: ci.entityType });
1235
+ } else if (buckets[date]) {
1236
+ buckets[date].push({ kind: ci.entityType, id: ci.id, title: ci.title, date, entityType: ci.entityType });
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ renderTimeline(overdue, buckets, dates);
1242
+ } catch {
1243
+ timelineBody.innerHTML = '<div class="empty-state">Failed to load timeline.</div>';
1244
+ }
1245
+ }
1246
+
1247
+ async function fetchScheduler() {
1248
+ try {
1249
+ const res = await fetch('/api/scheduler');
1250
+ const data = await res.json();
1251
+ renderScheduler(data);
1252
+ } catch {
1253
+ schedulerBody.innerHTML = '<div class="empty-state">Failed to load scheduler.</div>';
1254
+ }
1255
+ }
1256
+
1257
+ window.schedToggle = async function(jobName) {
1258
+ try {
1259
+ const res = await fetch('/api/scheduler/' + encodeURIComponent(jobName) + '/toggle', { method: 'POST' });
1260
+ const data = await res.json();
1261
+ if (data.jobs) renderScheduler(data);
1262
+ else await fetchScheduler();
1263
+ } catch { await fetchScheduler(); }
1264
+ };
1265
+
1266
+ window.schedRun = async function(jobName) {
1267
+ try {
1268
+ const res = await fetch('/api/scheduler/' + encodeURIComponent(jobName) + '/run', { method: 'POST' });
1269
+ const data = await res.json();
1270
+ if (data.jobs) renderScheduler(data);
1271
+ else await fetchScheduler();
1272
+ } catch { await fetchScheduler(); }
1273
+ };
1274
+
1275
+ async function fetchStatus() {
1276
+ try {
1277
+ const res = await fetch('/api/status');
1278
+ const data = await res.json();
1279
+ statVault.textContent = data.vaultRoot || '--';
1280
+ statQueue.textContent = String(data.queueSize ?? 0);
1281
+ statGraph.textContent = (data.graph ? data.graph.nodes + 'n / ' + data.graph.edges + 'e' : '--');
1282
+ statMem.textContent = (data.memory ? data.memory.rss + ' MB' : '--');
1283
+ statUptime.textContent = formatUptime(data.uptime || 0);
1284
+ // Update agent name in header if available
1285
+ if (data.agentName) {
1286
+ document.getElementById('header-agent-name').textContent = data.agentName;
1287
+ }
1288
+ } catch { /* ignore */ }
1289
+ }
1290
+
1291
+ function formatUptime(seconds) {
1292
+ if (seconds < 60) return seconds + 's';
1293
+ if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
1294
+ const h = Math.floor(seconds / 3600);
1295
+ const m = Math.floor((seconds % 3600) / 60);
1296
+ return h + 'h ' + m + 'm';
1297
+ }
1298
+
1299
+ // ── Rendering ──────────────────────────────────────────────
1300
+
1301
+ function renderTimeline(overdue, buckets, dates) {
1302
+ const dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
1303
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1304
+ const sections = [];
1305
+
1306
+ // A) Overdue
1307
+ if (overdue.length > 0) {
1308
+ overdue.sort((a, b) => (a.dueDate || '').localeCompare(b.dueDate || ''));
1309
+ sections.push(renderSection('Overdue', null, overdue, true));
1310
+ }
1311
+
1312
+ // B) Today, Tomorrow, Day After
1313
+ const labels = ['Today', null, null];
1314
+ for (let i = 0; i < dates.length; i++) {
1315
+ const date = dates[i];
1316
+ const d = new Date(date + 'T12:00:00');
1317
+ const label = labels[i] || dayNames[d.getDay()];
1318
+ const items = buckets[date] || [];
1319
+
1320
+ // Sort: events first (by time), then tasks (by priority), then other entities (by title)
1321
+ items.sort((a, b) => {
1322
+ const order = { event: 0, task: 1 };
1323
+ const ao = order[a.kind] ?? 2;
1324
+ const bo = order[b.kind] ?? 2;
1325
+ if (ao !== bo) return ao - bo;
1326
+ // Events: by start time
1327
+ if (a.kind === 'event' && b.kind === 'event') {
1328
+ return (a.startDate || '').localeCompare(b.startDate || '');
1329
+ }
1330
+ // Tasks: by priority
1331
+ if (a.kind === 'task' && b.kind === 'task') {
1332
+ const pa = priorityOrder[a.priority] ?? 2;
1333
+ const pb = priorityOrder[b.priority] ?? 2;
1334
+ return pa - pb;
1335
+ }
1336
+ // Other: by title
1337
+ return (a.title || '').localeCompare(b.title || '');
1338
+ });
1339
+
1340
+ sections.push(renderSection(label, date, items, i === 0));
1341
+ }
1342
+
1343
+ const html = sections.join('');
1344
+ timelineBody.innerHTML = html || '<div class="empty-state">Nothing on the horizon. All clear!</div>';
1345
+
1346
+ // Task checkboxes are handled by the delegated listener on timelineBody
1347
+ }
1348
+
1349
+ function renderSection(label, date, items, isHighlight) {
1350
+ const headerClass = isHighlight ? ' today' : '';
1351
+ const dateStr = date ? ' · ' + date : '';
1352
+ let body;
1353
+
1354
+ if (items.length === 0) {
1355
+ body = '<div class="day-empty">Nothing scheduled</div>';
1356
+ } else {
1357
+ body = items.map(item => {
1358
+ if (item.kind === 'event') {
1359
+ const time = item.startDate ? formatTime(item.startDate) : '';
1360
+ const calName = item.calendarId && calendarNames[item.calendarId] ? calendarNames[item.calendarId] : '';
1361
+ return `<div class="event-item">
1362
+ ${time ? '<span class="event-time">' + time + '</span> ' : ''}
1363
+ ${esc(item.title)}${calName ? '<span class="event-cal">' + esc(calName) + '</span>' : ''}
1364
+ ${item.location ? '<div class="event-location">' + esc(item.location) + '</div>' : ''}
1365
+ </div>`;
1366
+ } else if (item.kind === 'task') {
1367
+ return `<div class="task-item" data-task-id="${esc(item.id)}" data-entity-type="task">
1368
+ <div class="task-check" title="Mark done" onclick="markTaskDone(this)"></div>
1369
+ <div class="task-info">
1370
+ <div class="task-title">${esc(item.title)}</div>
1371
+ <div class="task-meta">
1372
+ <span class="badge badge-${item.priority}">${item.priority}</span>
1373
+ ${item.dueDate ? ' · due ' + item.dueDate : ''}
1374
+ </div>
1375
+ </div>
1376
+ </div>`;
1377
+ } else {
1378
+ // Generic calendar entity (custom types)
1379
+ return `<div class="entity-item">
1380
+ <span class="badge badge-entity">${esc(item.entityType || item.kind)}</span>
1381
+ <span class="entity-title">${esc(item.title)}</span>
1382
+ ${item.date ? '<span class="entity-date">' + item.date + '</span>' : ''}
1383
+ </div>`;
1384
+ }
1385
+ }).join('');
1386
+ }
1387
+
1388
+ return `<div class="day-group">
1389
+ <div class="day-header${headerClass}">${label}${dateStr}</div>
1390
+ ${body}
1391
+ </div>`;
1392
+ }
1393
+
1394
+ function renderScheduler(data) {
1395
+ if (!data.jobs || data.jobs.length === 0) {
1396
+ schedulerBody.innerHTML = '<div class="empty-state">No scheduler jobs configured.</div>';
1397
+ return;
1398
+ }
1399
+
1400
+ schedulerBody.innerHTML = data.jobs.map(j => {
1401
+ const dot = j.enabled ? 'on' : 'off';
1402
+ let detail;
1403
+ if (j.running) {
1404
+ detail = 'running…';
1405
+ } else if (!j.enabled) {
1406
+ detail = 'disabled';
1407
+ } else if (j.lastRunAt) {
1408
+ detail = timeAgo(j.lastRunAt);
1409
+ if (j.lastError) {
1410
+ detail += ' · <span class="sched-error">' + esc(j.lastError.slice(0, 40)) + '</span>';
1411
+ } else if (j.lastResult) {
1412
+ detail += ' · ' + esc(j.lastResult.slice(0, 40));
1413
+ }
1414
+ } else if (j.nextRunAt) {
1415
+ detail = 'next ' + timeAgo(j.nextRunAt).replace(' ago', '');
1416
+ } else {
1417
+ detail = 'every ' + formatInterval(j.intervalMinutes);
1418
+ }
1419
+ const toggleLabel = j.enabled ? 'disable' : 'enable';
1420
+ const runBtn = j.enabled
1421
+ ? `<button class="sched-btn" onclick="schedRun('${esc(j.name)}')"${j.running ? ' disabled' : ''}>run</button>`
1422
+ : '';
1423
+ return `<div class="sched-item">
1424
+ <span class="sched-dot ${dot}"></span>
1425
+ <span class="sched-name">${esc(j.name)}</span>
1426
+ <span class="sched-detail">${detail}</span>
1427
+ <span class="sched-actions">
1428
+ <button class="sched-btn" onclick="schedToggle('${esc(j.name)}')">${toggleLabel}</button>
1429
+ ${runBtn}
1430
+ </span>
1431
+ </div>`;
1432
+ }).join('');
1433
+ }
1434
+
1435
+ function formatInterval(minutes) {
1436
+ if (minutes < 60) return minutes + 'm';
1437
+ if (minutes < 1440) return Math.floor(minutes / 60) + 'h';
1438
+ return Math.floor(minutes / 1440) + 'd';
1439
+ }
1440
+
1441
+ function timeAgo(isoString) {
1442
+ try {
1443
+ const diff = Date.now() - new Date(isoString).getTime();
1444
+ if (diff < 0) return 'just now';
1445
+ const secs = Math.floor(diff / 1000);
1446
+ if (secs < 60) return secs + 's ago';
1447
+ const mins = Math.floor(secs / 60);
1448
+ if (mins < 60) return mins + 'm ago';
1449
+ const hrs = Math.floor(mins / 60);
1450
+ if (hrs < 24) return hrs + 'h ago';
1451
+ const days = Math.floor(hrs / 24);
1452
+ return days + 'd ago';
1453
+ } catch { return 'unknown'; }
1454
+ }
1455
+
1456
+ // ── WebSocket ──────────────────────────────────────────────
1457
+
1458
+ function connectWs() {
1459
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1460
+ ws = new WebSocket(proto + '//' + location.host);
1461
+
1462
+ ws.onopen = () => {
1463
+ wsStatusEl.innerHTML = '<span class="dot"></span>Connected';
1464
+ };
1465
+
1466
+ ws.onclose = () => {
1467
+ wsStatusEl.innerHTML = '<span class="dot" style="background:var(--red)"></span>Disconnected';
1468
+ // Reconnect after 3s
1469
+ setTimeout(connectWs, 3000);
1470
+ };
1471
+
1472
+ ws.onmessage = (evt) => {
1473
+ let msg;
1474
+ try { msg = JSON.parse(evt.data); } catch { return; }
1475
+
1476
+ try {
1477
+ switch (msg.type) {
1478
+ case 'chat.start':
1479
+ currentChatId = msg.chatId || null;
1480
+ break;
1481
+ case 'chat.thinking':
1482
+ showThinking(msg.status);
1483
+ break;
1484
+ case 'chat.question':
1485
+ clearThinking();
1486
+ showQuestion(msg.question, msg.options);
1487
+ break;
1488
+ case 'chat.process':
1489
+ clearThinking();
1490
+ showProcessDiagram(msg.process);
1491
+ break;
1492
+ case 'chat.response':
1493
+ clearThinking();
1494
+ if (msg.chatId) currentChatId = msg.chatId;
1495
+ addMessage('bot', msg.message, currentChatId);
1496
+ currentChatId = null;
1497
+ unlockChat();
1498
+ break;
1499
+ case 'chat.error':
1500
+ clearThinking();
1501
+ addMessage('error', msg.error);
1502
+ currentChatId = null;
1503
+ unlockChat();
1504
+ break;
1505
+ case 'refresh':
1506
+ if (msg.panel === 'today' || msg.panel === 'calendar') fetchTimeline();
1507
+ break;
1508
+ }
1509
+ } catch (err) {
1510
+ console.error('WS message handler error:', err);
1511
+ clearThinking();
1512
+ addMessage('error', 'Client error: ' + (err.message || err));
1513
+ unlockChat();
1514
+ }
1515
+ };
1516
+ }
1517
+
1518
+ // ── Chat UI ────────────────────────────────────────────────
1519
+
1520
+ chatForm.addEventListener('submit', (e) => {
1521
+ e.preventDefault();
1522
+ const text = chatInput.value.trim();
1523
+ if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
1524
+
1525
+ addMessage('user', text);
1526
+ ws.send(JSON.stringify({ type: 'chat', message: text }));
1527
+ chatInput.value = '';
1528
+ chatSend.disabled = true;
1529
+ chatInput.disabled = true;
1530
+ });
1531
+
1532
+ function addMessage(role, text, chatId) {
1533
+ const div = document.createElement('div');
1534
+ div.className = 'chat-msg ' + role;
1535
+ const bubble = document.createElement('div');
1536
+ bubble.className = 'chat-bubble';
1537
+ if (role === 'bot') {
1538
+ bubble.innerHTML = renderMarkdown(text || '');
1539
+ } else {
1540
+ bubble.textContent = text || '';
1541
+ }
1542
+ div.appendChild(bubble);
1543
+ if (chatId && role === 'bot') {
1544
+ const idLabel = document.createElement('div');
1545
+ idLabel.className = 'chat-id-label';
1546
+ idLabel.textContent = chatId;
1547
+ div.appendChild(idLabel);
1548
+
1549
+ // Reaction buttons
1550
+ const reactions = document.createElement('div');
1551
+ reactions.className = 'chat-reactions';
1552
+
1553
+ const upBtn = document.createElement('button');
1554
+ upBtn.className = 'reaction-btn';
1555
+ upBtn.innerHTML = '+';
1556
+ upBtn.title = 'Helpful';
1557
+
1558
+ const downBtn = document.createElement('button');
1559
+ downBtn.className = 'reaction-btn';
1560
+ downBtn.innerHTML = '&minus;';
1561
+ downBtn.title = 'Not helpful';
1562
+
1563
+ function sendReaction(reaction, details) {
1564
+ if (ws && ws.readyState === WebSocket.OPEN) {
1565
+ ws.send(JSON.stringify({
1566
+ type: 'chat.reaction',
1567
+ chatId: chatId,
1568
+ reaction: reaction,
1569
+ details: details || undefined,
1570
+ }));
1571
+ }
1572
+ }
1573
+
1574
+ upBtn.addEventListener('click', function() {
1575
+ upBtn.classList.add('selected');
1576
+ upBtn.disabled = true;
1577
+ downBtn.disabled = true;
1578
+ sendReaction('positive');
1579
+ });
1580
+
1581
+ downBtn.addEventListener('click', function() {
1582
+ downBtn.classList.add('selected');
1583
+ upBtn.disabled = true;
1584
+ downBtn.disabled = true;
1585
+
1586
+ var detailsWrap = document.createElement('div');
1587
+ detailsWrap.className = 'reaction-details-wrap';
1588
+ var detailsInput = document.createElement('input');
1589
+ detailsInput.className = 'reaction-details-input';
1590
+ detailsInput.type = 'text';
1591
+ detailsInput.placeholder = 'What went wrong? (optional, Enter to send)';
1592
+ var detailsSubmit = document.createElement('button');
1593
+ detailsSubmit.className = 'reaction-details-submit';
1594
+ detailsSubmit.textContent = 'Send';
1595
+
1596
+ function submitDetails() {
1597
+ sendReaction('negative', detailsInput.value.trim());
1598
+ detailsWrap.remove();
1599
+ }
1600
+
1601
+ detailsSubmit.addEventListener('click', submitDetails);
1602
+ detailsInput.addEventListener('keydown', function(e) {
1603
+ if (e.key === 'Enter') { e.preventDefault(); submitDetails(); }
1604
+ if (e.key === 'Escape') { sendReaction('negative'); detailsWrap.remove(); }
1605
+ });
1606
+
1607
+ detailsWrap.appendChild(detailsInput);
1608
+ detailsWrap.appendChild(detailsSubmit);
1609
+ div.appendChild(detailsWrap);
1610
+ setTimeout(function() { detailsInput.focus(); }, 50);
1611
+ });
1612
+
1613
+ reactions.appendChild(upBtn);
1614
+ reactions.appendChild(downBtn);
1615
+ div.appendChild(reactions);
1616
+ }
1617
+ chatMessages.appendChild(div);
1618
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1619
+ }
1620
+
1621
+ function unlockChat() {
1622
+ chatSend.disabled = false;
1623
+ chatInput.disabled = false;
1624
+ chatInput.focus();
1625
+ }
1626
+
1627
+ function renderMarkdown(text) {
1628
+ try {
1629
+ if (typeof marked !== 'undefined' && marked.parse) {
1630
+ return marked.parse(text, { breaks: true, gfm: true });
1631
+ }
1632
+ } catch (err) {
1633
+ console.warn('marked.parse failed:', err);
1634
+ }
1635
+ // Fallback: escape HTML and convert newlines to <br>
1636
+ return esc(text).replace(/\n/g, '<br>');
1637
+ }
1638
+
1639
+ function showThinking(status) {
1640
+ if (!thinkingEl) {
1641
+ thinkingEl = document.createElement('div');
1642
+ thinkingEl.className = 'chat-msg thinking';
1643
+ thinkingEl.innerHTML = '<div class="chat-bubble"></div>';
1644
+ chatMessages.appendChild(thinkingEl);
1645
+ }
1646
+ thinkingEl.querySelector('.chat-bubble').textContent = status;
1647
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1648
+ }
1649
+
1650
+ function clearThinking() {
1651
+ if (thinkingEl) {
1652
+ thinkingEl.remove();
1653
+ thinkingEl = null;
1654
+ }
1655
+ }
1656
+
1657
+ // ── Question UI ────────────────────────────────────────────
1658
+
1659
+ function showQuestion(question, options) {
1660
+ const div = document.createElement('div');
1661
+ div.className = 'chat-msg question';
1662
+ const bubble = document.createElement('div');
1663
+ bubble.className = 'chat-bubble';
1664
+
1665
+ const qText = document.createElement('div');
1666
+ qText.className = 'question-text';
1667
+ qText.textContent = question || 'A question for you:';
1668
+ bubble.appendChild(qText);
1669
+
1670
+ var cancelled = false;
1671
+
1672
+ function sendAnswer(answer) {
1673
+ // Show user's answer as a message
1674
+ addMessage('user', answer);
1675
+ // Send to server
1676
+ if (ws && ws.readyState === WebSocket.OPEN) {
1677
+ ws.send(JSON.stringify({ type: 'chat.answer', answer: answer }));
1678
+ }
1679
+ // Show thinking again while process resumes
1680
+ showThinking('Resuming…');
1681
+ }
1682
+
1683
+ function sendCancel() {
1684
+ if (cancelled) return;
1685
+ cancelled = true;
1686
+ addMessage('user', 'Cancelled');
1687
+ if (ws && ws.readyState === WebSocket.OPEN) {
1688
+ ws.send(JSON.stringify({ type: 'chat.answer', answer: '__cancel__' }));
1689
+ }
1690
+ // Disable all interactive elements in this question
1691
+ bubble.querySelectorAll('button, input').forEach(function(el) { el.disabled = true; });
1692
+ }
1693
+
1694
+ if (options && options.length > 0) {
1695
+ const optWrap = document.createElement('div');
1696
+ optWrap.className = 'question-options';
1697
+ options.forEach(function(opt) {
1698
+ const btn = document.createElement('button');
1699
+ btn.className = 'question-option-btn';
1700
+ btn.textContent = opt;
1701
+ btn.addEventListener('click', function() {
1702
+ // Disable all buttons
1703
+ optWrap.querySelectorAll('button').forEach(function(b) { b.disabled = true; });
1704
+ sendAnswer(opt);
1705
+ });
1706
+ optWrap.appendChild(btn);
1707
+ });
1708
+ const cancelBtn = document.createElement('button');
1709
+ cancelBtn.className = 'question-cancel-btn';
1710
+ cancelBtn.textContent = 'Cancel';
1711
+ cancelBtn.addEventListener('click', sendCancel);
1712
+ optWrap.appendChild(cancelBtn);
1713
+ bubble.appendChild(optWrap);
1714
+ } else {
1715
+ const inputWrap = document.createElement('div');
1716
+ inputWrap.className = 'question-input-wrap';
1717
+ const input = document.createElement('input');
1718
+ input.className = 'question-input';
1719
+ input.type = 'text';
1720
+ input.placeholder = 'Type your answer…';
1721
+ const submitBtn = document.createElement('button');
1722
+ submitBtn.className = 'question-submit-btn';
1723
+ submitBtn.textContent = 'Send';
1724
+ const cancelBtn = document.createElement('button');
1725
+ cancelBtn.className = 'question-cancel-btn';
1726
+ cancelBtn.textContent = 'Cancel';
1727
+
1728
+ function submitAnswer() {
1729
+ const val = input.value.trim();
1730
+ if (!val) return;
1731
+ input.disabled = true;
1732
+ submitBtn.disabled = true;
1733
+ cancelBtn.disabled = true;
1734
+ sendAnswer(val);
1735
+ }
1736
+
1737
+ submitBtn.addEventListener('click', submitAnswer);
1738
+ cancelBtn.addEventListener('click', sendCancel);
1739
+ input.addEventListener('keydown', function(e) {
1740
+ if (e.key === 'Enter') { e.preventDefault(); submitAnswer(); }
1741
+ if (e.key === 'Escape') { e.preventDefault(); sendCancel(); }
1742
+ });
1743
+
1744
+ inputWrap.appendChild(input);
1745
+ inputWrap.appendChild(submitBtn);
1746
+ inputWrap.appendChild(cancelBtn);
1747
+ bubble.appendChild(inputWrap);
1748
+
1749
+ // Auto-focus the question input
1750
+ setTimeout(function() { input.focus(); }, 50);
1751
+ }
1752
+
1753
+ div.appendChild(bubble);
1754
+ chatMessages.appendChild(div);
1755
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1756
+ }
1757
+
1758
+ // ── Process diagram ────────────────────────────────────────
1759
+
1760
+ let mermaidCounter = 0;
1761
+
1762
+ function processToMermaid(proc) {
1763
+ const nodes = proc.nodes || [];
1764
+ if (nodes.length === 0) return null;
1765
+
1766
+ const lines = ['graph LR'];
1767
+
1768
+ // Define node shapes
1769
+ for (const n of nodes) {
1770
+ const label = n.catalog_node_key || n.key;
1771
+ if (label === 'start') {
1772
+ lines.push(' ' + n.key + '([start])');
1773
+ } else if (label === 'stop') {
1774
+ lines.push(' ' + n.key + '([stop])');
1775
+ } else {
1776
+ lines.push(' ' + n.key + '[' + label + ']');
1777
+ }
1778
+ }
1779
+
1780
+ // Define edges
1781
+ for (const n of nodes) {
1782
+ if (!n.edges) continue;
1783
+ for (const [status, target] of Object.entries(n.edges)) {
1784
+ if (status === 'ok') {
1785
+ lines.push(' ' + n.key + ' --> ' + target);
1786
+ } else {
1787
+ lines.push(' ' + n.key + ' -->|' + status + '| ' + target);
1788
+ }
1789
+ }
1790
+ }
1791
+
1792
+ return lines.join('\n');
1793
+ }
1794
+
1795
+ async function showProcessDiagram(proc) {
1796
+ if (typeof mermaid === 'undefined') return;
1797
+
1798
+ const mermaidSrc = processToMermaid(proc);
1799
+ if (!mermaidSrc) return;
1800
+
1801
+ const id = 'mermaid-' + (++mermaidCounter);
1802
+ const wrapper = document.createElement('div');
1803
+ wrapper.className = 'chat-process';
1804
+
1805
+ const toggle = document.createElement('div');
1806
+ toggle.className = 'process-toggle';
1807
+ toggle.textContent = '▶ Process diagram';
1808
+
1809
+ const diagram = document.createElement('div');
1810
+ diagram.className = 'process-diagram';
1811
+ diagram.style.display = 'none';
1812
+ diagram.id = id;
1813
+
1814
+ const pre = document.createElement('pre');
1815
+ pre.className = 'mermaid';
1816
+ pre.textContent = mermaidSrc;
1817
+ diagram.appendChild(pre);
1818
+
1819
+ toggle.addEventListener('click', async () => {
1820
+ const showing = diagram.style.display !== 'none';
1821
+ if (showing) {
1822
+ diagram.style.display = 'none';
1823
+ toggle.textContent = '▶ Process diagram';
1824
+ } else {
1825
+ diagram.style.display = 'block';
1826
+ toggle.textContent = '▼ Process diagram';
1827
+ // Render mermaid if not yet rendered
1828
+ if (!diagram.dataset.rendered) {
1829
+ try {
1830
+ await mermaid.run({ nodes: [pre] });
1831
+ diagram.dataset.rendered = '1';
1832
+ } catch (err) {
1833
+ console.warn('Mermaid render failed:', err);
1834
+ pre.textContent = mermaidSrc;
1835
+ }
1836
+ }
1837
+ }
1838
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1839
+ });
1840
+
1841
+ wrapper.appendChild(toggle);
1842
+ wrapper.appendChild(diagram);
1843
+ chatMessages.appendChild(wrapper);
1844
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1845
+ }
1846
+
1847
+ // ── Helpers ────────────────────────────────────────────────
1848
+
1849
+ function esc(s) {
1850
+ const d = document.createElement('div');
1851
+ d.textContent = s || '';
1852
+ return d.innerHTML;
1853
+ }
1854
+
1855
+ function formatTime(iso) {
1856
+ try {
1857
+ const d = new Date(iso);
1858
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1859
+ } catch { return ''; }
1860
+ }
1861
+
1862
+ // ── Onboarding ──────────────────────────────────────────────
1863
+
1864
+ const onboardingOverlay = document.getElementById('onboarding');
1865
+ let obPersonality = '';
1866
+
1867
+ async function checkOnboarding() {
1868
+ try {
1869
+ const res = await fetch('/api/status');
1870
+ const data = await res.json();
1871
+ if (!data.vaultRoot || !data.interviewComplete) {
1872
+ showOnboarding();
1873
+ return true;
1874
+ }
1875
+ } catch { /* ignore */ }
1876
+ return false;
1877
+ }
1878
+
1879
+ async function showOnboarding() {
1880
+ onboardingOverlay.classList.remove('hidden');
1881
+ // Load personalities
1882
+ try {
1883
+ const res = await fetch('/api/personalities');
1884
+ const list = await res.json();
1885
+ const grid = document.getElementById('personality-grid');
1886
+ grid.innerHTML = list.map(function(p) {
1887
+ return '<div class="personality-option" data-id="' + esc(p.id) + '">'
1888
+ + '<div class="p-label">' + esc(p.label) + '</div>'
1889
+ + '<div class="p-desc">' + esc(p.description) + '</div>'
1890
+ + '</div>';
1891
+ }).join('');
1892
+
1893
+ grid.addEventListener('click', function(e) {
1894
+ var opt = e.target.closest('.personality-option');
1895
+ if (!opt) return;
1896
+ grid.querySelectorAll('.personality-option').forEach(function(el) { el.classList.remove('selected'); });
1897
+ opt.classList.add('selected');
1898
+ obPersonality = opt.dataset.id;
1899
+ document.getElementById('ob-next-1').disabled = false;
1900
+ });
1901
+ } catch (err) {
1902
+ console.error('Failed to load personalities', err);
1903
+ }
1904
+
1905
+ // Step navigation
1906
+ document.getElementById('ob-next-1').addEventListener('click', function() { obGoStep(2); });
1907
+ document.getElementById('ob-back-2').addEventListener('click', function() { obGoStep(1); });
1908
+ document.getElementById('ob-next-2').addEventListener('click', function() {
1909
+ if (!document.getElementById('ob-user-name').value.trim()) {
1910
+ document.getElementById('ob-user-name').focus();
1911
+ return;
1912
+ }
1913
+ obGoStep(3);
1914
+ });
1915
+ document.getElementById('ob-back-3').addEventListener('click', function() { obGoStep(2); });
1916
+ document.getElementById('ob-submit').addEventListener('click', submitOnboarding);
1917
+ }
1918
+
1919
+ function obGoStep(n) {
1920
+ document.querySelectorAll('.onboarding-step').forEach(function(el) { el.classList.remove('active'); });
1921
+ document.getElementById('ob-step-' + n).classList.add('active');
1922
+ }
1923
+
1924
+ async function submitOnboarding() {
1925
+ var btn = document.getElementById('ob-submit');
1926
+ btn.disabled = true;
1927
+ btn.textContent = 'Setting up…';
1928
+
1929
+ var payload = {
1930
+ personality: obPersonality,
1931
+ agentName: document.getElementById('ob-agent-name').value.trim() || 'Agent',
1932
+ userName: document.getElementById('ob-user-name').value.trim(),
1933
+ gender: document.getElementById('ob-gender').value,
1934
+ answers: {
1935
+ location: document.getElementById('ob-location').value.trim() || undefined,
1936
+ work: document.getElementById('ob-work').value.trim() || undefined,
1937
+ workGoals: document.getElementById('ob-goals').value.trim() || undefined,
1938
+ relationships: document.getElementById('ob-relationships').value.trim() || undefined,
1939
+ health: document.getElementById('ob-health').value.trim() || undefined,
1940
+ growth: document.getElementById('ob-growth').value.trim() || undefined,
1941
+ },
1942
+ };
1943
+
1944
+ try {
1945
+ var res = await fetch('/api/setup', {
1946
+ method: 'POST',
1947
+ headers: { 'Content-Type': 'application/json' },
1948
+ body: JSON.stringify(payload),
1949
+ });
1950
+ if (!res.ok) {
1951
+ var err = await res.json().catch(function() { return {}; });
1952
+ throw new Error(err.error || 'Setup failed');
1953
+ }
1954
+ // Success — reload the page
1955
+ onboardingOverlay.classList.add('hidden');
1956
+ location.reload();
1957
+ } catch (err) {
1958
+ btn.disabled = false;
1959
+ btn.textContent = 'Get Started';
1960
+ alert('Setup failed: ' + (err.message || err));
1961
+ }
1962
+ }
1963
+
1964
+ // ── Init ───────────────────────────────────────────────────
1965
+
1966
+ checkOnboarding().then(function(needsOnboarding) {
1967
+ if (!needsOnboarding) {
1968
+ fetchCalendarNames().then(function() { fetchTimeline(); });
1969
+ fetchScheduler();
1970
+ connectWs();
1971
+ } else {
1972
+ // Still connect WS and fetch basic status even during onboarding
1973
+ connectWs();
1974
+ }
1975
+ fetchStatus();
1976
+ });
1977
+
1978
+ // Poll status and scheduler every 30s
1979
+ setInterval(fetchStatus, 30000);
1980
+ setInterval(fetchScheduler, 30000);
1981
+ })();
1982
+ </script>
1983
+ </body>
1984
+ </html>