aura-code 0.3.7 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/dist/agent/compactor.d.ts +26 -0
- package/dist/agent/compactor.js +156 -0
- package/dist/agent/compactor.js.map +1 -0
- package/dist/agent/context.d.ts +4 -0
- package/dist/agent/context.js +38 -0
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/design.d.ts +17 -0
- package/dist/agent/design.js +175 -0
- package/dist/agent/design.js.map +1 -0
- package/dist/agent/loop.js +24 -1
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -3
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/cli/display.js +46 -5
- package/dist/cli/display.js.map +1 -1
- package/dist/cli/index.js +323 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/config/defaults.js +8 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/project-config.d.ts +6 -0
- package/dist/config/project-config.js +2 -0
- package/dist/config/project-config.js.map +1 -1
- package/dist/dream/dream.d.ts +71 -0
- package/dist/dream/dream.js +217 -0
- package/dist/dream/dream.js.map +1 -0
- package/dist/dream/okf.d.ts +10 -0
- package/dist/dream/okf.js +280 -0
- package/dist/dream/okf.js.map +1 -0
- package/dist/dream/parser.d.ts +60 -0
- package/dist/dream/parser.js +176 -0
- package/dist/dream/parser.js.map +1 -0
- package/dist/dream/reconcile.d.ts +69 -0
- package/dist/dream/reconcile.js +303 -0
- package/dist/dream/reconcile.js.map +1 -0
- package/dist/kanban/index.d.ts +5 -0
- package/dist/kanban/index.js +11 -0
- package/dist/kanban/index.js.map +1 -0
- package/dist/kanban/pipeline.d.ts +16 -0
- package/dist/kanban/pipeline.js +910 -0
- package/dist/kanban/pipeline.js.map +1 -0
- package/dist/kanban/server.d.ts +5 -0
- package/dist/kanban/server.js +820 -0
- package/dist/kanban/server.js.map +1 -0
- package/dist/kanban/types.d.ts +75 -0
- package/dist/kanban/types.js +6 -0
- package/dist/kanban/types.js.map +1 -0
- package/dist/learnlight/driven.d.ts +2 -0
- package/dist/learnlight/driven.js +109 -0
- package/dist/learnlight/driven.js.map +1 -0
- package/dist/learnlight/index.d.ts +4 -0
- package/dist/learnlight/index.js +32 -0
- package/dist/learnlight/index.js.map +1 -0
- package/dist/learnlight/lesson-prep.d.ts +36 -0
- package/dist/learnlight/lesson-prep.js +224 -0
- package/dist/learnlight/lesson-prep.js.map +1 -0
- package/dist/learnlight/report.d.ts +2 -0
- package/dist/learnlight/report.js +37 -0
- package/dist/learnlight/report.js.map +1 -0
- package/dist/learnlight/types.d.ts +72 -0
- package/dist/learnlight/types.js +6 -0
- package/dist/learnlight/types.js.map +1 -0
- package/dist/machina/index.d.ts +16 -0
- package/dist/machina/index.js +61 -0
- package/dist/machina/index.js.map +1 -0
- package/dist/machina/render-html.d.ts +2 -0
- package/dist/machina/render-html.js +189 -0
- package/dist/machina/render-html.js.map +1 -0
- package/dist/machina/render-terminal.d.ts +2 -0
- package/dist/machina/render-terminal.js +92 -0
- package/dist/machina/render-terminal.js.map +1 -0
- package/dist/machina/spec.d.ts +61 -0
- package/dist/machina/spec.js +124 -0
- package/dist/machina/spec.js.map +1 -0
- package/dist/machina/verify.d.ts +27 -0
- package/dist/machina/verify.js +85 -0
- package/dist/machina/verify.js.map +1 -0
- package/dist/perception/graph-store.d.ts +11 -0
- package/dist/perception/graph-store.js +33 -0
- package/dist/perception/graph-store.js.map +1 -1
- package/dist/perception/index.d.ts +1 -1
- package/dist/perception/index.js +2 -1
- package/dist/perception/index.js.map +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +49 -0
- package/dist/providers/factory.js +99 -0
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/google.js +1 -1
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai-compatible.js +81 -50
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/rem/graph.d.ts +72 -0
- package/dist/rem/graph.js +139 -0
- package/dist/rem/graph.js.map +1 -0
- package/dist/rem/index.d.ts +16 -0
- package/dist/rem/index.js +60 -0
- package/dist/rem/index.js.map +1 -0
- package/dist/rem/render-html.d.ts +2 -0
- package/dist/rem/render-html.js +189 -0
- package/dist/rem/render-html.js.map +1 -0
- package/dist/rem/render-terminal.d.ts +4 -0
- package/dist/rem/render-terminal.js +94 -0
- package/dist/rem/render-terminal.js.map +1 -0
- package/dist/research/council.d.ts +27 -0
- package/dist/research/council.js +263 -0
- package/dist/research/council.js.map +1 -0
- package/dist/research/research.d.ts +24 -0
- package/dist/research/research.js +116 -0
- package/dist/research/research.js.map +1 -0
- package/dist/ruby/alternator.d.ts +36 -1
- package/dist/ruby/alternator.js +40 -31
- package/dist/ruby/alternator.js.map +1 -1
- package/dist/ruby/stats.js +10 -11
- package/dist/ruby/stats.js.map +1 -1
- package/dist/ruby/types.js +1 -1
- package/dist/ruby/types.js.map +1 -1
- package/dist/safety/permissions.d.ts +8 -0
- package/dist/safety/permissions.js +12 -0
- package/dist/safety/permissions.js.map +1 -1
- package/dist/setup/provider-registry.js +14 -0
- package/dist/setup/provider-registry.js.map +1 -1
- package/dist/setup/provider-test.js +24 -2
- package/dist/setup/provider-test.js.map +1 -1
- package/dist/setup/telegram-wizard.d.ts +57 -0
- package/dist/setup/telegram-wizard.js +343 -0
- package/dist/setup/telegram-wizard.js.map +1 -0
- package/dist/tools/audio-transcribe.d.ts +20 -0
- package/dist/tools/audio-transcribe.js +39 -28
- package/dist/tools/audio-transcribe.js.map +1 -1
- package/dist/tools/browser.js +8 -10
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/gmail-tool.d.ts +15 -0
- package/dist/tools/gmail-tool.js +361 -0
- package/dist/tools/gmail-tool.js.map +1 -0
- package/dist/tools/read-file.js +13 -0
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/telegram-bot.js +103 -20
- package/dist/tools/telegram-bot.js.map +1 -1
- package/dist/tools/telegram-voice.d.ts +22 -0
- package/dist/tools/telegram-voice.js +203 -0
- package/dist/tools/telegram-voice.js.map +1 -0
- package/dist/tools/telegram.d.ts +6 -1
- package/dist/tools/telegram.js +115 -5
- package/dist/tools/telegram.js.map +1 -1
- package/dist/tools/video-render.d.ts +11 -0
- package/dist/tools/video-render.js +189 -0
- package/dist/tools/video-render.js.map +1 -0
- package/dist/tools/web-search.d.ts +20 -0
- package/dist/tools/web-search.js +134 -38
- package/dist/tools/web-search.js.map +1 -1
- package/dist/util/errors.js +6 -0
- package/dist/util/errors.js.map +1 -1
- package/dist/viz/index.js +816 -30
- package/dist/viz/index.js.map +1 -1
- package/dist/viz/reader.d.ts +33 -0
- package/dist/viz/reader.js +343 -0
- package/dist/viz/reader.js.map +1 -0
- package/package.json +3 -1
package/dist/viz/index.js
CHANGED
|
@@ -151,23 +151,144 @@ function stripPlan(p) {
|
|
|
151
151
|
})),
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
const episode_capture_js_1 = require("../ruby/episode-capture.js");
|
|
155
|
+
const competence_js_1 = require("../ruby/competence.js");
|
|
156
|
+
/**
|
|
157
|
+
* Reads the REAL memory store: `~/.aura/memory/{namespace}.json`, one flat
|
|
158
|
+
* file per namespace, each `{ [key]: { value, updated, created? } }`.
|
|
159
|
+
*
|
|
160
|
+
* This memory store is global, not project-scoped — the `memory` tool never
|
|
161
|
+
* keyed entries by project root. (The previous version of this function read
|
|
162
|
+
* from a per-project-hash subdirectory that nothing in the codebase ever
|
|
163
|
+
* wrote to, so this panel has always returned an empty list.)
|
|
164
|
+
*/
|
|
165
|
+
function loadMemory() {
|
|
166
|
+
const dir = path.join(process.env.HOME ?? '/tmp', '.aura', 'memory');
|
|
158
167
|
if (!fs.existsSync(dir))
|
|
159
168
|
return [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.
|
|
169
|
+
const facts = [];
|
|
170
|
+
for (const file of fs.readdirSync(dir)) {
|
|
171
|
+
if (!file.endsWith('.json') || file.endsWith('.tmp'))
|
|
172
|
+
continue;
|
|
173
|
+
const namespace = file.replace(/\.json$/, '');
|
|
163
174
|
try {
|
|
164
|
-
|
|
175
|
+
const store = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
176
|
+
for (const [key, entry] of Object.entries(store)) {
|
|
177
|
+
if (!entry || typeof entry.value !== 'string')
|
|
178
|
+
continue;
|
|
179
|
+
facts.push({
|
|
180
|
+
key,
|
|
181
|
+
namespace,
|
|
182
|
+
value: entry.value,
|
|
183
|
+
updated: entry.updated,
|
|
184
|
+
// Entries written before `created` existed fall back to `updated`
|
|
185
|
+
// — the earliest timestamp we actually have for them.
|
|
186
|
+
created: entry.created ?? entry.updated,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
165
189
|
}
|
|
166
|
-
catch {
|
|
167
|
-
|
|
190
|
+
catch { /* skip an unreadable/corrupt namespace file */ }
|
|
191
|
+
}
|
|
192
|
+
return facts.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Synchronous episode reader. Mirrors `episodeStore.loadEpisodes()` exactly
|
|
196
|
+
* (same directory via the store's own `projectDir()`, same "never throws,
|
|
197
|
+
* skip bad files" behaviour) but stays sync — `generateDashboard` must
|
|
198
|
+
* remain sync, since existing tests call it without `await`.
|
|
199
|
+
*/
|
|
200
|
+
function loadEpisodesSync(projectRoot) {
|
|
201
|
+
try {
|
|
202
|
+
const dir = episode_capture_js_1.episodeStore.projectDir(projectRoot);
|
|
203
|
+
if (!fs.existsSync(dir))
|
|
204
|
+
return [];
|
|
205
|
+
const episodes = [];
|
|
206
|
+
for (const file of fs.readdirSync(dir)) {
|
|
207
|
+
if (!file.endsWith('.json'))
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
211
|
+
if (parsed && typeof parsed.timestamp === 'number')
|
|
212
|
+
episodes.push(parsed);
|
|
213
|
+
}
|
|
214
|
+
catch { /* skip a corrupt episode file */ }
|
|
168
215
|
}
|
|
169
|
-
|
|
170
|
-
|
|
216
|
+
return episodes.sort((a, b) => a.timestamp - b.timestamp);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Day-bucketed cumulative growth of remembered facts, by first-created date. */
|
|
223
|
+
function buildMemoryGrowth(facts) {
|
|
224
|
+
const byDay = new Map();
|
|
225
|
+
for (const f of facts) {
|
|
226
|
+
const day = (f.created || '').slice(0, 10);
|
|
227
|
+
if (!day)
|
|
228
|
+
continue;
|
|
229
|
+
byDay.set(day, (byDay.get(day) ?? 0) + 1);
|
|
230
|
+
}
|
|
231
|
+
const days = Array.from(byDay.keys()).sort();
|
|
232
|
+
let running = 0;
|
|
233
|
+
return days.map(date => {
|
|
234
|
+
running += byDay.get(date);
|
|
235
|
+
return { date, cumulative: running };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Day-bucketed learning curve: cumulative episode count, plus a rolling
|
|
240
|
+
* Ruby (small-model) success rate over the last `windowSize` attempted
|
|
241
|
+
* episodes up to and including each day. This is the actual "is it getting
|
|
242
|
+
* better at routing to the small model" signal — competence.ts itself
|
|
243
|
+
* doesn't persist a timeline, it recomputes fresh from episodes each call,
|
|
244
|
+
* so this reconstructs the trend from each episode's own timestamp.
|
|
245
|
+
*/
|
|
246
|
+
function buildLearningSeries(episodes, windowSize = 10) {
|
|
247
|
+
const byDay = new Map();
|
|
248
|
+
for (const ep of episodes) {
|
|
249
|
+
const day = new Date(ep.timestamp).toISOString().slice(0, 10);
|
|
250
|
+
const arr = byDay.get(day) ?? [];
|
|
251
|
+
arr.push(ep);
|
|
252
|
+
byDay.set(day, arr);
|
|
253
|
+
}
|
|
254
|
+
const days = Array.from(byDay.keys()).sort();
|
|
255
|
+
let cumulative = 0;
|
|
256
|
+
const seenSoFar = [];
|
|
257
|
+
return days.map(date => {
|
|
258
|
+
const todays = byDay.get(date);
|
|
259
|
+
cumulative += todays.length;
|
|
260
|
+
seenSoFar.push(...todays);
|
|
261
|
+
const attempted = seenSoFar.filter(e => e.rubyAttempted);
|
|
262
|
+
const recent = attempted.slice(-windowSize);
|
|
263
|
+
const rollingSuccessRate = recent.length === 0
|
|
264
|
+
? 0
|
|
265
|
+
: recent.filter(e => e.rubySucceeded).length / recent.length;
|
|
266
|
+
return { date, cumulativeEpisodes: cumulative, rollingSuccessRate };
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Plain-data summary mirroring formatStats()'s definitions, for stat cards. */
|
|
270
|
+
function summariseEpisodes(episodes) {
|
|
271
|
+
const total = episodes.length;
|
|
272
|
+
const completed = episodes.filter(e => e.reviewerApproved).length;
|
|
273
|
+
const avgDurationMs = total === 0
|
|
274
|
+
? 0
|
|
275
|
+
: episodes.reduce((s, e) => s + (e.durationMs ?? 0), 0) / total;
|
|
276
|
+
const totalTokens = episodes.reduce((s, e) => s + (e.tokensUsed?.ruby ?? 0) + (e.tokensUsed?.largeModel ?? 0), 0);
|
|
277
|
+
const rubyAttempted = episodes.filter(e => e.rubyAttempted);
|
|
278
|
+
const rubySuccessRate = rubyAttempted.length === 0
|
|
279
|
+
? 0
|
|
280
|
+
: rubyAttempted.filter(e => e.rubySucceeded).length / rubyAttempted.length;
|
|
281
|
+
const modelCounts = new Map();
|
|
282
|
+
for (const e of episodes) {
|
|
283
|
+
const model = e.largeModelUsed ?? (e.rubySucceeded ? 'ruby (small model)' : undefined);
|
|
284
|
+
if (model)
|
|
285
|
+
modelCounts.set(model, (modelCounts.get(model) ?? 0) + 1);
|
|
286
|
+
}
|
|
287
|
+
const topModels = Array.from(modelCounts.entries())
|
|
288
|
+
.sort((a, b) => b[1] - a[1])
|
|
289
|
+
.slice(0, 6)
|
|
290
|
+
.map(([model, count]) => ({ model, count }));
|
|
291
|
+
return { total, completed, avgDurationMs, totalTokens, rubySuccessRate, topModels };
|
|
171
292
|
}
|
|
172
293
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
294
|
// HTML template
|
|
@@ -182,6 +303,8 @@ function buildHtml(data) {
|
|
|
182
303
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
183
304
|
<title>Aura — Memory Dashboard · ${data.projectName}</title>
|
|
184
305
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
306
|
+
<script src="https://unpkg.com/three@0.147.0/build/three.min.js"></script>
|
|
307
|
+
<script src="https://unpkg.com/three@0.147.0/examples/js/controls/OrbitControls.js"></script>
|
|
185
308
|
<style>
|
|
186
309
|
:root {
|
|
187
310
|
--bg: #0d1117;
|
|
@@ -221,7 +344,11 @@ function buildHtml(data) {
|
|
|
221
344
|
/* Graph panel */
|
|
222
345
|
#graph-svg { background: var(--canvas); border: 1px solid var(--border); border-radius: 8px; flex: 1; min-height: 0; cursor: grab; }
|
|
223
346
|
#graph-svg:active { cursor: grabbing; }
|
|
347
|
+
#graph-3d-wrap { background: var(--canvas); border: 1px solid var(--border); border-radius: 8px; flex: 1; min-height: 0; position: relative; overflow: hidden; display: none; }
|
|
348
|
+
#graph-3d-wrap canvas { display: block; width: 100%; height: 100%; }
|
|
349
|
+
.graph-3d-hint { position: absolute; bottom: 10px; right: 14px; color: var(--dim); font-size: 10px; pointer-events: none; z-index: 2; }
|
|
224
350
|
.graph-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
351
|
+
.layout-toggle { display: flex; gap: 4px; }
|
|
225
352
|
.graph-controls input {
|
|
226
353
|
background: var(--card); border: 1px solid var(--border2); border-radius: 6px;
|
|
227
354
|
color: var(--text); font: inherit; font-size: 12px; padding: 6px 12px; width: 220px; outline: none;
|
|
@@ -288,6 +415,29 @@ function buildHtml(data) {
|
|
|
288
415
|
.memory-table td:first-child { color: var(--primary); white-space: nowrap; font-weight: 600; }
|
|
289
416
|
.memory-table tr:hover td { background: var(--card); }
|
|
290
417
|
.memory-val { max-width: 560px; white-space: pre-wrap; word-break: break-word; color: var(--text); }
|
|
418
|
+
.ns-badge { background: var(--canvas); border: 1px solid var(--border2); border-radius: 10px; color: var(--blue); font-size: 10px; padding: 1px 8px; white-space: nowrap; }
|
|
419
|
+
|
|
420
|
+
/* Line charts (memory growth, learning curve) */
|
|
421
|
+
.chart-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
|
|
422
|
+
.chart-card .lbl { color: var(--muted); font-size: 10px; margin-bottom: 10px; text-transform: uppercase; letter-spacing: .08em; }
|
|
423
|
+
.chart-svg { width: 100%; height: 220px; overflow: visible; }
|
|
424
|
+
.chart-line { fill: none; stroke-width: 2; }
|
|
425
|
+
.chart-area { opacity: .12; }
|
|
426
|
+
.chart-dot { stroke: var(--bg); stroke-width: 1.5; }
|
|
427
|
+
.chart-axis text { fill: var(--dim); font-size: 9px; font-family: inherit; }
|
|
428
|
+
.chart-axis path, .chart-axis line { stroke: var(--border); }
|
|
429
|
+
.chart-empty { align-items: center; color: var(--dim); display: flex; font-size: 11px; height: 220px; justify-content: center; }
|
|
430
|
+
|
|
431
|
+
/* 3D bar charts (true.js cylinders) + their HTML legends */
|
|
432
|
+
.chart-3d-container { height: 260px; position: relative; width: 100%; }
|
|
433
|
+
.chart-3d-container canvas { display: block; height: 100%; width: 100%; }
|
|
434
|
+
.legend-3d { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 10px; }
|
|
435
|
+
.legend-3d-row { align-items: center; display: flex; gap: 7px; }
|
|
436
|
+
.legend-3d-dot { border-radius: 50%; flex-shrink: 0; height: 10px; width: 10px; }
|
|
437
|
+
.legend-3d-name { color: var(--text); font-size: 11px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
438
|
+
.legend-3d-val { color: var(--muted); font-size: 10px; }
|
|
439
|
+
.two-col { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }
|
|
440
|
+
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
|
|
291
441
|
|
|
292
442
|
.empty { color: var(--dim); font-size: 12px; padding: 32px; text-align: center; }
|
|
293
443
|
::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
@@ -305,7 +455,8 @@ function buildHtml(data) {
|
|
|
305
455
|
<button onclick="showPanel('graph',this)">Codebase Graph</button>
|
|
306
456
|
<button onclick="showPanel('sessions',this)">Sessions</button>
|
|
307
457
|
<button onclick="showPanel('plans',this)">Execution Plans</button>
|
|
308
|
-
<button onclick="showPanel('memory',this)">
|
|
458
|
+
<button onclick="showPanel('memory',this)">Memory Growth</button>
|
|
459
|
+
<button onclick="showPanel('learning',this)">Learning</button>
|
|
309
460
|
</nav>
|
|
310
461
|
|
|
311
462
|
<div id="overview" class="panel active"></div>
|
|
@@ -313,6 +464,7 @@ function buildHtml(data) {
|
|
|
313
464
|
<div id="sessions" class="panel"></div>
|
|
314
465
|
<div id="plans" class="panel"></div>
|
|
315
466
|
<div id="memory" class="panel"></div>
|
|
467
|
+
<div id="learning" class="panel"></div>
|
|
316
468
|
|
|
317
469
|
<div class="tooltip" id="tooltip" style="display:none"></div>
|
|
318
470
|
|
|
@@ -326,6 +478,8 @@ function showPanel(id, btn) {
|
|
|
326
478
|
if (btn) btn.classList.add('active');
|
|
327
479
|
if (id === 'graph' && !graphInit) initGraph();
|
|
328
480
|
if (id === 'plans' && !plansInit) initPlans();
|
|
481
|
+
if (id === 'memory' && !memoryInit) initMemoryCharts();
|
|
482
|
+
if (id === 'learning' && !learningInit) initLearningCharts();
|
|
329
483
|
}
|
|
330
484
|
|
|
331
485
|
// ── Overview ─────────────────────────────────────────────────────────────────
|
|
@@ -334,6 +488,7 @@ function showPanel(id, btn) {
|
|
|
334
488
|
const nc = g ? g.nodes.length : 0, ec = g ? g.edges.length : 0;
|
|
335
489
|
const sc = DATA.sessions.length, pc = DATA.plans.length;
|
|
336
490
|
const mc = DATA.memory.length, dc = DATA.plans.filter(p=>p.status==='done').length;
|
|
491
|
+
const epc = DATA.episodeSummary.total;
|
|
337
492
|
const last = sc ? new Date(DATA.sessions[0].updatedAt).toLocaleString()
|
|
338
493
|
: pc ? new Date(DATA.plans[0].created).toLocaleString() : '—';
|
|
339
494
|
|
|
@@ -354,6 +509,7 @@ function showPanel(id, btn) {
|
|
|
354
509
|
<div class="stat-card"><div class="num">\${pc}</div><div class="lbl">Exec Plans</div></div>
|
|
355
510
|
<div class="stat-card"><div class="num">\${dc}</div><div class="lbl">Plans Done</div></div>
|
|
356
511
|
<div class="stat-card"><div class="num">\${mc}</div><div class="lbl">Memory Entries</div></div>
|
|
512
|
+
<div class="stat-card"><div class="num">\${epc}</div><div class="lbl">Episodes Recorded</div></div>
|
|
357
513
|
</div>
|
|
358
514
|
<div class="stat-card" style="max-width:640px">
|
|
359
515
|
<div class="lbl" style="margin-bottom:10px">Codebase Breakdown</div>
|
|
@@ -384,9 +540,12 @@ function initGraph() {
|
|
|
384
540
|
const: '#ffa657',
|
|
385
541
|
type: '#79c0ff',
|
|
386
542
|
enum: '#f85149',
|
|
543
|
+
concept: '#e3b341',
|
|
544
|
+
decision: '#bc8cff',
|
|
545
|
+
constraint: '#56d4dd',
|
|
387
546
|
node: '#8b949e',
|
|
388
547
|
};
|
|
389
|
-
const NODE_R = { file: 13, class: 11, interface: 10, function: 8, const: 7, type: 7, enum: 8, node: 7 };
|
|
548
|
+
const NODE_R = { file: 13, class: 11, interface: 10, function: 8, const: 7, type: 7, enum: 8, concept: 9, decision: 9, constraint: 9, node: 7 };
|
|
390
549
|
|
|
391
550
|
const allTypes = [...new Set(DATA.graph.nodes.map(n => n.type || 'node'))];
|
|
392
551
|
const activeTypes = new Set(allTypes);
|
|
@@ -394,10 +553,17 @@ function initGraph() {
|
|
|
394
553
|
panel.innerHTML = \`
|
|
395
554
|
<div class="graph-controls">
|
|
396
555
|
<input id="graph-search" placeholder="🔍 Search nodes, files…" oninput="filterGraph()">
|
|
556
|
+
<div class="layout-toggle" id="layout-toggle">
|
|
557
|
+
<span class="legend-item on" id="layout-force">Force</span>
|
|
558
|
+
<span class="legend-item" id="layout-radial">Radial</span>
|
|
559
|
+
<span class="legend-item" id="layout-3d-spread">3D Spread</span>
|
|
560
|
+
<span class="legend-item" id="layout-3d-explore">3D Explore</span>
|
|
561
|
+
</div>
|
|
397
562
|
<div class="legend" id="legend"></div>
|
|
398
|
-
<span class="hint">scroll to zoom · drag to pan · drag nodes</span>
|
|
563
|
+
<span class="hint" id="graph-hint">scroll to zoom · drag to pan · drag nodes</span>
|
|
399
564
|
</div>
|
|
400
565
|
<svg id="graph-svg"></svg>
|
|
566
|
+
<div id="graph-3d-wrap"><div class="graph-3d-hint" id="graph-3d-hint"></div></div>
|
|
401
567
|
\`;
|
|
402
568
|
|
|
403
569
|
const legendEl = document.getElementById('legend');
|
|
@@ -461,6 +627,24 @@ function initGraph() {
|
|
|
461
627
|
.force('center', d3.forceCenter(W/2, H/2))
|
|
462
628
|
.force('collision', d3.forceCollide(d => (NODE_R[d.type||'node']||7) + 6));
|
|
463
629
|
|
|
630
|
+
// Ring distance per node type — files innermost (the structural anchors),
|
|
631
|
+
// then class/interface, then everything else outward. Same key set as
|
|
632
|
+
// NODE_COLORS/NODE_R above, so every type already has a sensible color
|
|
633
|
+
// and radius gets a sensible ring too.
|
|
634
|
+
const RADIAL_BY_TYPE = { file: 70, class: 170, interface: 170, concept: 220, decision: 220, constraint: 220, const: 260, type: 260, enum: 260, function: 320, node: 320 };
|
|
635
|
+
const radiusFor = d => RADIAL_BY_TYPE[d.type || 'node'] ?? 320;
|
|
636
|
+
|
|
637
|
+
function applyLayout(mode) {
|
|
638
|
+
if (mode === 'radial') {
|
|
639
|
+
sim.force('center', null);
|
|
640
|
+
sim.force('radial', d3.forceRadial(radiusFor, W / 2, H / 2).strength(0.9));
|
|
641
|
+
} else {
|
|
642
|
+
sim.force('radial', null);
|
|
643
|
+
sim.force('center', d3.forceCenter(W / 2, H / 2));
|
|
644
|
+
}
|
|
645
|
+
sim.alpha(1).restart();
|
|
646
|
+
}
|
|
647
|
+
|
|
464
648
|
const gLinks = g.append('g').selectAll('line').data(edges).enter().append('line')
|
|
465
649
|
.attr('stroke','#484f58').attr('stroke-width', d => {
|
|
466
650
|
const r = d.relation || '';
|
|
@@ -506,6 +690,321 @@ function initGraph() {
|
|
|
506
690
|
gNodes.attr('cx',d=>d.x).attr('cy',d=>d.y);
|
|
507
691
|
gLabels.attr('x',d=>d.x).attr('y',d=>d.y);
|
|
508
692
|
});
|
|
693
|
+
|
|
694
|
+
// ── 3D mode state ──────────────────────────────────────────────────────────
|
|
695
|
+
let three3dCleanup = null; // function to stop the 3D render loop
|
|
696
|
+
let current3dMode = null; // 'spread' | 'explore' | null
|
|
697
|
+
|
|
698
|
+
function destroy3d() {
|
|
699
|
+
if (three3dCleanup) { three3dCleanup(); three3dCleanup = null; }
|
|
700
|
+
current3dMode = null;
|
|
701
|
+
const wrap = document.getElementById('graph-3d-wrap');
|
|
702
|
+
// Remove old canvas but keep the hint div
|
|
703
|
+
const oldCanvas = wrap.querySelector('canvas');
|
|
704
|
+
if (oldCanvas) oldCanvas.remove();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function show2d() {
|
|
708
|
+
destroy3d();
|
|
709
|
+
document.getElementById('graph-svg').style.display = '';
|
|
710
|
+
document.getElementById('graph-3d-wrap').style.display = 'none';
|
|
711
|
+
document.getElementById('graph-hint').textContent = 'scroll to zoom · drag to pan · drag nodes';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function show3d(mode) {
|
|
715
|
+
destroy3d();
|
|
716
|
+
current3dMode = mode;
|
|
717
|
+
document.getElementById('graph-svg').style.display = 'none';
|
|
718
|
+
const wrap = document.getElementById('graph-3d-wrap');
|
|
719
|
+
wrap.style.display = 'block';
|
|
720
|
+
const hintEl = document.getElementById('graph-3d-hint');
|
|
721
|
+
if (mode === 'spread') {
|
|
722
|
+
document.getElementById('graph-hint').textContent = 'auto-rotating 3D layout · scroll to zoom';
|
|
723
|
+
hintEl.textContent = '✦ auto-rotate';
|
|
724
|
+
} else {
|
|
725
|
+
document.getElementById('graph-hint').textContent = 'drag to rotate · scroll to zoom · right-drag to pan';
|
|
726
|
+
hintEl.textContent = '✦ orbit controls';
|
|
727
|
+
}
|
|
728
|
+
init3dGraph(wrap, mode);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function init3dGraph(container, mode) {
|
|
732
|
+
const W = container.clientWidth || 900;
|
|
733
|
+
const H = container.clientHeight || 580;
|
|
734
|
+
|
|
735
|
+
// ── Scene setup ──────────────────────────────────────────────────
|
|
736
|
+
const scene = new THREE.Scene();
|
|
737
|
+
scene.background = new THREE.Color(0x1c2128);
|
|
738
|
+
scene.fog = new THREE.FogExp2(0x1c2128, 0.0008);
|
|
739
|
+
|
|
740
|
+
const camera = new THREE.PerspectiveCamera(55, W / H, 1, 8000);
|
|
741
|
+
camera.position.set(0, 0, 600);
|
|
742
|
+
|
|
743
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
744
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
745
|
+
renderer.setSize(W, H);
|
|
746
|
+
container.appendChild(renderer.domElement);
|
|
747
|
+
|
|
748
|
+
// ── OrbitControls (explore mode) or auto-rotate (spread mode) ─
|
|
749
|
+
let controls = null;
|
|
750
|
+
if (mode === 'explore' && typeof THREE.OrbitControls !== 'undefined') {
|
|
751
|
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
752
|
+
controls.enableDamping = true;
|
|
753
|
+
controls.dampingFactor = 0.08;
|
|
754
|
+
controls.rotateSpeed = 0.6;
|
|
755
|
+
controls.zoomSpeed = 1.2;
|
|
756
|
+
controls.minDistance = 80;
|
|
757
|
+
controls.maxDistance = 3000;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── Zoom via scroll (spread mode — no orbit controls) ────────
|
|
761
|
+
let spreadRadius = 600;
|
|
762
|
+
if (mode === 'spread') {
|
|
763
|
+
renderer.domElement.addEventListener('wheel', e => {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
spreadRadius = Math.max(120, Math.min(3000, spreadRadius + e.deltaY * 0.5));
|
|
766
|
+
}, { passive: false });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── Node data with 3D positions ──────────────────────────────
|
|
770
|
+
const n3 = DATA.graph.nodes.map((n, i) => {
|
|
771
|
+
// Initial sphere around origin, randomised
|
|
772
|
+
const phi = Math.acos(2 * Math.random() - 1);
|
|
773
|
+
const theta = Math.random() * Math.PI * 2;
|
|
774
|
+
const r = 100 + Math.random() * 250;
|
|
775
|
+
return {
|
|
776
|
+
...n,
|
|
777
|
+
x: r * Math.sin(phi) * Math.cos(theta),
|
|
778
|
+
y: r * Math.sin(phi) * Math.sin(theta),
|
|
779
|
+
z: r * Math.cos(phi),
|
|
780
|
+
vx: 0, vy: 0, vz: 0,
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
const idMap = new Map(n3.map((n, i) => [n.id, i]));
|
|
784
|
+
const e3 = DATA.graph.edges.map(e => ({
|
|
785
|
+
si: idMap.get(typeof e.source === 'object' ? e.source.id : e.source) ?? -1,
|
|
786
|
+
ti: idMap.get(typeof e.target === 'object' ? e.target.id : e.target) ?? -1,
|
|
787
|
+
relation: e.relation,
|
|
788
|
+
})).filter(e => e.si >= 0 && e.ti >= 0);
|
|
789
|
+
|
|
790
|
+
// ── 3D force simulation (simple Euler integration) ──────────
|
|
791
|
+
const SIM_ITERS = 200;
|
|
792
|
+
const REPULSION = 800;
|
|
793
|
+
const SPRING_K = 0.04;
|
|
794
|
+
const SPRING_REST = 60;
|
|
795
|
+
const DAMPING = 0.85;
|
|
796
|
+
const CENTER_PULL = 0.001;
|
|
797
|
+
|
|
798
|
+
for (let iter = 0; iter < SIM_ITERS; iter++) {
|
|
799
|
+
// Repulsion (O(n²) — fine for < 2000 nodes)
|
|
800
|
+
for (let i = 0; i < n3.length; i++) {
|
|
801
|
+
for (let j = i + 1; j < n3.length; j++) {
|
|
802
|
+
let dx = n3[i].x - n3[j].x, dy = n3[i].y - n3[j].y, dz = n3[i].z - n3[j].z;
|
|
803
|
+
let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
|
|
804
|
+
let force = REPULSION / (dist * dist);
|
|
805
|
+
let fx = dx / dist * force, fy = dy / dist * force, fz = dz / dist * force;
|
|
806
|
+
n3[i].vx += fx; n3[i].vy += fy; n3[i].vz += fz;
|
|
807
|
+
n3[j].vx -= fx; n3[j].vy -= fy; n3[j].vz -= fz;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// Springs (edges)
|
|
811
|
+
for (const e of e3) {
|
|
812
|
+
const s = n3[e.si], t = n3[e.ti];
|
|
813
|
+
let dx = t.x - s.x, dy = t.y - s.y, dz = t.z - s.z;
|
|
814
|
+
let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
|
|
815
|
+
let force = SPRING_K * (dist - SPRING_REST);
|
|
816
|
+
let fx = dx / dist * force, fy = dy / dist * force, fz = dz / dist * force;
|
|
817
|
+
s.vx += fx; s.vy += fy; s.vz += fz;
|
|
818
|
+
t.vx -= fx; t.vy -= fy; t.vz -= fz;
|
|
819
|
+
}
|
|
820
|
+
// Center pull + damping + integrate
|
|
821
|
+
for (const n of n3) {
|
|
822
|
+
n.vx -= n.x * CENTER_PULL;
|
|
823
|
+
n.vy -= n.y * CENTER_PULL;
|
|
824
|
+
n.vz -= n.z * CENTER_PULL;
|
|
825
|
+
n.vx *= DAMPING; n.vy *= DAMPING; n.vz *= DAMPING;
|
|
826
|
+
n.x += n.vx; n.y += n.vy; n.z += n.vz;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ── Build Three.js objects ───────────────────────────────────
|
|
831
|
+
const NC = {
|
|
832
|
+
file:'#58a6ff', function:'#ff7b72', class:'#d2a8ff', interface:'#3fb950',
|
|
833
|
+
const:'#ffa657', type:'#79c0ff', enum:'#f85149', concept:'#e3b341',
|
|
834
|
+
decision:'#bc8cff', constraint:'#56d4dd', node:'#8b949e', module:'#58a6ff',
|
|
835
|
+
};
|
|
836
|
+
const NR3 = { file:5, class:4.5, interface:4, function:3.5, const:3, type:3, enum:3.5, concept:3.5, decision:3.5, constraint:3.5, node:3, module:4 };
|
|
837
|
+
|
|
838
|
+
// Nodes — glowing spheres
|
|
839
|
+
const spheres = [];
|
|
840
|
+
for (const n of n3) {
|
|
841
|
+
const color = new THREE.Color(NC[n.type||'node'] || '#8b949e');
|
|
842
|
+
const r = NR3[n.type||'node'] || 3;
|
|
843
|
+
const geo = new THREE.SphereGeometry(r, 16, 12);
|
|
844
|
+
const mat = new THREE.MeshBasicMaterial({ color });
|
|
845
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
846
|
+
mesh.position.set(n.x, n.y, n.z);
|
|
847
|
+
mesh.userData = n;
|
|
848
|
+
scene.add(mesh);
|
|
849
|
+
spheres.push(mesh);
|
|
850
|
+
|
|
851
|
+
// Glow ring
|
|
852
|
+
const glowGeo = new THREE.SphereGeometry(r * 1.6, 12, 8);
|
|
853
|
+
const glowMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.08 });
|
|
854
|
+
const glow = new THREE.Mesh(glowGeo, glowMat);
|
|
855
|
+
glow.position.copy(mesh.position);
|
|
856
|
+
scene.add(glow);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Edges — lines
|
|
860
|
+
const edgeLineMat = new THREE.LineBasicMaterial({ color: 0x484f58, transparent: true, opacity: 0.35 });
|
|
861
|
+
for (const e of e3) {
|
|
862
|
+
const s = n3[e.si], t = n3[e.ti];
|
|
863
|
+
const geo = new THREE.BufferGeometry().setFromPoints([
|
|
864
|
+
new THREE.Vector3(s.x, s.y, s.z),
|
|
865
|
+
new THREE.Vector3(t.x, t.y, t.z),
|
|
866
|
+
]);
|
|
867
|
+
scene.add(new THREE.Line(geo, edgeLineMat));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Labels (sprite text) — only for file / class / interface
|
|
871
|
+
const LABEL_TYPES = new Set(['file','class','interface']);
|
|
872
|
+
for (const n of n3) {
|
|
873
|
+
if (!LABEL_TYPES.has(n.type || '')) continue;
|
|
874
|
+
const label = n.label.length > 18 ? n.label.slice(0, 16) + '…' : n.label;
|
|
875
|
+
const canvas2 = document.createElement('canvas');
|
|
876
|
+
const ctx2 = canvas2.getContext('2d');
|
|
877
|
+
canvas2.width = 256; canvas2.height = 48;
|
|
878
|
+
ctx2.fillStyle = 'transparent';
|
|
879
|
+
ctx2.fillRect(0, 0, 256, 48);
|
|
880
|
+
ctx2.font = 'bold 22px monospace';
|
|
881
|
+
ctx2.fillStyle = NC[n.type||'node'] || '#8b949e';
|
|
882
|
+
ctx2.fillText(label, 4, 32);
|
|
883
|
+
const tex = new THREE.CanvasTexture(canvas2);
|
|
884
|
+
tex.minFilter = THREE.LinearFilter;
|
|
885
|
+
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.85, depthWrite: false });
|
|
886
|
+
const sprite = new THREE.Sprite(spriteMat);
|
|
887
|
+
sprite.position.set(n.x + (NR3[n.type||'node']||3) + 6, n.y + 2, n.z);
|
|
888
|
+
sprite.scale.set(48, 9, 1);
|
|
889
|
+
scene.add(sprite);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ── Raycaster for tooltips ──────────────────────────────────
|
|
893
|
+
const raycaster = new THREE.Raycaster();
|
|
894
|
+
raycaster.params.Points = { threshold: 5 };
|
|
895
|
+
const mouse = new THREE.Vector2();
|
|
896
|
+
const tooltip3d = document.getElementById('tooltip');
|
|
897
|
+
let hoveredMesh = null;
|
|
898
|
+
|
|
899
|
+
renderer.domElement.addEventListener('mousemove', e => {
|
|
900
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
901
|
+
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
902
|
+
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
903
|
+
raycaster.setFromCamera(mouse, camera);
|
|
904
|
+
const hits = raycaster.intersectObjects(spheres);
|
|
905
|
+
if (hits.length > 0) {
|
|
906
|
+
const d = hits[0].object.userData;
|
|
907
|
+
tooltip3d.style.display = 'block';
|
|
908
|
+
tooltip3d.innerHTML = \`<strong>\${d.label}</strong><br>
|
|
909
|
+
<span class="t-type">\${d.type||'node'}</span>
|
|
910
|
+
\${d.file ? \`<br><span class="t-file">\${d.file}</span>\` : ''}\`;
|
|
911
|
+
tooltip3d.style.left = (e.clientX + 15) + 'px';
|
|
912
|
+
tooltip3d.style.top = (e.clientY - 8) + 'px';
|
|
913
|
+
if (hoveredMesh !== hits[0].object) {
|
|
914
|
+
if (hoveredMesh) hoveredMesh.scale.setScalar(1);
|
|
915
|
+
hoveredMesh = hits[0].object;
|
|
916
|
+
hoveredMesh.scale.setScalar(1.5);
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
tooltip3d.style.display = 'none';
|
|
920
|
+
if (hoveredMesh) { hoveredMesh.scale.setScalar(1); hoveredMesh = null; }
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// ── Ambient particles for depth perception ──────────────────
|
|
925
|
+
const particleCount = 200;
|
|
926
|
+
const pGeo = new THREE.BufferGeometry();
|
|
927
|
+
const pPos = new Float32Array(particleCount * 3);
|
|
928
|
+
for (let i = 0; i < particleCount; i++) {
|
|
929
|
+
pPos[i*3] = (Math.random() - 0.5) * 1200;
|
|
930
|
+
pPos[i*3+1] = (Math.random() - 0.5) * 1200;
|
|
931
|
+
pPos[i*3+2] = (Math.random() - 0.5) * 1200;
|
|
932
|
+
}
|
|
933
|
+
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
|
|
934
|
+
const pMat = new THREE.PointsMaterial({ color: 0x30363d, size: 1.5, transparent: true, opacity: 0.5 });
|
|
935
|
+
scene.add(new THREE.Points(pGeo, pMat));
|
|
936
|
+
|
|
937
|
+
// ── Animate ─────────────────────────────────────────────────
|
|
938
|
+
let animId = null;
|
|
939
|
+
let autoAngle = 0;
|
|
940
|
+
function animate() {
|
|
941
|
+
animId = requestAnimationFrame(animate);
|
|
942
|
+
|
|
943
|
+
if (mode === 'spread') {
|
|
944
|
+
autoAngle += 0.003;
|
|
945
|
+
// Orbit on a fixed radius. Previously x/y were derived from the live,
|
|
946
|
+
// mutating camera.position.z (read and overwritten in the same frame),
|
|
947
|
+
// which made the camera teleport between frames — the flicker. Driving
|
|
948
|
+
// the orbit from a stable radius keeps it smooth; the wheel adjusts it.
|
|
949
|
+
camera.position.x = Math.sin(autoAngle) * spreadRadius;
|
|
950
|
+
camera.position.z = Math.cos(autoAngle) * spreadRadius;
|
|
951
|
+
camera.position.y = Math.sin(autoAngle * 0.4) * spreadRadius * 0.3;
|
|
952
|
+
camera.lookAt(0, 0, 0);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (controls) controls.update();
|
|
956
|
+
renderer.render(scene, camera);
|
|
957
|
+
}
|
|
958
|
+
animate();
|
|
959
|
+
|
|
960
|
+
// ── Resize handler ──────────────────────────────────────────
|
|
961
|
+
function onResize() {
|
|
962
|
+
const w = container.clientWidth || 900;
|
|
963
|
+
const h = container.clientHeight || 580;
|
|
964
|
+
camera.aspect = w / h;
|
|
965
|
+
camera.updateProjectionMatrix();
|
|
966
|
+
renderer.setSize(w, h);
|
|
967
|
+
}
|
|
968
|
+
window.addEventListener('resize', onResize);
|
|
969
|
+
|
|
970
|
+
// ── Cleanup function ────────────────────────────────────────
|
|
971
|
+
three3dCleanup = () => {
|
|
972
|
+
if (animId) cancelAnimationFrame(animId);
|
|
973
|
+
window.removeEventListener('resize', onResize);
|
|
974
|
+
if (controls) controls.dispose();
|
|
975
|
+
renderer.dispose();
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const forcePill = document.getElementById('layout-force');
|
|
980
|
+
const radialPill = document.getElementById('layout-radial');
|
|
981
|
+
const spread3dPill = document.getElementById('layout-3d-spread');
|
|
982
|
+
const explore3dPill = document.getElementById('layout-3d-explore');
|
|
983
|
+
const allPills = [forcePill, radialPill, spread3dPill, explore3dPill];
|
|
984
|
+
|
|
985
|
+
function activatePill(pill) {
|
|
986
|
+
allPills.forEach(p => p.classList.remove('on'));
|
|
987
|
+
pill.classList.add('on');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
forcePill.onclick = () => {
|
|
991
|
+
activatePill(forcePill);
|
|
992
|
+
show2d();
|
|
993
|
+
applyLayout('force');
|
|
994
|
+
};
|
|
995
|
+
radialPill.onclick = () => {
|
|
996
|
+
activatePill(radialPill);
|
|
997
|
+
show2d();
|
|
998
|
+
applyLayout('radial');
|
|
999
|
+
};
|
|
1000
|
+
spread3dPill.onclick = () => {
|
|
1001
|
+
activatePill(spread3dPill);
|
|
1002
|
+
show3d('spread');
|
|
1003
|
+
};
|
|
1004
|
+
explore3dPill.onclick = () => {
|
|
1005
|
+
activatePill(explore3dPill);
|
|
1006
|
+
show3d('explore');
|
|
1007
|
+
};
|
|
509
1008
|
}
|
|
510
1009
|
|
|
511
1010
|
// ── Sessions ──────────────────────────────────────────────────────────────────
|
|
@@ -665,30 +1164,308 @@ function renderDag(plan) {
|
|
|
665
1164
|
});
|
|
666
1165
|
}
|
|
667
1166
|
|
|
668
|
-
// ──
|
|
1167
|
+
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
1168
|
+
function fmtDuration(ms) {
|
|
1169
|
+
if (!ms || ms < 0) return '—';
|
|
1170
|
+
const s = Math.round(ms / 1000);
|
|
1171
|
+
if (s < 60) return s + 's';
|
|
1172
|
+
return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
|
|
1173
|
+
}
|
|
1174
|
+
function fmtTokens(n) {
|
|
1175
|
+
if (!n) return '0';
|
|
1176
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
1177
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
1178
|
+
return String(n);
|
|
1179
|
+
}
|
|
1180
|
+
function fmtPct(r) { return Math.round(r * 100) + '%'; }
|
|
1181
|
+
|
|
1182
|
+
/** Minimal D3 line+area chart. data is an array of day-bucketed points. */
|
|
1183
|
+
function drawLineChart(svg, data, xKey, yKey, color, fmt) {
|
|
1184
|
+
fmt = fmt || (v => v);
|
|
1185
|
+
const W = svg.clientWidth || 600, H = 220;
|
|
1186
|
+
const margin = { top: 10, right: 16, bottom: 24, left: 40 };
|
|
1187
|
+
const x = d3.scaleTime()
|
|
1188
|
+
.domain(d3.extent(data, d => new Date(d[xKey])))
|
|
1189
|
+
.range([margin.left, W - margin.right]);
|
|
1190
|
+
const yMax = Math.max(d3.max(data, d => d[yKey]) || 1, 0.0001);
|
|
1191
|
+
const y = d3.scaleLinear().domain([0, yMax * 1.15]).range([H - margin.bottom, margin.top]);
|
|
1192
|
+
|
|
1193
|
+
const s = d3.select(svg).attr('viewBox', \`0 0 \${W} \${H}\`);
|
|
1194
|
+
|
|
1195
|
+
s.append('g').attr('class', 'chart-axis')
|
|
1196
|
+
.attr('transform', \`translate(0,\${H - margin.bottom})\`)
|
|
1197
|
+
.call(d3.axisBottom(x).ticks(Math.min(6, data.length)).tickFormat(d3.timeFormat('%b %d')));
|
|
1198
|
+
s.append('g').attr('class', 'chart-axis')
|
|
1199
|
+
.attr('transform', \`translate(\${margin.left},0)\`)
|
|
1200
|
+
.call(d3.axisLeft(y).ticks(4).tickFormat(fmt));
|
|
1201
|
+
|
|
1202
|
+
const line = d3.line().x(d => x(new Date(d[xKey]))).y(d => y(d[yKey])).curve(d3.curveMonotoneX);
|
|
1203
|
+
const area = d3.area().x(d => x(new Date(d[xKey]))).y0(H - margin.bottom).y1(d => y(d[yKey])).curve(d3.curveMonotoneX);
|
|
1204
|
+
|
|
1205
|
+
s.append('path').datum(data).attr('class', 'chart-area').attr('fill', color).attr('d', area);
|
|
1206
|
+
s.append('path').datum(data).attr('class', 'chart-line').attr('stroke', color).attr('d', line);
|
|
1207
|
+
|
|
1208
|
+
s.selectAll('.chart-dot').data(data).join('circle')
|
|
1209
|
+
.attr('class', 'chart-dot')
|
|
1210
|
+
.attr('cx', d => x(new Date(d[xKey]))).attr('cy', d => y(d[yKey])).attr('r', 3.5).attr('fill', color)
|
|
1211
|
+
.on('mouseover', (e, d) => {
|
|
1212
|
+
tooltip.style.display = 'block';
|
|
1213
|
+
tooltip.innerHTML = \`<strong>\${new Date(d[xKey]).toLocaleDateString()}</strong><br>\${fmt(d[yKey])}\`;
|
|
1214
|
+
})
|
|
1215
|
+
.on('mousemove', e => { tooltip.style.left = (e.clientX + 15) + 'px'; tooltip.style.top = (e.clientY - 8) + 'px'; })
|
|
1216
|
+
.on('mouseout', () => tooltip.style.display = 'none');
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ── 3D bar chart colors (shared between THREE materials and HTML legend swatches —
|
|
1220
|
+
// same string feeds both, so the legend always matches what's on screen) ────
|
|
1221
|
+
function successRateColor(rate) {
|
|
1222
|
+
const hue = Math.round(rate * 120); // 0=red (bad) → 120=green (good)
|
|
1223
|
+
return 'hsl(' + hue + ',65%,50%)';
|
|
1224
|
+
}
|
|
1225
|
+
const MODEL_PALETTE = ['#f0883e', '#58a6ff', '#bc8cff', '#3fb950', '#d29922', '#f85149'];
|
|
1226
|
+
|
|
1227
|
+
function legendRow(label, value, swatch) {
|
|
1228
|
+
return \`<div class="legend-3d-row">
|
|
1229
|
+
<span class="legend-3d-dot" style="background:\${swatch}"></span>
|
|
1230
|
+
<span class="legend-3d-name">\${String(label).replace(/</g,'<')}</span>
|
|
1231
|
+
<span class="legend-3d-val">\${value}</span>
|
|
1232
|
+
</div>\`;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* True rotatable 3D bar chart (cylinders) via three.js. Drag horizontally to
|
|
1237
|
+
* orbit; auto-rotates slowly when idle. Hover a bar for its value via
|
|
1238
|
+
* raycasting into the existing global tooltip div.
|
|
1239
|
+
*
|
|
1240
|
+
* 'items': [{ label, value, color }] — color is any CSS color string, used
|
|
1241
|
+
* directly as both the THREE.js material color and (by the caller) the
|
|
1242
|
+
* legend swatch, so the two can never drift out of sync.
|
|
1243
|
+
*
|
|
1244
|
+
* Must only be called once its canvas is actually visible — a hidden
|
|
1245
|
+
* (display:none) container has zero width, which would size the renderer
|
|
1246
|
+
* wrong. That's why this is invoked from the lazy init*Charts() functions
|
|
1247
|
+
* below, the same way the codebase graph defers initGraph() until shown.
|
|
1248
|
+
*/
|
|
1249
|
+
function init3DBarChart(canvas, items) {
|
|
1250
|
+
if (!canvas || !items.length) return;
|
|
1251
|
+
const container = canvas.parentElement;
|
|
1252
|
+
const W = container.clientWidth || 480, H = 260;
|
|
1253
|
+
canvas.width = W; canvas.height = H;
|
|
1254
|
+
|
|
1255
|
+
const scene = new THREE.Scene();
|
|
1256
|
+
const camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 100);
|
|
1257
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
1258
|
+
renderer.setSize(W, H, false);
|
|
1259
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
1260
|
+
|
|
1261
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
|
|
1262
|
+
const key = new THREE.DirectionalLight(0xffffff, 0.95);
|
|
1263
|
+
key.position.set(4, 8, 6);
|
|
1264
|
+
scene.add(key);
|
|
1265
|
+
const rim = new THREE.DirectionalLight(0x88aaff, 0.3);
|
|
1266
|
+
rim.position.set(-4, 2, -4);
|
|
1267
|
+
scene.add(rim);
|
|
1268
|
+
|
|
1269
|
+
const group = new THREE.Group();
|
|
1270
|
+
scene.add(group);
|
|
1271
|
+
|
|
1272
|
+
const maxVal = Math.max(1, ...items.map(it => it.value));
|
|
1273
|
+
const n = items.length;
|
|
1274
|
+
const spacing = 1.7;
|
|
1275
|
+
const totalWidth = (n - 1) * spacing;
|
|
1276
|
+
const meshes = [];
|
|
1277
|
+
|
|
1278
|
+
items.forEach((it, i) => {
|
|
1279
|
+
const h = Math.max(0.2, (it.value / maxVal) * 3.2);
|
|
1280
|
+
const geo = new THREE.CylinderGeometry(0.55, 0.62, h, 28);
|
|
1281
|
+
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(it.color), roughness: 0.45, metalness: 0.15 });
|
|
1282
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
1283
|
+
mesh.position.set(i * spacing - totalWidth / 2, h / 2, 0);
|
|
1284
|
+
mesh.userData = { label: it.label, value: it.value };
|
|
1285
|
+
group.add(mesh);
|
|
1286
|
+
meshes.push(mesh);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
const ground = new THREE.Mesh(
|
|
1290
|
+
new THREE.CircleGeometry(totalWidth / 2 + 1.5, 48),
|
|
1291
|
+
new THREE.MeshStandardMaterial({ color: 0x161b22, roughness: 0.95 }),
|
|
1292
|
+
);
|
|
1293
|
+
ground.rotation.x = -Math.PI / 2;
|
|
1294
|
+
group.add(ground);
|
|
1295
|
+
|
|
1296
|
+
camera.position.set(0, 3.6, totalWidth + 5.5);
|
|
1297
|
+
camera.lookAt(0, 1, 0);
|
|
1298
|
+
|
|
1299
|
+
let rotY = 0, dragging = false, lastX = 0, autoRotate = true;
|
|
1300
|
+
canvas.style.cursor = 'grab';
|
|
1301
|
+
|
|
1302
|
+
canvas.addEventListener('pointerdown', e => {
|
|
1303
|
+
dragging = true; autoRotate = false; lastX = e.clientX; canvas.style.cursor = 'grabbing';
|
|
1304
|
+
});
|
|
1305
|
+
window.addEventListener('pointerup', () => { dragging = false; canvas.style.cursor = 'grab'; });
|
|
1306
|
+
window.addEventListener('pointermove', e => {
|
|
1307
|
+
if (dragging) { rotY += (e.clientX - lastX) * 0.008; lastX = e.clientX; }
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
const raycaster = new THREE.Raycaster();
|
|
1311
|
+
const ndc = new THREE.Vector2();
|
|
1312
|
+
canvas.addEventListener('pointermove', e => {
|
|
1313
|
+
const rect = canvas.getBoundingClientRect();
|
|
1314
|
+
ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
1315
|
+
ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
1316
|
+
raycaster.setFromCamera(ndc, camera);
|
|
1317
|
+
const hit = raycaster.intersectObjects(meshes)[0];
|
|
1318
|
+
if (hit) {
|
|
1319
|
+
tooltip.style.display = 'block';
|
|
1320
|
+
tooltip.innerHTML = \`<strong>\${hit.object.userData.label}</strong><br>\${hit.object.userData.value}\`;
|
|
1321
|
+
tooltip.style.left = (e.clientX + 15) + 'px';
|
|
1322
|
+
tooltip.style.top = (e.clientY - 8) + 'px';
|
|
1323
|
+
} else {
|
|
1324
|
+
tooltip.style.display = 'none';
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
canvas.addEventListener('pointerleave', () => { tooltip.style.display = 'none'; });
|
|
1328
|
+
|
|
1329
|
+
(function tick() {
|
|
1330
|
+
if (autoRotate) rotY += 0.0025;
|
|
1331
|
+
group.rotation.y = rotY;
|
|
1332
|
+
renderer.render(scene, camera);
|
|
1333
|
+
requestAnimationFrame(tick);
|
|
1334
|
+
})();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// ── Memory Growth ────────────────────────────────────────────────────────────
|
|
1338
|
+
let memoryInit = false;
|
|
669
1339
|
(function() {
|
|
670
1340
|
const panel = document.getElementById('memory');
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
1341
|
+
const facts = DATA.memory;
|
|
1342
|
+
const growth = DATA.memoryGrowth;
|
|
1343
|
+
const namespaces = new Set(facts.map(f => f.namespace)).size;
|
|
1344
|
+
const oldest = facts.length ? new Date(facts[0].created).toLocaleDateString() : '—';
|
|
1345
|
+
const newest = facts.length ? new Date(facts[facts.length-1].created).toLocaleDateString() : '—';
|
|
1346
|
+
|
|
1347
|
+
const rows = [...facts].reverse().map(f => {
|
|
1348
|
+
const val = f.value || '';
|
|
678
1349
|
return \`<tr>
|
|
679
|
-
<td>\${
|
|
680
|
-
<td
|
|
681
|
-
<td style="color:var(--dim);white-space:nowrap">\${
|
|
1350
|
+
<td>\${f.key.replace(/</g,'<')}</td>
|
|
1351
|
+
<td><span class="ns-badge">\${f.namespace.replace(/</g,'<')}</span></td>
|
|
1352
|
+
<td style="color:var(--dim);white-space:nowrap">\${new Date(f.created).toLocaleString()}</td>
|
|
1353
|
+
<td style="color:var(--dim);white-space:nowrap">\${new Date(f.updated).toLocaleString()}</td>
|
|
682
1354
|
<td class="memory-val">\${val.slice(0,400).replace(/</g,'<')}\${val.length>400?'…':''}</td>
|
|
683
1355
|
</tr>\`;
|
|
684
1356
|
}).join('');
|
|
1357
|
+
|
|
685
1358
|
panel.innerHTML = \`
|
|
686
|
-
<
|
|
687
|
-
<
|
|
1359
|
+
<div class="stats-grid">
|
|
1360
|
+
<div class="stat-card"><div class="num">\${facts.length}</div><div class="lbl">Facts Remembered</div></div>
|
|
1361
|
+
<div class="stat-card"><div class="num">\${namespaces}</div><div class="lbl">Namespaces</div></div>
|
|
1362
|
+
<div class="stat-card"><div class="num" style="font-size:18px">\${oldest}</div><div class="lbl">First Remembered</div></div>
|
|
1363
|
+
<div class="stat-card"><div class="num" style="font-size:18px">\${newest}</div><div class="lbl">Most Recent</div></div>
|
|
1364
|
+
</div>
|
|
1365
|
+
<div class="chart-card">
|
|
1366
|
+
<div class="lbl">Cumulative facts remembered, over time</div>
|
|
1367
|
+
\${growth.length ? '<svg class="chart-svg" id="mem-growth-svg"></svg>' : '<div class="chart-empty">No memory yet — facts appear here as Aura remembers things.</div>'}
|
|
1368
|
+
</div>
|
|
1369
|
+
\${facts.length ? \`<table class="memory-table">
|
|
1370
|
+
<thead><tr><th>Key</th><th>Namespace</th><th>Created</th><th>Updated</th><th>Value</th></tr></thead>
|
|
688
1371
|
<tbody>\${rows}</tbody>
|
|
689
|
-
</table>
|
|
1372
|
+
</table>\` : '<div class="empty">No memory entries found.</div>'}
|
|
690
1373
|
\`;
|
|
691
1374
|
})();
|
|
1375
|
+
|
|
1376
|
+
/** Draws the memory-growth line chart. Called once, the first time the
|
|
1377
|
+
* Memory Growth tab is actually shown (see showPanel above) — doing it at
|
|
1378
|
+
* page-load time would measure a hidden, zero-width panel. */
|
|
1379
|
+
function initMemoryCharts() {
|
|
1380
|
+
memoryInit = true;
|
|
1381
|
+
const svg = document.getElementById('mem-growth-svg');
|
|
1382
|
+
if (svg && DATA.memoryGrowth.length) {
|
|
1383
|
+
drawLineChart(svg, DATA.memoryGrowth, 'date', 'cumulative', 'var(--blue)', v => Math.round(v));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// ── Learning ─────────────────────────────────────────────────────────────────
|
|
1388
|
+
let learningInit = false;
|
|
1389
|
+
(function() {
|
|
1390
|
+
const panel = document.getElementById('learning');
|
|
1391
|
+
const sum = DATA.episodeSummary;
|
|
1392
|
+
const series = DATA.learningSeries;
|
|
1393
|
+
const report = DATA.competenceReport;
|
|
1394
|
+
|
|
1395
|
+
if (sum.total === 0) {
|
|
1396
|
+
panel.innerHTML = '<div class="empty">No episodes recorded yet — this fills in as Aura completes tasks with self-improvement enabled.</div>';
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const completePct = sum.total === 0 ? 0 : sum.completed / sum.total;
|
|
1401
|
+
|
|
1402
|
+
const catLegend = report.length
|
|
1403
|
+
? report.map(r => legendRow(r.category, \`\${r.count} · \${fmtPct(r.successRate)}\`, successRateColor(r.successRate))).join('')
|
|
1404
|
+
: '<div class="empty" style="padding:14px">No Ruby-attempted episodes yet.</div>';
|
|
1405
|
+
|
|
1406
|
+
const modelLegend = sum.topModels.length
|
|
1407
|
+
? sum.topModels.map((m, i) => legendRow(m.model, String(m.count), MODEL_PALETTE[i % MODEL_PALETTE.length])).join('')
|
|
1408
|
+
: '<div class="empty" style="padding:14px">No model usage recorded yet.</div>';
|
|
1409
|
+
|
|
1410
|
+
panel.innerHTML = \`
|
|
1411
|
+
<div class="stats-grid">
|
|
1412
|
+
<div class="stat-card"><div class="num">\${sum.total}</div><div class="lbl">Episodes Recorded</div></div>
|
|
1413
|
+
<div class="stat-card"><div class="num">\${fmtPct(completePct)}</div><div class="lbl">Tasks Completed</div></div>
|
|
1414
|
+
<div class="stat-card"><div class="num">\${fmtPct(sum.rubySuccessRate)}</div><div class="lbl">Ruby Success Rate</div></div>
|
|
1415
|
+
<div class="stat-card"><div class="num" style="font-size:20px">\${fmtDuration(sum.avgDurationMs)}</div><div class="lbl">Avg Duration</div></div>
|
|
1416
|
+
<div class="stat-card"><div class="num" style="font-size:20px">\${fmtTokens(sum.totalTokens)}</div><div class="lbl">Total Tokens</div></div>
|
|
1417
|
+
</div>
|
|
1418
|
+
<div class="two-col">
|
|
1419
|
+
<div class="chart-card">
|
|
1420
|
+
<div class="lbl">Cumulative episodes, over time</div>
|
|
1421
|
+
\${series.length ? '<svg class="chart-svg" id="ep-growth-svg"></svg>' : '<div class="chart-empty">Not enough data yet.</div>'}
|
|
1422
|
+
</div>
|
|
1423
|
+
<div class="chart-card">
|
|
1424
|
+
<div class="lbl">Ruby (small-model) success rate — rolling, last 10 attempts</div>
|
|
1425
|
+
\${series.length ? '<svg class="chart-svg" id="ep-success-svg"></svg>' : '<div class="chart-empty">Not enough data yet.</div>'}
|
|
1426
|
+
</div>
|
|
1427
|
+
</div>
|
|
1428
|
+
<div class="two-col">
|
|
1429
|
+
<div class="chart-card">
|
|
1430
|
+
<div class="lbl">By task category — height = volume, color = Ruby success rate. Drag to rotate.</div>
|
|
1431
|
+
<div class="chart-3d-container"><canvas id="cat-3d-canvas"></canvas></div>
|
|
1432
|
+
<div class="legend-3d">\${catLegend}</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
<div class="chart-card">
|
|
1435
|
+
<div class="lbl">By model used — height = task count. Drag to rotate.</div>
|
|
1436
|
+
<div class="chart-3d-container"><canvas id="model-3d-canvas"></canvas></div>
|
|
1437
|
+
<div class="legend-3d">\${modelLegend}</div>
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
\`;
|
|
1441
|
+
})();
|
|
1442
|
+
|
|
1443
|
+
/** Draws the two D3 line charts and the two three.js 3D bar charts for the
|
|
1444
|
+
* Learning tab. Lazy — see initMemoryCharts() for why. */
|
|
1445
|
+
function initLearningCharts() {
|
|
1446
|
+
learningInit = true;
|
|
1447
|
+
const series = DATA.learningSeries;
|
|
1448
|
+
const report = DATA.competenceReport;
|
|
1449
|
+
const sum = DATA.episodeSummary;
|
|
1450
|
+
|
|
1451
|
+
const growthSvg = document.getElementById('ep-growth-svg');
|
|
1452
|
+
if (growthSvg && series.length) {
|
|
1453
|
+
drawLineChart(growthSvg, series, 'date', 'cumulativeEpisodes', 'var(--primary)', v => Math.round(v));
|
|
1454
|
+
}
|
|
1455
|
+
const successSvg = document.getElementById('ep-success-svg');
|
|
1456
|
+
if (successSvg && series.length) {
|
|
1457
|
+
drawLineChart(successSvg, series, 'date', 'rollingSuccessRate', 'var(--success)', v => fmtPct(v));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
init3DBarChart(
|
|
1461
|
+
document.getElementById('cat-3d-canvas'),
|
|
1462
|
+
report.map(r => ({ label: r.category, value: r.count, color: successRateColor(r.successRate) })),
|
|
1463
|
+
);
|
|
1464
|
+
init3DBarChart(
|
|
1465
|
+
document.getElementById('model-3d-canvas'),
|
|
1466
|
+
sum.topModels.map((m, i) => ({ label: m.model, value: m.count, color: MODEL_PALETTE[i % MODEL_PALETTE.length] })),
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
692
1469
|
</script>
|
|
693
1470
|
</body>
|
|
694
1471
|
</html>`;
|
|
@@ -700,7 +1477,12 @@ function generateDashboard(projectRoot) {
|
|
|
700
1477
|
const graph = loadGraph(projectRoot);
|
|
701
1478
|
const plans = loadPlans(projectRoot).map(stripPlan);
|
|
702
1479
|
const sessions = loadSessions(projectRoot).map(stripSession);
|
|
703
|
-
const memory = loadMemory(
|
|
1480
|
+
const memory = loadMemory();
|
|
1481
|
+
const memoryGrowth = buildMemoryGrowth(memory);
|
|
1482
|
+
const episodes = loadEpisodesSync(projectRoot);
|
|
1483
|
+
const learningSeries = buildLearningSeries(episodes);
|
|
1484
|
+
const episodeSummary = summariseEpisodes(episodes);
|
|
1485
|
+
const competenceReport = (0, competence_js_1.getCompetenceReport)(episodes);
|
|
704
1486
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
705
1487
|
let projectName = path.basename(projectRoot);
|
|
706
1488
|
try {
|
|
@@ -713,6 +1495,10 @@ function generateDashboard(projectRoot) {
|
|
|
713
1495
|
plans,
|
|
714
1496
|
sessions,
|
|
715
1497
|
memory,
|
|
1498
|
+
memoryGrowth,
|
|
1499
|
+
episodeSummary,
|
|
1500
|
+
learningSeries,
|
|
1501
|
+
competenceReport,
|
|
716
1502
|
projectName,
|
|
717
1503
|
generatedAt: new Date().toLocaleString(),
|
|
718
1504
|
});
|