aimeat 1.25.0 → 1.26.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/README.md +8 -1
- package/dist/locales/en.json +144 -0
- package/dist/locales/fi.json +144 -0
- package/dist/public/components/ImageDeliverable.js +2 -1
- package/dist/public/components/Modal.js +3 -2
- package/dist/public/components/NotificationBell.js +2 -1
- package/dist/public/components/PresenceDot.js +53 -0
- package/dist/public/css/components/markdown.css +67 -0
- package/dist/public/css/theme.css +30 -0
- package/dist/public/css/views/notebook.css +195 -0
- package/dist/public/css/views/profile.css +15 -0
- package/dist/public/js/api.js +5 -3
- package/dist/public/js/components/auth-image.js +3 -2
- package/dist/public/js/components/tag-cloud.js +1 -1
- package/dist/public/js/components/tag-editor.js +1 -1
- package/dist/public/js/hooks.js +1 -2
- package/dist/public/js/presence-store.js +97 -0
- package/dist/public/js/services/admin.js +14 -5
- package/dist/public/js/services/boards.js +1 -1
- package/dist/public/js/services/ecosystem.js +1 -1
- package/dist/public/js/services/foundry-prompts-base.js +2 -2
- package/dist/public/js/services/foundry-smoke.js +2 -1
- package/dist/public/js/services/foundry-validate.js +6 -8
- package/dist/public/js/services/foundry.js +4 -3
- package/dist/public/js/services/generator-prompts-base.js +2 -2
- package/dist/public/js/services/generator-smoke.js +2 -1
- package/dist/public/js/services/generator-validate.js +5 -7
- package/dist/public/js/services/generator.js +2 -1
- package/dist/public/js/services/memory.js +13 -0
- package/dist/public/js/services/messages.js +2 -1
- package/dist/public/js/services/notebook.js +124 -0
- package/dist/public/js/services/packages.js +3 -2
- package/dist/public/js/services/presence.js +35 -0
- package/dist/public/js/style-testbed.js +6 -5
- package/dist/public/lib/minidenticons.min.js +1 -0
- package/dist/public/lib/yaml.mjs +1 -0
- package/dist/public/spa.html +90 -8
- package/dist/public/types/aimeat-globals.d.ts +52 -0
- package/dist/public/types/vendored-libs.d.ts +56 -0
- package/dist/public/views/_template.js +3 -1
- package/dist/public/views/admin/agent-integration-tab.js +4 -3
- package/dist/public/views/admin/agents-tab.js +3 -2
- package/dist/public/views/admin/config-tab.js +4 -3
- package/dist/public/views/admin/economy-tab.js +4 -3
- package/dist/public/views/admin/federation-tab.js +138 -1
- package/dist/public/views/admin/maintenance-tab.js +1 -1
- package/dist/public/views/admin/messages-tab.js +115 -0
- package/dist/public/views/admin/overview-tab.js +4 -3
- package/dist/public/views/admin/portal-tab.js +90 -2
- package/dist/public/views/admin/push-tab.js +8 -6
- package/dist/public/views/admin/security-tab.js +7 -7
- package/dist/public/views/admin.js +29 -9
- package/dist/public/views/doc-solo.js +3 -1
- package/dist/public/views/guides.js +8 -4
- package/dist/public/views/hobbies.js +4 -2
- package/dist/public/views/marketplace.js +10 -9
- package/dist/public/views/openclaw.js +1 -1
- package/dist/public/views/portal-dev.js +5 -2
- package/dist/public/views/portal.js +3 -2
- package/dist/public/views/profile/agents/shared-board.js +4 -2
- package/dist/public/views/profile/agents/tab-messages.js +13 -11
- package/dist/public/views/profile/agents-messages-subtab.js +2 -1
- package/dist/public/views/profile/apps-tab.js +2 -1
- package/dist/public/views/profile/boards-tab.js +3 -2
- package/dist/public/views/profile/calibrator-batch.js +3 -2
- package/dist/public/views/profile/data-wallet-tab.js +2 -1
- package/dist/public/views/profile/extensions-tab.js +4 -3
- package/dist/public/views/profile/federation-tab.js +4 -0
- package/dist/public/views/profile/foundry-dashboard/use-autopilot.js +3 -2
- package/dist/public/views/profile/foundry-detail.js +1 -1
- package/dist/public/views/profile/generator-detail.js +1 -1
- package/dist/public/views/profile/inbox-tab.js +7 -3
- package/dist/public/views/profile/inline-panels.js +3 -1
- package/dist/public/views/profile/knowledge-tab.js +2 -1
- package/dist/public/views/profile/landing-page.js +88 -0
- package/dist/public/views/profile/memory-tab.js +6 -5
- package/dist/public/views/profile/notebook-tab.js +500 -0
- package/dist/public/views/profile/organisms/activity-panel.js +135 -0
- package/dist/public/views/profile/organisms/agents.js +141 -0
- package/dist/public/views/profile/organisms/document.js +274 -0
- package/dist/public/views/profile/organisms/helpers.js +61 -0
- package/dist/public/views/profile/organisms/home.js +315 -0
- package/dist/public/views/profile/organisms/members.js +220 -0
- package/dist/public/views/profile/organisms/panels.js +173 -0
- package/dist/public/views/profile/organisms/participants-panel.js +160 -0
- package/dist/public/views/profile/organisms/schema-form.js +127 -0
- package/dist/public/views/profile/organisms/sources-panel.js +164 -0
- package/dist/public/views/profile/organisms/widgets.js +58 -0
- package/dist/public/views/profile/organisms/workspace-comments.js +99 -0
- package/dist/public/views/profile/organisms/workspace-list.js +268 -0
- package/dist/public/views/profile/organisms/workspace.js +1272 -0
- package/dist/public/views/profile/organisms-tab.js +456 -3819
- package/dist/public/views/profile/shared.js +64 -2
- package/dist/public/views/profile/workflows-form.js +1 -1
- package/dist/public/views/profile.js +16 -14
- package/dist/public/views/public-knowledge-viewer.js +3 -2
- package/dist/public/views/public-workspace-viewer.js +2 -1
- package/dist/scripts/check-importmap.d.ts +2 -0
- package/dist/scripts/check-importmap.d.ts.map +1 -0
- package/dist/scripts/check-importmap.js +102 -0
- package/dist/scripts/check-importmap.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +26 -3
- package/dist/src/cli/scaffold.d.ts.map +1 -1
- package/dist/src/cli/scaffold.js +33 -3
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +5 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +927 -11
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +32 -6
- package/dist/src/generated/prisma-postgres/index-browser.js +29 -3
- package/dist/src/generated/prisma-postgres/index.d.ts +1970 -160
- package/dist/src/generated/prisma-postgres/index.js +32 -6
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +45 -12
- package/dist/src/generated/prisma-postgres/wasm.js +32 -6
- package/dist/src/index.js +36 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes/admin-monitoring.d.ts.map +1 -1
- package/dist/src/routes/admin-monitoring.js +8 -0
- package/dist/src/routes/admin-monitoring.js.map +1 -1
- package/dist/src/routes/federation-peer.d.ts.map +1 -1
- package/dist/src/routes/federation-peer.js +327 -43
- package/dist/src/routes/federation-peer.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts +2 -0
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +20 -1
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/librarian.d.ts +18 -0
- package/dist/src/routes/librarian.d.ts.map +1 -0
- package/dist/src/routes/librarian.js +88 -0
- package/dist/src/routes/librarian.js.map +1 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +2 -1
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +9 -1
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/presence.d.ts +23 -0
- package/dist/src/routes/presence.d.ts.map +1 -0
- package/dist/src/routes/presence.js +88 -0
- package/dist/src/routes/presence.js.map +1 -0
- package/dist/src/routes/site.d.ts +12 -0
- package/dist/src/routes/site.d.ts.map +1 -1
- package/dist/src/routes/site.js +45 -1
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/routes/sse.d.ts +2 -0
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +9 -0
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/wellknown.d.ts.map +1 -1
- package/dist/src/routes/wellknown.js +5 -0
- package/dist/src/routes/wellknown.js.map +1 -1
- package/dist/src/routes/work.d.ts.map +1 -1
- package/dist/src/routes/work.js +6 -0
- package/dist/src/routes/work.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +13 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +34 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +2 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/federation-availability.d.ts +28 -0
- package/dist/src/services/federation-availability.d.ts.map +1 -0
- package/dist/src/services/federation-availability.js +58 -0
- package/dist/src/services/federation-availability.js.map +1 -0
- package/dist/src/services/federation-book.d.ts +70 -0
- package/dist/src/services/federation-book.d.ts.map +1 -0
- package/dist/src/services/federation-book.js +198 -0
- package/dist/src/services/federation-book.js.map +1 -0
- package/dist/src/services/federation-tiers.d.ts +45 -0
- package/dist/src/services/federation-tiers.d.ts.map +1 -0
- package/dist/src/services/federation-tiers.js +58 -0
- package/dist/src/services/federation-tiers.js.map +1 -0
- package/dist/src/services/federation.d.ts +15 -0
- package/dist/src/services/federation.d.ts.map +1 -1
- package/dist/src/services/federation.js +12 -0
- package/dist/src/services/federation.js.map +1 -1
- package/dist/src/services/librarian.d.ts +62 -0
- package/dist/src/services/librarian.d.ts.map +1 -0
- package/dist/src/services/librarian.js +135 -0
- package/dist/src/services/librarian.js.map +1 -0
- package/dist/src/services/message-delivery.d.ts +3 -1
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +23 -2
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/network-policy.d.ts +88 -0
- package/dist/src/services/network-policy.d.ts.map +1 -0
- package/dist/src/services/network-policy.js +154 -0
- package/dist/src/services/network-policy.js.map +1 -0
- package/dist/src/services/notebook-classify-prompt.d.ts +15 -0
- package/dist/src/services/notebook-classify-prompt.d.ts.map +1 -0
- package/dist/src/services/notebook-classify-prompt.js +39 -0
- package/dist/src/services/notebook-classify-prompt.js.map +1 -0
- package/dist/src/services/notebook-classify.d.ts +80 -0
- package/dist/src/services/notebook-classify.d.ts.map +1 -0
- package/dist/src/services/notebook-classify.js +150 -0
- package/dist/src/services/notebook-classify.js.map +1 -0
- package/dist/src/services/presence.d.ts +108 -0
- package/dist/src/services/presence.d.ts.map +1 -0
- package/dist/src/services/presence.js +304 -0
- package/dist/src/services/presence.js.map +1 -0
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +13 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/site.d.ts +20 -0
- package/dist/src/services/site.d.ts.map +1 -1
- package/dist/src/services/site.js +70 -0
- package/dist/src/services/site.js.map +1 -1
- package/dist/src/storage/interface.d.ts +48 -0
- 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 +11 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +128 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +7 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +27 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +5 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +48 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +2 -0
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +55 -0
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +61 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +5 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +25 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/dist/src/utils/node-descriptor.d.ts +34 -0
- package/dist/src/utils/node-descriptor.d.ts.map +1 -0
- package/dist/src/utils/node-descriptor.js +42 -0
- package/dist/src/utils/node-descriptor.js.map +1 -0
- package/dist/src/utils/version.d.ts +3 -0
- package/dist/src/utils/version.d.ts.map +1 -0
- package/dist/src/utils/version.js +40 -0
- package/dist/src/utils/version.js.map +1 -0
- package/dist/static/app-catalog.html +643 -4
- package/package.json +5 -2
- package/prisma/schema.postgres.prisma +51 -18
- package/prisma/schema.prisma +29 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file panels.js
|
|
3
|
+
* @description Small standalone organism panels: OrgSearch (content search across a member's
|
|
4
|
+
* readable workspaces), IncomingInvitations (pending invites banner with Accept/Decline), and
|
|
5
|
+
* BoardPreview (embedded organism board: latest posts + composer). Extracted from organisms-tab.js
|
|
6
|
+
* with no behaviour change.
|
|
7
|
+
* @structure OrgSearch, IncomingInvitations, BoardPreview
|
|
8
|
+
* @usage import { OrgSearch, IncomingInvitations, BoardPreview } from '/views/profile/organisms/panels.js';
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { Spinner } from '/views/profile/shared.js';
|
|
18
|
+
import { EmptyState } from '/components/EmptyState.js';
|
|
19
|
+
import * as orgService from '/js/services/organisms.js';
|
|
20
|
+
import { listPosts, createPost } from '/js/services/boards.js';
|
|
21
|
+
import { copyToClipboard } from '/js/utils.js';
|
|
22
|
+
import { relTime } from '/views/profile/organisms/helpers.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Content search inside an organism — case-insensitive substring over the records + documents of
|
|
26
|
+
* every workspace the member can read. Backend: GET /v1/organisms/:id/search. Rendered in the
|
|
27
|
+
* expanded card for members; opening a hit takes the user to that workspace.
|
|
28
|
+
*/
|
|
29
|
+
export function OrgSearch({ orgId, onOpenWorkspace }) {
|
|
30
|
+
const [q, setQ] = useState('');
|
|
31
|
+
const [results, setResults] = useState(null);
|
|
32
|
+
const [busy, setBusy] = useState(false);
|
|
33
|
+
|
|
34
|
+
const doSearch = async () => {
|
|
35
|
+
const query = q.trim();
|
|
36
|
+
if (query.length < 2) return;
|
|
37
|
+
setBusy(true);
|
|
38
|
+
try {
|
|
39
|
+
const r = await orgService.searchOrganism(orgId, query);
|
|
40
|
+
setResults(r?.data?.results || []);
|
|
41
|
+
} catch { setResults([]); }
|
|
42
|
+
finally { setBusy(false); }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return html`
|
|
46
|
+
<div class="pj-orgsearch">
|
|
47
|
+
<div class="flex-row-wrap">
|
|
48
|
+
<input class="input-field input-sm pj-orgsearch-input" placeholder=${t('organisms.searchPlaceholder') || 'Find records & documents…'} value=${q}
|
|
49
|
+
onInput=${(e) => setQ(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') doSearch(); }} />
|
|
50
|
+
<button class="btn-outline btn-sm" disabled=${busy || q.trim().length < 2} onClick=${doSearch}>${t('organisms.search') || 'Search'}</button>
|
|
51
|
+
</div>
|
|
52
|
+
${results !== null && results.length === 0 ? html`<div class="section-desc">${t('organisms.searchNoResults') || 'No matches.'}</div>` : null}
|
|
53
|
+
${(results || []).map(r => html`
|
|
54
|
+
<div class="pj-access-row card-clickable" key=${r.ws + '/' + r.space + '/' + r.id} role="button" onClick=${() => onOpenWorkspace?.(r.ws)}>
|
|
55
|
+
<span>
|
|
56
|
+
<b>${(r.title)}</b>
|
|
57
|
+
<span class="pj-mini"> — ${(r.wsName || r.ws)} · ${(r.space)}</span>
|
|
58
|
+
<div class="pj-mini">${(r.snippet)}</div>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
`)}
|
|
62
|
+
</div>
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Banner listing the caller's pending organism invitations (status `invited`) across all
|
|
68
|
+
* organisms, with Accept / Decline. Invited organisms are not in the member's active list, so
|
|
69
|
+
* this is how an invitee discovers them. Backend: GET /v1/organisms/invitations/mine.
|
|
70
|
+
*/
|
|
71
|
+
export function IncomingInvitations({ showToast, onChanged }) {
|
|
72
|
+
const [invites, setInvites] = useState([]);
|
|
73
|
+
const [busy, setBusy] = useState(false);
|
|
74
|
+
const load = useCallback(async () => {
|
|
75
|
+
const r = await orgService.listMyInvitations().catch(() => null);
|
|
76
|
+
setInvites(r?.data?.invitations || []);
|
|
77
|
+
}, []);
|
|
78
|
+
useEffect(() => { load(); }, [load]);
|
|
79
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const h = () => liveRef.current();
|
|
82
|
+
window.addEventListener('aimeat-live-update', h);
|
|
83
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
84
|
+
}, []);
|
|
85
|
+
const act = async (id, accept) => {
|
|
86
|
+
setBusy(true);
|
|
87
|
+
try {
|
|
88
|
+
const r = accept ? await orgService.acceptInvitation(id) : await orgService.declineInvitation(id);
|
|
89
|
+
if (r?.ok === false) showToast(r?.error?.message || (t('organisms.inviteActionFailed') || 'Failed'));
|
|
90
|
+
else showToast(accept ? (t('organisms.invitationAccepted') || 'Joined') : (t('organisms.invitationDeclined') || 'Declined'));
|
|
91
|
+
await load(); onChanged?.();
|
|
92
|
+
} catch (e) { showToast((e && e.message) || (t('organisms.inviteActionFailed') || 'Failed')); }
|
|
93
|
+
finally { setBusy(false); }
|
|
94
|
+
};
|
|
95
|
+
if (!invites.length) return null;
|
|
96
|
+
return html`
|
|
97
|
+
<div class="card">
|
|
98
|
+
<div class="section-title">${t('organisms.youAreInvited') || 'You’re invited'}</div>
|
|
99
|
+
${invites.map(({ membership, organism }) => html`
|
|
100
|
+
<div class="pj-access-row" key=${organism.id}>
|
|
101
|
+
<span><b>${(organism.name)}</b>${membership.invitedBy ? html` <span class="pj-mini">— ${(t('organisms.invitedByLabel') || 'invited by {who}').replace('{who}', (membership.invitedBy))}</span>` : null}</span>
|
|
102
|
+
<span class="flex-row-wrap">
|
|
103
|
+
<button class="btn-success btn-sm" disabled=${busy} onClick=${() => act(organism.id, true)}>${t('organisms.acceptInvite') || 'Accept'}</button>
|
|
104
|
+
<button class="btn-ghost btn-sm" disabled=${busy} onClick=${() => act(organism.id, false)}>${t('organisms.declineInvite') || 'Decline'}</button>
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
`)}
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Board tab — embedded preview of the organism's discussion board: latest posts + a composer,
|
|
113
|
+
* with "Open in Boards" for the full view. The raw board UUID hides behind a copy icon. */
|
|
114
|
+
export function BoardPreview({ boardId, showToast }) {
|
|
115
|
+
const [posts, setPosts] = useState(null); // null = loading
|
|
116
|
+
const [text, setText] = useState('');
|
|
117
|
+
const [busy, setBusy] = useState(false);
|
|
118
|
+
|
|
119
|
+
const load = useCallback(() => {
|
|
120
|
+
listPosts(boardId).then(p => setPosts(Array.isArray(p) ? p : [])).catch(() => setPosts([]));
|
|
121
|
+
}, [boardId]);
|
|
122
|
+
useEffect(() => { load(); }, [load]);
|
|
123
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const h = () => liveRef.current();
|
|
126
|
+
window.addEventListener('aimeat-live-update', h);
|
|
127
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const send = async () => {
|
|
131
|
+
const body = text.trim();
|
|
132
|
+
if (!body) return;
|
|
133
|
+
setBusy(true);
|
|
134
|
+
try {
|
|
135
|
+
const r = await createPost(boardId, body);
|
|
136
|
+
if (r?.ok === false) showToast(r?.error?.message || 'Failed to post');
|
|
137
|
+
else { setText(''); load(); }
|
|
138
|
+
} catch (e) { showToast((e && e.message) || 'Failed to post'); }
|
|
139
|
+
finally { setBusy(false); }
|
|
140
|
+
};
|
|
141
|
+
const copyId = async () => {
|
|
142
|
+
const ok = await copyToClipboard(boardId);
|
|
143
|
+
showToast(ok ? (t('organisms.copied') || 'Copied') : (t('organisms.copyFailed') || 'Could not copy'));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const ts = (p) => p.created_at || p.createdAt || p.at || '';
|
|
147
|
+
const latest = [...(posts || [])].sort((a, b) => String(ts(b)).localeCompare(String(ts(a)))).slice(0, 5);
|
|
148
|
+
|
|
149
|
+
return html`
|
|
150
|
+
<div class="card-detail">
|
|
151
|
+
<div class="pj-tabhead">
|
|
152
|
+
<div class="section-desc pj-tabhead-desc">${t('organisms.boardPreviewDesc') || 'Latest messages on this organism’s board.'}</div>
|
|
153
|
+
<button class="pj-icon-btn" title=${(t('organisms.copyId') || 'Copy ID') + ': ' + boardId} onClick=${copyId}>${'📋'}</button>
|
|
154
|
+
<button class="btn-outline btn-sm" onClick=${() => window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'boards' } }))}>
|
|
155
|
+
${t('organisms.openBoardsTab') || 'Open in Boards'}</button>
|
|
156
|
+
</div>
|
|
157
|
+
${posts === null ? html`<${Spinner} />`
|
|
158
|
+
: latest.length === 0 ? html`<${EmptyState} icon="💬" text=${t('organisms.boardEmpty') || 'No messages yet — write the first one.'} />`
|
|
159
|
+
: latest.map(p => html`
|
|
160
|
+
<div class="pj-board-post" key=${p.id || ts(p)}>
|
|
161
|
+
<div class="pj-board-post-head">
|
|
162
|
+
<span class="pj-board-author">${(p.author_gaii || p.author || '?')}</span>
|
|
163
|
+
${ts(p) ? html`<span class="pj-board-time">${relTime(ts(p))}</span>` : null}
|
|
164
|
+
</div>
|
|
165
|
+
<div class="pj-board-body">${(String(p.body || p.content || '').slice(0, 400))}</div>
|
|
166
|
+
</div>`)}
|
|
167
|
+
<div class="flex-row-wrap pj-board-composer">
|
|
168
|
+
<input class="input-field input-sm pj-board-input" placeholder=${t('organisms.writePost') || 'Write a message…'} value=${text}
|
|
169
|
+
onInput=${(e) => setText(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') send(); }} />
|
|
170
|
+
<button class="btn-outline btn-sm" disabled=${busy || !text.trim()} onClick=${send}>${t('organisms.send') || 'Send'}</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>`;
|
|
173
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file participants-panel.js
|
|
3
|
+
* @description Workspace participants panel — who takes part in this workspace, as a node → owner →
|
|
4
|
+
* agents chart + listing, the viewer's contract agents (with one-click adopt), and (for the
|
|
5
|
+
* workspace creator) an access manager. Extracted from organisms-tab.js with no behaviour change.
|
|
6
|
+
* @structure ParticipantsPanel
|
|
7
|
+
* @usage import { ParticipantsPanel } from '/views/profile/organisms/participants-panel.js';
|
|
8
|
+
* @version-history
|
|
9
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
10
|
+
*/
|
|
11
|
+
import { h } from 'preact';
|
|
12
|
+
import { useState, useEffect } from 'preact/hooks';
|
|
13
|
+
import htm from 'htm';
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
import { t } from '/js/i18n.js';
|
|
16
|
+
import * as orgService from '/js/services/organisms.js';
|
|
17
|
+
import { listAgents, offersWorkspaceContract, contractNamesOf, adoptContractTask } from '/js/services/agents.js';
|
|
18
|
+
import { Mermaid } from '/components/Mermaid.js';
|
|
19
|
+
|
|
20
|
+
/* Participants panel — who takes part in this workspace, as a node → owner → agents chart plus a
|
|
21
|
+
* listing. Built from the records' identity traces (humans + their agents) + organism membership.
|
|
22
|
+
* The viewer's own agents are named; everyone else's appear as anonymous ghost boxes. */
|
|
23
|
+
export function ParticipantsPanel({ orgId, wsId, showToast }) {
|
|
24
|
+
const [data, setData] = useState(null);
|
|
25
|
+
const [show, setShow] = useState(true);
|
|
26
|
+
const [access, setAccess] = useState(null); // { requests, members } — only for the workspace creator
|
|
27
|
+
const [grantee, setGrantee] = useState('');
|
|
28
|
+
const [role, setRole] = useState('contributor');
|
|
29
|
+
const [busy, setBusy] = useState(false);
|
|
30
|
+
// The viewer's own agents advertising a workspace contract (tag 'workspace-contract' +
|
|
31
|
+
// optional 'contract.<name>' tags) — surfaced here so a user looking at a workspace sees
|
|
32
|
+
// straight away which of their agents can serve it.
|
|
33
|
+
const [contractAgents, setContractAgents] = useState([]);
|
|
34
|
+
const [adoptBusy, setAdoptBusy] = useState(''); // `${gaii}:${contract}` of the in-flight adopt
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
listAgents().then(a => setContractAgents((a || []).filter(offersWorkspaceContract))).catch(() => {});
|
|
37
|
+
}, []);
|
|
38
|
+
// One-click adoption: queue the agreed adopt-contract task (docs/agent-workspace-contracts.md
|
|
39
|
+
// §7c) — the agent provisions its contract's spaces itself and the task completion is the ack.
|
|
40
|
+
const adopt = async (a, contract) => {
|
|
41
|
+
const key = `${a.gaii}:${contract || ''}`;
|
|
42
|
+
setAdoptBusy(key);
|
|
43
|
+
try {
|
|
44
|
+
const r = await adoptContractTask(a.name, { organismId: orgId, ws: wsId, contract });
|
|
45
|
+
if (r?.ok === false) showToast?.(r?.error?.message || (t('organisms.adoptFailed') || 'Could not queue the adoption task'));
|
|
46
|
+
else showToast?.((t('organisms.adoptQueued') || 'Adoption task queued for {agent} — it provisions the contract and reports back').replace('{agent}', a.display_name || a.name));
|
|
47
|
+
} catch (e) { showToast?.((e && e.message) || (t('organisms.adoptFailed') || 'Could not queue the adoption task')); }
|
|
48
|
+
finally { setAdoptBusy(''); }
|
|
49
|
+
};
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
const fetchIt = () => orgService.getWorkspaceParticipants(orgId, wsId).then(d => { if (!cancelled) setData(d); }).catch(() => {});
|
|
53
|
+
fetchIt();
|
|
54
|
+
window.addEventListener('aimeat-live-update', fetchIt);
|
|
55
|
+
return () => { cancelled = true; window.removeEventListener('aimeat-live-update', fetchIt); };
|
|
56
|
+
}, [orgId, wsId]);
|
|
57
|
+
const isManager = !!(data && (data.nodes || []).some(n => (n.owners || []).some(o => o.isSelf && o.isCreator)));
|
|
58
|
+
const loadAccess = () => orgService.getWorkspaceAccess(orgId, wsId).then(setAccess).catch(() => {});
|
|
59
|
+
useEffect(() => { if (isManager) loadAccess(); }, [isManager, orgId, wsId, data]); if (!data || !(data.nodes || []).length) return null;
|
|
60
|
+
const owners = [];
|
|
61
|
+
for (const n of data.nodes) for (const o of (n.owners || [])) owners.push({ ...o, node: n.id, isLocalNode: n.isLocal });
|
|
62
|
+
|
|
63
|
+
const after = async (p) => { setBusy(true); try { await p; await loadAccess(); } catch (e) { /* the row stays so the user can retry */ } finally { setBusy(false); } };
|
|
64
|
+
const doGrant = (g, r) => { const name = (g || '').trim(); if (name) { after(orgService.grantWorkspaceRole(orgId, wsId, name, r)); setGrantee(''); } };
|
|
65
|
+
const doRevoke = (g) => after(orgService.revokeWorkspaceRole(orgId, wsId, g));
|
|
66
|
+
const doDecide = (requester, decision) => after(orgService.decideWorkspaceAccess(orgId, wsId, requester, decision));
|
|
67
|
+
const pending = (access?.requests || []).filter(r => r.status === 'pending');
|
|
68
|
+
const members = access?.members || [];
|
|
69
|
+
|
|
70
|
+
return html`
|
|
71
|
+
<div class="pj-chart">
|
|
72
|
+
<div class="pj-chart-head">
|
|
73
|
+
<span class="pj-chart-title">${'👥 '}${t('organisms.participants') || 'Who works here'}</span>
|
|
74
|
+
<button class="btn-ghost btn-sm" onClick=${() => setShow(s => !s)}>${show ? (t('organisms.hide') || 'Hide') : (t('organisms.show') || 'Show')}</button>
|
|
75
|
+
</div>
|
|
76
|
+
${show ? html`
|
|
77
|
+
<div class="pj-parts">
|
|
78
|
+
${contractAgents.length ? html`
|
|
79
|
+
<div class="pj-contract-agents">
|
|
80
|
+
<div class="pj-access-title">${'📜 '}${t('organisms.contractAgents') || 'Your contract agents'}</div>
|
|
81
|
+
<div class="section-desc">${t('organisms.contractAgentsDesc') || 'These agents of yours advertise a workspace contract — they can process a workspace like this one. Grant access (below) or attach them in the organism Agents tab.'}</div>
|
|
82
|
+
<div class="pj-part-agents">
|
|
83
|
+
${contractAgents.map(a => {
|
|
84
|
+
const names = contractNamesOf(a);
|
|
85
|
+
// An agent already working in THIS workspace (it appears among the viewer's own
|
|
86
|
+
// agents in the participants data) gets a ✓ instead of Adopt buttons — offering
|
|
87
|
+
// adoption for an agent that is already here is noise, and the task would be a
|
|
88
|
+
// no-op re-provision anyway.
|
|
89
|
+
const here = owners.some(o => o.isSelf && (o.agents || []).some(ag => ag.isOwn && ag.name === a.name));
|
|
90
|
+
// One adopt action per advertised contract (a single unnamed one falls back to
|
|
91
|
+
// the bare marker). The agent does the rest — join, provision, complete the task.
|
|
92
|
+
const actions = names.length ? names : [''];
|
|
93
|
+
return html`
|
|
94
|
+
<span class="pj-part-agent own" key=${a.gaii} title=${a.gaii}>
|
|
95
|
+
${'📜 '}${a.display_name || a.name}
|
|
96
|
+
${here ? html`<span class="badge badge-success pj-mini" title=${t('organisms.contractActiveHint') || 'This agent already works in this workspace'}>${'✓ '}${t('organisms.contractActive') || 'active here'}</span>`
|
|
97
|
+
: actions.map(c => html`
|
|
98
|
+
<button class="btn-outline btn-sm pj-adopt-btn" key=${c} disabled=${adoptBusy === `${a.gaii}:${c}`}
|
|
99
|
+
title=${t('organisms.adoptHint') || 'Queue a task for this agent to adopt its contract into THIS workspace (it provisions the spaces itself)'}
|
|
100
|
+
onClick=${() => adopt(a, c)}>
|
|
101
|
+
${adoptBusy === `${a.gaii}:${c}` ? '…' : `${t('organisms.adoptContract') || 'Adopt'}${c ? ` ${c}` : ''}`}
|
|
102
|
+
</button>`)}
|
|
103
|
+
</span>`;
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
</div>` : null}
|
|
107
|
+
<${Mermaid} chart=${orgService.buildParticipantsMermaid(data)} />
|
|
108
|
+
<div class="pj-parts-list">
|
|
109
|
+
${owners.map((o, i) => html`<div class="pj-part-owner" key=${i}>
|
|
110
|
+
<div class="pj-part-human">
|
|
111
|
+
<span>${'👤 '}<strong>${(o.owner)}</strong></span>
|
|
112
|
+
${o.isSelf ? html`<span class="badge badge-info pj-mini">${t('organisms.you') || 'you'}</span>` : null}
|
|
113
|
+
${o.isCreator ? html`<span class="badge badge-success pj-mini">${t('organisms.creatorTag') || 'creator'}</span>` : null}
|
|
114
|
+
${!o.isMember && !o.isSelf ? html`<span class="badge badge-warn pj-mini">${t('organisms.guest') || 'guest'}</span>` : null}
|
|
115
|
+
${!o.isLocalNode ? html`<span class="pj-part-node">${'🌐 '}${(o.node)}</span>` : null}
|
|
116
|
+
${o.contributions ? html`<span class="pj-part-count" title=${t('organisms.contributions') || 'contributions'}>${o.contributions}</span>` : null}
|
|
117
|
+
</div>
|
|
118
|
+
${(o.agents || []).length ? html`<div class="pj-part-agents">
|
|
119
|
+
${o.agents.map((a, j) => html`
|
|
120
|
+
<span class="pj-part-agent ${a.isOwn ? 'own' : 'ghost'}" key=${j}
|
|
121
|
+
title=${a.isOwn ? '' : (t('organisms.otherAgentHint') || 'Another owner’s agent — you see what it has done here, not its live status')}>
|
|
122
|
+
${'🤖 '}${(a.name)}<span class="pj-part-count">${a.contributions}</span>
|
|
123
|
+
</span>`)}
|
|
124
|
+
</div>` : null}
|
|
125
|
+
</div>`)}
|
|
126
|
+
</div>
|
|
127
|
+
${isManager ? html`
|
|
128
|
+
<div class="pj-access">
|
|
129
|
+
<div class="pj-access-title">${t('organisms.manageAccess') || 'Manage who can work here'}</div>
|
|
130
|
+
${pending.map(r => html`<div class="pj-access-row" key=${'req-' + r.requester}>
|
|
131
|
+
<span class="pj-access-who">${'🙋 '}<strong>${(r.requester)}</strong> <span class="pj-access-note">${t('organisms.requestedAccess') || 'requested access'}</span></span>
|
|
132
|
+
<span class="pj-access-actions">
|
|
133
|
+
<button class="btn-success btn-sm" disabled=${busy} onClick=${() => doDecide(r.requester, 'contributor')}>${t('organisms.addAsContributor') || 'Add as contributor'}</button>
|
|
134
|
+
<button class="btn-outline btn-sm" disabled=${busy} onClick=${() => doDecide(r.requester, 'viewer')}>${t('organisms.addAsViewer') || 'as viewer'}</button>
|
|
135
|
+
<button class="btn-danger btn-sm" disabled=${busy} onClick=${() => doDecide(r.requester, 'deny')}>${t('organisms.deny') || 'Deny'}</button>
|
|
136
|
+
</span>
|
|
137
|
+
</div>`)}
|
|
138
|
+
${members.map(m => html`<div class="pj-access-row" key=${'mem-' + m.owner}>
|
|
139
|
+
<span class="pj-access-who">${'👤 '}<strong>${(m.owner)}</strong> <span class="badge ${m.role === 'contributor' ? 'badge-success' : 'badge-info'} pj-mini">${m.role === 'contributor' ? (t('organisms.roleContributorShort') || 'contributor') : (t('organisms.roleViewerShort') || 'viewer')}</span></span>
|
|
140
|
+
<span class="pj-access-actions">
|
|
141
|
+
${m.role === 'viewer'
|
|
142
|
+
? html`<button class="btn-outline btn-sm" disabled=${busy} onClick=${() => doGrant(m.owner, 'contributor')}>${t('organisms.makeContributor') || '→ can write'}</button>`
|
|
143
|
+
: html`<button class="btn-outline btn-sm" disabled=${busy} onClick=${() => doGrant(m.owner, 'viewer')}>${t('organisms.makeViewer') || '→ read only'}</button>`}
|
|
144
|
+
<button class="btn-danger btn-sm" disabled=${busy} onClick=${() => doRevoke(m.owner)}>${t('organisms.remove') || 'Remove'}</button>
|
|
145
|
+
</span>
|
|
146
|
+
</div>`)}
|
|
147
|
+
<div class="pj-access-add">
|
|
148
|
+
<input class="pj-access-input" placeholder=${t('organisms.addMemberPlaceholder') || 'owner name (or owner@node / agent#owner@node)'} value=${grantee}
|
|
149
|
+
onInput=${e => setGrantee(e.target.value)} onKeyDown=${e => { if (e.key === 'Enter') doGrant(grantee, role); }} />
|
|
150
|
+
<select class="pj-access-role" value=${role} onChange=${e => setRole(e.target.value)}>
|
|
151
|
+
<option value="contributor">${t('organisms.roleContributor') || 'contributor (read + write)'}</option>
|
|
152
|
+
<option value="viewer">${t('organisms.roleViewer') || 'viewer (read only)'}</option>
|
|
153
|
+
</select>
|
|
154
|
+
<button class="btn-primary btn-sm" disabled=${busy || !grantee.trim()} onClick=${() => doGrant(grantee, role)}>${'+ '}${t('organisms.addMember') || 'Add'}</button>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="pj-access-hint">${t('organisms.accessHint') || 'Members can be a different account; their agents inherit the role. Viewers read; contributors read + write.'}</div>
|
|
157
|
+
</div>` : null}
|
|
158
|
+
</div>` : null}
|
|
159
|
+
</div>`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file schema-form.js
|
|
3
|
+
* @description A form rendered from a JSON Schema — typed inputs (enum→select, integer→number,
|
|
4
|
+
* array→lines, boolean→checkbox, date/datetime→pickers, else text). Used by the workspace record
|
|
5
|
+
* spaces to add/edit drafts of any objectType. Extracted from organisms-tab.js, no behaviour change.
|
|
6
|
+
* @structure SchemaForm
|
|
7
|
+
* @usage import { SchemaForm } from '/views/profile/organisms/schema-form.js';
|
|
8
|
+
* @version-history
|
|
9
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
10
|
+
*/
|
|
11
|
+
import { h } from 'preact';
|
|
12
|
+
import { useState } from 'preact/hooks';
|
|
13
|
+
import htm from 'htm';
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
import { t } from '/js/i18n.js';
|
|
16
|
+
import { getGhii } from '/js/services/auth.js';
|
|
17
|
+
|
|
18
|
+
/* A form rendered from a JSON Schema — typed inputs (enum→select, integer→number,
|
|
19
|
+
* array→lines, boolean→checkbox, else text). Works for any objectType, including ones a
|
|
20
|
+
* generated manifest declares. New records pre-fill `id` (editable) and requester-style
|
|
21
|
+
* fields ("x-default": "currentUser", or a requested_by/created_by-shaped name) with the
|
|
22
|
+
* signed-in GHII. readOnly properties are agent-filled results — hidden here, shown only in
|
|
23
|
+
* the record view. Labels/hints resolve manifest i18n (wsT) → schema description → raw key.
|
|
24
|
+
* Save is always clickable: a failed attempt outlines the still-empty required fields. */
|
|
25
|
+
export function SchemaForm({ schema, busy, onSave, onCancel, initial, idPrefix, namespace, wsT }) {
|
|
26
|
+
const props = (schema && schema.properties) || {};
|
|
27
|
+
const editable = Object.entries(props).filter(([, def]) => !(def && def.readOnly));
|
|
28
|
+
const required = new Set(((schema && schema.required) || []).filter(k => k !== 'id' && !(props[k] && props[k].readOnly)));
|
|
29
|
+
const fieldNames = editable.map(([k]) => k);
|
|
30
|
+
const [missing, setMissing] = useState(null); // required keys empty at the last save attempt
|
|
31
|
+
// Seed from an existing record when editing (arrays → newline text for the textarea inputs).
|
|
32
|
+
const [vals, setVals] = useState(() => {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [k, def] of Object.entries(props)) {
|
|
35
|
+
const v = initial && initial[k];
|
|
36
|
+
if (v === undefined || v === null) continue;
|
|
37
|
+
out[k] = Array.isArray(v) ? v.join('\n') : (def.type === 'boolean' ? !!v : String(v));
|
|
38
|
+
}
|
|
39
|
+
if (!initial) {
|
|
40
|
+
if (props.id && out.id === undefined) out.id = `${idPrefix || 'rec'}-${Date.now().toString(36)}`;
|
|
41
|
+
const me = getGhii() || '';
|
|
42
|
+
for (const [k, def] of editable) {
|
|
43
|
+
if (out[k] !== undefined || def.type !== 'string' || def.enum) continue;
|
|
44
|
+
if (def['x-default'] === 'currentUser' || /^(requested_by|created_by|requester|author)$/i.test(k)) out[k] = me;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
});
|
|
49
|
+
const set = (k, v) => setVals(s => ({ ...s, [k]: v }));
|
|
50
|
+
|
|
51
|
+
const buildValue = () => {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [k, def] of Object.entries(props)) {
|
|
54
|
+
const raw = vals[k];
|
|
55
|
+
if (raw === undefined || raw === '') continue;
|
|
56
|
+
if (def.type === 'integer' || def.type === 'number') out[k] = Number(raw);
|
|
57
|
+
else if (def.type === 'boolean') out[k] = !!raw;
|
|
58
|
+
else if (def.type === 'array') out[k] = String(raw).split('\n').map(s => s.trim()).filter(Boolean);
|
|
59
|
+
else out[k] = raw;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const isEmpty = (k) => vals[k] === undefined || String(vals[k]).trim() === '';
|
|
65
|
+
// Of the fields flagged at the last attempt, only the ones STILL empty stay outlined.
|
|
66
|
+
const stillMissing = (missing || []).filter(isEmpty);
|
|
67
|
+
const trySave = () => {
|
|
68
|
+
const miss = [...required].filter(isEmpty);
|
|
69
|
+
setMissing(miss.length ? miss : null);
|
|
70
|
+
if (!miss.length) onSave(buildValue());
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const labelOf = (k) => (wsT && wsT(`${namespace}.${k}`)) || k;
|
|
74
|
+
const hintOf = (k, def) => (wsT && wsT(`${namespace}.${k}.hint`)) || def.description
|
|
75
|
+
|| (k === 'id' && !initial ? (t('organisms.autoIdHint') || 'Pre-filled automatically — change it if you want a memorable id.') : '');
|
|
76
|
+
|
|
77
|
+
const control = (k, def) => {
|
|
78
|
+
if (def.type === 'string' && Array.isArray(def.enum)) {
|
|
79
|
+
return html`<select class="input-field input-sm" value=${vals[k] ?? ''} onChange=${e => set(k, e.target.value)}>
|
|
80
|
+
<option value="">—</option>${def.enum.map(o => html`<option value=${o} key=${o}>${o}</option>`)}
|
|
81
|
+
</select>`;
|
|
82
|
+
}
|
|
83
|
+
// Date / datetime fields → native pickers (by schema format, or a name ending in _date).
|
|
84
|
+
if (def.format === 'date-time') {
|
|
85
|
+
return html`<input type="datetime-local" class="input-field input-sm" value=${vals[k] ?? ''} onInput=${e => set(k, e.target.value)} />`;
|
|
86
|
+
}
|
|
87
|
+
if (def.format === 'date' || (def.type === 'string' && !def.enum && /(_date$|^date$)/i.test(k))) {
|
|
88
|
+
return html`<input type="date" class="input-field input-sm" value=${vals[k] ?? ''} onInput=${e => set(k, e.target.value)} />`;
|
|
89
|
+
}
|
|
90
|
+
if (def.type === 'integer' || def.type === 'number') {
|
|
91
|
+
return html`<input type="number" class="input-field input-sm" value=${vals[k] ?? ''} onInput=${e => set(k, e.target.value)} />`;
|
|
92
|
+
}
|
|
93
|
+
if (def.type === 'array') {
|
|
94
|
+
return html`<textarea class="input-field input-sm" rows="2" value=${vals[k] ?? ''} onInput=${e => set(k, e.target.value)}></textarea>`;
|
|
95
|
+
}
|
|
96
|
+
return html`<input type="text" class="input-field input-sm" value=${vals[k] ?? ''} onInput=${e => set(k, e.target.value)} />`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const field = (k, def) => {
|
|
100
|
+
const label = labelOf(k) + (required.has(k) ? ' *' : '');
|
|
101
|
+
if (def.type === 'boolean') {
|
|
102
|
+
return html`<label class="pj-field pj-field-inline" key=${k}><input type="checkbox" checked=${!!vals[k]} onChange=${e => set(k, e.target.checked)} /><span>${label}</span></label>`;
|
|
103
|
+
}
|
|
104
|
+
const hint = hintOf(k, def);
|
|
105
|
+
const suffix = def.type === 'array' ? ' (' + (t('organisms.onePerLine') || 'one per line') + ')' : '';
|
|
106
|
+
return html`<label class="pj-field ${stillMissing.includes(k) ? 'pj-field-invalid' : ''}" key=${k}>
|
|
107
|
+
<span>${label}${suffix}</span>
|
|
108
|
+
${hint ? html`<span class="pj-field-hint">${hint}</span>` : null}
|
|
109
|
+
${control(k, def)}</label>`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return html`
|
|
113
|
+
<div class="create-form pj-draft-form">
|
|
114
|
+
<div class="flex-col">
|
|
115
|
+
${fieldNames.length === 0
|
|
116
|
+
? html`<div class="pj-empty">${t('organisms.loading') || 'Loading...'}</div>`
|
|
117
|
+
: fieldNames.map(k => field(k, props[k]))}
|
|
118
|
+
${stillMissing.length ? html`<div class="pj-form-error">${(t('organisms.fillRequired') || 'Please fill in the required fields: {fields}')
|
|
119
|
+
.replace('{fields}', stillMissing.map(labelOf).join(', '))}</div>` : null}
|
|
120
|
+
<div class="form-actions">
|
|
121
|
+
<button class="btn-primary btn-sm" onClick=${trySave} disabled=${busy}>${t('organisms.saveDraft') || 'Save draft'}</button>
|
|
122
|
+
<button class="btn-ghost btn-sm" onClick=${onCancel}>${t('organisms.cancel') || 'Cancel'}</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file sources-panel.js
|
|
3
|
+
* @description Workspace Sources panel — references the workspace draws on (memory entries, storage
|
|
4
|
+
* files, knowledge packages; own or external/read-only). Pointers ONLY: nothing is copied or moved.
|
|
5
|
+
* Attach via a picker with Memory / Storage / Knowledge tabs. Extracted from organisms-tab.js with
|
|
6
|
+
* no behaviour change.
|
|
7
|
+
* @structure SRC_ICON (internal), SourcesPanel
|
|
8
|
+
* @usage import { SourcesPanel } from '/views/profile/organisms/sources-panel.js';
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import * as orgService from '/js/services/organisms.js';
|
|
18
|
+
import * as memoryService from '/js/services/memory.js';
|
|
19
|
+
import * as knowledgeService from '/js/services/knowledge.js';
|
|
20
|
+
import { EmptyState } from '/components/EmptyState.js';
|
|
21
|
+
import { SearchBar } from '/components/SearchBar.js';
|
|
22
|
+
import { fmtBytes } from '/js/format.js';
|
|
23
|
+
|
|
24
|
+
const SRC_ICON = { memory: '🧠', storage: '📎', knowledge: '📚' };
|
|
25
|
+
|
|
26
|
+
/* Sources: references the workspace draws on — memory entries, storage files, and knowledge
|
|
27
|
+
* packages (own, or external/read-only). Pointers ONLY: nothing is copied or moved; the referenced
|
|
28
|
+
* data stays where it lives (organism.{id}.meta.sources holds just the pointers). Attach via a
|
|
29
|
+
* picker with Memory / Storage / Knowledge tabs (Mine, or Discover for memory + knowledge). */
|
|
30
|
+
export function SourcesPanel({ orgId, wsId, showToast }) {
|
|
31
|
+
const [sources, setSources] = useState([]);
|
|
32
|
+
const [picking, setPicking] = useState(false);
|
|
33
|
+
const [tab, setTab] = useState('knowledge'); // memory | storage | knowledge
|
|
34
|
+
const [scope, setScope] = useState('mine'); // mine | discover (storage has no discover)
|
|
35
|
+
const [q, setQ] = useState('');
|
|
36
|
+
const [results, setResults] = useState([]);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [busy, setBusy] = useState(false);
|
|
39
|
+
|
|
40
|
+
const load = useCallback(async () => { setSources(await orgService.getWorkspaceSources(orgId, wsId)); }, [orgId, wsId]);
|
|
41
|
+
useEffect(() => { load(); }, [load]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const h = () => load();
|
|
44
|
+
window.addEventListener('aimeat-live-update', h);
|
|
45
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
46
|
+
}, [load]);
|
|
47
|
+
|
|
48
|
+
const persist = async (next) => {
|
|
49
|
+
setSources(next);
|
|
50
|
+
const r = await orgService.saveWorkspaceSources(orgId, wsId, next).catch(() => ({ ok: false }));
|
|
51
|
+
if (r?.ok === false) showToast(t('organisms.sourcesSaveError') || 'Failed to save sources');
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const doSearch = async () => {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
const ql = q.trim().toLowerCase();
|
|
57
|
+
try {
|
|
58
|
+
if (tab === 'memory') {
|
|
59
|
+
if (scope === 'mine') {
|
|
60
|
+
const items = await memoryService.listMemories();
|
|
61
|
+
setResults(items.filter(i => !ql || String(i.key).toLowerCase().includes(ql)).slice(0, 100));
|
|
62
|
+
} else {
|
|
63
|
+
const d = await memoryService.discoverPublicMemories({ q: q.trim(), limit: 50 });
|
|
64
|
+
setResults(d.items || []);
|
|
65
|
+
}
|
|
66
|
+
} else if (tab === 'storage') {
|
|
67
|
+
const files = await orgService.listOwnStorageFiles();
|
|
68
|
+
setResults(files.filter(f => !ql || String(f.key).toLowerCase().includes(ql)));
|
|
69
|
+
} else if (scope === 'mine') {
|
|
70
|
+
const pkgs = await knowledgeService.listMyPackages();
|
|
71
|
+
setResults(pkgs.filter(p => { const n = String(p.value?.name || p.key || ''); return !ql || n.toLowerCase().includes(ql); }));
|
|
72
|
+
} else {
|
|
73
|
+
const r = await knowledgeService.discoverPackages({ limit: 50, sort: 'recent' });
|
|
74
|
+
setResults((r?.data?.packages || []).filter(p => !ql || String(p.name || '').toLowerCase().includes(ql)));
|
|
75
|
+
}
|
|
76
|
+
} catch (e) { setResults([]); }
|
|
77
|
+
finally { setLoading(false); }
|
|
78
|
+
};
|
|
79
|
+
const searchRef = useRef(doSearch); searchRef.current = doSearch;
|
|
80
|
+
// Auto-search when the picker opens or the tab/scope changes — but NOT on every keystroke
|
|
81
|
+
// (typing only updates q; Enter or the Search button runs it).
|
|
82
|
+
useEffect(() => { if (picking) searchRef.current(); }, [picking, tab, scope]);
|
|
83
|
+
// storage has no cross-owner discovery — force 'mine' there
|
|
84
|
+
useEffect(() => { if (tab === 'storage' && scope !== 'mine') setScope('mine'); }, [tab, scope]);
|
|
85
|
+
|
|
86
|
+
const keyOf = (s) => `${s.type}:${s.packageId || (s.ownerGaii || '') + '|' + (s.key || '')}`;
|
|
87
|
+
const attach = async (item) => {
|
|
88
|
+
setBusy(true);
|
|
89
|
+
try {
|
|
90
|
+
const base = { id: 's-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5), addedAt: new Date().toISOString() };
|
|
91
|
+
let src;
|
|
92
|
+
if (tab === 'memory') {
|
|
93
|
+
src = { ...base, type: 'memory', key: item.key, ownerGaii: item.owner_gaii || orgService.currentGhii(), label: item.key, external: scope === 'discover' };
|
|
94
|
+
} else if (tab === 'storage') {
|
|
95
|
+
src = { ...base, type: 'storage', key: item.key, ownerGaii: orgService.currentGhii(), label: item.key, mime: item.mime_type, external: false };
|
|
96
|
+
} else {
|
|
97
|
+
const pid = scope === 'mine' ? ((String(item.key).match(/packages\/([^/]+)\/manifest/) || [])[1] || item.key) : item.package_id;
|
|
98
|
+
const name = scope === 'mine' ? (item.value?.name || pid) : (item.name || pid);
|
|
99
|
+
src = { ...base, type: 'knowledge', packageId: pid, label: name, external: scope === 'discover' };
|
|
100
|
+
}
|
|
101
|
+
if (sources.some(s => keyOf(s) === keyOf(src))) { showToast(t('organisms.sourceExists') || 'Already added'); return; }
|
|
102
|
+
await persist([...sources, src]);
|
|
103
|
+
showToast(t('organisms.sourceAdded') || 'Source added');
|
|
104
|
+
} finally { setBusy(false); }
|
|
105
|
+
};
|
|
106
|
+
const removeSource = (id) => persist(sources.filter(s => s.id !== id));
|
|
107
|
+
|
|
108
|
+
const resultRow = (item, i) => {
|
|
109
|
+
let label, meta;
|
|
110
|
+
if (tab === 'memory') { label = item.key; meta = (scope === 'discover' ? (item.owner_gaii + ' · ') : '') + (item.visibility || ''); }
|
|
111
|
+
else if (tab === 'storage') { label = item.key; meta = (item.mime_type || '') + ' · ' + fmtBytes(item.size || 0); }
|
|
112
|
+
else { label = scope === 'mine' ? (item.value?.name || item.key) : (item.name || item.package_id); meta = (scope === 'mine' ? (item.value?.entries?.length || 0) : (item.entries_count || 0)) + ' ' + (t('organisms.entries') || 'entries'); }
|
|
113
|
+
return html`
|
|
114
|
+
<div class="pj-src-result" key=${'r' + i}>
|
|
115
|
+
<span class="pj-src-result-label" title=${String(label)}>${(String(label))}</span>
|
|
116
|
+
<span class="pj-src-result-meta">${(String(meta))}</span>
|
|
117
|
+
<button class="btn-ghost btn-sm" disabled=${busy} onClick=${() => attach(item)}>${t('organisms.attach') || 'Attach'}</button>
|
|
118
|
+
</div>`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return html`
|
|
122
|
+
<div class="pj-section pj-sources">
|
|
123
|
+
<div class="pj-section-head">
|
|
124
|
+
<span class="pj-section-title">${t('organisms.sources') || 'Sources'}<span class="pj-doc-tag">${sources.length}</span></span>
|
|
125
|
+
<button class="btn-outline btn-sm" onClick=${() => setPicking(p => !p)}>
|
|
126
|
+
${picking ? (t('organisms.close') || 'Close') : ('+ ' + (t('organisms.addSource') || 'Add source'))}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="section-desc pj-sources-desc">${t('organisms.sourcesDesc') || 'References this workspace draws on — memory, files, and knowledge packages. Pointers only; the originals stay where they live.'}</div>
|
|
130
|
+
|
|
131
|
+
${picking ? html`
|
|
132
|
+
<div class="pj-src-picker">
|
|
133
|
+
<div class="seg" role="tablist">
|
|
134
|
+
${['memory', 'storage', 'knowledge'].map(tk => html`<button class="seg-btn ${tab === tk ? 'active' : ''}" key=${tk} onClick=${() => setTab(tk)}>${SRC_ICON[tk]} ${t('organisms.src_' + tk) || tk}</button>`)}
|
|
135
|
+
</div>
|
|
136
|
+
<div class="pj-src-controls">
|
|
137
|
+
${tab !== 'storage' ? html`
|
|
138
|
+
<div class="seg">
|
|
139
|
+
<button class="seg-btn ${scope === 'mine' ? 'active' : ''}" onClick=${() => setScope('mine')}>${t('organisms.mine') || 'Mine'}</button>
|
|
140
|
+
<button class="seg-btn ${scope === 'discover' ? 'active' : ''}" onClick=${() => setScope('discover')}>${t('organisms.discover') || 'Discover'}</button>
|
|
141
|
+
</div>` : null}
|
|
142
|
+
<div class="pj-src-search"><${SearchBar} value=${q} onInput=${e => setQ(e.target.value)} onSubmit=${() => doSearch()} placeholder=${t('organisms.searchSources') || 'Search…'} /></div>
|
|
143
|
+
<button class="btn-ghost btn-sm" onClick=${doSearch} disabled=${loading}>${t('organisms.search') || 'Search'}</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="pj-src-results">
|
|
146
|
+
${loading ? html`<${EmptyState} text=${t('organisms.loading') || 'Loading…'} />`
|
|
147
|
+
: results.length === 0 ? html`<${EmptyState} text=${t('organisms.noResults') || 'No results'} />`
|
|
148
|
+
: results.slice(0, 100).map(resultRow)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>` : null}
|
|
151
|
+
|
|
152
|
+
${sources.length === 0 ? html`<${EmptyState} text=${t('organisms.noSources') || 'No sources yet'} />`
|
|
153
|
+
: html`<div class="pj-src-list">
|
|
154
|
+
${sources.map(s => html`
|
|
155
|
+
<div class="pj-src-item" key=${s.id}>
|
|
156
|
+
<span class="pj-src-icon">${SRC_ICON[s.type] || '•'}</span>
|
|
157
|
+
<span class="pj-src-label" title=${s.key || s.packageId || ''}>${(String(s.label || s.key || s.packageId || ''))}</span>
|
|
158
|
+
${s.external ? html`<span class="badge badge-muted pj-mini">${t('organisms.external') || 'external'}</span>` : null}
|
|
159
|
+
<span class="badge badge-info pj-mini">${t('organisms.src_' + s.type) || s.type}</span>
|
|
160
|
+
<button class="pj-icon-btn" title=${t('organisms.remove') || 'Remove'} onClick=${() => removeSource(s.id)}>✕</button>
|
|
161
|
+
</div>`)}
|
|
162
|
+
</div>`}
|
|
163
|
+
</div>`;
|
|
164
|
+
}
|