phaibel 4.0.35 → 4.0.37

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 (56) hide show
  1. package/README.md +26 -31
  2. package/dist/commands/chat.d.ts.map +1 -1
  3. package/dist/commands/chat.js +39 -53
  4. package/dist/commands/chat.js.map +1 -1
  5. package/dist/context/context-tree-serializer.d.ts +8 -0
  6. package/dist/context/context-tree-serializer.d.ts.map +1 -0
  7. package/dist/context/context-tree-serializer.js +150 -0
  8. package/dist/context/context-tree-serializer.js.map +1 -0
  9. package/dist/context/context-tree.d.ts +67 -0
  10. package/dist/context/context-tree.d.ts.map +1 -0
  11. package/dist/context/context-tree.js +207 -0
  12. package/dist/context/context-tree.js.map +1 -0
  13. package/dist/context/scope-classifier.d.ts +9 -0
  14. package/dist/context/scope-classifier.d.ts.map +1 -0
  15. package/dist/context/scope-classifier.js +43 -0
  16. package/dist/context/scope-classifier.js.map +1 -0
  17. package/dist/feral/bootstrap.d.ts.map +1 -1
  18. package/dist/feral/bootstrap.js +9 -1
  19. package/dist/feral/bootstrap.js.map +1 -1
  20. package/dist/feral/catalog/usage-catalog-source.d.ts +7 -0
  21. package/dist/feral/catalog/usage-catalog-source.d.ts.map +1 -0
  22. package/dist/feral/catalog/usage-catalog-source.js +48 -0
  23. package/dist/feral/catalog/usage-catalog-source.js.map +1 -0
  24. package/dist/feral/node-code/data/chart-token-usage-node-code.d.ts +11 -0
  25. package/dist/feral/node-code/data/chart-token-usage-node-code.d.ts.map +1 -0
  26. package/dist/feral/node-code/data/chart-token-usage-node-code.js +130 -0
  27. package/dist/feral/node-code/data/chart-token-usage-node-code.js.map +1 -0
  28. package/dist/feral/node-code/data/query-token-usage-node-code.d.ts +10 -0
  29. package/dist/feral/node-code/data/query-token-usage-node-code.d.ts.map +1 -0
  30. package/dist/feral/node-code/data/query-token-usage-node-code.js +45 -0
  31. package/dist/feral/node-code/data/query-token-usage-node-code.js.map +1 -0
  32. package/dist/llm/providers/claude.d.ts.map +1 -1
  33. package/dist/llm/providers/claude.js +5 -0
  34. package/dist/llm/providers/claude.js.map +1 -1
  35. package/dist/llm/providers/openai.d.ts.map +1 -1
  36. package/dist/llm/providers/openai.js +9 -0
  37. package/dist/llm/providers/openai.js.map +1 -1
  38. package/dist/llm/token-usage.d.ts +39 -0
  39. package/dist/llm/token-usage.d.ts.map +1 -0
  40. package/dist/llm/token-usage.js +156 -0
  41. package/dist/llm/token-usage.js.map +1 -0
  42. package/dist/service/api-router.d.ts.map +1 -1
  43. package/dist/service/api-router.js +41 -0
  44. package/dist/service/api-router.js.map +1 -1
  45. package/dist/service/cron/scheduler.d.ts.map +1 -1
  46. package/dist/service/cron/scheduler.js +8 -0
  47. package/dist/service/cron/scheduler.js.map +1 -1
  48. package/dist/service/cron/world-model-synthesis.d.ts +22 -0
  49. package/dist/service/cron/world-model-synthesis.d.ts.map +1 -0
  50. package/dist/service/cron/world-model-synthesis.js +202 -0
  51. package/dist/service/cron/world-model-synthesis.js.map +1 -0
  52. package/dist/service/web-client.html +323 -1
  53. package/dist/service/web-server.d.ts.map +1 -1
  54. package/dist/service/web-server.js +1 -0
  55. package/dist/service/web-server.js.map +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,202 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // World Model Synthesis — proactive intelligence about the user's world
