aimeat 1.28.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/en.json +71 -2
- package/dist/locales/fi.json +71 -2
- package/dist/public/css/views/agents-detail.css +11 -0
- package/dist/public/css/views/inbox.css +62 -1
- package/dist/public/css/views/profile.css +87 -10
- package/dist/public/js/services/memory.js +62 -4
- package/dist/public/js/services/messages.js +4 -3
- package/dist/public/js/services/organisms.js +49 -3
- package/dist/public/js/services/workflows.js +4 -3
- package/dist/public/llms-template.txt +38 -1
- package/dist/public/spa.html +3 -0
- package/dist/public/views/command-palette.js +145 -0
- package/dist/public/views/profile/agents-tab.js +1 -0
- package/dist/public/views/profile/inbox-tab.js +175 -28
- package/dist/public/views/profile/knowledge-tab.js +6 -9
- package/dist/public/views/profile/landing-page.js +74 -29
- package/dist/public/views/profile/memory-tab.js +251 -26
- package/dist/public/views/profile/organisms/agents.js +6 -11
- package/dist/public/views/profile/organisms/home.js +1 -1
- package/dist/public/views/profile/organisms/members.js +9 -5
- package/dist/public/views/profile/organisms/mindmap.js +157 -59
- package/dist/public/views/profile/organisms/panels.js +36 -19
- package/dist/public/views/profile/organisms/timeline-panel.js +30 -1
- package/dist/public/views/profile/organisms/workspace-comments.js +15 -5
- package/dist/public/views/profile/organisms/workspace-list.js +35 -23
- package/dist/public/views/profile/organisms/workspace.js +166 -31
- package/dist/public/views/profile/organisms-tab.js +78 -5
- package/dist/public/views/profile/workflows-tab.js +6 -6
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.js +28 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +13 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +62 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/index.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +527 -12
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +4 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +1 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +30 -1
- package/dist/src/generated/prisma-postgres/index.js +4 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +1 -0
- package/dist/src/generated/prisma-postgres/wasm.js +4 -3
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +3 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +33 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +6 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/dm-messages.d.ts +20 -0
- package/dist/src/mcp/dm-messages.d.ts.map +1 -0
- package/dist/src/mcp/dm-messages.js +120 -0
- package/dist/src/mcp/dm-messages.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +3 -1
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/models/message-schemas.d.ts +3 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +4 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +13 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +2 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/bootstrap.js +1 -1
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/catalogue.d.ts.map +1 -1
- package/dist/src/routes/catalogue.js +17 -8
- package/dist/src/routes/catalogue.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +1 -0
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/memory.d.ts +3 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +242 -14
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +22 -19
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +321 -3
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +2 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +93 -104
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +6 -0
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/usage.d.ts +17 -0
- package/dist/src/routes/usage.d.ts.map +1 -0
- package/dist/src/routes/usage.js +35 -0
- package/dist/src/routes/usage.js.map +1 -0
- package/dist/src/routes/wallet.d.ts +2 -0
- package/dist/src/routes/wallet.d.ts.map +1 -1
- package/dist/src/routes/wallet.js +13 -6
- package/dist/src/routes/wallet.js.map +1 -1
- package/dist/src/routes/workflows.d.ts.map +1 -1
- package/dist/src/routes/workflows.js +13 -2
- package/dist/src/routes/workflows.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +18 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/cache.d.ts +61 -0
- package/dist/src/services/cache.d.ts.map +1 -0
- package/dist/src/services/cache.js +143 -0
- package/dist/src/services/cache.js.map +1 -0
- package/dist/src/services/connect-tunnel.d.ts +11 -0
- package/dist/src/services/connect-tunnel.d.ts.map +1 -1
- package/dist/src/services/connect-tunnel.js +36 -0
- package/dist/src/services/connect-tunnel.js.map +1 -1
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +1 -0
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/message-send.d.ts +13 -0
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +28 -5
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/organism-search.d.ts +12 -7
- package/dist/src/services/organism-search.d.ts.map +1 -1
- package/dist/src/services/organism-search.js +93 -60
- package/dist/src/services/organism-search.js.map +1 -1
- package/dist/src/services/usage-summary.d.ts +77 -0
- package/dist/src/services/usage-summary.d.ts.map +1 -0
- package/dist/src/services/usage-summary.js +85 -0
- package/dist/src/services/usage-summary.js.map +1 -0
- package/dist/src/services/workspace-enrichment.d.ts +76 -0
- package/dist/src/services/workspace-enrichment.d.ts.map +1 -0
- package/dist/src/services/workspace-enrichment.js +144 -0
- package/dist/src/services/workspace-enrichment.js.map +1 -0
- package/dist/src/storage/interface.d.ts +5 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +16 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +51 -1
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +16 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +20 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +15 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +28 -2
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +4 -0
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +4 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +20 -0
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +1 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +1 -0
- package/prisma/schema.prisma +1 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file mindmap.js
|
|
3
|
-
* @description Interactive, DETERMINISTIC structure
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* @description Interactive, DETERMINISTIC structure map for an organism or a single workspace, with
|
|
4
|
+
* selectable chart types — **mindmap** (radial, the default; most compact / best at-a-glance),
|
|
5
|
+
* **flowchart LR** and **flowchart TD** (familiar tree views). Reuses the offers "Kartta" pattern:
|
|
6
|
+
* strict-security Mermaid (no Mermaid click handlers); flowchart nodes carry ids we generate
|
|
7
|
+
* (`ws{i}`, `ws{i}_sp{j}`, `mem{i}`, `ag{i}`, `sp{j}`) and a click resolves them by id, while mindmap
|
|
8
|
+
* nodes resolve by their label text (mindmap can't carry arbitrary ids). Options (chart type, detail
|
|
9
|
+
* level, show users, show activity, heatmap) regenerate the source and are PERSISTED per user per
|
|
10
|
+
* organism/workspace in localStorage. Generated from the /graph data — separate from the README and
|
|
11
|
+
* the OKF outline, and never persisted server-side. Heatmap colours come from theme CSS variables.
|
|
12
|
+
* @structure StructureMindmap({ scope, graph, onNavigate, label, storageKey });
|
|
13
|
+
* buildOrganismMindmap / buildWorkspaceMindmap (chart-type aware, used here + by the timeline)
|
|
11
14
|
* @usage import { StructureMindmap } from '/views/profile/organisms/mindmap.js';
|
|
12
15
|
* @version-history
|
|
13
16
|
* v1.0.0 — 2026-06-22 — Initial: clickable organism/workspace mindmap with level/users/activity/heatmap.
|
|
17
|
+
* v1.1.0 — 2026-06-22 — Chart types (mindmap default + flowchart LR/TD), per-org/ws localStorage
|
|
18
|
+
* persistence, label-text click resolution for mindmap mode.
|
|
14
19
|
*/
|
|
15
20
|
import { h } from 'preact';
|
|
16
21
|
import { useState } from 'preact/hooks';
|
|
@@ -19,12 +24,16 @@ const html = htm.bind(h);
|
|
|
19
24
|
import { t } from '/js/i18n.js';
|
|
20
25
|
import { Mermaid } from '/components/Mermaid.js';
|
|
21
26
|
|
|
22
|
-
/** Sanitize a label for a Mermaid quoted string
|
|
27
|
+
/** Sanitize a label for a Mermaid quoted flowchart string. */
|
|
23
28
|
function mmdLabel(s) {
|
|
24
29
|
return String(s ?? '').replace(/["\n\r]/g, ' ').replace(/[<>|{}[\]]/g, '').trim().slice(0, 48) || '—';
|
|
25
30
|
}
|
|
31
|
+
/** Stricter sanitize for MINDMAP labels — also strips parentheses (mindmap treats `(...)` as a node
|
|
32
|
+
* shape, so a label like "Ledger (410)" would mis-parse). Counts are shown with " · N" instead. */
|
|
33
|
+
function mmLabel(s) {
|
|
34
|
+
return String(s ?? '').replace(/["\n\r]/g, ' ').replace(/[<>|{}[\]()]/g, '').trim().slice(0, 48) || '—';
|
|
35
|
+
}
|
|
26
36
|
|
|
27
|
-
/** Bucket an item count into a heat class index (0 = none → no class). */
|
|
28
37
|
function heatBucket(n) {
|
|
29
38
|
if (!n) return 0;
|
|
30
39
|
if (n <= 3) return 1;
|
|
@@ -32,7 +41,7 @@ function heatBucket(n) {
|
|
|
32
41
|
return 3;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
/**
|
|
44
|
+
/** Heat colours from theme CSS variables (source of truth = theme.css). */
|
|
36
45
|
function heatColors() {
|
|
37
46
|
const cs = typeof window !== 'undefined' ? getComputedStyle(document.documentElement) : null;
|
|
38
47
|
const v = (n, fb) => ((cs && cs.getPropertyValue(n).trim()) || fb);
|
|
@@ -43,8 +52,8 @@ function heatColors() {
|
|
|
43
52
|
};
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
/**
|
|
47
|
-
function
|
|
55
|
+
/** classDef + class lines for the flowchart heatmap (mindmap uses :::class + CSS instead). */
|
|
56
|
+
function flowchartHeatLines(assignments) {
|
|
48
57
|
if (!assignments.length) return [];
|
|
49
58
|
const c = heatColors();
|
|
50
59
|
const lines = [
|
|
@@ -56,20 +65,21 @@ function heatLines(assignments) {
|
|
|
56
65
|
return lines;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
/** Short activity label suffix for a node (counts + last update), when showActivity is on. */
|
|
60
68
|
function activitySuffix(opts, { records, documents, lastActivity }) {
|
|
61
69
|
if (!opts.showActivity) return '';
|
|
62
70
|
const bits = [];
|
|
63
|
-
if (documents
|
|
64
|
-
if (records
|
|
71
|
+
if (documents) bits.push(`${documents}d`);
|
|
72
|
+
if (records) bits.push(`${records}r`);
|
|
65
73
|
let s = bits.length ? ` · ${bits.join('+')}` : '';
|
|
66
74
|
if (lastActivity) s += ` · ${String(lastActivity).slice(0, 10)}`;
|
|
67
75
|
return s;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
/* ── Flowchart builders (LR / TD) — node ids encode the target for click resolution ── */
|
|
79
|
+
|
|
80
|
+
function buildOrgFlowchart(graph, opts) {
|
|
81
|
+
const dir = opts.chartType === 'flowchart-td' ? 'TD' : 'LR';
|
|
82
|
+
const lines = [`flowchart ${dir}`, ` org["${mmdLabel(graph.name)}"]`];
|
|
73
83
|
const heat = [];
|
|
74
84
|
(graph.workspaces || []).forEach((w, i) => {
|
|
75
85
|
const wsTotal = (w.totalRecords || 0) + (w.totalDocuments || 0);
|
|
@@ -78,8 +88,8 @@ export function buildOrganismMindmap(graph, opts) {
|
|
|
78
88
|
if (opts.heatmap) { const b = heatBucket(wsTotal); if (b) heat.push({ id: `ws${i}`, bucket: b }); }
|
|
79
89
|
if (opts.level === 'spaces' || opts.level === 'counts') {
|
|
80
90
|
(w.spaces || []).forEach((sp, j) => {
|
|
81
|
-
const
|
|
82
|
-
lines.push(` ws${i} --> ws${i}_sp${j}["${mmdLabel(sp.name)}${
|
|
91
|
+
const c = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
|
|
92
|
+
lines.push(` ws${i} --> ws${i}_sp${j}["${mmdLabel(sp.name)}${c}"]`);
|
|
83
93
|
if (opts.heatmap) { const b = heatBucket(sp.count || 0); if (b) heat.push({ id: `ws${i}_sp${j}`, bucket: b }); }
|
|
84
94
|
});
|
|
85
95
|
}
|
|
@@ -88,81 +98,163 @@ export function buildOrganismMindmap(graph, opts) {
|
|
|
88
98
|
(graph.members || []).forEach((m, i) => lines.push(` org --> mem${i}["👤 ${mmdLabel(m.name)}"]`));
|
|
89
99
|
(graph.agents || []).forEach((a, i) => lines.push(` org --> ag${i}["🤖 ${mmdLabel(a.name || a.gaii)}"]`));
|
|
90
100
|
}
|
|
91
|
-
return lines.concat(
|
|
101
|
+
return lines.concat(flowchartHeatLines(heat)).join('\n');
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
function buildWsFlowchart(node, opts) {
|
|
105
|
+
const dir = opts.chartType === 'flowchart-td' ? 'TD' : 'LR';
|
|
96
106
|
const wsLabel = mmdLabel(node.name) + activitySuffix(opts, { records: node.totalRecords, documents: node.totalDocuments, lastActivity: node.lastActivity });
|
|
97
|
-
const lines = [
|
|
107
|
+
const lines = [`flowchart ${dir}`, ` wsroot["${wsLabel}"]`];
|
|
98
108
|
const heat = [];
|
|
99
109
|
if (opts.level !== 'ws') {
|
|
100
110
|
(node.spaces || []).forEach((sp, j) => {
|
|
101
|
-
const
|
|
102
|
-
lines.push(` wsroot --> sp${j}["${mmdLabel(sp.name)}${
|
|
111
|
+
const c = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
|
|
112
|
+
lines.push(` wsroot --> sp${j}["${mmdLabel(sp.name)}${c}"]`);
|
|
103
113
|
if (opts.heatmap) { const b = heatBucket(sp.count || 0); if (b) heat.push({ id: `sp${j}`, bucket: b }); }
|
|
104
114
|
});
|
|
105
115
|
}
|
|
106
|
-
return lines.concat(
|
|
116
|
+
return lines.concat(flowchartHeatLines(heat)).join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── Mindmap builders (radial) — labels resolve clicks by text. Mermaid's mindmap does NOT support
|
|
120
|
+
* the `:::class` node-class syntax (it renders it as literal text), so the heatmap is shown with a
|
|
121
|
+
* trailing marker emoji instead of a coloured shape. ── */
|
|
122
|
+
|
|
123
|
+
const HEAT_MARK = { 1: '🟢', 2: '🔶', 3: '🔥' };
|
|
124
|
+
function heatMark(opts, n) { if (!opts.heatmap) return ''; const b = heatBucket(n); return b ? ` ${HEAT_MARK[b]}` : ''; }
|
|
125
|
+
|
|
126
|
+
function buildOrgMindmap(graph, opts) {
|
|
127
|
+
const lines = ['mindmap', ` root((${mmLabel(graph.name)}))`];
|
|
128
|
+
(graph.workspaces || []).forEach((w) => {
|
|
129
|
+
const wsTotal = (w.totalRecords || 0) + (w.totalDocuments || 0);
|
|
130
|
+
const wsLabel = mmLabel(w.name) + activitySuffix(opts, { records: w.totalRecords, documents: w.totalDocuments, lastActivity: w.lastActivity });
|
|
131
|
+
lines.push(` ${wsLabel}${heatMark(opts, wsTotal)}`);
|
|
132
|
+
if (opts.level === 'spaces' || opts.level === 'counts') {
|
|
133
|
+
(w.spaces || []).forEach((sp) => {
|
|
134
|
+
const c = opts.level === 'counts' ? ` · ${sp.count || 0}` : '';
|
|
135
|
+
lines.push(` ${mmLabel(sp.name)}${c}${heatMark(opts, sp.count || 0)}`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (opts.showUsers) {
|
|
140
|
+
(graph.members || []).forEach((m) => lines.push(` 👤 ${mmLabel(m.name)}`));
|
|
141
|
+
(graph.agents || []).forEach((a) => lines.push(` 🤖 ${mmLabel(a.name || a.gaii)}`));
|
|
142
|
+
}
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildWsMindmap(node, opts) {
|
|
147
|
+
const wsLabel = mmLabel(node.name) + activitySuffix(opts, { records: node.totalRecords, documents: node.totalDocuments, lastActivity: node.lastActivity });
|
|
148
|
+
const lines = ['mindmap', ` root((${wsLabel}))`];
|
|
149
|
+
if (opts.level !== 'ws') {
|
|
150
|
+
(node.spaces || []).forEach((sp) => {
|
|
151
|
+
const c = opts.level === 'counts' ? ` · ${sp.count || 0}` : '';
|
|
152
|
+
lines.push(` ${mmLabel(sp.name)}${c}${heatMark(opts, sp.count || 0)}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
107
156
|
}
|
|
108
157
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
158
|
+
/** Build deterministic Mermaid source for an ORGANISM graph honouring opts.chartType. */
|
|
159
|
+
export function buildOrganismMindmap(graph, opts) {
|
|
160
|
+
return opts.chartType === 'mindmap' ? buildOrgMindmap(graph, opts) : buildOrgFlowchart(graph, opts);
|
|
161
|
+
}
|
|
162
|
+
/** Build deterministic Mermaid source for a single WORKSPACE graph node honouring opts.chartType. */
|
|
163
|
+
export function buildWorkspaceMindmap(node, opts) {
|
|
164
|
+
return opts.chartType === 'mindmap' ? buildWsMindmap(node, opts) : buildWsFlowchart(node, opts);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── Click resolution ── */
|
|
168
|
+
|
|
169
|
+
/** Flowchart: parse the clicked node's DOM id (it embeds the generated id). */
|
|
170
|
+
function resolveFlowchartClick(scope, graph, domId) {
|
|
112
171
|
if (scope === 'organism') {
|
|
113
172
|
let m = /ws(\d+)_sp(\d+)/.exec(domId);
|
|
114
|
-
if (m) {
|
|
115
|
-
const w = graph.workspaces?.[+m[1]]; const sp = w?.spaces?.[+m[2]];
|
|
116
|
-
if (w && sp) return { type: 'space', wsId: w.id, space: sp.name };
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
173
|
+
if (m) { const w = graph.workspaces?.[+m[1]]; const sp = w?.spaces?.[+m[2]]; return (w && sp) ? { type: 'space', wsId: w.id, space: sp.name } : null; }
|
|
119
174
|
m = /ws(\d+)(?![_\d])/.exec(domId);
|
|
120
175
|
if (m) { const w = graph.workspaces?.[+m[1]]; return w ? { type: 'workspace', wsId: w.id } : null; }
|
|
121
176
|
if (/mem\d+/.test(domId) || /ag\d+/.test(domId)) return { type: 'members' };
|
|
122
177
|
return null;
|
|
123
178
|
}
|
|
124
|
-
// workspace scope
|
|
125
179
|
const m = /sp(\d+)/.exec(domId);
|
|
126
180
|
if (m) { const sp = graph.spaces?.[+m[1]]; return sp ? { type: 'space', space: sp.name } : null; }
|
|
127
181
|
return null;
|
|
128
182
|
}
|
|
129
183
|
|
|
184
|
+
/** Mindmap: resolve by the clicked node's label text (mindmap nodes have no stable id). Strips the
|
|
185
|
+
* activity/count suffix and the user emoji, then matches the core name to a workspace / space /
|
|
186
|
+
* member. Workspace + member matches are exact; a space name is matched to the first workspace that
|
|
187
|
+
* has it (mindmap can't disambiguate a repeated space name across workspaces — opens that workspace). */
|
|
188
|
+
function resolveMindmapClick(scope, graph, text) {
|
|
189
|
+
const core = String(text || '').trim().split(' · ')[0].replace(/^👤\s*|^🤖\s*/, '').trim();
|
|
190
|
+
if (!core) return null;
|
|
191
|
+
if (scope === 'organism') {
|
|
192
|
+
for (const w of (graph.workspaces || [])) if (w.name === core) return { type: 'workspace', wsId: w.id };
|
|
193
|
+
for (const w of (graph.workspaces || [])) for (const sp of (w.spaces || [])) if (sp.name === core) return { type: 'workspace', wsId: w.id };
|
|
194
|
+
if ((graph.members || []).some(m => m.name === core) || (graph.agents || []).some(a => (a.name || a.gaii) === core)) return { type: 'members' };
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
for (const sp of (graph.spaces || [])) if (sp.name === core) return { type: 'space', space: sp.name };
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const CHART_TYPES = [
|
|
202
|
+
{ key: 'mindmap', label: 'mindmap.chartMindmap', fallback: 'Mindmap' },
|
|
203
|
+
{ key: 'flowchart-lr', label: 'mindmap.chartLr', fallback: 'Flowchart →' },
|
|
204
|
+
{ key: 'flowchart-td', label: 'mindmap.chartTd', fallback: 'Flowchart ↓' },
|
|
205
|
+
];
|
|
130
206
|
const LEVELS = [
|
|
131
207
|
{ key: 'ws', label: 'mindmap.levelWs', fallback: 'Workspaces' },
|
|
132
208
|
{ key: 'spaces', label: 'mindmap.levelSpaces', fallback: '+ Spaces' },
|
|
133
209
|
{ key: 'counts', label: 'mindmap.levelCounts', fallback: '+ Counts' },
|
|
134
210
|
];
|
|
135
211
|
|
|
212
|
+
const DEFAULTS = { chartType: 'mindmap', level: 'spaces', showUsers: false, showActivity: true, heatmap: true };
|
|
213
|
+
|
|
214
|
+
/** Load persisted options for this org/ws (per-user via localStorage), merged over defaults. */
|
|
215
|
+
function loadOpts(storageKey) {
|
|
216
|
+
if (!storageKey) return { ...DEFAULTS };
|
|
217
|
+
try {
|
|
218
|
+
const raw = localStorage.getItem(`aimeat.mm.${storageKey}`);
|
|
219
|
+
return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS };
|
|
220
|
+
} catch { return { ...DEFAULTS }; }
|
|
221
|
+
}
|
|
222
|
+
function saveOpts(storageKey, opts) {
|
|
223
|
+
if (!storageKey) return;
|
|
224
|
+
try { localStorage.setItem(`aimeat.mm.${storageKey}`, JSON.stringify(opts)); } catch { /* quota/private mode — non-fatal */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
136
227
|
/**
|
|
137
|
-
* StructureMindmap — collapsible interactive map. Default collapsed so the
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param {{ scope: 'organism'|'workspace', graph: object, onNavigate: (t:object)=>void, label?: string }} props
|
|
228
|
+
* StructureMindmap — collapsible interactive map. Default collapsed so the Mermaid bundle only loads
|
|
229
|
+
* when asked for. scope 'organism' (root → workspaces → spaces + members/agents) or 'workspace'.
|
|
230
|
+
* onNavigate(target): { type:'workspace', wsId } | { type:'space', wsId?, space } | { type:'members' }.
|
|
231
|
+
* @param {{ scope:'organism'|'workspace', graph:object, onNavigate:(t:object)=>void, label?:string, storageKey?:string }} props
|
|
142
232
|
*/
|
|
143
|
-
export function StructureMindmap({ scope, graph, onNavigate, label }) {
|
|
233
|
+
export function StructureMindmap({ scope, graph, onNavigate, label, storageKey }) {
|
|
144
234
|
const [open, setOpen] = useState(false);
|
|
145
|
-
const [
|
|
146
|
-
const [showUsers, setShowUsers] = useState(false);
|
|
147
|
-
const [showActivity, setShowActivity] = useState(true);
|
|
148
|
-
const [heatmap, setHeatmap] = useState(true);
|
|
235
|
+
const [opts, setOpts] = useState(() => loadOpts(storageKey));
|
|
149
236
|
|
|
150
237
|
if (!graph) return null;
|
|
151
|
-
const
|
|
238
|
+
const set = (patch) => setOpts(prev => { const next = { ...prev, ...patch }; saveOpts(storageKey, next); return next; });
|
|
239
|
+
|
|
152
240
|
const src = open
|
|
153
241
|
? (scope === 'organism' ? buildOrganismMindmap(graph, opts) : buildWorkspaceMindmap(graph, opts))
|
|
154
242
|
: '';
|
|
155
243
|
|
|
156
244
|
const onMapClick = (e) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
245
|
+
if (opts.chartType === 'mindmap') {
|
|
246
|
+
const g = e.target?.closest?.('.mindmap-node, [class*="mindmap"], g');
|
|
247
|
+
const target = resolveMindmapClick(scope, graph, (g?.textContent) || e.target?.textContent || '');
|
|
248
|
+
if (target) onNavigate?.(target);
|
|
249
|
+
} else {
|
|
250
|
+
const node = e.target?.closest?.('.node');
|
|
251
|
+
if (!node) return;
|
|
252
|
+
const target = resolveFlowchartClick(scope, graph, node.id || '');
|
|
253
|
+
if (target) onNavigate?.(target);
|
|
254
|
+
}
|
|
161
255
|
};
|
|
162
256
|
|
|
163
|
-
const lbl = label || (scope === 'organism'
|
|
164
|
-
? (t('mindmap.titleOrg') || 'Organism map')
|
|
165
|
-
: (t('mindmap.titleWs') || 'Workspace map'));
|
|
257
|
+
const lbl = label || (scope === 'organism' ? (t('mindmap.titleOrg') || 'Organism map') : (t('mindmap.titleWs') || 'Workspace map'));
|
|
166
258
|
|
|
167
259
|
return html`
|
|
168
260
|
<div class="pj-mindmap">
|
|
@@ -173,17 +265,23 @@ export function StructureMindmap({ scope, graph, onNavigate, label }) {
|
|
|
173
265
|
${open ? html`
|
|
174
266
|
<div class="pj-mindmap-body card-detail">
|
|
175
267
|
<div class="pj-mindmap-opts">
|
|
268
|
+
<label class="pj-mm-opt">
|
|
269
|
+
<span>${t('mindmap.chart') || 'Chart'}</span>
|
|
270
|
+
<select class="input-field input-sm" value=${opts.chartType} onChange=${e => set({ chartType: e.target.value })}>
|
|
271
|
+
${CHART_TYPES.map(c => html`<option value=${c.key}>${t(c.label) || c.fallback}</option>`)}
|
|
272
|
+
</select>
|
|
273
|
+
</label>
|
|
176
274
|
<label class="pj-mm-opt">
|
|
177
275
|
<span>${t('mindmap.level') || 'Level'}</span>
|
|
178
|
-
<select class="input-field input-sm" value=${level} onChange=${e =>
|
|
276
|
+
<select class="input-field input-sm" value=${opts.level} onChange=${e => set({ level: e.target.value })}>
|
|
179
277
|
${LEVELS.map(l => html`<option value=${l.key}>${t(l.label) || l.fallback}</option>`)}
|
|
180
278
|
</select>
|
|
181
279
|
</label>
|
|
182
280
|
${scope === 'organism' ? html`
|
|
183
|
-
<label class="pj-mm-check"><input type="checkbox" checked=${showUsers} onChange=${e =>
|
|
281
|
+
<label class="pj-mm-check"><input type="checkbox" checked=${opts.showUsers} onChange=${e => set({ showUsers: e.target.checked })} /> ${t('mindmap.showUsers') || 'Users'}</label>
|
|
184
282
|
` : null}
|
|
185
|
-
<label class="pj-mm-check"><input type="checkbox" checked=${showActivity} onChange=${e =>
|
|
186
|
-
<label class="pj-mm-check"><input type="checkbox" checked=${heatmap} onChange=${e =>
|
|
283
|
+
<label class="pj-mm-check"><input type="checkbox" checked=${opts.showActivity} onChange=${e => set({ showActivity: e.target.checked })} /> ${t('mindmap.showActivity') || 'Activity'}</label>
|
|
284
|
+
<label class="pj-mm-check"><input type="checkbox" checked=${opts.heatmap} onChange=${e => set({ heatmap: e.target.checked })} /> ${t('mindmap.heatmap') || 'Heatmap'}</label>
|
|
187
285
|
</div>
|
|
188
286
|
<div class="pj-mindmap-canvas of-mapwrap--clickable" onClick=${onMapClick}>
|
|
189
287
|
<${Mermaid} chart=${src} />
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @usage import { OrgSearch, IncomingInvitations, BoardPreview } from '/views/profile/organisms/panels.js';
|
|
9
9
|
* @version-history
|
|
10
10
|
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
* v1.1.0 — 2026-06-22 — OrgSearch: instant (debounced) indexed search, results grouped by workspace,
|
|
12
|
+
* and clicking a hit deep-links to the exact record/document (via the workspace openDoc handoff).
|
|
11
13
|
*/
|
|
12
14
|
import { h } from 'preact';
|
|
13
15
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -32,34 +34,49 @@ export function OrgSearch({ orgId, onOpenWorkspace }) {
|
|
|
32
34
|
const [results, setResults] = useState(null);
|
|
33
35
|
const [busy, setBusy] = useState(false);
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
// Instant (debounced) search across the organism — indexed FTS backend (GET /:id/search).
|
|
38
|
+
useEffect(() => {
|
|
36
39
|
const query = q.trim();
|
|
37
|
-
if (query.length < 2) return;
|
|
40
|
+
if (query.length < 2) { setResults(null); setBusy(false); return undefined; }
|
|
41
|
+
let cancelled = false;
|
|
38
42
|
setBusy(true);
|
|
39
|
-
|
|
40
|
-
const r = await orgService.searchOrganism(orgId, query);
|
|
41
|
-
setResults(
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const tid = setTimeout(async () => {
|
|
44
|
+
try { const r = await orgService.searchOrganism(orgId, query); if (!cancelled) setResults(r?.data?.results || []); }
|
|
45
|
+
catch { if (!cancelled) setResults([]); }
|
|
46
|
+
finally { if (!cancelled) setBusy(false); }
|
|
47
|
+
}, 220);
|
|
48
|
+
return () => { cancelled = true; clearTimeout(tid); };
|
|
49
|
+
}, [q, orgId]);
|
|
50
|
+
|
|
51
|
+
// Open a hit at the exact record/document: stash the deep-link the workspace reads on mount, then
|
|
52
|
+
// navigate to that workspace (its openDoc effect resolves namespace → space tab + opens the item).
|
|
53
|
+
const openHit = (r) => {
|
|
54
|
+
try { sessionStorage.setItem(`aimeat.ws.${orgId}.${r.ws}.openDoc`, JSON.stringify({ namespace: r.namespace, id: r.id })); } catch { /* noop */ }
|
|
55
|
+
onOpenWorkspace?.(r.ws);
|
|
44
56
|
};
|
|
45
57
|
|
|
58
|
+
// Group hits by workspace so a big organism's results stay legible.
|
|
59
|
+
const byWs = {};
|
|
60
|
+
for (const r of (results || [])) (byWs[r.ws] = byWs[r.ws] || { name: r.wsName || r.ws, hits: [] }).hits.push(r);
|
|
61
|
+
|
|
46
62
|
return html`
|
|
47
63
|
<div class="pj-orgsearch">
|
|
48
64
|
<div class="flex-row-wrap">
|
|
49
65
|
<input class="input-field input-sm pj-orgsearch-input" placeholder=${t('organisms.searchPlaceholder') || 'Find records & documents…'} value=${q}
|
|
50
|
-
onInput=${(e) => setQ(e.target.value)}
|
|
51
|
-
|
|
66
|
+
onInput=${(e) => setQ(e.target.value)} />
|
|
67
|
+
${busy ? html`<${Spinner} />` : null}
|
|
68
|
+
${results !== null ? html`<button class="btn-ghost btn-sm" onClick=${() => setQ('')}>${t('search.clear') || 'Clear'}</button>` : null}
|
|
52
69
|
</div>
|
|
53
|
-
${results !== null && results.length === 0 ? html`<div class="section-desc">${t('
|
|
54
|
-
${(
|
|
55
|
-
<div class="pj-
|
|
56
|
-
<span>
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
${results !== null && results.length === 0 && !busy ? html`<div class="section-desc">${t('search.noMatches') || 'No matches.'}</div>` : null}
|
|
71
|
+
${Object.entries(byWs).map(([ws, grp]) => html`
|
|
72
|
+
<div class="pj-search-group" key=${ws}>
|
|
73
|
+
<div class="pj-search-group-head">${(grp.name)}<span class="pj-org-tab-count">${grp.hits.length}</span></div>
|
|
74
|
+
${grp.hits.map(r => html`
|
|
75
|
+
<button class="pj-search-hit" key=${r.space + '/' + r.id} onClick=${() => openHit(r)}>
|
|
76
|
+
<span class="pj-search-hit-title">${(r.title)} <span class="pj-mini">· ${(r.space)}</span></span>
|
|
77
|
+
<span class="pj-search-hit-snippet">${(r.snippet)}</span>
|
|
78
|
+
</button>`)}
|
|
79
|
+
</div>`)}
|
|
63
80
|
</div>
|
|
64
81
|
`;
|
|
65
82
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* @usage import { TimelinePanel } from '/views/profile/organisms/timeline-panel.js';
|
|
10
10
|
* @version-history
|
|
11
11
|
* v1.0.0 — 2026-06-22 — Initial: structure timeline view over trackable-memory history (Osa D3).
|
|
12
|
+
* v1.1.0 — 2026-06-22 — Add a Mermaid `timeline` diagram of the changes; the selected-snapshot map
|
|
13
|
+
* honours the chart type the user picked for this organism's mindmap (localStorage).
|
|
12
14
|
*/
|
|
13
15
|
import { h } from 'preact';
|
|
14
16
|
import { useState } from 'preact/hooks';
|
|
@@ -41,6 +43,30 @@ function totals(fp) {
|
|
|
41
43
|
return { workspaces: ws.length, documents: docs, records: recs, members: fp.memberCount || 0 };
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** The chart type the user picked for THIS organism's mindmap (shared with the snapshot map below). */
|
|
47
|
+
function readChartType(orgId) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = localStorage.getItem(`aimeat.mm.org.${orgId}`);
|
|
50
|
+
return (raw && JSON.parse(raw).chartType) || 'mindmap';
|
|
51
|
+
} catch { return 'mindmap'; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A Mermaid `timeline` diagram of the structural changes (chronological). `:` is the timeline
|
|
55
|
+
* separator, so it is stripped from event text; multiple events on one day share a row. */
|
|
56
|
+
function buildTimelineDiagram(rows) {
|
|
57
|
+
const chron = rows.slice().reverse(); // rows are newest-first → oldest-first for the time axis
|
|
58
|
+
const byDate = new Map();
|
|
59
|
+
for (const r of chron) {
|
|
60
|
+
const d = String(r.at).slice(0, 10) || '—';
|
|
61
|
+
const ev = String(r.event || '').replace(/[:\n\r]/g, ' ').replace(/[<>|{}]/g, '').trim().slice(0, 60) || 'muutos';
|
|
62
|
+
if (!byDate.has(d)) byDate.set(d, []);
|
|
63
|
+
byDate.get(d).push(ev);
|
|
64
|
+
}
|
|
65
|
+
const lines = ['timeline'];
|
|
66
|
+
for (const [d, evs] of byDate) lines.push(` ${d} : ${evs.join(' : ')}`);
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
export function TimelinePanel({ orgId }) {
|
|
45
71
|
const [open, setOpen] = useState(false);
|
|
46
72
|
const [loaded, setLoaded] = useState(false);
|
|
@@ -79,6 +105,9 @@ export function TimelinePanel({ orgId }) {
|
|
|
79
105
|
: (!rows.length
|
|
80
106
|
? html`<div class="section-desc">${t('timeline.empty') || 'No structural history yet.'}</div>`
|
|
81
107
|
: html`
|
|
108
|
+
<div class="pj-timeline-diagram">
|
|
109
|
+
<${Mermaid} chart=${buildTimelineDiagram(rows)} />
|
|
110
|
+
</div>
|
|
82
111
|
<div class="pj-timeline-grid">
|
|
83
112
|
<ul class="pj-timeline-list">
|
|
84
113
|
${rows.map(r => {
|
|
@@ -95,7 +124,7 @@ export function TimelinePanel({ orgId }) {
|
|
|
95
124
|
</ul>
|
|
96
125
|
<div class="pj-timeline-map">
|
|
97
126
|
${selected
|
|
98
|
-
? html`<${Mermaid} chart=${buildOrganismMindmap({ name: t('timeline.snapshot') || 'Snapshot', workspaces: selected.workspaces || [], members: [], agents: [] }, { level: 'counts', showUsers: false, showActivity: true, heatmap: true })} />`
|
|
127
|
+
? html`<${Mermaid} chart=${buildOrganismMindmap({ name: t('timeline.snapshot') || 'Snapshot', workspaces: selected.workspaces || [], members: [], agents: [] }, { chartType: readChartType(orgId), level: 'counts', showUsers: false, showActivity: true, heatmap: true })} />`
|
|
99
128
|
: html`<div class="section-desc">${t('timeline.pick') || 'Pick a point to see the structure then.'}</div>`}
|
|
100
129
|
</div>
|
|
101
130
|
</div>`)}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* @usage import { WorkspaceComments } from '/views/profile/organisms/workspace-comments.js';
|
|
8
8
|
* @version-history
|
|
9
9
|
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
10
|
+
* v1.1.0 — 2026-06-22 — Optional BATCHED mode (batched/initialComments/onReload): the parent fetches
|
|
11
|
+
* all visible threads in one /comments/batch request instead of each thread self-fetching +
|
|
12
|
+
* self-subscribing to 'organisms' events (which was the per-document comments request storm).
|
|
10
13
|
*/
|
|
11
14
|
import { h } from 'preact';
|
|
12
15
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -23,8 +26,12 @@ import * as orgService from '/js/services/organisms.js';
|
|
|
23
26
|
* (comment on a specific passage) and threaded replies (parentId). Backend: /v1/organisms/:id/comments.
|
|
24
27
|
* Agents use the same endpoints via aimeat_workspace_comment(s).
|
|
25
28
|
*/
|
|
26
|
-
export function WorkspaceComments({ orgId, ws, space, instanceId, showToast }) {
|
|
27
|
-
|
|
29
|
+
export function WorkspaceComments({ orgId, ws, space, instanceId, showToast, batched, initialComments, onReload }) {
|
|
30
|
+
// BATCHED mode (parent owns the fetch): the container fetches every visible thread in one
|
|
31
|
+
// /comments/batch request and passes this thread's slice via `initialComments`; we don't self-fetch
|
|
32
|
+
// or self-subscribe (that per-thread fan-out was the comments request storm). STANDALONE mode
|
|
33
|
+
// (no `batched`): unchanged — self-fetch on mount + refresh on 'organisms' events.
|
|
34
|
+
const [comments, setComments] = useState(batched ? (initialComments ?? null) : null);
|
|
28
35
|
const [body, setBody] = useState('');
|
|
29
36
|
const [anchorQuote, setAnchorQuote] = useState('');
|
|
30
37
|
const [replyTo, setReplyTo] = useState(null); // { id, body } of the comment being replied to
|
|
@@ -33,13 +40,16 @@ export function WorkspaceComments({ orgId, ws, space, instanceId, showToast }) {
|
|
|
33
40
|
const mine = (author) => author && (author === me.gaii || author === me.ghii);
|
|
34
41
|
|
|
35
42
|
const load = useCallback(async () => {
|
|
43
|
+
if (batched) { await onReload?.(); return; } // parent re-runs the batch → flows back via initialComments
|
|
36
44
|
if (!ws || !space || !instanceId) return;
|
|
37
45
|
const r = await orgService.listComments(orgId, ws, space, instanceId).catch(() => null);
|
|
38
46
|
setComments(r?.data?.comments || []);
|
|
39
|
-
}, [orgId, ws, space, instanceId]);
|
|
40
|
-
|
|
47
|
+
}, [orgId, ws, space, instanceId, batched, onReload]);
|
|
48
|
+
// Batched: keep local view in sync with the prop (undefined = still loading → null). Standalone: self-fetch.
|
|
49
|
+
useEffect(() => { if (batched) setComments(initialComments ?? null); }, [batched, initialComments]);
|
|
50
|
+
useEffect(() => { if (!batched) load(); }, [load, batched]);
|
|
41
51
|
const liveRef = useRef(load); liveRef.current = load;
|
|
42
|
-
useEffect(() => onLiveUpdate(['organisms'], () => liveRef.current()), []);
|
|
52
|
+
useEffect(() => { if (batched) return undefined; return onLiveUpdate(['organisms'], () => liveRef.current()); }, [batched]);
|
|
43
53
|
|
|
44
54
|
const submit = async () => {
|
|
45
55
|
const text = body.trim();
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* @version-history
|
|
11
11
|
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split; the hidden file
|
|
12
12
|
* input uses the .pj-hidden-input class instead of an inline style (Rule 8).
|
|
13
|
+
* v1.1.0 — 2026-06-22 — Kill the per-workspace fetch storm: one discoverWorkspaces({include:'enrichment'})
|
|
14
|
+
* replaces the 1 + 3N (getWorkspace+activity+participants) fan-out; pending-review counts come inline;
|
|
15
|
+
* the 'organisms' live refresh is debounced (1.5s) so an agent-driven event burst is one reload.
|
|
13
16
|
*/
|
|
14
17
|
import { h } from 'preact';
|
|
15
18
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -48,32 +51,33 @@ export function WorkspaceList({ org, showToast, onOpen, onCount }) {
|
|
|
48
51
|
// Discovery: every workspace in the org (membership-gated) with this user's access status. A member
|
|
49
52
|
// sees workspaces they can't yet read (access:'none') so they can request access. Row enrichment
|
|
50
53
|
// (record/doc counts, last event, review counter) loads per workspace afterwards, best effort.
|
|
54
|
+
// ONE enriched request replaces the old discover + per-row getWorkspace+activity+participants
|
|
55
|
+
// fan-out (1 + 3N requests, each re-scanning the same workspace memory). The server computes the
|
|
56
|
+
// same recs/docs/lastEvent/participants + pending-review counts and returns them inline.
|
|
51
57
|
const load = useCallback(async () => {
|
|
52
|
-
const l = await orgService.discoverWorkspaces(orgId);
|
|
58
|
+
const l = await orgService.discoverWorkspaces(orgId, { include: 'enrichment' });
|
|
53
59
|
setList(l);
|
|
54
60
|
onCount?.(Array.isArray(l) ? l.length : 0);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const owners = [];
|
|
73
|
-
for (const n of (parts?.nodes || [])) for (const o of (n.owners || [])) owners.push({ ...o, node: n.id, isLocalNode: n.isLocal });
|
|
74
|
-
setWsStats(s => ({ ...s, [w.id]: { recs, docs, lastEv, hasManifest: !!wsData?.manifest, owners } }));
|
|
75
|
-
}).catch(() => {});
|
|
61
|
+
const stats = {};
|
|
62
|
+
const reviews = {};
|
|
63
|
+
for (const w of (l || [])) {
|
|
64
|
+
const e = w.enrichment;
|
|
65
|
+
if (w.access === 'none' || !e) continue;
|
|
66
|
+
if (e.pendingReviews) reviews[w.id] = e.pendingReviews;
|
|
67
|
+
stats[w.id] = {
|
|
68
|
+
recs: e.recs || 0,
|
|
69
|
+
docs: e.docs || 0,
|
|
70
|
+
lastEv: e.lastEvent || null,
|
|
71
|
+
hasManifest: !!e.hasManifest,
|
|
72
|
+
// UI only reads owners[].length and per-owner {owner,isCreator,isSelf,isLocalNode,agents.length}.
|
|
73
|
+
owners: (e.participants || []).map(p => ({
|
|
74
|
+
owner: p.owner, node: p.node, isLocalNode: p.isLocalNode,
|
|
75
|
+
isCreator: p.isCreator, isSelf: p.isSelf, agents: new Array(p.agentsCount || 0),
|
|
76
|
+
})),
|
|
77
|
+
};
|
|
76
78
|
}
|
|
79
|
+
setWsStats(stats);
|
|
80
|
+
setApprByWs(reviews);
|
|
77
81
|
}, [orgId, onCount]);
|
|
78
82
|
useEffect(() => { load(); }, [load]);
|
|
79
83
|
|
|
@@ -103,7 +107,15 @@ export function WorkspaceList({ org, showToast, onOpen, onCount }) {
|
|
|
103
107
|
orgService.buildOrganismOverviewMermaid(orgId).then(c => { if (!cancelled) setOverview(c); }).catch(() => {});
|
|
104
108
|
return () => { cancelled = true; };
|
|
105
109
|
}, [orgId, list, showOverview]);
|
|
106
|
-
|
|
110
|
+
// Re-load on organism changes, but DEBOUNCE: on a busy node dozens of agents emit 'organisms'
|
|
111
|
+
// events ~1/sec; a trailing debounce collapses a burst into one reload (the load is now a single
|
|
112
|
+
// cheap request). `onLiveUpdate` reads the Set-typed domains correctly — never via e.detail directly.
|
|
113
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
let timer = null;
|
|
116
|
+
const off = onLiveUpdate(['organisms'], () => { clearTimeout(timer); timer = setTimeout(() => liveRef.current(), 1500); });
|
|
117
|
+
return () => { clearTimeout(timer); off(); };
|
|
118
|
+
}, []);
|
|
107
119
|
|
|
108
120
|
const create = async () => {
|
|
109
121
|
const name = newName.trim();
|