aimeat 1.28.0 → 1.29.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 +46 -1
- package/dist/locales/fi.json +46 -1
- package/dist/public/css/views/agents-detail.css +11 -0
- package/dist/public/css/views/profile.css +71 -10
- package/dist/public/js/services/memory.js +62 -4
- package/dist/public/js/services/organisms.js +49 -3
- package/dist/public/js/services/workflows.js +4 -3
- package/dist/public/spa.html +3 -0
- package/dist/public/views/command-palette.js +145 -0
- package/dist/public/views/profile/knowledge-tab.js +6 -9
- package/dist/public/views/profile/landing-page.js +7 -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 +17 -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/generated/api-types.d.ts +333 -10
- package/dist/src/generated/api-types.d.ts.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/memory.d.ts +3 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +232 -11
- package/dist/src/routes/memory.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/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/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/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/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/providers/mongodb/index.d.ts +1 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +2 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +4 -0
- package/dist/src/storage/providers/sqlite/index.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/repositories/memory.repository.d.ts +1 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @usage import { OrgAgentsPanel } from '/views/profile/organisms/agents.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 — Agent activity context comes from one getAgentsActivity(orgId) call instead
|
|
12
|
+
* of a per-workspace getWorkspaceActivity fan-out.
|
|
11
13
|
*/
|
|
12
14
|
import { h } from 'preact';
|
|
13
15
|
import { useState, useEffect } from 'preact/hooks';
|
|
@@ -41,22 +43,15 @@ export function OrgAgentsPanel({ org, ghii, canManage, showToast, onChanged }) {
|
|
|
41
43
|
listAgents(ghii).then(a => setMine((a || []).filter(x => !String(x.name || '').startsWith('session-')))).catch(() => setMine([]));
|
|
42
44
|
}, [ghii]);
|
|
43
45
|
|
|
44
|
-
// Best-effort activity context: which workspaces each agent has touched, and when last.
|
|
46
|
+
// Best-effort activity context: which workspaces each agent has touched, and when last. ONE
|
|
47
|
+
// aggregated request replaces the old per-workspace getWorkspaceActivity fan-out.
|
|
45
48
|
useEffect(() => {
|
|
46
49
|
let cancelled = false;
|
|
47
50
|
(async () => {
|
|
48
51
|
try {
|
|
49
|
-
const
|
|
52
|
+
const agents = await orgService.getAgentsActivity(orgId);
|
|
50
53
|
const map = {};
|
|
51
|
-
|
|
52
|
-
const a = await orgService.getWorkspaceActivity(orgId, w.id);
|
|
53
|
-
for (const e of (a.events || [])) {
|
|
54
|
-
if (!e.agent) continue;
|
|
55
|
-
const m = map[e.agent] || (map[e.agent] = { count: 0, lastAt: '', ws: new Set() });
|
|
56
|
-
m.count++; m.ws.add(w.name || w.id);
|
|
57
|
-
if (!m.lastAt || e.at > m.lastAt) m.lastAt = e.at;
|
|
58
|
-
}
|
|
59
|
-
}));
|
|
54
|
+
for (const [name, v] of Object.entries(agents)) map[name] = { count: v.count, lastAt: v.lastAt, ws: new Set(v.workspaces || []) };
|
|
60
55
|
if (!cancelled) setActed(map);
|
|
61
56
|
} catch { /* context is optional */ }
|
|
62
57
|
})();
|
|
@@ -326,7 +326,7 @@ export function OrganismHome({ org, ghii, showToast, initialSettings, onOpenWs,
|
|
|
326
326
|
<${ReadmePanel} markdown=${readme} canEdit=${canEdit} kind="organism" name=${org.name}
|
|
327
327
|
aiPromptSeed=${tocSeed} onSave=${saveReadme} />
|
|
328
328
|
|
|
329
|
-
<${StructureMindmap} scope="organism" graph=${graph} onNavigate=${onMapNav} />
|
|
329
|
+
<${StructureMindmap} scope="organism" graph=${graph} onNavigate=${onMapNav} storageKey=${'org.' + org.id} />
|
|
330
330
|
|
|
331
331
|
<${StructureOverview} label=${t('organisms.structureOverviewOrg') || 'Organism structure — table of contents'}
|
|
332
332
|
load=${() => orgService.getOrganismOverview(org.id)} />
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @usage import { OrgMemberManager } from '/views/profile/organisms/members.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 — Per-member workspace access uses one getWorkspaceAccessAll(orgId) call
|
|
12
|
+
* instead of a per-owned-workspace getWorkspaceAccess fan-out.
|
|
11
13
|
*/
|
|
12
14
|
import { h } from 'preact';
|
|
13
15
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -71,7 +73,12 @@ export function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, c
|
|
|
71
73
|
let cancelled = false;
|
|
72
74
|
(async () => {
|
|
73
75
|
try {
|
|
74
|
-
|
|
76
|
+
// discoverWorkspaces → creators of every workspace; getWorkspaceAccessAll → members of MY
|
|
77
|
+
// workspaces in ONE request (was one getWorkspaceAccess per owned workspace).
|
|
78
|
+
const [wss, accessAll] = await Promise.all([
|
|
79
|
+
orgService.discoverWorkspaces(orgId),
|
|
80
|
+
orgService.getWorkspaceAccessAll(orgId),
|
|
81
|
+
]);
|
|
75
82
|
const map = {};
|
|
76
83
|
const add = (who, wsName, role) => {
|
|
77
84
|
const bare = String(who || '').split('@')[0];
|
|
@@ -80,10 +87,7 @@ export function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, c
|
|
|
80
87
|
if (!list.some(x => x.ws === wsName)) list.push({ ws: wsName, role });
|
|
81
88
|
};
|
|
82
89
|
for (const w of wss) if (w.created_by) add(w.created_by, w.name || w.id, 'owner');
|
|
83
|
-
|
|
84
|
-
const acc = await orgService.getWorkspaceAccess(orgId, w.id).catch(() => null);
|
|
85
|
-
for (const m of (acc?.members || [])) add(m.owner, w.name || w.id, m.role);
|
|
86
|
-
}));
|
|
90
|
+
for (const w of accessAll) for (const m of (w.members || [])) add(m.owner, w.name || w.ws, m.role);
|
|
87
91
|
if (!cancelled) setWsAccess(map);
|
|
88
92
|
} catch { /* the access line is optional */ }
|
|
89
93
|
})();
|
|
@@ -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();
|