aimeat 1.23.2 → 1.23.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/en.json +536 -21
- package/dist/locales/fi.json +548 -33
- package/dist/public/components/ContactCard.js +28 -0
- package/dist/public/css/views/admin.css +12 -0
- package/dist/public/css/views/agents-detail.css +62 -10
- package/dist/public/css/views/landing.css +87 -0
- package/dist/public/css/views/offers.css +96 -0
- package/dist/public/css/views/portfolio.css +18 -0
- package/dist/public/css/views/profile.css +153 -8
- package/dist/public/css/views/start.css +96 -0
- package/dist/public/img/business-hero.png +0 -0
- package/dist/public/img/toimitus.png +0 -0
- package/dist/public/js/components/tag-cloud.js +46 -31
- package/dist/public/js/components/tag-editor.js +2 -2
- package/dist/public/js/recents.js +40 -0
- package/dist/public/js/services/admin.js +6 -0
- package/dist/public/js/services/agents.js +34 -0
- package/dist/public/js/services/offers.js +152 -0
- package/dist/public/js/services/packages.js +1 -0
- package/dist/public/spa.html +32 -2
- package/dist/public/views/admin/subdomains-tab.js +182 -0
- package/dist/public/views/admin.js +2 -0
- package/dist/public/views/business.js +119 -0
- package/dist/public/views/how-it-works.js +159 -0
- package/dist/public/views/landing.js +235 -0
- package/dist/public/views/portfolio.js +120 -23
- package/dist/public/views/pricing.js +75 -0
- package/dist/public/views/profile/access-tab.js +124 -51
- package/dist/public/views/profile/agents/agent-card.js +68 -37
- package/dist/public/views/profile/agents/shared-board.js +56 -22
- package/dist/public/views/profile/agents/tab-activity.js +24 -12
- package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
- package/dist/public/views/profile/agents/tab-data-access.js +9 -13
- package/dist/public/views/profile/agents/tab-integration.js +27 -1
- package/dist/public/views/profile/agents/tab-messages.js +6 -11
- package/dist/public/views/profile/agents/tab-quality.js +45 -5
- package/dist/public/views/profile/agents-tab.js +41 -6
- package/dist/public/views/profile/apps-tab.js +7 -1
- package/dist/public/views/profile/knowledge-tab.js +4 -1
- package/dist/public/views/profile/landing-page.js +427 -123
- package/dist/public/views/profile/memory-tab.js +409 -211
- package/dist/public/views/profile/nodes-tab.js +18 -3
- package/dist/public/views/profile/offers-tab.js +310 -0
- package/dist/public/views/profile/organisms-tab.js +289 -28
- package/dist/public/views/profile/packages-tab.js +152 -16
- package/dist/public/views/profile/portfolio-tab.js +98 -31
- package/dist/public/views/profile.js +2 -0
- package/dist/public/views/start-flows.js +281 -0
- package/dist/public/views/start.js +258 -0
- package/dist/src/generated/api-types.d.ts +856 -17
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +15 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
- package/dist/src/generated/prisma-postgres/index.js +15 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
- package/dist/src/generated/prisma-postgres/wasm.js +15 -3
- package/dist/src/middleware/subdomain.d.ts +28 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -0
- package/dist/src/middleware/subdomain.js +32 -0
- package/dist/src/middleware/subdomain.js.map +1 -0
- package/dist/src/models/offer-schemas.d.ts +229 -0
- package/dist/src/models/offer-schemas.d.ts.map +1 -0
- package/dist/src/models/offer-schemas.js +100 -0
- package/dist/src/models/offer-schemas.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +33 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +146 -1
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/apps-backup.d.ts +20 -0
- package/dist/src/routes/apps-backup.d.ts.map +1 -0
- package/dist/src/routes/apps-backup.js +165 -0
- package/dist/src/routes/apps-backup.js.map +1 -0
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +4 -0
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/instances.d.ts +2 -1
- package/dist/src/routes/instances.d.ts.map +1 -1
- package/dist/src/routes/instances.js +59 -1
- package/dist/src/routes/instances.js.map +1 -1
- package/dist/src/routes/packages.d.ts.map +1 -1
- package/dist/src/routes/packages.js +32 -0
- package/dist/src/routes/packages.js.map +1 -1
- package/dist/src/routes/permissions.d.ts +12 -0
- package/dist/src/routes/permissions.d.ts.map +1 -1
- package/dist/src/routes/permissions.js +35 -3
- package/dist/src/routes/permissions.js.map +1 -1
- package/dist/src/routes/portal.d.ts +3 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +12 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +15 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -0
- package/dist/src/routes/public-stats.js +90 -0
- package/dist/src/routes/public-stats.js.map +1 -0
- package/dist/src/routes/sse.d.ts +7 -2
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +35 -5
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +33 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -0
- package/dist/src/routes/subdomains.js +208 -0
- package/dist/src/routes/subdomains.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +12 -1
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +4 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/apps-backup-export.d.ts +22 -0
- package/dist/src/services/apps-backup-export.d.ts.map +1 -0
- package/dist/src/services/apps-backup-export.js +136 -0
- package/dist/src/services/apps-backup-export.js.map +1 -0
- package/dist/src/services/apps-backup-import.d.ts +120 -0
- package/dist/src/services/apps-backup-import.d.ts.map +1 -0
- package/dist/src/services/apps-backup-import.js +259 -0
- package/dist/src/services/apps-backup-import.js.map +1 -0
- package/dist/src/services/component-registrar.d.ts +17 -0
- package/dist/src/services/component-registrar.d.ts.map +1 -1
- package/dist/src/services/component-registrar.js +63 -5
- package/dist/src/services/component-registrar.js.map +1 -1
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +5 -2
- package/dist/src/services/handbooks/appdev.js.map +1 -1
- package/dist/src/storage/interface.d.ts +11 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +76 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +61 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +11 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/package.repository.d.ts +2 -0
- package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
- package/dist/static/app-catalog.html +1139 -28
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +11 -0
- package/prisma/schema.prisma +11 -0
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
* <Modal> component (Escape/backdrop close + header ✕).
|
|
12
12
|
* v1.5.0 — 2026-06-02 — Component unification (#1): "Copy URLs" button uses canonical
|
|
13
13
|
* <CopyButton> (toast preserved via onCopied).
|
|
14
|
+
* v2.0.0 — 2026-06-10 — Structural rework: (1) keys render as COLLAPSIBLE GROUPS by first
|
|
15
|
+
* segment (organism.<uuid> groups resolve the uuid to the organism's name; remainders
|
|
16
|
+
* shortened with '›' + middle-ellipsis, full key in title + expanded detail); (2) the
|
|
17
|
+
* per-row click-to-cycle visibility pill is GONE — list shows a static VisibilityPill,
|
|
18
|
+
* editing happens in the edit modal (entries) / an explicit select (files), and the
|
|
19
|
+
* "Skip → public" fallback was removed; (3) top-level tabs Mine · Public · Remote nodes
|
|
20
|
+
* replace the inline browse panels; (4) search is type-to-filter (client-side over
|
|
21
|
+
* key/tags/value); (5) tag cloud capped at top-10-by-use with "show all"; (6) shield
|
|
22
|
+
* icon only on rows that actually have sharing rules; rules popover shows localized
|
|
23
|
+
* visibility from the record's REAL owner (backend fix in permissions.ts); (7) edit
|
|
24
|
+
* modal: tall JSON editor + parse validation (invalid JSON can't save; valid JSON is
|
|
25
|
+
* stored parsed, not as a string); (8) "no sharing groups" dead end now offers a
|
|
26
|
+
* "Create a group →" jump to the Access tab; Delete separated to the row's far right;
|
|
27
|
+
* discover/pull copy buttons neutralized.
|
|
28
|
+
* v2.1.0 — 2026-06-10 — Visibility one-click-away again, deliberately: clicking the row's
|
|
29
|
+
* badge opens a small popover with all options INCLUDING the sharing groups (the Access
|
|
30
|
+
* group-share brought into Memory; "Create a group →" when none exist). Plus bulk edit:
|
|
31
|
+
* per-row checkboxes + a bulk bar (change visibility incl. group / delete with confirm /
|
|
32
|
+
* clear). The detail-row select and edit-modal select remain as alternate paths.
|
|
14
33
|
*/
|
|
15
34
|
import { h } from 'preact';
|
|
16
35
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
@@ -26,6 +45,7 @@ import { listPeers } from '/js/services/federation.js';
|
|
|
26
45
|
import { getKeyPermissions, listConsents, grantConsent, revokeConsent } from '/js/services/consent.js';
|
|
27
46
|
import { getNodeUrl } from '/js/services/auth.js';
|
|
28
47
|
import { listGroups } from '/js/services/sharing-groups.js';
|
|
48
|
+
import { listOrganisms } from '/js/services/organisms.js';
|
|
29
49
|
import TagCloud from '/js/components/tag-cloud.js';
|
|
30
50
|
import TagEditor from '/js/components/tag-editor.js';
|
|
31
51
|
import { Modal, useConfirm } from '/components/Modal.js';
|
|
@@ -48,10 +68,17 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
48
68
|
const [agents, setAgents] = useState([]);
|
|
49
69
|
const [selectedAgent, setSelectedAgent] = useState('');
|
|
50
70
|
const [fedConsents, setFedConsents] = useState({}); // { memoryKey: consentId }
|
|
71
|
+
const [allConsents, setAllConsents] = useState([]); // every active consent — drives the per-row shield
|
|
51
72
|
const [togglingFed, setTogglingFed] = useState(null);
|
|
52
73
|
const [groups, setGroups] = useState([]);
|
|
53
|
-
|
|
54
|
-
const [
|
|
74
|
+
// Top-level view: own list / public discovery / remote nodes (home node when federated).
|
|
75
|
+
const [mainTab, setMainTab] = useState('own'); // 'own' | 'discover' | 'remote'
|
|
76
|
+
const [browseMode, setBrowseMode] = useState(null); // 'home' | 'remote' | 'discover' | null
|
|
77
|
+
const [filterText, setFilterText] = useState('');
|
|
78
|
+
const [orgNames, setOrgNames] = useState({}); // organism uuid → display name
|
|
79
|
+
const [collapsedGroups, setCollapsedGroups] = useState(() => {
|
|
80
|
+
try { return new Set(JSON.parse(sessionStorage.getItem('aimeat.mem.groups.collapsed') || '[]')); } catch { return new Set(); }
|
|
81
|
+
});
|
|
55
82
|
const [remoteEntries, setRemoteEntries] = useState(null);
|
|
56
83
|
const [remotePeers, setRemotePeers] = useState([]);
|
|
57
84
|
const [selectedPeer, setSelectedPeer] = useState('');
|
|
@@ -67,18 +94,38 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
67
94
|
const [expandedDiscover, setExpandedDiscover] = useState(null);
|
|
68
95
|
|
|
69
96
|
useEffect(() => {
|
|
70
|
-
if (session) { loadAgents(); loadMemories(); loadFiles(); loadFedConsents(); loadGroups(); }
|
|
97
|
+
if (session) { loadAgents(); loadMemories(); loadFiles(); loadFedConsents(); loadGroups(); loadOrgNames(); }
|
|
71
98
|
}, [session]);
|
|
72
99
|
|
|
100
|
+
// Resolve organism UUIDs to display names for the grouped key list — nobody
|
|
101
|
+
// recognizes e97781a5… but everyone recognizes "Ultima-V Remake".
|
|
102
|
+
async function loadOrgNames() {
|
|
103
|
+
try {
|
|
104
|
+
const resp = await listOrganisms({ member: session.owner });
|
|
105
|
+
const map = {};
|
|
106
|
+
for (const o of (resp?.data?.organisms || [])) map[o.id] = o.name;
|
|
107
|
+
setOrgNames(map);
|
|
108
|
+
} catch { /* names are a nicety — ids still render */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toggleGroupCollapsed(id) {
|
|
112
|
+
setCollapsedGroups(prev => {
|
|
113
|
+
const next = new Set(prev);
|
|
114
|
+
if (next.has(id)) next.delete(id); else next.add(id);
|
|
115
|
+
try { sessionStorage.setItem('aimeat.mem.groups.collapsed', JSON.stringify([...next])); } catch { /* noop */ }
|
|
116
|
+
return next;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
73
120
|
useEffect(() => {
|
|
74
121
|
if (session) { loadMemories(); }
|
|
75
122
|
}, [selectedAgent]);
|
|
76
123
|
|
|
77
124
|
async function loadFedConsents() {
|
|
78
125
|
try {
|
|
79
|
-
const
|
|
126
|
+
const consents = await listConsents();
|
|
80
127
|
const map = {};
|
|
81
|
-
for (const c of
|
|
128
|
+
for (const c of consents) {
|
|
82
129
|
if (c.scope === 'federation') {
|
|
83
130
|
const pat = c.data_pattern || c.pattern || '';
|
|
84
131
|
// Direct key match (no wildcard) or exact pattern
|
|
@@ -88,9 +135,22 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
88
135
|
}
|
|
89
136
|
}
|
|
90
137
|
setFedConsents(map);
|
|
138
|
+
setAllConsents(consents.filter(c => !c.status || c.status === 'active'));
|
|
91
139
|
} catch { /* ignore */ }
|
|
92
140
|
}
|
|
93
141
|
|
|
142
|
+
// Does any active consent pattern cover this key? Drives the per-row shield:
|
|
143
|
+
// shown only when rules exist, so its presence carries information.
|
|
144
|
+
function keyHasRules(key) {
|
|
145
|
+
return allConsents.some(c => {
|
|
146
|
+
const pat = c.data_pattern || c.pattern || '';
|
|
147
|
+
if (!pat) return false;
|
|
148
|
+
if (pat === '*' || pat === key) return true;
|
|
149
|
+
if (pat.endsWith('*')) return key.startsWith(pat.slice(0, -1));
|
|
150
|
+
return false;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
94
154
|
async function handleShareToFederation(key) {
|
|
95
155
|
setTogglingFed(key);
|
|
96
156
|
try {
|
|
@@ -183,7 +243,12 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
183
243
|
}
|
|
184
244
|
|
|
185
245
|
async function handleSaveEdit(key, value, visibility, version, groupId) {
|
|
186
|
-
|
|
246
|
+
// The editor works on text; a value that parses as JSON is stored as the parsed
|
|
247
|
+
// object (not a string of JSON) so readers get back what the writer stored.
|
|
248
|
+
let v = value;
|
|
249
|
+
const s = String(value || '').trim();
|
|
250
|
+
if (/^[[{]/.test(s)) { try { v = JSON.parse(s); } catch { /* modal validates; keep raw as fallback */ } }
|
|
251
|
+
const body = { value: v, visibility, version };
|
|
187
252
|
if (visibility === 'group' && groupId) body.group_id = groupId;
|
|
188
253
|
const resp = await memoryService.updateMemoryFull(key, body);
|
|
189
254
|
if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
|
|
@@ -192,15 +257,6 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
192
257
|
loadMemories();
|
|
193
258
|
}
|
|
194
259
|
|
|
195
|
-
async function handleSearch(query) {
|
|
196
|
-
if (!query) { loadMemories(); return; }
|
|
197
|
-
try {
|
|
198
|
-
const agentGaii = selectedAgent || undefined;
|
|
199
|
-
const list = await memoryService.searchMemory(query, agentGaii);
|
|
200
|
-
setMemories(Array.isArray(list) ? list : []);
|
|
201
|
-
} catch { showToast(t('profile.memory.searchFailed'), true); }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
260
|
async function handleUploadFiles(fileItems, visibility, tags) {
|
|
205
261
|
if (!fileItems || fileItems.length === 0) return;
|
|
206
262
|
let ok = 0, fail = 0;
|
|
@@ -264,19 +320,72 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
264
320
|
} catch { setKeyRulesPopover(null); }
|
|
265
321
|
}
|
|
266
322
|
|
|
267
|
-
/*
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
323
|
+
/* Visibility is edited via an explicit select inside the EXPANDED detail (and in the
|
|
324
|
+
edit modal) — the old per-row click-to-cycle pill meant one stray click in the list
|
|
325
|
+
could publish a memory. The list rows show a static badge only. */
|
|
326
|
+
const VIS_OPTIONS = ['private', 'owner', 'group', 'public'];
|
|
327
|
+
|
|
328
|
+
// Quick visibility change from the expanded detail. 'group' needs a group pick,
|
|
329
|
+
// so it routes to the edit modal where the group select lives.
|
|
330
|
+
async function handleQuickVis(m, newVis) {
|
|
331
|
+
if (newVis === (m.visibility || 'private')) return;
|
|
332
|
+
if (newVis === 'group') {
|
|
333
|
+
setEditModal({ key: m.key, value: typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || ''), visibility: 'group', version: m.version, isJson: typeof m.value === 'object' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const resp = await memoryService.updateMemoryVisibility(m.key, newVis, m.version);
|
|
337
|
+
if (resp.ok === false) showToast(resp.error?.message || t('profile.error'), true);
|
|
338
|
+
loadMemories();
|
|
339
|
+
}
|
|
271
340
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
341
|
+
// Badge-click popover: pick a visibility (incl. a sharing group) right in the row.
|
|
342
|
+
// Deliberate two clicks — open, then choose — so a stray click can't publish anything.
|
|
343
|
+
const [visPopoverFor, setVisPopoverFor] = useState(null);
|
|
344
|
+
async function applyVis(m, newVis, groupId) {
|
|
345
|
+
setVisPopoverFor(null);
|
|
346
|
+
if (newVis === (m.visibility || 'private') && !groupId) return;
|
|
347
|
+
const resp = await memoryService.updateMemoryVisibility(m.key, newVis, m.version, groupId);
|
|
348
|
+
if (resp.ok === false) showToast(resp.error?.message || t('profile.error'), true);
|
|
349
|
+
loadMemories();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Bulk edit: checkbox-select rows, then change visibility (or delete) for all at once.
|
|
353
|
+
const [selectedKeys, setSelectedKeys] = useState(new Set());
|
|
354
|
+
const [bulkVis, setBulkVis] = useState('private');
|
|
355
|
+
const toggleSelected = (key) => setSelectedKeys(prev => {
|
|
356
|
+
const next = new Set(prev);
|
|
357
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
358
|
+
return next;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
async function applyBulkVis() {
|
|
362
|
+
const targets = (memories || []).filter(m => selectedKeys.has(m.key));
|
|
363
|
+
const groupId = bulkVis.startsWith('group:') ? bulkVis.slice(6) : undefined;
|
|
364
|
+
const vis = groupId ? 'group' : bulkVis;
|
|
365
|
+
let ok = 0, fail = 0;
|
|
366
|
+
for (const m of targets) {
|
|
367
|
+
try {
|
|
368
|
+
const resp = await memoryService.updateMemoryVisibility(m.key, vis, m.version, groupId);
|
|
369
|
+
if (resp.ok === false) fail++; else ok++;
|
|
370
|
+
} catch { fail++; }
|
|
279
371
|
}
|
|
372
|
+
showToast((t('profile.memory.bulkDone') || '{n} updated').replace('{n}', String(ok)) + (fail ? ` · ${fail} ${t('profile.error') || 'failed'}` : ''), fail > 0);
|
|
373
|
+
setSelectedKeys(new Set());
|
|
374
|
+
loadMemories();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function bulkDelete() {
|
|
378
|
+
const n = selectedKeys.size;
|
|
379
|
+
confirm((t('profile.memory.bulkDeleteConfirm') || 'Delete {n} memory entries? This cannot be undone.').replace('{n}', String(n)), async () => {
|
|
380
|
+
let ok = 0;
|
|
381
|
+
for (const key of selectedKeys) {
|
|
382
|
+
try { const r = await memoryService.deleteMemory(key); if (r.ok !== false) ok++; } catch { /* count below */ }
|
|
383
|
+
}
|
|
384
|
+
showToast((t('profile.memory.bulkDeleted') || '{n} deleted').replace('{n}', String(ok)));
|
|
385
|
+
setSelectedKeys(new Set());
|
|
386
|
+
setExpandedMem(null);
|
|
387
|
+
loadMemories();
|
|
388
|
+
}, { danger: true });
|
|
280
389
|
}
|
|
281
390
|
|
|
282
391
|
async function handleUpdateMemoryTags(key, tags, version) {
|
|
@@ -330,22 +439,59 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
330
439
|
}
|
|
331
440
|
}
|
|
332
441
|
|
|
442
|
+
/* ── Key grouping helpers: keys are already hierarchical (agents.*, organism.<uuid>.*,
|
|
443
|
+
notif.*) — render collapsible groups instead of a flat list of near-identical rows. */
|
|
444
|
+
const shortTok = (tok) => (tok.length >= 18 ? tok.slice(0, 4) + '…' + tok.slice(-5) : tok);
|
|
445
|
+
|
|
446
|
+
function groupOfKey(key) {
|
|
447
|
+
if (key.startsWith('organism.')) {
|
|
448
|
+
const uuid = key.split('.')[1] || '';
|
|
449
|
+
return { id: 'organism.' + uuid, kind: 'organism', uuid };
|
|
450
|
+
}
|
|
451
|
+
const dot = key.indexOf('.');
|
|
452
|
+
if (dot < 0) return { id: '_other', kind: 'other' };
|
|
453
|
+
return { id: key.slice(0, dot), kind: 'plain' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function groupLabel(g) {
|
|
457
|
+
if (g.kind === 'organism') return 'organism: ' + (orgNames[g.uuid] || shortTok(g.uuid));
|
|
458
|
+
if (g.kind === 'other') return t('profile.memory.groupOther');
|
|
459
|
+
return g.id;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Shortened remainder inside a group: strip the group prefix, drop the 'w.' workspace
|
|
463
|
+
// marker, middle-ellipsize uuid-ish tokens, split the leading container with '›'.
|
|
464
|
+
// The full key stays in the row's title attribute (and in the expanded detail).
|
|
465
|
+
function displayRemainder(key, g) {
|
|
466
|
+
let rest = g.kind === 'organism' ? key.slice(('organism.' + g.uuid + '.').length)
|
|
467
|
+
: g.kind === 'plain' ? key.slice(g.id.length + 1)
|
|
468
|
+
: key;
|
|
469
|
+
if (rest.startsWith('w.')) rest = rest.slice(2);
|
|
470
|
+
const toks = rest.split('.').map(shortTok);
|
|
471
|
+
if (toks.length > 1 && toks[0].startsWith('ws-')) return toks[0] + ' › ' + toks.slice(1).join('.');
|
|
472
|
+
if (toks.length > 1 && toks[toks.length - 1].includes('…')) return toks.slice(0, -1).join('.') + ' › ' + toks[toks.length - 1];
|
|
473
|
+
return toks.join('.');
|
|
474
|
+
}
|
|
475
|
+
|
|
333
476
|
const renderEntries = () => {
|
|
334
|
-
const searchRef = useRef(null);
|
|
335
477
|
if (!memories) return html`<${Spinner} text=${t('profile.memory.loading')} />`;
|
|
336
478
|
|
|
337
|
-
//
|
|
338
|
-
const
|
|
479
|
+
// Tag counts across memories — the cloud shows the most-used first, capped at 10.
|
|
480
|
+
const tagCounts = new Map();
|
|
339
481
|
for (const m of memories) {
|
|
340
|
-
if (m.tags) for (const tag of m.tags)
|
|
482
|
+
if (m.tags) for (const tag of m.tags) tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
341
483
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
)
|
|
348
|
-
|
|
484
|
+
const tagsByFreq = [...tagCounts.keys()].sort((a, b) => (tagCounts.get(b) - tagCounts.get(a)) || a.localeCompare(b));
|
|
485
|
+
|
|
486
|
+
// Filter: selected tags AND the live type-to-filter text (key, tags, value).
|
|
487
|
+
const ft = filterText.trim().toLowerCase();
|
|
488
|
+
const filtered = sortEntries(memories.filter(m => {
|
|
489
|
+
if (memTagFilter.size > 0 && !(m.tags && [...memTagFilter].every(tag => m.tags.includes(tag)))) return false;
|
|
490
|
+
if (!ft) return true;
|
|
491
|
+
if (m.key.toLowerCase().includes(ft)) return true;
|
|
492
|
+
if (m.tags && m.tags.some(tag => tag.toLowerCase().includes(ft))) return true;
|
|
493
|
+
try { return JSON.stringify(m.value).toLowerCase().includes(ft); } catch { return false; }
|
|
494
|
+
}));
|
|
349
495
|
|
|
350
496
|
const toggleMemTag = (tag) => {
|
|
351
497
|
setMemTagFilter(prev => {
|
|
@@ -355,12 +501,126 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
355
501
|
});
|
|
356
502
|
};
|
|
357
503
|
|
|
504
|
+
// Group in sorted order (group order = first appearance, so sort semantics hold).
|
|
505
|
+
const groupsOrdered = [];
|
|
506
|
+
const byId = new Map();
|
|
507
|
+
for (const m of filtered) {
|
|
508
|
+
const g = groupOfKey(m.key);
|
|
509
|
+
let entry = byId.get(g.id);
|
|
510
|
+
if (!entry) { entry = { ...g, items: [] }; byId.set(g.id, entry); groupsOrdered.push(entry); }
|
|
511
|
+
entry.items.push(m);
|
|
512
|
+
}
|
|
513
|
+
// An active filter force-expands all groups — a hit hidden in a collapsed group reads as "no hit".
|
|
514
|
+
const filtering = !!ft || memTagFilter.size > 0;
|
|
515
|
+
|
|
516
|
+
const renderRow = (m, g) => html`
|
|
517
|
+
<div key=${m.key}>
|
|
518
|
+
<div class="mem-item mem-item--grouped" onClick=${() => setExpandedMem(expandedMem === m.key ? null : m.key)}>
|
|
519
|
+
<input type="checkbox" class="mem-row-check" checked=${selectedKeys.has(m.key)}
|
|
520
|
+
onClick=${(e) => e.stopPropagation()} onChange=${() => toggleSelected(m.key)} />
|
|
521
|
+
<span class="mem-key" title=${m.key}>${escHtml(displayRemainder(m.key, g))}</span>
|
|
522
|
+
<span class="mem-time" title="${m.created_at ? new Date(m.created_at).toLocaleString() : ''} / ${m.updated_at ? new Date(m.updated_at).toLocaleString() : ''}">
|
|
523
|
+
${formatRelativeTime(m.updated_at || m.created_at)}
|
|
524
|
+
</span>
|
|
525
|
+
<${VisibilityPill} visibility=${m.visibility || 'private'}
|
|
526
|
+
onClick=${(e) => { e.stopPropagation(); setVisPopoverFor(visPopoverFor === m.key ? null : m.key); }} />
|
|
527
|
+
${keyHasRules(m.key) && html`<span class="shield-icon" title=${t('permissions.sharingRules')} onClick=${(e) => { e.stopPropagation(); loadKeyPerms(m.key); }}>\u{1F6E1}️</span>`}
|
|
528
|
+
${fedConsents[m.key] && html`<span class="badge badge-success pf-fed-badge">${t('profile.memory.syncedToFederation')}</span>`}
|
|
529
|
+
</div>
|
|
530
|
+
${visPopoverFor === m.key && html`
|
|
531
|
+
<div class="mem-vis-pop" onClick=${(e) => e.stopPropagation()}>
|
|
532
|
+
${VIS_OPTIONS.filter(v => v !== 'group').map(v => html`
|
|
533
|
+
<button key=${v} class="mem-vis-opt ${(m.visibility || 'private') === v ? 'mem-vis-opt--current' : ''}"
|
|
534
|
+
onClick=${() => applyVis(m, v)}>${t('knowledge.visibility.' + v)}</button>
|
|
535
|
+
`)}
|
|
536
|
+
${groups.length > 0
|
|
537
|
+
? groups.map(grp => html`
|
|
538
|
+
<button key=${grp.id} class="mem-vis-opt ${m.visibility === 'group' && m.group_id === grp.id ? 'mem-vis-opt--current' : ''}"
|
|
539
|
+
onClick=${() => applyVis(m, 'group', grp.id)}>${t('knowledge.visibility.group')}: ${escHtml(grp.name)}</button>
|
|
540
|
+
`)
|
|
541
|
+
: html`<button class="mem-vis-opt mem-vis-opt--dim" onClick=${() => {
|
|
542
|
+
try { sessionStorage.setItem('aimeat.access.focus', 'groups'); } catch { /* noop */ }
|
|
543
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'access' } }));
|
|
544
|
+
}}>${t('profile.memory.createGroupBtn')}</button>`}
|
|
545
|
+
</div>
|
|
546
|
+
`}
|
|
547
|
+
${expandedMem === m.key && html`
|
|
548
|
+
<div class="mem-detail">
|
|
549
|
+
<div class="mem-detail-key" title=${m.key}>${escHtml(m.key)}</div>
|
|
550
|
+
<pre>${typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || '')}</pre>
|
|
551
|
+
<div class="mem-detail-visrow mb-half">
|
|
552
|
+
<span class="text-meta-sm">${t('profile.memory.visLabel')}</span>
|
|
553
|
+
<select class="input-field mem-vis-select" value=${m.visibility || 'private'}
|
|
554
|
+
onClick=${(e) => e.stopPropagation()}
|
|
555
|
+
onChange=${(e) => handleQuickVis(m, e.target.value)}>
|
|
556
|
+
${VIS_OPTIONS.map(v => html`<option key=${v} value=${v}>${t('knowledge.visibility.' + v)}</option>`)}
|
|
557
|
+
</select>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="mb-half">
|
|
560
|
+
<button class="btn-outline btn-sm" onClick=${(e) => { e.stopPropagation(); setEditingMemTags(editingMemTags === m.key ? null : m.key); }}>
|
|
561
|
+
${t('tags.editTags') || 'Edit tags'}
|
|
562
|
+
</button>
|
|
563
|
+
<button class="btn-outline btn-sm" onClick=${(e) => { e.stopPropagation(); if (keyRulesPopover?.key === m.key) setKeyRulesPopover(null); else loadKeyPerms(m.key); }}>
|
|
564
|
+
\u{1F6E1}️ ${t('permissions.sharingRules')}
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
${editingMemTags === m.key && html`
|
|
568
|
+
<div class="mb-half">
|
|
569
|
+
<${TagEditor} tags=${m.tags || []} onSave=${(tags) => handleUpdateMemoryTags(m.key, tags, m.version)} />
|
|
570
|
+
</div>
|
|
571
|
+
`}
|
|
572
|
+
${editingMemTags !== m.key && m.tags?.length > 0 && html`<div class="text-meta-sm mb-half">${m.tags.join(', ')}</div>`}
|
|
573
|
+
${keyRulesPopover && keyRulesPopover.key === m.key && html`
|
|
574
|
+
<div class="key-rules-box">
|
|
575
|
+
<div class="flex-between mb-half">
|
|
576
|
+
<strong class="text-caption">\u{1F6E1}️ ${t('permissions.sharingRules')}</strong>
|
|
577
|
+
<button class="btn-outline btn-sm" onClick=${() => setKeyRulesPopover(null)}>✕</button>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="text-meta-sm mb-half">${t('profile.memory.visLabel')} ${t('knowledge.visibility.' + keyRulesPopover.visibility) || keyRulesPopover.visibility}</div>
|
|
580
|
+
${keyRulesPopover.rules.length === 0
|
|
581
|
+
? html`<div class="text-meta pf-italic">${t('permissions.noRules')}</div>`
|
|
582
|
+
: keyRulesPopover.rules.map(r => html`
|
|
583
|
+
<div class="pf-rule-row">
|
|
584
|
+
${recipientBadge(r.recipient)}
|
|
585
|
+
<span class="text-code text-meta-sm">${escHtml(r.data_pattern)}</span>
|
|
586
|
+
<span class="text-meta-sm pf-ml-auto">${escHtml(r.scope || '-')}</span>
|
|
587
|
+
</div>`)
|
|
588
|
+
}
|
|
589
|
+
</div>
|
|
590
|
+
`}
|
|
591
|
+
<div class="mem-actions">
|
|
592
|
+
<button class="btn-sm" onClick=${() => setEditModal({ key: m.key, value: typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || ''), visibility: m.visibility || 'private', version: m.version, isJson: typeof m.value === 'object' })}>${t('profile.memory.editBtn')}</button>
|
|
593
|
+
${fedConsents[m.key]
|
|
594
|
+
? html`<button class="btn-outline btn-sm" disabled=${togglingFed === m.key}
|
|
595
|
+
onClick=${() => handleStopSharing(m.key)}>
|
|
596
|
+
${togglingFed === m.key ? '...' : t('profile.memory.stopSharing')}
|
|
597
|
+
</button>`
|
|
598
|
+
: html`<button class="btn-ghost btn-sm" disabled=${togglingFed === m.key}
|
|
599
|
+
onClick=${() => handleShareToFederation(m.key)}>
|
|
600
|
+
${togglingFed === m.key ? '...' : t('profile.memory.shareToFederation')}
|
|
601
|
+
</button>`
|
|
602
|
+
}
|
|
603
|
+
${session.federated && html`
|
|
604
|
+
<button class="btn-ghost" onClick=${() => doPull(m.key)} title=${t('profile.memory.pullFromHome')}>
|
|
605
|
+
↓ ${t('profile.memory.pullFromHome')}
|
|
606
|
+
</button>
|
|
607
|
+
<button class="btn-ghost" onClick=${() => doPush(m.key)} title=${t('profile.memory.pushToHome')}>
|
|
608
|
+
↑ ${t('profile.memory.pushToHome')}
|
|
609
|
+
</button>
|
|
610
|
+
`}
|
|
611
|
+
<button class="btn-danger mem-delete-btn" onClick=${() => handleDeleteMemory(m.key)}>${t('profile.memory.deleteBtn')}</button>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
`}
|
|
615
|
+
</div>
|
|
616
|
+
`;
|
|
617
|
+
|
|
358
618
|
return html`
|
|
359
619
|
<div class="action-bar">
|
|
360
620
|
<div class="search-bar">
|
|
361
|
-
<input type="text"
|
|
362
|
-
|
|
363
|
-
|
|
621
|
+
<input type="text" class="input-field" placeholder=${t('profile.memory.filterType')}
|
|
622
|
+
value=${filterText} onInput=${e => setFilterText(e.target.value)} />
|
|
623
|
+
${filterText && html`<button class="btn-ghost btn-sm" onClick=${() => setFilterText('')}>✕</button>`}
|
|
364
624
|
</div>
|
|
365
625
|
<div class="mem-sort-bar">
|
|
366
626
|
<label class="text-meta-sm">${t('profile.memory.sortLabel')}</label>
|
|
@@ -372,102 +632,36 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
372
632
|
</div>
|
|
373
633
|
<button class="btn-primary" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
|
|
374
634
|
</div>
|
|
375
|
-
<${TagCloud} tags=${
|
|
635
|
+
<${TagCloud} tags=${tagsByFreq} selected=${memTagFilter} onToggle=${toggleMemTag} onClear=${() => setMemTagFilter(new Set())} limit=${10} />
|
|
376
636
|
${showMemForm && html`<${MemoryForm} onSave=${handleCreateMemory} onCancel=${() => setShowMemForm(false)} groups=${groups} />`}
|
|
637
|
+
${selectedKeys.size > 0 && html`
|
|
638
|
+
<div class="mem-bulkbar">
|
|
639
|
+
<span class="mem-bulkbar-count">${(t('profile.memory.bulkSelected') || '{n} selected').replace('{n}', String(selectedKeys.size))}</span>
|
|
640
|
+
<select class="input-field mem-vis-select" value=${bulkVis} onChange=${e => setBulkVis(e.target.value)}>
|
|
641
|
+
${VIS_OPTIONS.filter(v => v !== 'group').map(v => html`<option key=${v} value=${v}>${t('knowledge.visibility.' + v)}</option>`)}
|
|
642
|
+
${groups.map(grp => html`<option key=${grp.id} value=${'group:' + grp.id}>${t('knowledge.visibility.group')}: ${grp.name}</option>`)}
|
|
643
|
+
</select>
|
|
644
|
+
<button class="btn-outline btn-sm" onClick=${applyBulkVis}>${t('profile.memory.bulkApply') || 'Change visibility'}</button>
|
|
645
|
+
<button class="btn-danger btn-sm" onClick=${bulkDelete}>${t('profile.memory.deleteBtn')}</button>
|
|
646
|
+
<button class="btn-ghost btn-sm" onClick=${() => setSelectedKeys(new Set())}>✕ ${t('profile.memory.bulkClear') || 'Clear selection'}</button>
|
|
647
|
+
</div>
|
|
648
|
+
`}
|
|
377
649
|
${filtered.length === 0
|
|
378
650
|
? html`<div class="empty">${memories.length > 0 ? (t('tags.noMatch') || 'No items match selected tags') : t('profile.memory.empty')}</div>`
|
|
379
|
-
:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
onClick=${(e) => { e.stopPropagation(); if (nextVis === 'group') { setGroupPickerFor(m.key); } else { handleUpdateVisibility(m.key, nextVis, m.version); } }}
|
|
391
|
-
title="${t('knowledge.visibility.' + vis)} → ${t('knowledge.visibility.' + nextVis)}"
|
|
392
|
-
style="background:${visBg[vis]};color:${visColor[vis]};border-color:${visColor[vis]}"
|
|
393
|
-
>${t('knowledge.visibility.' + vis)} ▾</button>`;
|
|
394
|
-
})()}
|
|
395
|
-
<span class="shield-icon" title=${t('permissions.sharingRules')} onClick=${(e) => { e.stopPropagation(); loadKeyPerms(m.key); }}>\u{1F6E1}\uFE0F</span>
|
|
396
|
-
${fedConsents[m.key] && html`<span class="badge badge-success pf-fed-badge">${t('profile.memory.syncedToFederation')}</span>`}
|
|
397
|
-
</div>
|
|
398
|
-
${groupPickerFor === m.key && html`
|
|
399
|
-
<div class="agd-group-picker" onClick=${(e) => e.stopPropagation()}>
|
|
400
|
-
<div class="text-caption mb-xs">${t('profile.memory.selectGroup')}</div>
|
|
401
|
-
${groups.length === 0
|
|
402
|
-
? html`<div class="text-meta-sm">${t('profile.memory.noGroups')}</div>`
|
|
403
|
-
: groups.map(g => html`
|
|
404
|
-
<button class="btn-ghost agd-group-option" onClick=${() => {
|
|
405
|
-
handleUpdateVisibility(m.key, 'group', m.version, g.id);
|
|
406
|
-
setGroupPickerFor(null);
|
|
407
|
-
}}>${g.name} (${g.members?.length || 0} ${t('profile.access.sharingGroups.members')})</button>
|
|
408
|
-
`)
|
|
409
|
-
}
|
|
410
|
-
<button class="btn-outline btn-sm mt-xs" onClick=${() => { setGroupPickerFor(null); handleUpdateVisibility(m.key, 'public', m.version); }}>${t('profile.memory.skipGroup')}</button>
|
|
411
|
-
</div>
|
|
412
|
-
`}
|
|
413
|
-
${expandedMem === m.key && html`
|
|
414
|
-
<div class="mem-detail">
|
|
415
|
-
<pre>${typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || '')}</pre>
|
|
416
|
-
<div class="mb-half">
|
|
417
|
-
<button class="btn-outline btn-sm" onClick=${(e) => { e.stopPropagation(); setEditingMemTags(editingMemTags === m.key ? null : m.key); }}>
|
|
418
|
-
${t('tags.editTags') || 'Edit tags'}
|
|
419
|
-
</button>
|
|
420
|
-
</div>
|
|
421
|
-
${editingMemTags === m.key && html`
|
|
422
|
-
<div class="mb-half">
|
|
423
|
-
<${TagEditor} tags=${m.tags || []} onSave=${(tags) => handleUpdateMemoryTags(m.key, tags, m.version)} />
|
|
424
|
-
</div>
|
|
425
|
-
`}
|
|
426
|
-
${editingMemTags !== m.key && m.tags?.length > 0 && html`<div class="text-meta-sm mb-half">${m.tags.join(', ')}</div>`}
|
|
427
|
-
${keyRulesPopover && keyRulesPopover.key === m.key && html`
|
|
428
|
-
<div class="key-rules-box">
|
|
429
|
-
<div class="flex-between mb-half">
|
|
430
|
-
<strong class="text-caption">\u{1F6E1}\uFE0F ${t('permissions.sharingRules')}</strong>
|
|
431
|
-
<button class="btn-outline btn-sm" onClick=${() => setKeyRulesPopover(null)}>\u2715</button>
|
|
432
|
-
</div>
|
|
433
|
-
<div class="text-meta-sm mb-half">Visibility: ${keyRulesPopover.visibility}</div>
|
|
434
|
-
${keyRulesPopover.rules.length === 0
|
|
435
|
-
? html`<div class="text-meta pf-italic">${t('permissions.noRules')}</div>`
|
|
436
|
-
: keyRulesPopover.rules.map(r => html`
|
|
437
|
-
<div class="pf-rule-row">
|
|
438
|
-
${recipientBadge(r.recipient)}
|
|
439
|
-
<span class="text-code text-meta-sm">${escHtml(r.data_pattern)}</span>
|
|
440
|
-
<span class="text-meta-sm pf-ml-auto">${escHtml(r.scope || '-')}</span>
|
|
441
|
-
</div>`)
|
|
442
|
-
}
|
|
443
|
-
</div>
|
|
444
|
-
`}
|
|
445
|
-
<div class="mem-actions">
|
|
446
|
-
<button class="btn-sm" onClick=${() => setEditModal({ key: m.key, value: typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || ''), visibility: m.visibility || 'private', version: m.version })}>${t('profile.memory.editBtn')}</button>
|
|
447
|
-
<button class="btn-danger" onClick=${() => handleDeleteMemory(m.key)}>${t('profile.memory.deleteBtn')}</button>
|
|
448
|
-
${fedConsents[m.key]
|
|
449
|
-
? html`<button class="btn-outline btn-sm" disabled=${togglingFed === m.key}
|
|
450
|
-
onClick=${() => handleStopSharing(m.key)}>
|
|
451
|
-
${togglingFed === m.key ? '...' : t('profile.memory.stopSharing')}
|
|
452
|
-
</button>`
|
|
453
|
-
: html`<button class="btn-ghost btn-sm" disabled=${togglingFed === m.key}
|
|
454
|
-
onClick=${() => handleShareToFederation(m.key)}>
|
|
455
|
-
${togglingFed === m.key ? '...' : t('profile.memory.shareToFederation')}
|
|
456
|
-
</button>`
|
|
457
|
-
}
|
|
458
|
-
${session.federated && html`
|
|
459
|
-
<button class="btn-ghost" onClick=${() => doPull(m.key)} title=${t('profile.memory.pullFromHome')}>
|
|
460
|
-
↓ ${t('profile.memory.pullFromHome')}
|
|
461
|
-
</button>
|
|
462
|
-
<button class="btn-ghost" onClick=${() => doPush(m.key)} title=${t('profile.memory.pushToHome')}>
|
|
463
|
-
↑ ${t('profile.memory.pushToHome')}
|
|
464
|
-
</button>
|
|
465
|
-
`}
|
|
466
|
-
</div>
|
|
651
|
+
: groupsOrdered.map(g => {
|
|
652
|
+
const collapsed = !filtering && collapsedGroups.has(g.id);
|
|
653
|
+
return html`
|
|
654
|
+
<div class="mem-group" key=${g.id}>
|
|
655
|
+
<button class="mem-group-header" onClick=${() => toggleGroupCollapsed(g.id)}>
|
|
656
|
+
<span class="pf-chevron ${collapsed ? '' : 'pf-chevron-open'}">▼</span>
|
|
657
|
+
<span class="mem-group-name">${escHtml(groupLabel(g))}</span>
|
|
658
|
+
<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>
|
|
659
|
+
${g.kind === 'organism' && orgNames[g.uuid] && html`<span class="mem-group-sub">${shortTok(g.uuid)}</span>`}
|
|
660
|
+
</button>
|
|
661
|
+
${!collapsed && g.items.map(m => renderRow(m, g))}
|
|
467
662
|
</div>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
`)
|
|
663
|
+
`;
|
|
664
|
+
})
|
|
471
665
|
}`;
|
|
472
666
|
};
|
|
473
667
|
|
|
@@ -517,7 +711,6 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
517
711
|
const fKey = f.key || f.name;
|
|
518
712
|
const isImage = f.mime_type?.startsWith('image');
|
|
519
713
|
const vis = f.visibility || 'private';
|
|
520
|
-
const nextVis = cycleVis[(cycleVis.indexOf(vis) + 1) % cycleVis.length];
|
|
521
714
|
return html`
|
|
522
715
|
<div class="file-card">
|
|
523
716
|
${isImage
|
|
@@ -528,11 +721,11 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
528
721
|
<div class="file-name">${escHtml(fKey)}</div>
|
|
529
722
|
<div class="file-meta">
|
|
530
723
|
${f.size ? Math.round(f.size / 1024) + ' KB' : ''}
|
|
531
|
-
<
|
|
532
|
-
onClick=${(e) =>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
724
|
+
<select class="input-field mem-vis-select" value=${vis} title=${t('profile.files.visLabel')}
|
|
725
|
+
onClick=${(e) => e.stopPropagation()}
|
|
726
|
+
onChange=${(e) => handleUpdateFileVisibility(fKey, e.target.value)}>
|
|
727
|
+
${VIS_OPTIONS.map(v => html`<option key=${v} value=${v}>${t('knowledge.visibility.' + v)}</option>`)}
|
|
728
|
+
</select>
|
|
536
729
|
</div>
|
|
537
730
|
${editingFileTags === fKey
|
|
538
731
|
? html`<${TagEditor} tags=${f.tags || []} onSave=${(tags) => handleUpdateFileTags(fKey, tags)} />`
|
|
@@ -709,10 +902,8 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
709
902
|
<div class="mem-browse-panel">
|
|
710
903
|
<div class="mem-browse-header">
|
|
711
904
|
<div>
|
|
712
|
-
<div class="section-title">${t('profile.memory.discoverTitle')}</div>
|
|
713
905
|
<div class="section-desc">${t('profile.memory.discoverDesc')}</div>
|
|
714
906
|
</div>
|
|
715
|
-
<button class="btn-outline btn-sm" onClick=${closeBrowse}>${t('profile.memory.cancelBtn')}</button>
|
|
716
907
|
</div>
|
|
717
908
|
<div class="mem-discover-search mb-half">
|
|
718
909
|
<input type="text" class="input-field" placeholder=${t('profile.memory.discoverSearchPlaceholder')}
|
|
@@ -749,7 +940,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
749
940
|
${entry.tags?.length > 0 && html`<span class="text-meta-sm mem-browse-tags" title=${entry.tags.join(', ')}>${entry.tags.join(', ')}</span>`}
|
|
750
941
|
<span class="mem-time">${formatRelativeTime(entry.updated_at || entry.created_at)}</span>
|
|
751
942
|
</div>
|
|
752
|
-
<button class="btn-
|
|
943
|
+
<button class="btn-outline btn-sm"
|
|
753
944
|
disabled=${copyingKeys.has(entry.key)}
|
|
754
945
|
onClick=${(e) => { e.stopPropagation(); handleCopyEntry(entry.owner_gaii, entry.key); }}>
|
|
755
946
|
${copyingKeys.has(entry.key) ? '...' : t('profile.memory.discoverCopy')}
|
|
@@ -769,17 +960,14 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
769
960
|
}
|
|
770
961
|
|
|
771
962
|
const isHome = browseMode === 'home';
|
|
772
|
-
const title = isHome ? t('profile.memory.browseHome') : t('profile.memory.browseRemote');
|
|
773
963
|
const desc = isHome ? t('profile.memory.browseHomeDesc') : t('profile.memory.browseRemoteDesc');
|
|
774
964
|
|
|
775
965
|
return html`
|
|
776
966
|
<div class="mem-browse-panel">
|
|
777
967
|
<div class="mem-browse-header">
|
|
778
968
|
<div>
|
|
779
|
-
<div class="section-title">${title}</div>
|
|
780
969
|
<div class="section-desc">${desc}</div>
|
|
781
970
|
</div>
|
|
782
|
-
<button class="btn-outline btn-sm" onClick=${closeBrowse}>${t('profile.memory.cancelBtn')}</button>
|
|
783
971
|
</div>
|
|
784
972
|
|
|
785
973
|
${!isHome && !selectedPeer && html`
|
|
@@ -822,7 +1010,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
822
1010
|
<${VisibilityPill} visibility=${entry.visibility} />
|
|
823
1011
|
${entry.tags?.length > 0 && html`<span class="text-meta-sm mem-browse-tags" title=${entry.tags.join(', ')}>${entry.tags.join(', ')}</span>`}
|
|
824
1012
|
</div>
|
|
825
|
-
<button class="btn-
|
|
1013
|
+
<button class="btn-outline btn-sm"
|
|
826
1014
|
disabled=${pullingKeys.has(entry.key)}
|
|
827
1015
|
onClick=${() => handlePullRemoteEntry(entry.key)}>
|
|
828
1016
|
${pullingKeys.has(entry.key) ? '...' : t('profile.memory.pullEntry')}
|
|
@@ -836,55 +1024,55 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
836
1024
|
`;
|
|
837
1025
|
};
|
|
838
1026
|
|
|
1027
|
+
// Three states of the same list = tabs, not inline panels with their own Cancel buttons.
|
|
1028
|
+
const pickMainTab = (id) => {
|
|
1029
|
+
setMainTab(id);
|
|
1030
|
+
if (id === 'discover') initDiscover();
|
|
1031
|
+
else if (id === 'remote') { if (session.federated) loadBrowseHome(); else initBrowseRemote(); }
|
|
1032
|
+
else closeBrowse();
|
|
1033
|
+
};
|
|
1034
|
+
|
|
839
1035
|
return html`
|
|
840
1036
|
<div class="section-title">${t('profile.memory.title')}</div>
|
|
841
1037
|
<div class="section-desc">${t('profile.memory.desc')}</div>
|
|
842
1038
|
|
|
843
|
-
<div class="
|
|
844
|
-
<button class="
|
|
845
|
-
|
|
846
|
-
</button>
|
|
847
|
-
${session.federated && html`
|
|
848
|
-
<button class="btn-outline btn-sm" onClick=${loadBrowseHome}>
|
|
849
|
-
${t('profile.memory.browseHome')}
|
|
850
|
-
</button>
|
|
851
|
-
`}
|
|
852
|
-
${!session.federated && html`
|
|
853
|
-
<button class="btn-outline btn-sm" onClick=${initBrowseRemote}>
|
|
854
|
-
${t('profile.memory.browseRemote')}
|
|
855
|
-
</button>
|
|
856
|
-
`}
|
|
1039
|
+
<div class="sub-tabs">
|
|
1040
|
+
<button class="sub-tab ${mainTab === 'own' ? 'active' : ''}" onClick=${() => pickMainTab('own')}>${t('profile.memory.tabOwn')}</button>
|
|
1041
|
+
<button class="sub-tab ${mainTab === 'discover' ? 'active' : ''}" onClick=${() => pickMainTab('discover')}>${t('profile.memory.tabPublic')}</button>
|
|
1042
|
+
<button class="sub-tab ${mainTab === 'remote' ? 'active' : ''}" onClick=${() => pickMainTab('remote')}>${session.federated ? t('profile.memory.browseHome') : t('profile.memory.tabRemote')}</button>
|
|
857
1043
|
</div>
|
|
1044
|
+
|
|
858
1045
|
${session.federated && html`
|
|
859
1046
|
<div class="alert alert-info mb-half">
|
|
860
1047
|
<span class="alert-msg">${t('profile.memory.federatedSession')}</span>
|
|
861
1048
|
</div>
|
|
862
1049
|
`}
|
|
863
1050
|
|
|
864
|
-
${renderBrowsePanel()
|
|
1051
|
+
${mainTab !== 'own' ? renderBrowsePanel() : html`
|
|
1052
|
+
${agents.length > 1 && html`
|
|
1053
|
+
<div class="agent-selector mb-half">
|
|
1054
|
+
<label class="text-meta pf-mr-half">${t('profile.memory.agent') || 'Agent'}:</label>
|
|
1055
|
+
<select class="input-field pf-select-inline"
|
|
1056
|
+
value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); }}>
|
|
1057
|
+
<option value="">${t('profile.memory.defaultAgent') || 'Default agent'}</option>
|
|
1058
|
+
${agents.map(a => html`<option key=${a.gaii} value=${a.gaii}>${escHtml(a.name || a.gaii)}${a.display_name ? ` — ${escHtml(a.display_name)}` : ''}</option>`)}
|
|
1059
|
+
</select>
|
|
1060
|
+
</div>
|
|
1061
|
+
`}
|
|
865
1062
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
<
|
|
869
|
-
<select class="input-field pf-select-inline"
|
|
870
|
-
value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); }}>
|
|
871
|
-
<option value="">${t('profile.memory.defaultAgent') || 'Default agent'}</option>
|
|
872
|
-
${agents.map(a => html`<option key=${a.gaii} value=${a.gaii}>${escHtml(a.name || a.gaii)}${a.display_name ? ` — ${escHtml(a.display_name)}` : ''}</option>`)}
|
|
873
|
-
</select>
|
|
1063
|
+
<div class="sub-tabs">
|
|
1064
|
+
<button class="sub-tab ${memSubTab === 'entries' ? 'active' : ''}" onClick=${() => setMemSubTab('entries')}>${t('profile.memory.entries')}</button>
|
|
1065
|
+
<button class="sub-tab ${memSubTab === 'files' ? 'active' : ''}" onClick=${() => setMemSubTab('files')}>${t('profile.memory.files')}</button>
|
|
874
1066
|
</div>
|
|
1067
|
+
${memSubTab === 'entries' ? renderEntries() : renderFilesList()}
|
|
875
1068
|
`}
|
|
876
1069
|
|
|
877
|
-
<div class="sub-tabs">
|
|
878
|
-
<button class="sub-tab ${memSubTab === 'entries' ? 'active' : ''}" onClick=${() => setMemSubTab('entries')}>${t('profile.memory.entries')}</button>
|
|
879
|
-
<button class="sub-tab ${memSubTab === 'files' ? 'active' : ''}" onClick=${() => setMemSubTab('files')}>${t('profile.memory.files')}</button>
|
|
880
|
-
</div>
|
|
881
|
-
${memSubTab === 'entries' ? renderEntries() : renderFilesList()}
|
|
882
|
-
|
|
883
1070
|
${editModal && html`<${EditMemoryModal}
|
|
884
1071
|
memKey=${editModal.key}
|
|
885
1072
|
initialValue=${editModal.value}
|
|
886
1073
|
initialVisibility=${editModal.visibility}
|
|
887
1074
|
initialVersion=${editModal.version}
|
|
1075
|
+
isJson=${editModal.isJson}
|
|
888
1076
|
groups=${groups}
|
|
889
1077
|
onSave=${(v, vis, ver, gId) => handleSaveEdit(editModal.key, v, vis, ver, gId)}
|
|
890
1078
|
onCancel=${() => setEditModal(null)} />`}
|
|
@@ -926,16 +1114,23 @@ function MemoryForm({ onSave, onCancel, groups }) {
|
|
|
926
1114
|
<select class="input-field" value=${vis} onChange=${e => setVis(e.target.value)}>
|
|
927
1115
|
<option value="private">${t('profile.memory.visPrivate')}</option>
|
|
928
1116
|
<option value="shared">${t('profile.memory.visShared')}</option>
|
|
929
|
-
<option value="group"
|
|
1117
|
+
<option value="group">${t('knowledge.visibility.group')}</option>
|
|
930
1118
|
<option value="public">${t('profile.memory.visPublic')}</option>
|
|
931
1119
|
</select>
|
|
932
1120
|
</div>
|
|
933
1121
|
${vis === 'group' && html`
|
|
934
1122
|
<div class="form-row"><label>${t('profile.memory.selectGroup')}</label>
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1123
|
+
${(groups || []).length === 0
|
|
1124
|
+
? html`
|
|
1125
|
+
<div class="text-meta-sm mb-xs">${t('profile.memory.noGroups')}</div>
|
|
1126
|
+
<button type="button" class="btn-outline btn-sm" onClick=${() => {
|
|
1127
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'access' } }));
|
|
1128
|
+
}}>${t('profile.memory.createGroupBtn')}</button>`
|
|
1129
|
+
: html`
|
|
1130
|
+
<select class="input-field" value=${groupId} onChange=${e => setGroupId(e.target.value)}>
|
|
1131
|
+
<option value="">-- ${t('profile.memory.selectGroup')} --</option>
|
|
1132
|
+
${(groups || []).map(g => html`<option value=${g.id}>${g.name}</option>`)}
|
|
1133
|
+
</select>`}
|
|
939
1134
|
</div>
|
|
940
1135
|
`}
|
|
941
1136
|
<div class="form-row"><label>${t('profile.memory.tagsLabel')}</label><input class="input-field" placeholder=${t('profile.memory.tagsPlaceholder')} value=${tags} onInput=${e => setTags(e.target.value)} /></div>
|
|
@@ -1069,51 +1264,54 @@ function FileUploadForm({ onUpload, onCancel }) {
|
|
|
1069
1264
|
</div>`;
|
|
1070
1265
|
}
|
|
1071
1266
|
|
|
1072
|
-
function EditMemoryModal({ memKey, initialValue, initialVisibility, initialVersion, onSave, onCancel, groups }) {
|
|
1267
|
+
function EditMemoryModal({ memKey, initialValue, initialVisibility, initialVersion, isJson, onSave, onCancel, groups }) {
|
|
1073
1268
|
const [value, setValue] = useState(initialValue);
|
|
1074
1269
|
const [vis, setVis] = useState(initialVisibility || 'private');
|
|
1075
1270
|
const [groupId, setGroupId] = useState('');
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
setGroupId('');
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1271
|
+
|
|
1272
|
+
// Broken JSON in a memory key crashes the agent that reads it — validate before save.
|
|
1273
|
+
// Validation applies when the stored value was an object, or the draft clearly is one.
|
|
1274
|
+
const looksJson = isJson || /^[[{]/.test(String(value || '').trim());
|
|
1275
|
+
let jsonError = null;
|
|
1276
|
+
if (looksJson) {
|
|
1277
|
+
try { JSON.parse(value); } catch (e) { jsonError = e.message; }
|
|
1278
|
+
}
|
|
1279
|
+
const groupMissing = vis === 'group' && !groupId && (groups || []).length === 0;
|
|
1280
|
+
const canSave = !jsonError && !(vis === 'group' && !groupId);
|
|
1281
|
+
|
|
1090
1282
|
return html`
|
|
1091
1283
|
<${Modal} open=${true} onClose=${onCancel} title=${`${t('profile.memory.editTitle')}: ${memKey}`}>
|
|
1092
1284
|
<div class="form-row flex-row mb-half">
|
|
1093
1285
|
<label class="pf-label-inline">${t('profile.memory.visLabel')}</label>
|
|
1094
|
-
<
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
>${t('knowledge.visibility.' + vis)} ▾</button>
|
|
1286
|
+
<select class="input-field mem-vis-select" value=${vis}
|
|
1287
|
+
onChange=${e => { setVis(e.target.value); if (e.target.value !== 'group') setGroupId(''); }}>
|
|
1288
|
+
${['private', 'owner', 'group', 'public'].map(v => html`<option key=${v} value=${v}>${t('knowledge.visibility.' + v)}</option>`)}
|
|
1289
|
+
</select>
|
|
1099
1290
|
</div>
|
|
1100
|
-
${
|
|
1291
|
+
${vis === 'group' && html`
|
|
1101
1292
|
<div class="form-row mb-half">
|
|
1102
1293
|
<label>${t('profile.memory.selectGroup')}</label>
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1294
|
+
${(groups || []).length === 0
|
|
1295
|
+
? html`
|
|
1296
|
+
<div class="text-meta-sm mb-xs">${t('profile.memory.noGroups')}</div>
|
|
1297
|
+
<button class="btn-outline btn-sm" onClick=${() => {
|
|
1298
|
+
onCancel();
|
|
1299
|
+
try { sessionStorage.setItem('aimeat.access.focus', 'groups'); } catch { /* noop */ }
|
|
1300
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'access' } }));
|
|
1301
|
+
}}>${t('profile.memory.createGroupBtn')}</button>`
|
|
1302
|
+
: html`
|
|
1303
|
+
<select class="input-field" value=${groupId} onChange=${e => setGroupId(e.target.value)}>
|
|
1304
|
+
<option value="">-- ${t('profile.memory.selectGroup')} --</option>
|
|
1305
|
+
${(groups || []).map(g => html`<option value=${g.id}>${g.name}</option>`)}
|
|
1306
|
+
</select>`}
|
|
1112
1307
|
</div>
|
|
1113
1308
|
`}
|
|
1114
|
-
<textarea class="input-field
|
|
1309
|
+
<textarea class="input-field mem-edit-textarea ${jsonError ? 'mem-edit-textarea--error' : ''}" rows="14"
|
|
1310
|
+
value=${value} onInput=${e => setValue(e.target.value)}></textarea>
|
|
1311
|
+
${jsonError && html`<div class="mem-json-error">${t('profile.memory.invalidJson')} — ${jsonError}</div>`}
|
|
1115
1312
|
<div class="form-actions mt-1">
|
|
1116
|
-
<button class="btn-primary"
|
|
1313
|
+
<button class="btn-primary" disabled=${!canSave || groupMissing}
|
|
1314
|
+
onClick=${() => onSave(value, vis, initialVersion, vis === 'group' ? groupId : undefined)}>${t('profile.save')}</button>
|
|
1117
1315
|
<button class="btn-outline" onClick=${onCancel}>${t('profile.cancel')}</button>
|
|
1118
1316
|
</div>
|
|
1119
1317
|
<//>`;
|