3
+ //
4
+ // Periodically reads all entities, synthesizes them into a coherent model of
5
+ // the user's current state, and generates proactive insights/nudges. Results
6
+ // are stored in {vault}/.phaibel/world-model.json for the web client to display.
7
+ // Users grade each insight so the system learns what's useful over time.
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ import { promises as fs } from 'fs';
10
+ import path from 'path';
11
+ import { findVaultRoot, getUserName, getAgentName } from '../../state/manager.js';
12
+ import { getVaultConfigDir } from '../../paths.js';
13
+ import { getModelForCapability } from '../../llm/router.js';
14
+ import { listEntities } from '../../entities/entity.js';
15
+ import { loadEntityTypes } from '../../entities/entity-type-config.js';
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // FILE I/O
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ async function getWorldModelPath() {
20
+ const dir = await getVaultConfigDir();
21
+ return path.join(dir, 'world-model.json');
22
+ }
23
+ export async function loadWorldModel() {
24
+ try {
25
+ const raw = await fs.readFile(await getWorldModelPath(), 'utf-8');
26
+ return JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export async function saveWorldModel(model) {
33
+ const dir = await getVaultConfigDir();
34
+ await fs.mkdir(dir, { recursive: true });
35
+ await fs.writeFile(await getWorldModelPath(), JSON.stringify(model, null, 2));
36
+ }
37
+ export async function gradeInsight(insightId, grade) {
38
+ const model = await loadWorldModel();
39
+ if (!model)
40
+ return false;
41
+ const insight = model.insights.find(i => i.id === insightId);
42
+ if (!insight)
43
+ return false;
44
+ insight.grade = grade;
45
+ insight.gradedAt = new Date().toISOString();
46
+ await saveWorldModel(model);
47
+ return true;
48
+ }
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // SYNTHESIS
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ export async function synthesizeWorldModel() {
53
+ const vaultRoot = await findVaultRoot();
54
+ if (!vaultRoot)
55
+ return 'skipped (no vault)';
56
+ const userName = await getUserName() || 'User';
57
+ const agentName = await getAgentName() || 'Phaibel';
58
+ const types = await loadEntityTypes();
59
+ const today = new Date().toISOString().split('T')[0];
60
+ const now = new Date();
61
+ // ── Gather all entities ──────────────────────────────────────────────
62
+ const entityCounts = {};
63
+ const summaries = [];
64
+ for (const type of types) {
65
+ try {
66
+ const entities = await listEntities(type.name);
67
+ entityCounts[type.name] = entities.length;
68
+ if (entities.length === 0)
69
+ continue;
70
+ // Build a compact summary of each entity type
71
+ const lines = [`## ${type.plural} (${entities.length})`];
72
+ for (const e of entities.slice(0, 50)) { // cap at 50 per type
73
+ const title = e.meta.title || '(untitled)';
74
+ const status = e.meta.status || '';
75
+ const dueDate = e.meta.dueDate || e.meta.startDate || '';
76
+ const priority = e.meta.priority || '';
77
+ const tags = Array.isArray(e.meta.tags) ? e.meta.tags.join(', ') : '';
78
+ const parts = [`- **${title}**`];
79
+ if (status)
80
+ parts.push(`[${status}]`);
81
+ if (priority)
82
+ parts.push(`(${priority})`);
83
+ if (dueDate)
84
+ parts.push(`due: ${dueDate}`);
85
+ if (tags)
86
+ parts.push(`tags: ${tags}`);
87
+ // Include body snippet for context
88
+ if (e.content.trim()) {
89
+ parts.push(`— ${e.content.trim().slice(0, 120)}`);
90
+ }
91
+ lines.push(parts.join(' '));
92
+ }
93
+ if (entities.length > 50) {
94
+ lines.push(`- ... and ${entities.length - 50} more`);
95
+ }
96
+ summaries.push(lines.join('\n'));
97
+ }
98
+ catch {
99
+ entityCounts[type.name] = 0;
100
+ }
101
+ }
102
+ if (summaries.length === 0) {
103
+ return 'skipped (no entities)';
104
+ }
105
+ // ── Load previous model and check capacity ─────────────────────────
106
+ const MAX_INSIGHTS = 5;
107
+ const previousModel = await loadWorldModel();
108
+ const existingInsights = previousModel?.insights.filter(i => !i.grade) ?? [];
109
+ const slotsAvailable = MAX_INSIGHTS - existingInsights.length;
110
+ if (slotsAvailable <= 0) {
111
+ return `skipped (${existingInsights.length} ungraded insights already at cap)`;
112
+ }
113
+ let gradeFeedback = '';
114
+ if (previousModel?.insights.some(i => i.grade)) {
115
+ const graded = previousModel.insights.filter(i => i.grade);
116
+ const useful = graded.filter(i => i.grade === 'useful');
117
+ const notUseful = graded.filter(i => i.grade === 'not_useful');
118
+ gradeFeedback = `\n\nPREVIOUS INSIGHT FEEDBACK:
119
+ ${useful.length} insights were graded "useful": ${useful.map(i => `"${i.title}" (${i.category})`).join(', ') || 'none'}
120
+ ${notUseful.length} insights were graded "not useful": ${notUseful.map(i => `"${i.title}" (${i.category})`).join(', ') || 'none'}
121
+
122
+ Generate MORE insights like the useful ones and FEWER like the not-useful ones.`;
123
+ }
124
+ // ── Call LLM ─────────────────────────────────────────────────────────
125
+ const llm = await getModelForCapability('reason');
126
+ const prompt = `You are ${agentName}, a proactive personal assistant for ${userName}. Today is ${today}.
127
+
128
+ Analyze the following complete picture of ${userName}'s world — all their tasks, events, goals, people, notes, and other tracked items. Your job is to be PROACTIVE: surface things ${userName} should know, act on, or think about BEFORE they ask.
129
+
130
+ ${summaries.join('\n\n')}
131
+ ${gradeFeedback}
132
+
133
+ Generate exactly ${slotsAvailable} proactive insight${slotsAvailable === 1 ? '' : 's'}. Each insight should be something ${userName} would genuinely find valuable — not obvious observations, but connections, risks, opportunities, and timely nudges.
134
+
135
+ CATEGORIES:
136
+ - "reminder": Something time-sensitive that needs attention soon
137
+ - "opportunity": A chance to make progress or take advantage of timing
138
+ - "risk": Something that could go wrong if not addressed
139
+ - "progress": Positive momentum or milestone worth acknowledging
140
+ - "connection": A relationship between items ${userName} might not have noticed
141
+ - "suggestion": A proactive recommendation to improve their workflow or life
142
+
143
+ RULES:
144
+ - Be specific — reference actual entity titles and dates
145
+ - Be actionable — tell ${userName} what they could do, not just what you noticed
146
+ - Prioritize time-sensitive items (overdue tasks, upcoming events, approaching deadlines)
147
+ - Look for conflicts (double-booked events, competing deadlines)
148
+ - Notice stalled goals or forgotten tasks
149
+ - Spot opportunities to link people with events or tasks
150
+ - Keep each insight concise (1-3 sentences)
151
+
152
+ Respond with ONLY valid JSON (no markdown fences), matching this schema:
153
+ {
154
+ "insights": [
155
+ {
156
+ "category": "reminder|opportunity|risk|progress|connection|suggestion",
157
+ "title": "Short headline (under 60 chars)",
158
+ "body": "1-3 sentence explanation with specific details",
159
+ "priority": "high|medium|low",
160
+ "relatedEntities": ["type:id", "type:id"]
161
+ }
162
+ ]
163
+ }`;
164
+ const response = await llm.chat([{ role: 'user', content: prompt }], {
165
+ systemPrompt: `You are a proactive personal intelligence engine. You analyze a person's complete set of commitments, goals, relationships, and plans to surface timely, actionable insights. Respond with ONLY valid JSON — no markdown, no explanation, no fences.`,
166
+ temperature: 0.4,
167
+ });
168
+ // ── Parse response ───────────────────────────────────────────────────
169
+ let insights;
170
+ try {
171
+ const cleaned = response.replace(/```json?\n?/g, '').replace(/```\n?/g, '').trim();
172
+ const parsed = JSON.parse(cleaned);
173
+ insights = parsed.insights.map((raw, i) => ({
174
+ id: `wm-${Date.now()}-${i}`,
175
+ category: raw.category || 'suggestion',
176
+ title: raw.title || 'Insight',
177
+ body: raw.body || '',
178
+ priority: raw.priority || 'medium',
179
+ relatedEntities: raw.relatedEntities || [],
180
+ grade: null,
181
+ gradedAt: null,
182
+ }));
183
+ }
184
+ catch (err) {
185
+ console.error('[world-model] Failed to parse LLM response:', err);
186
+ return 'error: failed to parse insights';
187
+ }
188
+ // ── Merge with existing insights ────────────────────────────────────
189
+ // Keep existing ungraded insights + append new ones (cap at MAX_INSIGHTS)
190
+ const mergedInsights = [...existingInsights, ...insights].slice(0, MAX_INSIGHTS);
191
+ const model = {
192
+ synthesizedAt: now.toISOString(),
193
+ userName,
194
+ agentName,
195
+ entityCounts,
196
+ insights: mergedInsights,
197
+ };
198
+ await saveWorldModel(model);
199
+ const highCount = insights.filter(i => i.priority === 'high').length;
200
+ return `${insights.length} insights generated (${highCount} high priority)`;
201
+ }
202
+ //# sourceMappingURL=world-model-synthesis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world-model-synthesis.js","sourceRoot":"","sources":["../../../src/service/cron/world-model-synthesis.ts"],"names":[],"mappings":"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"}
@@ -289,6 +289,160 @@
289
289
  flex-shrink: 0;
290
290
  }
291
291
 
292
+ /* ── Content Types panel ────────────────────────────── */
293
+
294
+ .ctype-item {
295
+ padding: 6px 8px;
296
+ border-bottom: 1px solid rgba(255,255,255,0.04);
297
+ border-radius: 4px;
298
+ cursor: pointer;
299
+ transition: background 0.15s;
300
+ }
301
+
302
+ .ctype-item:last-child { border-bottom: none; }
303
+ .ctype-item:hover { background: rgba(255,255,255,0.06); }
304
+
305
+ .ctype-header {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 6px;
309
+ }
310
+
311
+ .ctype-name {
312
+ font-size: 12px;
313
+ font-weight: 600;
314
+ }
315
+
316
+ .ctype-count {
317
+ font-size: 10px;
318
+ color: var(--dim);
319
+ margin-left: auto;
320
+ flex-shrink: 0;
321
+ }
322
+
323
+ .ctype-desc {
324
+ font-size: 10px;
325
+ color: var(--dim);
326
+ line-height: 1.3;
327
+ margin-top: 2px;
328
+ }
329
+
330
+ .ctype-fields {
331
+ display: flex;
332
+ flex-wrap: wrap;
333
+ gap: 3px;
334
+ margin-top: 4px;
335
+ }
336
+
337
+ .ctype-field {
338
+ font-size: 9px;
339
+ padding: 1px 5px;
340
+ border-radius: 3px;
341
+ background: rgba(255,255,255,0.06);
342
+ color: var(--dim);
343
+ }
344
+
345
+ /* ── Insights panel ─────────────────────────────────── */
346
+
347
+ .insight-item {
348
+ padding: 8px 0;
349
+ border-bottom: 1px solid rgba(255,255,255,0.04);
350
+ }
351
+
352
+ .insight-item:last-child { border-bottom: none; }
353
+
354
+ .insight-header {
355
+ display: flex;
356
+ align-items: center;
357
+ gap: 6px;
358
+ margin-bottom: 4px;
359
+ }
360
+
361
+ .insight-category {
362
+ font-size: 9px;
363
+ font-weight: 600;
364
+ text-transform: uppercase;
365
+ letter-spacing: 0.05em;
366
+ padding: 1px 5px;
367
+ border-radius: 3px;
368
+ flex-shrink: 0;
369
+ }
370
+
371
+ .insight-category.reminder { background: rgba(239,68,68,0.2); color: #f87171; }
372
+ .insight-category.risk { background: rgba(239,68,68,0.15); color: #fca5a5; }
373
+ .insight-category.opportunity { background: rgba(34,197,94,0.2); color: #4ade80; }
374
+ .insight-category.progress { background: rgba(59,130,246,0.2); color: #60a5fa; }
375
+ .insight-category.connection { background: rgba(168,85,247,0.2); color: #c084fc; }
376
+ .insight-category.suggestion { background: rgba(245,158,11,0.2); color: #fbbf24; }
377
+
378
+ .insight-priority {
379
+ font-size: 9px;
380
+ color: var(--dim);
381
+ margin-left: auto;
382
+ flex-shrink: 0;
383
+ }
384
+
385
+ .insight-priority.high { color: #f87171; }
386
+
387
+ .insight-title {
388
+ font-size: 12px;
389
+ font-weight: 600;
390
+ margin-bottom: 2px;
391
+ line-height: 1.3;
392
+ }
393
+
394
+ .insight-body {
395
+ font-size: 11px;
396
+ color: var(--dim);
397
+ line-height: 1.4;
398
+ margin-bottom: 6px;
399
+ }
400
+
401
+ .insight-grade {
402
+ display: flex;
403
+ gap: 4px;
404
+ }
405
+
406
+ .insight-grade-btn {
407
+ background: rgba(255,255,255,0.06);
408
+ border: 1px solid rgba(255,255,255,0.1);
409
+ color: var(--dim);
410
+ font-size: 11px;
411
+ padding: 2px 8px;
412
+ border-radius: 3px;
413
+ cursor: pointer;
414
+ }
415
+
416
+ .insight-grade-btn:hover {
417
+ background: rgba(255,255,255,0.12);
418
+ color: var(--text);
419
+ }
420
+
421
+ .insight-grade-btn.graded {
422
+ cursor: default;
423
+ opacity: 0.7;
424
+ }
425
+
426
+ .insight-grade-btn.graded.useful {
427
+ background: rgba(34,197,94,0.2);
428
+ border-color: rgba(34,197,94,0.3);
429
+ color: #4ade80;
430
+ }
431
+
432
+ .insight-grade-btn.graded.not-useful {
433
+ background: rgba(239,68,68,0.15);
434
+ border-color: rgba(239,68,68,0.2);
435
+ color: #fca5a5;
436
+ }
437
+
438
+ .insights-meta {
439
+ font-size: 10px;
440
+ color: var(--dim);
441
+ margin-top: 6px;
442
+ padding-top: 6px;
443
+ border-top: 1px solid rgba(255,255,255,0.04);
444
+ }
445
+
292
446
  /* ── Scheduler panel ────────────────────────────────── */
293
447
 
294
448
  .sched-item {
@@ -1035,6 +1189,14 @@
1035
1189
  <div id="timeline-body">
1036
1190
  <div class="empty-state">Loading…</div>
1037
1191
  </div>
1192
+ <div class="panel-section-title" style="margin-top: 16px;">Content Types</div>
1193
+ <div id="content-types-body">
1194
+ <div class="empty-state">Loading…</div>
1195
+ </div>
1196
+ <div class="panel-section-title" style="margin-top: 16px;">Insights</div>
1197
+ <div id="insights-body">
1198
+ <div class="empty-state">No insights yet</div>
1199
+ </div>
1038
1200
  <div class="panel-section-title" style="margin-top: 16px;">Scheduler</div>
1039
1201
  <div id="scheduler-body">
1040
1202
  <div class="empty-state">Loading scheduler…</div>
@@ -1301,6 +1463,163 @@ async function markTaskDone(checkEl) {
1301
1463
  } catch { await fetchScheduler(); }
1302
1464
  };
1303
1465
 
1466
+ // ── Insights (World Model Synthesis) ─────────────────────
1467
+ var insightsBody = document.getElementById('insights-body');
1468
+
1469
+ async function fetchInsights() {
1470
+ try {
1471
+ const res = await fetch('/api/insights');
1472
+ const data = await res.json();
1473
+ renderInsights(data);
1474
+ } catch {
1475
+ insightsBody.innerHTML = '<div class="empty-state">Failed to load insights.</div>';
1476
+ }
1477
+ }
1478
+
1479
+ function renderInsights(data) {
1480
+ if (!data.insights || data.insights.length === 0) {
1481
+ insightsBody.innerHTML = '<div class="empty-state">No insights yet. The World Model runs every 10 minutes.</div>';
1482
+ return;
1483
+ }
1484
+
1485
+ // Sort: high priority first, then ungraded first
1486
+ var sorted = data.insights.slice().sort(function(a, b) {
1487
+ var pOrd = { high: 0, medium: 1, low: 2 };
1488
+ var pDiff = (pOrd[a.priority] || 1) - (pOrd[b.priority] || 1);
1489
+ if (pDiff !== 0) return pDiff;
1490
+ // Ungraded before graded
1491
+ if (!a.grade && b.grade) return -1;
1492
+ if (a.grade && !b.grade) return 1;
1493
+ return 0;
1494
+ });
1495
+
1496
+ var html = '';
1497
+ for (var i = 0; i < sorted.length; i++) {
1498
+ var ins = sorted[i];
1499
+ var gradeHtml = '';
1500
+ if (ins.grade) {
1501
+ var cls = ins.grade === 'useful' ? 'graded useful' : 'graded not-useful';
1502
+ var label = ins.grade === 'useful' ? 'Useful' : 'Not useful';
1503
+ gradeHtml = '<span class="insight-grade-btn ' + cls + '">' + esc(label) + '</span>';
1504
+ } else {
1505
+ gradeHtml = '<button class="insight-grade-btn" onclick="gradeInsight(\'' + esc(ins.id) + '\', \'useful\')">Useful</button>'
1506
+ + '<button class="insight-grade-btn" onclick="gradeInsight(\'' + esc(ins.id) + '\', \'not_useful\')">Not useful</button>';
1507
+ }
1508
+
1509
+ html += '<div class="insight-item">'
1510
+ + '<div class="insight-header">'
1511
+ + '<span class="insight-category ' + esc(ins.category) + '">' + esc(ins.category) + '</span>'
1512
+ + '<span class="insight-priority ' + esc(ins.priority) + '">' + esc(ins.priority) + '</span>'
1513
+ + '</div>'
1514
+ + '<div class="insight-title">' + esc(ins.title) + '</div>'
1515
+ + '<div class="insight-body">' + esc(ins.body) + '</div>'
1516
+ + '<div class="insight-grade">' + gradeHtml + '</div>'
1517
+ + '</div>';
1518
+ }
1519
+
1520
+ if (data.synthesizedAt) {
1521
+ var ago = timeAgo(new Date(data.synthesizedAt));
1522
+ html += '<div class="insights-meta">Synthesized ' + esc(ago) + '</div>';
1523
+ }
1524
+
1525
+ insightsBody.innerHTML = html;
1526
+ }
1527
+
1528
+ window.gradeInsight = async function(insightId, grade) {
1529
+ try {
1530
+ await fetch('/api/insights/' + encodeURIComponent(insightId) + '/grade', {
1531
+ method: 'POST',
1532
+ headers: { 'Content-Type': 'application/json' },
1533
+ body: JSON.stringify({ grade: grade }),
1534
+ });
1535
+ await fetchInsights();
1536
+ } catch { /* ignore */ }
1537
+ };
1538
+
1539
+ function timeAgo(date) {
1540
+ var seconds = Math.floor((Date.now() - date.getTime()) / 1000);
1541
+ if (seconds < 60) return 'just now';
1542
+ var minutes = Math.floor(seconds / 60);
1543
+ if (minutes < 60) return minutes + 'm ago';
1544
+ var hours = Math.floor(minutes / 60);
1545
+ if (hours < 24) return hours + 'h ago';
1546
+ var days = Math.floor(hours / 24);
1547
+ return days + 'd ago';
1548
+ }
1549
+
1550
+ // ── Content Types ────────────────────────────────────────
1551
+ var contentTypesBody = document.getElementById('content-types-body');
1552
+
1553
+ async function fetchContentTypes() {
1554
+ try {
1555
+ const [typesRes, calRes] = await Promise.all([
1556
+ fetch('/api/types'),
1557
+ fetch('/api/calendar'),
1558
+ ]);
1559
+ const types = await typesRes.json();
1560
+ const calItems = await calRes.json();
1561
+
1562
+ // Count entities per type from calendar data (which includes all dated entities)
1563
+ // Also fetch entity counts per type via listing
1564
+ const counts = {};
1565
+ for (const item of calItems) {
1566
+ var t = item.entityType || '';
1567
+ counts[t] = (counts[t] || 0) + 1;
1568
+ }
1569
+
1570
+ renderContentTypes(types, counts);
1571
+ } catch {
1572
+ contentTypesBody.innerHTML = '<div class="empty-state">Failed to load.</div>';
1573
+ }
1574
+ }
1575
+
1576
+ function renderContentTypes(types, counts) {
1577
+ if (!types || types.length === 0) {
1578
+ contentTypesBody.innerHTML = '<div class="empty-state">No content types.</div>';
1579
+ return;
1580
+ }
1581
+
1582
+ var html = '';
1583
+ for (var i = 0; i < types.length; i++) {
1584
+ var t = types[i];
1585
+ var count = counts[t.name] || '';
1586
+ var countLabel = count ? count + '' : '';
1587
+ var desc = t.description || '';
1588
+ var fields = (t.fields || []).slice(0, 6);
1589
+
1590
+ html += '<div class="ctype-item" onclick="chatContentType(\'' + esc(t.plural) + '\')">'
1591
+ + '<div class="ctype-header">'
1592
+ + '<span class="ctype-name">' + esc(t.plural) + '</span>'
1593
+ + (countLabel ? '<span class="ctype-count">' + esc(countLabel) + '</span>' : '')
1594
+ + '</div>';
1595
+
1596
+ if (desc) {
1597
+ html += '<div class="ctype-desc">' + esc(desc) + '</div>';
1598
+ }
1599
+
1600
+ if (fields.length > 0) {
1601
+ html += '<div class="ctype-fields">';
1602
+ for (var j = 0; j < fields.length; j++) {
1603
+ html += '<span class="ctype-field">' + esc(fields[j].key) + '</span>';
1604
+ }
1605
+ if (t.fields.length > 6) {
1606
+ html += '<span class="ctype-field">+' + (t.fields.length - 6) + '</span>';
1607
+ }
1608
+ html += '</div>';
1609
+ }
1610
+
1611
+ html += '</div>';
1612
+ }
1613
+
1614
+ contentTypesBody.innerHTML = html;
1615
+ }
1616
+
1617
+ window.chatContentType = function(plural) {
1618
+ var msg = 'show me the content for ' + plural;
1619
+ chatInput.value = msg;
1620
+ chatForm.dispatchEvent(new Event('submit'));
1621
+ };
1622
+
1304
1623
  async function fetchStatus() {
1305
1624
  try {
1306
1625
  const res = await fetch('/api/status');
@@ -2036,7 +2355,9 @@ async function markTaskDone(checkEl) {
2036
2355
  checkOnboarding().then(function(needsOnboarding) {
2037
2356
  if (!needsOnboarding) {
2038
2357
  fetchCalendarNames().then(function() { fetchTimeline(); });
2358
+ fetchContentTypes();
2039
2359
  fetchScheduler();
2360
+ fetchInsights();
2040
2361
  connectWs();
2041
2362
  } else {
2042
2363
  // Still connect WS and fetch basic status even during onboarding
@@ -2045,9 +2366,10 @@ async function markTaskDone(checkEl) {
2045
2366
  fetchStatus();
2046
2367
  });
2047
2368
 
2048
- // Poll status and scheduler every 30s
2369
+ // Poll status, scheduler, and insights periodically
2049
2370
  setInterval(fetchStatus, 30000);
2050
2371
  setInterval(fetchScheduler, 30000);
2372
+ setInterval(fetchInsights, 120000); // every 2 minutes
2051
2373
  })();
2052
2374
  </script>
2053
2375
  </body>
@@ -1 +1 @@
1
- {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/service/web-server.ts"],"names":[],"mappings":"AAyBA,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,WAAW,CAAc;IAE3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB3B,4DAA4D;IAC5D,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI;YAY9B,UAAU;IAgJxB,OAAO,CAAC,QAAQ;YAqDF,UAAU;YA0DV,aAAa;YAwBb,SAAS;YAmCT,SAAS;YAuDT,YAAY;YAqBZ,WAAW;IA2JzB,OAAO,CAAC,QAAQ;CAQnB"}
1
+ {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../../src/service/web-server.ts"],"names":[],"mappings":"AAyBA,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,WAAW,CAAc;IAE3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB3B,4DAA4D;IAC5D,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI;YAY9B,UAAU;IAiJxB,OAAO,CAAC,QAAQ;YAqDF,UAAU;YA0DV,aAAa;YAwBb,SAAS;YAmCT,SAAS;YAuDT,YAAY;YAqBZ,WAAW;IA2JzB,OAAO,CAAC,QAAQ;CAQnB"}
@@ -88,6 +88,7 @@ export class WebServer {
88
88
  url.pathname.startsWith('/api/entities') ||
89
89
  url.pathname.startsWith('/api/search') ||
90
90
  url.pathname.startsWith('/api/processes') ||
91
+ url.pathname.startsWith('/api/insights') ||
91
92
  url.pathname === '/api/calendar') {
92
93
  const handled = await handleApiRoute(req, res, url);
93
94
  if (handled)