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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file command-palette.js
|
|
3
|
+
* @description Global command palette / quick-switcher (Cmd/Ctrl-K) — Layer 2 of the search feature.
|
|
4
|
+
* Mounted once at the app shell so it works on any authenticated page. Empty query shows recents;
|
|
5
|
+
* typing runs the indexed cross-owner librarian search (GET /v1/librarian/search, own scope) plus a
|
|
6
|
+
* name match over the caller's organisms. Results are grouped (Organisms / content hits), keyboard-
|
|
7
|
+
* navigable (↑↓/Enter/Esc), and selecting one jumps to that organism/workspace — pre-filling the
|
|
8
|
+
* in-workspace search (Layer 1) with the same query so you land on the filtered list. Navigation
|
|
9
|
+
* hands off via sessionStorage (read by OrganismsTab on mount) + an `aimeat-open-organism` event
|
|
10
|
+
* (picked up if the tab is already mounted) + a route change to the organisms tab.
|
|
11
|
+
* @structure CommandPalette({ navigate })
|
|
12
|
+
* @usage import { CommandPalette } from '/views/command-palette.js'; html`<${CommandPalette} navigate=${navigate} />`
|
|
13
|
+
* @version-history
|
|
14
|
+
* v1.0.0 — 2026-06-22 — Initial: Cmd-K quick-switcher over librarian search + organism names + recents.
|
|
15
|
+
*/
|
|
16
|
+
import { h } from 'preact';
|
|
17
|
+
import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
|
|
18
|
+
import htm from 'htm';
|
|
19
|
+
const html = htm.bind(h);
|
|
20
|
+
import { t } from '/js/i18n.js';
|
|
21
|
+
import { Modal } from '/components/Modal.js';
|
|
22
|
+
import { SearchBar } from '/components/SearchBar.js';
|
|
23
|
+
import { Spinner } from '/components/Spinner.js';
|
|
24
|
+
import { EmptyState } from '/components/EmptyState.js';
|
|
25
|
+
import { librarianSearch } from '/js/services/memory.js';
|
|
26
|
+
import { listOrganisms } from '/js/services/organisms.js';
|
|
27
|
+
import { getOwner } from '/js/services/auth.js';
|
|
28
|
+
import { listRecents } from '/js/recents.js';
|
|
29
|
+
|
|
30
|
+
/** Hand off to the organisms tab using the app's existing nav contract: prime the sessionStorage the
|
|
31
|
+
* OrganismsTab reads on mount (open id + workspace + an optional ws-search pre-fill), then switch to
|
|
32
|
+
* the Organisms tab via `aimeat-open-tab` (the LandingPage listener). Also fire `aimeat-open-organism`
|
|
33
|
+
* for an ALREADY-mounted OrganismsTab (it won't remount, so it can't re-read sessionStorage), and a
|
|
34
|
+
* route fallback for when we're not on /v1/profile at all. */
|
|
35
|
+
function openTarget(navigate, { orgId, wsId, query }) {
|
|
36
|
+
try {
|
|
37
|
+
if (orgId) sessionStorage.setItem('aimeat.ws.openId', orgId);
|
|
38
|
+
if (wsId) sessionStorage.setItem('aimeat.ws.openWs', wsId); else sessionStorage.removeItem('aimeat.ws.openWs');
|
|
39
|
+
if (orgId && wsId && query) sessionStorage.setItem(`aimeat.ws.${orgId}.${wsId}.search`, query);
|
|
40
|
+
} catch { /* private mode — the route fallback still works */ }
|
|
41
|
+
try {
|
|
42
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'organisms' } }));
|
|
43
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-organism', { detail: { orgId, wsId: wsId || null } }));
|
|
44
|
+
} catch { /* noop */ }
|
|
45
|
+
if (!location.pathname.startsWith('/v1/profile')) navigate('/v1/profile?tab=organisms');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function CommandPalette({ navigate }) {
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const [q, setQ] = useState('');
|
|
51
|
+
const [hits, setHits] = useState(null); // librarian content hits
|
|
52
|
+
const [orgs, setOrgs] = useState([]); // caller's organisms (for name match)
|
|
53
|
+
const [busy, setBusy] = useState(false);
|
|
54
|
+
const [sel, setSel] = useState(0);
|
|
55
|
+
const inputRef = useRef(null);
|
|
56
|
+
|
|
57
|
+
// Global Cmd/Ctrl-K to open (ignore when typing in a field, except to toggle); Esc handled by Modal.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const onKey = (e) => {
|
|
60
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setOpen(o => !o);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener('keydown', onKey);
|
|
66
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// On open: load recents + the caller's organisms (for name matching), focus the input.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) { setQ(''); setHits(null); setSel(0); return; }
|
|
72
|
+
const me = getOwner();
|
|
73
|
+
if (me) listOrganisms({ member: me }).then(r => setOrgs((r?.data?.organisms) || [])).catch(() => {});
|
|
74
|
+
setTimeout(() => inputRef.current?.querySelector('input')?.focus(), 30);
|
|
75
|
+
}, [open]);
|
|
76
|
+
|
|
77
|
+
// Debounced content search.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!open) return undefined;
|
|
80
|
+
const query = q.trim();
|
|
81
|
+
if (query.length < 2) { setHits(null); setBusy(false); return undefined; }
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
setBusy(true);
|
|
84
|
+
const tid = setTimeout(async () => {
|
|
85
|
+
const r = await librarianSearch(query, 30, 'own').catch(() => []);
|
|
86
|
+
if (!cancelled) { setHits(r || []); setBusy(false); setSel(0); }
|
|
87
|
+
}, 200);
|
|
88
|
+
return () => { cancelled = true; clearTimeout(tid); };
|
|
89
|
+
}, [q, open]);
|
|
90
|
+
|
|
91
|
+
const orgName = useCallback((id) => orgs.find(o => o.id === id)?.name || id, [orgs]);
|
|
92
|
+
|
|
93
|
+
// Build a flat, selectable item list (with group headers derived for rendering).
|
|
94
|
+
const query = q.trim();
|
|
95
|
+
const orgMatches = query.length >= 2
|
|
96
|
+
? orgs.filter(o => (o.name || '').toLowerCase().includes(query.toLowerCase())).slice(0, 6)
|
|
97
|
+
.map(o => ({ kind: 'org', orgId: o.id, label: o.name, sub: t(`organisms.types.${o.type}`) || o.type }))
|
|
98
|
+
: [];
|
|
99
|
+
const contentItems = (hits || []).map(hh => ({
|
|
100
|
+
kind: 'hit', orgId: hh.organismId, wsId: hh.workspaceId, label: hh.title || hh.key,
|
|
101
|
+
sub: [orgName(hh.organismId), hh.workspaceId, hh.space].filter(Boolean).join(' · '), snippet: hh.snippet,
|
|
102
|
+
})).filter(it => it.orgId);
|
|
103
|
+
const recents = (!query) ? listRecents(8).filter(r => r.type === 'workspace' && r.data?.orgId)
|
|
104
|
+
.map(r => ({ kind: 'recent', orgId: r.data.orgId, wsId: r.data.wsId, label: r.label, sub: r.sub })) : [];
|
|
105
|
+
const items = [...orgMatches, ...contentItems, ...recents];
|
|
106
|
+
|
|
107
|
+
const choose = (it) => {
|
|
108
|
+
if (!it) return;
|
|
109
|
+
openTarget(navigate, { orgId: it.orgId, wsId: it.wsId, query: it.kind === 'hit' ? query : '' });
|
|
110
|
+
setOpen(false);
|
|
111
|
+
};
|
|
112
|
+
const onInputKey = (e) => {
|
|
113
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); setSel(s => Math.min(s + 1, items.length - 1)); }
|
|
114
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); setSel(s => Math.max(s - 1, 0)); }
|
|
115
|
+
else if (e.key === 'Enter') { e.preventDefault(); choose(items[sel]); }
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!open) return null;
|
|
119
|
+
let idx = -1;
|
|
120
|
+
const row = (it) => { idx++; const i = idx; return html`
|
|
121
|
+
<button class=${'cmdk-item' + (i === sel ? ' is-sel' : '')} key=${it.kind + (it.orgId || '') + (it.wsId || '') + it.label}
|
|
122
|
+
onMouseEnter=${() => setSel(i)} onClick=${() => choose(it)}>
|
|
123
|
+
<span class="cmdk-item-label">${it.kind === 'org' ? '🏢 ' : it.kind === 'recent' ? '🕘 ' : '📄 '}${it.label}</span>
|
|
124
|
+
${it.sub ? html`<span class="cmdk-item-sub">${it.sub}</span>` : null}
|
|
125
|
+
${it.snippet ? html`<span class="cmdk-item-snippet">${it.snippet}</span>` : null}
|
|
126
|
+
</button>`; };
|
|
127
|
+
|
|
128
|
+
return html`
|
|
129
|
+
<${Modal} open=${open} onClose=${() => setOpen(false)} title=${t('search.paletteTitle') || 'Search everything'} className="cmdk-modal">
|
|
130
|
+
<div class="cmdk" ref=${inputRef} onKeyDown=${onInputKey}>
|
|
131
|
+
<${SearchBar} value=${q} onInput=${e => setQ(e.target.value)} placeholder=${t('search.palettePlaceholder') || 'Search organisms, workspaces, records…'} />
|
|
132
|
+
<div class="cmdk-hint section-desc">${t('search.openHint') || '↵ open · Esc close'}</div>
|
|
133
|
+
<div class="cmdk-list">
|
|
134
|
+
${busy && !items.length ? html`<${Spinner} text=${t('search.searching') || 'Searching…'} />` : null}
|
|
135
|
+
${!busy && query.length >= 2 && !items.length ? html`<${EmptyState} text=${t('search.noMatches') || 'No matches'} />` : null}
|
|
136
|
+
${orgMatches.length ? html`<div class="cmdk-group">${t('organisms.title') || 'Organisms'}</div>` : null}
|
|
137
|
+
${orgMatches.map(row)}
|
|
138
|
+
${contentItems.length ? html`<div class="cmdk-group">${t('search.results') || 'Results'}</div>` : null}
|
|
139
|
+
${contentItems.map(row)}
|
|
140
|
+
${recents.length ? html`<div class="cmdk-group">${t('search.recents') || 'Recent'}</div>` : null}
|
|
141
|
+
${recents.map(row)}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</${Modal}>`;
|
|
145
|
+
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* import KnowledgeTab from './knowledge-tab.js';
|
|
14
14
|
* html`<${KnowledgeTab} session=${session} showToast=${showToast} onStats=${onStats} />`
|
|
15
15
|
* @version-history
|
|
16
|
+
* v1.1.0 — 2026-06-22 — Drop the per-package /v1/memory/{key} hydration fallback — the memory list
|
|
17
|
+
* already returns each package's full value inline.
|
|
16
18
|
* v1.0.0 — 2026-03-17 — Refactor: remove all inline style="" attributes, use CSS
|
|
17
19
|
* classes (pf-ref-row, pf-ref-icon, pf-external-icon, pf-ref-title, pf-badge-xs,
|
|
18
20
|
* pf-sharing-box, pf-sharing-heading, kpkg-detail-loading). Replace inline
|
|
@@ -68,16 +70,11 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
|
|
|
68
70
|
const loadPackages = useCallback(async ({ showSpinner = true } = {}) => {
|
|
69
71
|
try {
|
|
70
72
|
if (showSpinner) setLoading(true);
|
|
73
|
+
// The memory list (GET /v1/memory) already returns each entry's full `value`, so there's no need
|
|
74
|
+
// for a per-package /v1/memory/{key} hydration fetch (that fallback never fired in practice).
|
|
71
75
|
const list = await knowledgeService.listMyPackages();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const resp = await apiGet('/v1/memory/' + encodeURIComponent(pkg.key));
|
|
76
|
-
return { ...pkg, value: resp?.data?.value };
|
|
77
|
-
} catch { return pkg; }
|
|
78
|
-
}));
|
|
79
|
-
setPackages(hydrated);
|
|
80
|
-
onStats?.({ knowledge: hydrated.length });
|
|
76
|
+
setPackages(list);
|
|
77
|
+
onStats?.({ knowledge: list.length });
|
|
81
78
|
} catch { if (showSpinner) setPackages([]); } // keep old list on a transient live-update refetch
|
|
82
79
|
finally { setLoading(false); }
|
|
83
80
|
}, [onStats]);
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* - PresencePill + PresenceDialog — header status pill that opens the availability settings dialog
|
|
16
16
|
* - LandingPage — main orchestrator (default export)
|
|
17
17
|
* @version-history
|
|
18
|
+
* v3.7.0 — 2026-06-22 — WaitingForYou uses one getWaiting() call (server-aggregated) instead of the
|
|
19
|
+
* per-organism listApprovals + listJoinRequests + listWorkspaces fan-out.
|
|
18
20
|
* v3.6.0 — 2026-06-21 — Change-password modal handles OAuth accounts with no password yet
|
|
19
21
|
* (e.g. Google sign-in): fetches has_password from /v1/ghii/me and, when none is set, switches
|
|
20
22
|
* to a "set a password" flow — drops the current-password field, shows an explanatory hint, and
|
|
@@ -434,36 +436,12 @@ function ChangePasswordModal({ onClose, onChanged }) {
|
|
|
434
436
|
/* "Waiting for you" — everything that needs the user's decision, aggregated across organisms:
|
|
435
437
|
* pending publish approvals (per workspace), pending join requests (orgs they manage), and
|
|
436
438
|
* incoming organism invitations. Renders nothing when there is nothing to do. */
|
|
437
|
-
function WaitingForYou(
|
|
439
|
+
function WaitingForYou() {
|
|
438
440
|
const [items, setItems] = useState(null);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const orgs = orgsResp?.data?.organisms || [];
|
|
444
|
-
await Promise.all(orgs.map(async (org) => {
|
|
445
|
-
const canManage = org.creatorGhii === owner || (org.admins || []).includes(owner);
|
|
446
|
-
const [aps, reqs] = await Promise.all([
|
|
447
|
-
orgService.listApprovals(org.id, 'pending').catch(() => []),
|
|
448
|
-
canManage ? orgService.listJoinRequests(org.id).catch(() => null) : Promise.resolve(null),
|
|
449
|
-
]);
|
|
450
|
-
if ((aps || []).length > 0) {
|
|
451
|
-
const byWs = {};
|
|
452
|
-
for (const a of aps) { const w = a.arguments?.ws || ''; byWs[w] = (byWs[w] || 0) + 1; }
|
|
453
|
-
let names = {};
|
|
454
|
-
try { names = Object.fromEntries((await orgService.listWorkspaces(org.id)).map(w => [w.id, w.name])); } catch { /* ids will do */ }
|
|
455
|
-
for (const [wsId, n] of Object.entries(byWs)) {
|
|
456
|
-
out.push({ kind: 'review', n, orgId: org.id, orgName: org.name, wsId, wsName: names[wsId] || wsId });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
const pend = ((reqs?.data?.join_requests) || []).filter(r => r.status === 'pending').length;
|
|
460
|
-
if (pend > 0) out.push({ kind: 'join', n: pend, orgId: org.id, orgName: org.name });
|
|
461
|
-
}));
|
|
462
|
-
const inv = await orgService.listMyInvitations().catch(() => null);
|
|
463
|
-
for (const e of (inv?.data?.invitations || [])) out.push({ kind: 'invite', orgName: e?.organism?.name || '' });
|
|
464
|
-
setItems(out);
|
|
465
|
-
} catch { setItems([]); }
|
|
466
|
-
}, [owner]);
|
|
441
|
+
// ONE aggregated request replaces the old per-org fan-out (listOrganisms → per-org listApprovals +
|
|
442
|
+
// listJoinRequests + listWorkspaces + a final listMyInvitations). The server returns the same flat
|
|
443
|
+
// {kind:'review'|'join'|'invite', …} items this widget renders.
|
|
444
|
+
const load = useCallback(async () => { setItems(await orgService.getWaiting()); }, []);
|
|
467
445
|
useEffect(() => { load(); }, [load]);
|
|
468
446
|
const liveRef = useRef(load); liveRef.current = load;
|
|
469
447
|
// Only re-run the per-organism approvals/join-requests fan-out when ORGANISMS actually
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
* per-row checkboxes + a bulk bar (change visibility incl. group / delete with confirm /
|
|
35
35
|
* clear). The detail-row select and edit-modal select remain as alternate paths.
|
|
36
36
|
* v2.2.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
|
|
37
|
+
* v2.3.0 — 2026-06-22 — Scale to thousands of keys: default load is metadata-only (include=meta,
|
|
38
|
+
* no values) with per-key lazy value fetch on expand + "Load all contents"; server-side content
|
|
39
|
+
* search (key/value/tags) with per-group scoping; storage-usage bar; value-size column + "largest
|
|
40
|
+
* first" sort; bulk-delete a whole group; JSON export/import (skip/overwrite/rename).
|
|
41
|
+
* v2.3.1 — 2026-06-22 — Toolbar layout: data tools (Load all / Export / Import) grouped in their
|
|
42
|
+
* own bordered .mem-tools-section up top; content-search + filter on one row; sort + "New memory"
|
|
43
|
+
* moved to a bottom bar (New-memory pushed right).
|
|
37
44
|
*/
|
|
38
45
|
import { h } from 'preact';
|
|
39
46
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
@@ -92,6 +99,19 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
92
99
|
const [browseError, setBrowseError] = useState(null);
|
|
93
100
|
const [pullingKeys, setPullingKeys] = useState(new Set());
|
|
94
101
|
const [sortBy, setSortBy] = useState('updated');
|
|
102
|
+
// Scalable Memory tab (v2.3): the list loads metadata-only by default (no values) so thousands of
|
|
103
|
+
// keys open fast; values are fetched per-key on expand. memQuota drives the storage-usage bar.
|
|
104
|
+
const [memQuota, setMemQuota] = useState(null);
|
|
105
|
+
const [fullLoaded, setFullLoaded] = useState(false); // true once "Load all contents" pulls values
|
|
106
|
+
const [valueCache, setValueCache] = useState({}); // key → value (lazy-fetched in meta mode)
|
|
107
|
+
const [loadingValueKeys, setLoadingValueKeys] = useState(new Set());
|
|
108
|
+
// Server-side content search (matches key + value + tags) — separate from the instant client filter.
|
|
109
|
+
const [searchInput, setSearchInput] = useState('');
|
|
110
|
+
const [searchResults, setSearchResults] = useState(null); // null = not searching; array = results
|
|
111
|
+
const [searchScopePrefix, setSearchScopePrefix] = useState(''); // optional namespace/group scope
|
|
112
|
+
const [searchLoading, setSearchLoading] = useState(false);
|
|
113
|
+
const [importMode, setImportMode] = useState('skip'); // skip | overwrite | rename
|
|
114
|
+
const [importing, setImporting] = useState(false);
|
|
95
115
|
const [discoverEntries, setDiscoverEntries] = useState(null);
|
|
96
116
|
const [discoverLoading, setDiscoverLoading] = useState(false);
|
|
97
117
|
const [discoverError, setDiscoverError] = useState(null);
|
|
@@ -206,15 +226,151 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
206
226
|
} catch { /* ignore */ }
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
// Refresh the list in whatever mode is active: metadata-only by default (fast for thousands of
|
|
230
|
+
// keys), or full values once the user pressed "Load all contents".
|
|
209
231
|
async function loadMemories() {
|
|
210
232
|
try {
|
|
211
233
|
const agentGaii = selectedAgent || undefined;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
if (fullLoaded) {
|
|
235
|
+
const list = await memoryService.listMemories(agentGaii);
|
|
236
|
+
setMemories(Array.isArray(list) ? list : []);
|
|
237
|
+
onStats?.({ memory: Array.isArray(list) ? list.length : 0 });
|
|
238
|
+
} else {
|
|
239
|
+
const { items, quota } = await memoryService.listMemoriesMeta(agentGaii);
|
|
240
|
+
setMemories(items);
|
|
241
|
+
setMemQuota(quota);
|
|
242
|
+
setValueCache({});
|
|
243
|
+
onStats?.({ memory: items.length });
|
|
244
|
+
}
|
|
215
245
|
} catch { setMemories([]); }
|
|
216
246
|
}
|
|
217
247
|
|
|
248
|
+
// "Load all contents" — pull every entry's value so the instant client filter can search values too.
|
|
249
|
+
async function loadFullContents() {
|
|
250
|
+
setFullLoaded(true);
|
|
251
|
+
try {
|
|
252
|
+
const list = await memoryService.listMemories(selectedAgent || undefined);
|
|
253
|
+
setMemories(Array.isArray(list) ? list : []);
|
|
254
|
+
} catch { /* keep the meta list we already have */ }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Lazy value fetch (meta mode): on row expand, fetch the value once and cache it.
|
|
258
|
+
async function ensureValue(key) {
|
|
259
|
+
if (fullLoaded || valueCache[key] !== undefined) return;
|
|
260
|
+
setLoadingValueKeys(prev => new Set(prev).add(key));
|
|
261
|
+
try {
|
|
262
|
+
const resp = await memoryService.getMemory(key, { soft: true, agent: selectedAgent || undefined });
|
|
263
|
+
setValueCache(prev => ({ ...prev, [key]: resp?.data?.value ?? null }));
|
|
264
|
+
} catch {
|
|
265
|
+
setValueCache(prev => ({ ...prev, [key]: null }));
|
|
266
|
+
} finally {
|
|
267
|
+
setLoadingValueKeys(prev => { const n = new Set(prev); n.delete(key); return n; });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// The effective value for a row: from the full list, the lazy cache, or undefined (still loading).
|
|
272
|
+
const valueOf = (m) => (fullLoaded ? m.value : valueCache[m.key]);
|
|
273
|
+
|
|
274
|
+
// Server-side content search (key + value + tags), optionally scoped to a namespace/group prefix.
|
|
275
|
+
async function runServerSearch(query, prefix) {
|
|
276
|
+
const q = (query ?? searchInput).trim();
|
|
277
|
+
setSearchScopePrefix(prefix || '');
|
|
278
|
+
if (!q) { setSearchResults(null); return; }
|
|
279
|
+
setSearchLoading(true);
|
|
280
|
+
try {
|
|
281
|
+
const results = await memoryService.searchMemory(q, selectedAgent || undefined, prefix || undefined);
|
|
282
|
+
const list = Array.isArray(results) ? results : [];
|
|
283
|
+
setSearchResults(list);
|
|
284
|
+
// Search results carry their values — cache them so expanding a result shows the value at once.
|
|
285
|
+
setValueCache(prev => { const next = { ...prev }; for (const r of list) next[r.key] = r.value; return next; });
|
|
286
|
+
} catch (e) {
|
|
287
|
+
showToast(e.message || t('profile.error'), true);
|
|
288
|
+
setSearchResults([]);
|
|
289
|
+
} finally { setSearchLoading(false); }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clearServerSearch() {
|
|
293
|
+
setSearchInput('');
|
|
294
|
+
setSearchResults(null);
|
|
295
|
+
setSearchScopePrefix('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Export all (or a prefix) of the caller's memory as a downloaded JSON backup.
|
|
299
|
+
async function handleExport(prefix) {
|
|
300
|
+
try {
|
|
301
|
+
const data = await memoryService.exportMemory(selectedAgent || undefined, prefix || undefined);
|
|
302
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
303
|
+
const url = URL.createObjectURL(blob);
|
|
304
|
+
const a = document.createElement('a');
|
|
305
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
306
|
+
a.href = url;
|
|
307
|
+
a.download = `aimeat-memory-${stamp}.json`;
|
|
308
|
+
document.body.appendChild(a);
|
|
309
|
+
a.click();
|
|
310
|
+
a.remove();
|
|
311
|
+
URL.revokeObjectURL(url);
|
|
312
|
+
showToast((t('profile.memory.exportDone') || 'Exported {n} entries').replace('{n}', String(data.count ?? (data.entries || []).length)));
|
|
313
|
+
} catch (e) { showToast(e.message || t('profile.error'), true); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Import a JSON backup; the user picks a conflict mode (skip/overwrite/rename) before it runs.
|
|
317
|
+
const importFileRef = useRef(null);
|
|
318
|
+
function triggerImport() { importFileRef.current?.click(); }
|
|
319
|
+
async function handleImportFile(e) {
|
|
320
|
+
const file = e.target.files?.[0];
|
|
321
|
+
e.target.value = ''; // allow re-picking the same file
|
|
322
|
+
if (!file) return;
|
|
323
|
+
let parsed;
|
|
324
|
+
try {
|
|
325
|
+
const text = await file.text();
|
|
326
|
+
parsed = JSON.parse(text);
|
|
327
|
+
} catch { showToast(t('profile.memory.importBadJson') || 'Not a valid JSON backup', true); return; }
|
|
328
|
+
const entries = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.entries) ? parsed.entries : null);
|
|
329
|
+
if (!entries || entries.length === 0) { showToast(t('profile.memory.importEmpty') || 'No entries found in file', true); return; }
|
|
330
|
+
|
|
331
|
+
const modeLabel = (m) => t('profile.memory.importMode.' + m) || m;
|
|
332
|
+
confirm(
|
|
333
|
+
(t('profile.memory.importConfirm') || 'Import {n} entries? Existing keys are handled by mode: {mode}.')
|
|
334
|
+
.replace('{n}', String(entries.length)).replace('{mode}', modeLabel(importMode)),
|
|
335
|
+
async () => {
|
|
336
|
+
setImporting(true);
|
|
337
|
+
try {
|
|
338
|
+
const resp = await memoryService.importMemory(entries, importMode, selectedAgent || undefined);
|
|
339
|
+
if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
|
|
340
|
+
const s = resp.data || {};
|
|
341
|
+
showToast((t('profile.memory.importDone') || 'Imported: {c} new, {u} updated, {s} skipped')
|
|
342
|
+
.replace('{c}', String(s.created || 0)).replace('{u}', String(s.updated || 0)).replace('{s}', String(s.skipped || 0))
|
|
343
|
+
+ ((s.failed && s.failed.length) ? ` · ${s.failed.length} ${t('profile.error') || 'failed'}` : ''),
|
|
344
|
+
!!(s.failed && s.failed.length));
|
|
345
|
+
loadMemories();
|
|
346
|
+
} catch (e) { showToast(e.message || t('profile.error'), true); }
|
|
347
|
+
finally { setImporting(false); }
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Delete a whole namespace/group at once (server-side bulk-delete by prefix).
|
|
353
|
+
function deleteGroup(g, count) {
|
|
354
|
+
const prefix = g.kind === 'organism' ? 'organism.' + g.uuid + '.'
|
|
355
|
+
: g.kind === 'plain' ? g.id + '.'
|
|
356
|
+
: null;
|
|
357
|
+
if (!prefix) { showToast(t('profile.memory.deleteGroupUnsupported') || 'This group cannot be bulk-deleted', true); return; }
|
|
358
|
+
confirm(
|
|
359
|
+
(t('profile.memory.deleteGroupConfirm') || 'Delete all {n} entries under "{g}"? This cannot be undone.')
|
|
360
|
+
.replace('{n}', String(count)).replace('{g}', groupLabel(g)),
|
|
361
|
+
async () => {
|
|
362
|
+
try {
|
|
363
|
+
const resp = await memoryService.bulkDeleteMemory({ prefix, agentGaii: selectedAgent || undefined });
|
|
364
|
+
if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
|
|
365
|
+
showToast((t('profile.memory.bulkDeleted') || '{n} deleted').replace('{n}', String(resp.data?.deleted ?? 0)));
|
|
366
|
+
setExpandedMem(null);
|
|
367
|
+
loadMemories();
|
|
368
|
+
} catch (e) { showToast(e.message || t('profile.error'), true); }
|
|
369
|
+
},
|
|
370
|
+
{ danger: true },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
218
374
|
async function loadFiles() {
|
|
219
375
|
try {
|
|
220
376
|
const list = await memoryService.listFiles();
|
|
@@ -412,6 +568,13 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
412
568
|
}
|
|
413
569
|
}
|
|
414
570
|
|
|
571
|
+
function formatBytes(n) {
|
|
572
|
+
if (n == null) return '';
|
|
573
|
+
if (n < 1024) return n + ' B';
|
|
574
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(n < 10240 ? 1 : 0) + ' KB';
|
|
575
|
+
return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
|
576
|
+
}
|
|
577
|
+
|
|
415
578
|
function formatRelativeTime(isoStr) {
|
|
416
579
|
if (!isoStr) return '';
|
|
417
580
|
const d = new Date(isoStr);
|
|
@@ -436,6 +599,8 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
436
599
|
return sorted.sort((a, b) => +new Date(b.created_at || 0) - +new Date(a.created_at || 0));
|
|
437
600
|
case 'alpha':
|
|
438
601
|
return sorted.sort((a, b) => a.key.localeCompare(b.key));
|
|
602
|
+
case 'size':
|
|
603
|
+
return sorted.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0));
|
|
439
604
|
default:
|
|
440
605
|
return sorted;
|
|
441
606
|
}
|
|
@@ -492,7 +657,9 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
492
657
|
if (!ft) return true;
|
|
493
658
|
if (m.key.toLowerCase().includes(ft)) return true;
|
|
494
659
|
if (m.tags && m.tags.some(tag => tag.toLowerCase().includes(ft))) return true;
|
|
495
|
-
|
|
660
|
+
const v = valueOf(m); // undefined in meta mode (value not loaded) → key/tag filter only
|
|
661
|
+
if (v === undefined) return false;
|
|
662
|
+
try { return JSON.stringify(v).toLowerCase().includes(ft); } catch { return false; }
|
|
496
663
|
}));
|
|
497
664
|
|
|
498
665
|
const toggleMemTag = (tag) => {
|
|
@@ -517,10 +684,11 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
517
684
|
|
|
518
685
|
const renderRow = (m, g) => html`
|
|
519
686
|
<div key=${m.key}>
|
|
520
|
-
<div class="mem-item mem-item--grouped" onClick=${() =>
|
|
687
|
+
<div class="mem-item mem-item--grouped" onClick=${() => { const opening = expandedMem !== m.key; setExpandedMem(opening ? m.key : null); if (opening) ensureValue(m.key); }}>
|
|
521
688
|
<input type="checkbox" class="mem-row-check" checked=${selectedKeys.has(m.key)}
|
|
522
689
|
onClick=${(e) => e.stopPropagation()} onChange=${() => toggleSelected(m.key)} />
|
|
523
690
|
<span class="mem-key" title=${m.key}>${escHtml(displayRemainder(m.key, g))}</span>
|
|
691
|
+
${typeof m.bytes === 'number' && html`<span class="pf-mem-size" title=${t('profile.memory.sizeLabel') || 'Value size'}>${formatBytes(m.bytes)}</span>`}
|
|
524
692
|
<span class="mem-time" title="${m.created_at ? new Date(m.created_at).toLocaleString() : ''} / ${m.updated_at ? new Date(m.updated_at).toLocaleString() : ''}">
|
|
525
693
|
${formatRelativeTime(m.updated_at || m.created_at)}
|
|
526
694
|
</span>
|
|
@@ -549,8 +717,12 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
549
717
|
${expandedMem === m.key && html`
|
|
550
718
|
<div class="mem-detail">
|
|
551
719
|
<div class="mem-detail-key" title=${m.key}>${escHtml(m.key)}</div>
|
|
552
|
-
${(
|
|
553
|
-
|
|
720
|
+
${(!fullLoaded && valueOf(m) === undefined)
|
|
721
|
+
? html`<div class="text-meta-sm">${loadingValueKeys.has(m.key) ? (t('profile.memory.loadingValue') || 'Loading value…') : '…'}</div>`
|
|
722
|
+
: html`
|
|
723
|
+
${(() => { const v = valueOf(m); const im = detectImage(v, m.key); return im ? html`<${ImageView} desc=${im} />` : null; })()}
|
|
724
|
+
<pre>${(() => { const v = valueOf(m); return typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v ?? ''); })()}</pre>
|
|
725
|
+
`}
|
|
554
726
|
<div class="mem-detail-visrow mb-half">
|
|
555
727
|
<span class="text-meta-sm">${t('profile.memory.visLabel')}</span>
|
|
556
728
|
<select class="input-field mem-vis-select" value=${m.visibility || 'private'}
|
|
@@ -592,7 +764,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
592
764
|
</div>
|
|
593
765
|
`}
|
|
594
766
|
<div class="mem-actions">
|
|
595
|
-
<button class="btn-sm" onClick=${() => setEditModal({ key: m.key, value: typeof
|
|
767
|
+
<button class="btn-sm" onClick=${() => { const v = valueOf(m); setEditModal({ key: m.key, value: typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v ?? ''), visibility: m.visibility || 'private', version: m.version, isJson: typeof v === 'object' && v !== null }); }}>${t('profile.memory.editBtn')}</button>
|
|
596
768
|
${fedConsents[m.key]
|
|
597
769
|
? html`<button class="btn-outline btn-sm" disabled=${togglingFed === m.key}
|
|
598
770
|
onClick=${() => handleStopSharing(m.key)}>
|
|
@@ -618,22 +790,58 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
618
790
|
</div>
|
|
619
791
|
`;
|
|
620
792
|
|
|
793
|
+
const quotaPct = memQuota && memQuota.max_bytes ? Math.min(100, Math.round((memQuota.used_bytes / memQuota.max_bytes) * 100)) : 0;
|
|
794
|
+
|
|
621
795
|
return html`
|
|
796
|
+
${memQuota && html`
|
|
797
|
+
<div class="pf-mem-quota">
|
|
798
|
+
<div class="pf-mem-quota-row">
|
|
799
|
+
<span class="text-meta-sm">${t('profile.memory.storageUsed') || 'Storage'}: ${memQuota.used_keys}/${memQuota.max_keys} ${t('profile.memory.keysWord') || 'keys'} · ${formatBytes(memQuota.used_bytes)} / ${formatBytes(memQuota.max_bytes)}</span>
|
|
800
|
+
</div>
|
|
801
|
+
<div class="pf-mem-quota-bar"><div class="pf-mem-quota-fill ${quotaPct >= 90 ? 'pf-mem-quota-fill--danger' : ''}" style=${`width:${quotaPct}%`}></div></div>
|
|
802
|
+
</div>
|
|
803
|
+
`}
|
|
804
|
+
<div class="mem-tools-section">
|
|
805
|
+
<span class="mem-tools-label">${t('profile.memory.toolsLabel') || 'Tools'}</span>
|
|
806
|
+
<div class="mem-tools-actions">
|
|
807
|
+
${!fullLoaded && html`<button class="btn-outline btn-sm" onClick=${loadFullContents}>${t('profile.memory.loadContents') || 'Load all contents'}</button>`}
|
|
808
|
+
<span class="mem-import-group">
|
|
809
|
+
<button class="btn-outline btn-sm" onClick=${() => handleExport()}>${t('profile.memory.exportBtn') || 'Export'}</button>
|
|
810
|
+
<button class="btn-outline btn-sm" disabled=${importing} onClick=${triggerImport}>${importing ? '…' : (t('profile.memory.importBtn') || 'Import')}</button>
|
|
811
|
+
<select class="input-field mem-vis-select" value=${importMode} onChange=${e => setImportMode(e.target.value)} title=${t('profile.memory.importModeLabel') || 'Conflict handling'}>
|
|
812
|
+
<option value="skip">${t('profile.memory.importMode.skip') || 'Skip existing'}</option>
|
|
813
|
+
<option value="overwrite">${t('profile.memory.importMode.overwrite') || 'Overwrite'}</option>
|
|
814
|
+
<option value="rename">${t('profile.memory.importMode.rename') || 'Import as new'}</option>
|
|
815
|
+
</select>
|
|
816
|
+
<input type="file" accept="application/json,.json" ref=${importFileRef} class="pf-hidden" onChange=${handleImportFile} />
|
|
817
|
+
</span>
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
622
820
|
<div class="action-bar">
|
|
821
|
+
<div class="search-bar">
|
|
822
|
+
<input type="text" class="input-field" placeholder=${t('profile.memory.searchContents') || 'Search content or key…'}
|
|
823
|
+
value=${searchInput} onInput=${e => setSearchInput(e.target.value)}
|
|
824
|
+
onKeyDown=${e => { if (e.key === 'Enter') runServerSearch(searchInput, searchScopePrefix); }} />
|
|
825
|
+
<button class="btn-sm" disabled=${searchLoading} onClick=${() => runServerSearch(searchInput, searchScopePrefix)}>${searchLoading ? '…' : (t('profile.memory.searchBtn') || 'Search')}</button>
|
|
826
|
+
${searchResults !== null && html`<button class="btn-ghost btn-sm" onClick=${clearServerSearch}>✕</button>`}
|
|
827
|
+
</div>
|
|
623
828
|
<div class="search-bar">
|
|
624
829
|
<input type="text" class="input-field" placeholder=${t('profile.memory.filterType')}
|
|
625
830
|
value=${filterText} onInput=${e => setFilterText(e.target.value)} />
|
|
626
831
|
${filterText && html`<button class="btn-ghost btn-sm" onClick=${() => setFilterText('')}>✕</button>`}
|
|
627
832
|
</div>
|
|
833
|
+
</div>
|
|
834
|
+
<div class="action-bar mem-bottom-bar">
|
|
628
835
|
<div class="mem-sort-bar">
|
|
629
836
|
<label class="text-meta-sm">${t('profile.memory.sortLabel')}</label>
|
|
630
837
|
<select class="input-field mem-sort-select" value=${sortBy} onChange=${e => setSortBy(e.target.value)}>
|
|
631
838
|
<option value="updated">${t('profile.memory.sortUpdated')}</option>
|
|
632
839
|
<option value="created">${t('profile.memory.sortCreated')}</option>
|
|
633
840
|
<option value="alpha">${t('profile.memory.sortAlpha')}</option>
|
|
841
|
+
<option value="size">${t('profile.memory.sortSize') || 'Largest first'}</option>
|
|
634
842
|
</select>
|
|
635
843
|
</div>
|
|
636
|
-
<button class="btn-primary" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
|
|
844
|
+
<button class="btn-primary mem-new-btn" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
|
|
637
845
|
</div>
|
|
638
846
|
<${TagCloud} tags=${tagsByFreq} selected=${memTagFilter} onToggle=${toggleMemTag} onClear=${() => setMemTagFilter(new Set())} limit=${10} />
|
|
639
847
|
${showMemForm && html`<${MemoryForm} onSave=${handleCreateMemory} onCancel=${() => setShowMemForm(false)} groups=${groups} />`}
|
|
@@ -649,22 +857,39 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
649
857
|
<button class="btn-ghost btn-sm" onClick=${() => setSelectedKeys(new Set())}>✕ ${t('profile.memory.bulkClear') || 'Clear selection'}</button>
|
|
650
858
|
</div>
|
|
651
859
|
`}
|
|
652
|
-
${
|
|
653
|
-
? html
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
860
|
+
${searchResults !== null
|
|
861
|
+
? html`
|
|
862
|
+
<div class="mem-search-summary">
|
|
863
|
+
<span class="text-meta-sm">${(t('profile.memory.searchResultCount') || '{n} matches').replace('{n}', String(searchResults.length))}${searchScopePrefix ? ` · ${escHtml(searchScopePrefix)}` : ''}</span>
|
|
864
|
+
<button class="btn-ghost btn-sm" onClick=${clearServerSearch}>✕ ${t('profile.memory.searchClear') || 'Clear search'}</button>
|
|
865
|
+
</div>
|
|
866
|
+
${searchResults.length === 0
|
|
867
|
+
? html`<div class="empty">${t('profile.memory.searchEmpty') || 'No matches'}</div>`
|
|
868
|
+
: sortEntries(searchResults).map(m => renderRow(m, groupOfKey(m.key)))}
|
|
869
|
+
`
|
|
870
|
+
: filtered.length === 0
|
|
871
|
+
? html`<div class="empty">${memories.length > 0 ? (t('tags.noMatch') || 'No items match selected tags') : t('profile.memory.empty')}</div>`
|
|
872
|
+
: groupsOrdered.map(g => {
|
|
873
|
+
const collapsed = !filtering && collapsedGroups.has(g.id);
|
|
874
|
+
const groupPrefix = g.kind === 'organism' ? 'organism.' + g.uuid + '.' : g.kind === 'plain' ? g.id + '.' : null;
|
|
875
|
+
return html`
|
|
876
|
+
<div class="mem-group" key=${g.id}>
|
|
877
|
+
<div class="mem-group-header" role="button" tabindex="0" onClick=${() => toggleGroupCollapsed(g.id)}>
|
|
878
|
+
<span class="pf-chevron ${collapsed ? '' : 'pf-chevron-open'}">▼</span>
|
|
879
|
+
<span class="mem-group-name">${escHtml(groupLabel(g))}</span>
|
|
880
|
+
<span class="mem-group-count">${g.items.length === 1 ? (t('profile.memory.keysOne') || '1 key') : (t('profile.memory.keysCount') || '{n} keys').replace('{n}', String(g.items.length))}</span>
|
|
881
|
+
${g.kind === 'organism' && orgNames[g.uuid] && html`<span class="mem-group-sub">${shortTok(g.uuid)}</span>`}
|
|
882
|
+
<span class="mem-group-actions">
|
|
883
|
+
${groupPrefix && html`<button class="btn-ghost btn-sm" title=${t('profile.memory.searchInGroup') || 'Search in this group'}
|
|
884
|
+
onClick=${(e) => { e.stopPropagation(); setSearchInput(''); setSearchScopePrefix(groupPrefix); showToast((t('profile.memory.searchInGroupHint') || 'Type a query to search within {g}').replace('{g}', groupLabel(g))); }}>🔍</button>`}
|
|
885
|
+
${groupPrefix && html`<button class="btn-ghost btn-sm" title=${t('profile.memory.deleteGroup') || 'Delete group'}
|
|
886
|
+
onClick=${(e) => { e.stopPropagation(); deleteGroup(g, g.items.length); }}>🗑️</button>`}
|
|
887
|
+
</span>
|
|
888
|
+
</div>
|
|
889
|
+
${!collapsed && g.items.map(m => renderRow(m, g))}
|
|
890
|
+
</div>
|
|
891
|
+
`;
|
|
892
|
+
})
|
|
668
893
|
}`;
|
|
669
894
|
};
|
|
670
895
|
|
|
@@ -1056,7 +1281,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
1056
1281
|
<div class="agent-selector mb-half">
|
|
1057
1282
|
<label class="text-meta pf-mr-half">${t('profile.memory.agent') || 'Agent'}:</label>
|
|
1058
1283
|
<select class="input-field pf-select-inline"
|
|
1059
|
-
value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); }}>
|
|
1284
|
+
value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); setFullLoaded(false); clearServerSearch(); }}>
|
|
1060
1285
|
<option value="">${t('profile.memory.defaultAgent') || 'Default agent'}</option>
|
|
1061
1286
|
${agents.map(a => html`<option key=${a.gaii} value=${a.gaii}>${escHtml(a.name || a.gaii)}${a.display_name ? ` — ${escHtml(a.display_name)}` : ''}</option>`)}
|
|
1062
1287
|
</select>
|