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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file activity-panel.js
|
|
3
|
+
* @description Workspace activity panel — a GitHub-style contribution heatmap (every day split 2×2
|
|
4
|
+
* into documents vs records × draft vs published) plus the recent activity log. Built from the
|
|
5
|
+
* workspace activity feed (derived from version history). Extracted from organisms-tab.js, no
|
|
6
|
+
* behaviour change.
|
|
7
|
+
* @structure buildHeatmap, hmCell (internal helpers), ActivityPanel
|
|
8
|
+
* @usage import { ActivityPanel } from '/views/profile/organisms/activity-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 } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { dt } from '/js/format.js';
|
|
18
|
+
import * as orgService from '/js/services/organisms.js';
|
|
19
|
+
|
|
20
|
+
/* Build a GitHub-style contribution calendar from activity events. Each day holds FOUR counters —
|
|
21
|
+
* documents draft/published and records (schema'd) draft/published — so a cell can be drawn as a 2×2
|
|
22
|
+
* quadrant. Returns { cols, monthLabels }: each col is 7 day-slots (Sun→Sat); a future slot is null.
|
|
23
|
+
* Deterministic from the event timestamps. */
|
|
24
|
+
function buildHeatmap(byDay, today) {
|
|
25
|
+
const iso = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
26
|
+
const weeks = 53; // always a full year ending today, GitHub-style
|
|
27
|
+
const start = new Date(today);
|
|
28
|
+
start.setDate(start.getDate() - (weeks * 7 - 1));
|
|
29
|
+
start.setDate(start.getDate() - start.getDay()); // align to the start of a week (Sunday)
|
|
30
|
+
const cols = []; const monthLabels = [];
|
|
31
|
+
let cur = new Date(start); let prevMonth = -1;
|
|
32
|
+
while (cur <= today) {
|
|
33
|
+
monthLabels.push(cur.getMonth() !== prevMonth ? cur.toLocaleString(undefined, { month: 'short' }) : '');
|
|
34
|
+
prevMonth = cur.getMonth();
|
|
35
|
+
const col = [];
|
|
36
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
37
|
+
if (cur > today) { col.push(null); }
|
|
38
|
+
else { col.push({ date: iso(cur), b: byDay.get(iso(cur)) || null }); }
|
|
39
|
+
cur = new Date(cur); cur.setDate(cur.getDate() + 1);
|
|
40
|
+
}
|
|
41
|
+
cols.push(col);
|
|
42
|
+
}
|
|
43
|
+
return { cols, monthLabels };
|
|
44
|
+
}
|
|
45
|
+
const hmLevel = (n) => (n === 0 ? 0 : n <= 1 ? 1 : n <= 3 ? 2 : n <= 6 ? 3 : 4);
|
|
46
|
+
const ZERO_DAY = { dd: 0, dp: 0, rd: 0, rp: 0, total: 0 };
|
|
47
|
+
|
|
48
|
+
/* One heatmap day = a 2×2 grid: ↖ docs draft, ↗ docs published, ↙ records draft, ↘ records published.
|
|
49
|
+
* Each quadrant's shade is its own count's intensity. */
|
|
50
|
+
function hmCell(date, b, key) {
|
|
51
|
+
const c = b || ZERO_DAY;
|
|
52
|
+
const tip = b
|
|
53
|
+
? `${date} — docs: ${c.dd} draft / ${c.dp} published · records: ${c.rd} draft / ${c.rp} published`
|
|
54
|
+
: `${date} — no activity`;
|
|
55
|
+
return html`<span class="pj-hm-cell" key=${key} title=${tip}>
|
|
56
|
+
<i class="q lvl${hmLevel(c.dd)}"></i><i class="q lvl${hmLevel(c.dp)}"></i>
|
|
57
|
+
<i class="q lvl${hmLevel(c.rd)}"></i><i class="q lvl${hmLevel(c.rp)}"></i>
|
|
58
|
+
</span>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Activity panel — a GitHub-style contribution heatmap of the workspace's history, where every day is
|
|
62
|
+
* split 2×2 into documents vs records × draft vs published (quadrant shade = intensity) — plus the
|
|
63
|
+
* recent activity log (who did what, where, when), which doubles as an audit trail. Built from
|
|
64
|
+
* GET …/workspace/activity (derived from version history). */
|
|
65
|
+
export function ActivityPanel({ orgId, wsId }) {
|
|
66
|
+
const [data, setData] = useState(null);
|
|
67
|
+
const [show, setShow] = useState(true);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
const fetchIt = () => orgService.getWorkspaceActivity(orgId, wsId).then(d => { if (!cancelled) setData(d); }).catch(() => {});
|
|
71
|
+
fetchIt();
|
|
72
|
+
window.addEventListener('aimeat-live-update', fetchIt);
|
|
73
|
+
return () => { cancelled = true; window.removeEventListener('aimeat-live-update', fetchIt); };
|
|
74
|
+
}, [orgId, wsId]);
|
|
75
|
+
if (!data || !(data.events || []).length) return null;
|
|
76
|
+
const events = data.events;
|
|
77
|
+
const byDay = new Map();
|
|
78
|
+
for (const e of events) {
|
|
79
|
+
const day = (e.at || '').slice(0, 10); if (!day) continue;
|
|
80
|
+
const b = byDay.get(day) || { dd: 0, dp: 0, rd: 0, rp: 0, total: 0 };
|
|
81
|
+
const doc = e.mode === 'document';
|
|
82
|
+
if (e.action === 'draft') { if (doc) b.dd++; else b.rd++; } else if (doc) b.dp++; else b.rp++;
|
|
83
|
+
b.total++; byDay.set(day, b);
|
|
84
|
+
}
|
|
85
|
+
const today = new Date(); today.setHours(0, 0, 0, 0);
|
|
86
|
+
const { cols, monthLabels } = buildHeatmap(byDay, today);
|
|
87
|
+
|
|
88
|
+
return html`
|
|
89
|
+
<div class="pj-chart">
|
|
90
|
+
<div class="pj-chart-head">
|
|
91
|
+
<span class="pj-chart-title">${'📊 '}${t('organisms.activity') || 'Activity'}<span class="pj-act-count">${data.total} ${t('organisms.events') || 'events'}</span></span>
|
|
92
|
+
<button class="btn-ghost btn-sm" onClick=${() => setShow(s => !s)}>${show ? (t('organisms.hide') || 'Hide') : (t('organisms.show') || 'Show')}</button>
|
|
93
|
+
</div>
|
|
94
|
+
${show ? html`
|
|
95
|
+
<div class="pj-act">
|
|
96
|
+
<div class="pj-hm">
|
|
97
|
+
<div class="pj-hm-monthrow">${monthLabels.map((m, i) => html`<span class="pj-hm-month" key=${i}>${m}</span>`)}</div>
|
|
98
|
+
<div class="pj-hm-body">
|
|
99
|
+
<div class="pj-hm-daycol"><span></span><span>${t('organisms.mon') || 'Mon'}</span><span></span><span>${t('organisms.wed') || 'Wed'}</span><span></span><span>${t('organisms.fri') || 'Fri'}</span><span></span></div>
|
|
100
|
+
<div class="pj-hm-cols">
|
|
101
|
+
${cols.map((col, ci) => html`<div class="pj-hm-col" key=${ci}>
|
|
102
|
+
${col.map((cell, ri) => cell === null
|
|
103
|
+
? html`<span class="pj-hm-cell future" key=${ri}></span>`
|
|
104
|
+
: hmCell(cell.date, cell.b, ri))}
|
|
105
|
+
</div>`)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="pj-hm-legend">
|
|
110
|
+
<div class="pj-hm-quadkey">
|
|
111
|
+
<span class="pj-hm-cell"><i class="q lvl1"></i><i class="q lvl3"></i><i class="q lvl2"></i><i class="q lvl4"></i></span>
|
|
112
|
+
<div class="pj-hm-quadlabels">
|
|
113
|
+
<span>${'↖ '}${t('organisms.docsDraft') || 'Docs draft'}</span><span>${'↗ '}${t('organisms.docsPublished') || 'Docs published'}</span>
|
|
114
|
+
<span>${'↙ '}${t('organisms.recordsDraft') || 'Records draft'}</span><span>${'↘ '}${t('organisms.recordsPublished') || 'Records published'}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="pj-hm-intensity">
|
|
118
|
+
<span>${t('organisms.less') || 'Less'}</span>
|
|
119
|
+
<i class="q lvl0"></i><i class="q lvl1"></i><i class="q lvl2"></i><i class="q lvl3"></i><i class="q lvl4"></i>
|
|
120
|
+
<span>${t('organisms.more') || 'More'}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="pj-act-list">
|
|
124
|
+
${events.slice(0, 20).map((e, i) => html`<div class="pj-act-item" key=${i}>
|
|
125
|
+
<span class="pj-act-dot ${e.action}"></span>
|
|
126
|
+
<span class="pj-act-time">${dt(e.at)}</span>
|
|
127
|
+
<span class="pj-act-who">${(e.actor)}</span>
|
|
128
|
+
${e.agent ? html`<span class="pj-act-agent" title=${t('organisms.viaAgent') || 'via this agent'}>${'🤖 '}${(e.agent)}</span>` : null}
|
|
129
|
+
<span class="pj-act-act">${e.action === 'publish' ? (t('organisms.publishedVerb') || 'published') : (t('organisms.editedVerb') || 'edited')}</span>
|
|
130
|
+
<span class="pj-act-what">${(e.mode === 'document' ? '📄' : '🗂')} ${(e.type)}${' / '}${(e.instance)}</span>
|
|
131
|
+
</div>`)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>` : null}
|
|
134
|
+
</div>`;
|
|
135
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file agents.js
|
|
3
|
+
* @description Organism Agents tab — attached agents listed first; "+ Attach agent" opens a picker
|
|
4
|
+
* of the user's OWN agents (free-text GAII attach behind "Advanced"), with per-agent activity
|
|
5
|
+
* context aggregated from the accessible workspaces. Extracted from organisms-tab.js, no behaviour
|
|
6
|
+
* change.
|
|
7
|
+
* @structure OrgAgentsPanel
|
|
8
|
+
* @usage import { OrgAgentsPanel } from '/views/profile/organisms/agents.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 } 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 { listAgents, offersWorkspaceContract, contractNamesOf } from '/js/services/agents.js';
|
|
21
|
+
import { relTime } from '/views/profile/organisms/helpers.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Agents tab — attached agents listed first; "+ Attach agent" opens a picker of the user's OWN
|
|
25
|
+
* agents (the node knows them — no GAII syntax needed), with free-text attach-by-ID behind an
|
|
26
|
+
* "Advanced" link for cross-node agents. Each row shows where the agent has acted (aggregated
|
|
27
|
+
* from the accessible workspaces' activity feeds) or, for own agents, when it was last active.
|
|
28
|
+
*/
|
|
29
|
+
export function OrgAgentsPanel({ org, ghii, canManage, showToast, onChanged }) {
|
|
30
|
+
const orgId = org.id;
|
|
31
|
+
const attached = org.agentGaiis || [];
|
|
32
|
+
const [mine, setMine] = useState(null); // own agents (picker) — null = loading
|
|
33
|
+
const [pick, setPick] = useState('');
|
|
34
|
+
const [showAttach, setShowAttach] = useState(false);
|
|
35
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
36
|
+
const [agentId, setAgentId] = useState('');
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
const [acted, setActed] = useState({}); // agent name → { count, lastAt, ws: Set }
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
listAgents(ghii).then(a => setMine((a || []).filter(x => !String(x.name || '').startsWith('session-')))).catch(() => setMine([]));
|
|
42
|
+
}, [ghii]);
|
|
43
|
+
|
|
44
|
+
// Best-effort activity context: which workspaces each agent has touched, and when last.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
(async () => {
|
|
48
|
+
try {
|
|
49
|
+
const wss = (await orgService.discoverWorkspaces(orgId)).filter(w => w.access !== 'none');
|
|
50
|
+
const map = {};
|
|
51
|
+
await Promise.all(wss.map(async (w) => {
|
|
52
|
+
const a = await orgService.getWorkspaceActivity(orgId, w.id);
|
|
53
|
+
for (const e of (a.events || [])) {
|
|
54
|
+
if (!e.agent) continue;
|
|
55
|
+
const m = map[e.agent] || (map[e.agent] = { count: 0, lastAt: '', ws: new Set() });
|
|
56
|
+
m.count++; m.ws.add(w.name || w.id);
|
|
57
|
+
if (!m.lastAt || e.at > m.lastAt) m.lastAt = e.at;
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
if (!cancelled) setActed(map);
|
|
61
|
+
} catch { /* context is optional */ }
|
|
62
|
+
})();
|
|
63
|
+
return () => { cancelled = true; };
|
|
64
|
+
}, [orgId, attached.length]);
|
|
65
|
+
|
|
66
|
+
const run = async (fn, okMsg) => {
|
|
67
|
+
setBusy(true);
|
|
68
|
+
try {
|
|
69
|
+
const r = await fn();
|
|
70
|
+
if (r?.ok === false) showToast(r?.error?.message || (t('organisms.agentFailed') || 'Failed'));
|
|
71
|
+
else { if (okMsg) showToast(okMsg); onChanged?.(); }
|
|
72
|
+
} catch (e) { showToast((e && e.message) || (t('organisms.agentFailed') || 'Failed')); }
|
|
73
|
+
finally { setBusy(false); }
|
|
74
|
+
};
|
|
75
|
+
const attach = (g) => {
|
|
76
|
+
const id = (g || '').trim();
|
|
77
|
+
if (!id) return;
|
|
78
|
+
setPick(''); setAgentId(''); setShowAttach(false);
|
|
79
|
+
run(() => orgService.attachAgent(orgId, id), t('organisms.agentAttached') || 'Agent attached');
|
|
80
|
+
};
|
|
81
|
+
const detach = (g) => run(() => orgService.detachAgent(orgId, g), t('organisms.agentDetached') || 'Agent detached');
|
|
82
|
+
|
|
83
|
+
const attachedSet = new Set(attached);
|
|
84
|
+
const pickable = (mine || []).filter(a => a.gaii && !attachedSet.has(a.gaii));
|
|
85
|
+
const parseGaii = (g) => { const m = /^([^#]+)#([^@]+)@(.+)$/.exec(g) || []; return { name: m[1] || g, owner: m[2] || '', node: m[3] || '' }; };
|
|
86
|
+
const ownByGaii = new Map((mine || []).map(a => [a.gaii, a]));
|
|
87
|
+
|
|
88
|
+
return html`
|
|
89
|
+
<div class="card-detail">
|
|
90
|
+
<div class="pj-tabhead">
|
|
91
|
+
<div class="section-desc pj-tabhead-desc">${t('organisms.agentsDesc') || 'An attached agent works in this organism with its owner’s member rights — it shows up in workspace participants and activity.'}</div>
|
|
92
|
+
<button class="btn-primary btn-sm" onClick=${() => setShowAttach(s => !s)}>${'+ '}${t('organisms.attachAgent') || 'Attach agent'}</button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
${showAttach ? html`
|
|
96
|
+
<div class="pj-attach">
|
|
97
|
+
${mine === null ? html`<${Spinner} />` : (pickable.length > 0 ? html`
|
|
98
|
+
<div class="flex-row-wrap">
|
|
99
|
+
<select class="input-field input-sm" value=${pick} onChange=${e => setPick(e.target.value)}>
|
|
100
|
+
<option value="">${t('organisms.pickAgent') || 'Choose one of your agents…'}</option>
|
|
101
|
+
${pickable.map(a => html`<option value=${a.gaii} key=${a.gaii}>${(a.display_name || a.name) + (offersWorkspaceContract(a) ? ` · 📜 ${t('organisms.contractTag') || 'contract'}` : '')}</option>`)}
|
|
102
|
+
</select>
|
|
103
|
+
<button class="btn-outline btn-sm" disabled=${busy || !pick} onClick=${() => attach(pick)}>${t('organisms.attach') || 'Attach'}</button>
|
|
104
|
+
</div>` : html`
|
|
105
|
+
<div class="section-desc">${t('organisms.noOwnAgentsLeft') || 'All your agents are already attached (or you have none yet).'}</div>`)}
|
|
106
|
+
<button class="pj-linklike" onClick=${() => setShowAdvanced(s => !s)}>${t('organisms.advancedAttach') || 'Advanced: attach by ID'}</button>
|
|
107
|
+
${showAdvanced ? html`
|
|
108
|
+
<div class="flex-row-wrap">
|
|
109
|
+
<input class="input-field input-sm" placeholder=${t('organisms.agentGaiiPlaceholder') || 'agent#owner@node'} value=${agentId}
|
|
110
|
+
onInput=${(e) => setAgentId(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') attach(agentId); }} />
|
|
111
|
+
<button class="btn-outline btn-sm" disabled=${busy || !agentId.trim()} onClick=${() => attach(agentId)}>${t('organisms.attach') || 'Attach'}</button>
|
|
112
|
+
</div>` : null}
|
|
113
|
+
</div>` : null}
|
|
114
|
+
|
|
115
|
+
${attached.length === 0 ? html`<${EmptyState} icon="🤖" text=${t('organisms.noAgents') || 'No agents attached.'} />` : null}
|
|
116
|
+
${attached.map(g => {
|
|
117
|
+
const p = parseGaii(g);
|
|
118
|
+
const own = ownByGaii.get(g);
|
|
119
|
+
const act = acted[p.name];
|
|
120
|
+
return html`
|
|
121
|
+
<div class="pj-org-row" key=${'ag-' + g}>
|
|
122
|
+
<div class="pj-org-avatar" aria-hidden="true">${'🤖'}</div>
|
|
123
|
+
<div class="pj-org-main pj-org-main-static">
|
|
124
|
+
<div class="pj-org-titlerow">
|
|
125
|
+
<span class="pj-org-name">${(own?.display_name || p.name)}</span>
|
|
126
|
+
${p.node ? html`<span class="badge badge-info">${(p.node)}</span>` : null}
|
|
127
|
+
${own && offersWorkspaceContract(own) ? html`<span class="badge badge-success" title=${(t('organisms.contractAgentHint') || 'Advertises a workspace contract') + (contractNamesOf(own).length ? `: ${contractNamesOf(own).join(', ')}` : '')}>${'📜 '}${t('organisms.contractTag') || 'contract'}</span>` : null}
|
|
128
|
+
</div>
|
|
129
|
+
<div class="pj-org-desc" title=${g}>
|
|
130
|
+
<span class="mono">${(p.owner ? `${p.name}#${p.owner}` : g)}</span>
|
|
131
|
+
${act ? html` · ${t('organisms.agentActedIn') || 'active in'} ${([...act.ws].join(', '))} (${act.count}) · ${relTime(act.lastAt)}`
|
|
132
|
+
: (own?.last_seen ? html` · ${t('organisms.lastActive') || 'last active'} ${relTime(own.last_seen)}` : null)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
${(canManage || g.includes('#' + ghii + '@'))
|
|
136
|
+
? html`<button class="btn-outline btn-sm" disabled=${busy} onClick=${() => detach(g)}>${t('organisms.detach') || 'Detach'}</button>`
|
|
137
|
+
: null}
|
|
138
|
+
</div>`;
|
|
139
|
+
})}
|
|
140
|
+
</div>`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file document.js
|
|
3
|
+
* @description Document-space editor + viewer for organism workspaces: a read-only DocumentView
|
|
4
|
+
* (markdown render, private-image resolution, Draft/Published compare toggle) and a DocumentEditor
|
|
5
|
+
* (Toast UI WYSIWYG with a markdown fallback, image upload + per-image visibility). Lazy-loads the
|
|
6
|
+
* vendored Toast UI Editor. Extracted from organisms-tab.js with no behaviour change.
|
|
7
|
+
* @structure loadToastUI (internal), DocumentView, DocumentEditor
|
|
8
|
+
* @usage import { DocumentView, DocumentEditor } from '/views/profile/organisms/document.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, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { VisibilityPill } from '/views/profile/shared.js';
|
|
18
|
+
import { KeyValueRow } from '/components/KeyValueRow.js';
|
|
19
|
+
import { Markdown } from '/components/Markdown.js';
|
|
20
|
+
import { dt } from '/js/format.js';
|
|
21
|
+
import * as orgService from '/js/services/organisms.js';
|
|
22
|
+
|
|
23
|
+
// Lazy-load the vendored Toast UI Editor (MIT, /lib/toastui/) only when a document is edited —
|
|
24
|
+
// it's ~520KB, so it stays out of the main bundle. Resolves window.toastui.Editor.
|
|
25
|
+
let _tuiPromise = null;
|
|
26
|
+
function loadToastUI() {
|
|
27
|
+
if (window.toastui && window.toastui.Editor) return Promise.resolve(window.toastui.Editor);
|
|
28
|
+
if (_tuiPromise) return _tuiPromise;
|
|
29
|
+
_tuiPromise = new Promise((resolve, reject) => {
|
|
30
|
+
const css = document.createElement('link');
|
|
31
|
+
css.rel = 'stylesheet'; css.href = '/lib/toastui/toastui-editor.min.css';
|
|
32
|
+
document.head.appendChild(css);
|
|
33
|
+
const s = document.createElement('script');
|
|
34
|
+
s.src = '/lib/toastui/toastui-editor-all.min.js';
|
|
35
|
+
s.onload = () => (window.toastui && window.toastui.Editor) ? resolve(window.toastui.Editor) : reject(new Error('editor missing'));
|
|
36
|
+
s.onerror = () => reject(new Error('failed to load editor'));
|
|
37
|
+
document.head.appendChild(s);
|
|
38
|
+
});
|
|
39
|
+
return _tuiPromise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Read-only document view. Renders the markdown, resolves private /v1/storage images (the GET needs
|
|
43
|
+
* the session token, so a plain <img> would break), and — when the document has BOTH an unpublished
|
|
44
|
+
* draft and a published version — offers a Draft/Published toggle so the two can be compared. The
|
|
45
|
+
* parent passes the merged `page` (the draft, carrying the published copy on `page._pub`). Remounted
|
|
46
|
+
* per document via `key`, so the toggle resets to "Draft" each time a document is opened. */
|
|
47
|
+
export function DocumentView({ page, busy, onEdit, onPublish, onWikiLink, onPopOut }) {
|
|
48
|
+
const hasBoth = page._draft && page._pub;
|
|
49
|
+
const [tab, setTab] = useState('draft');
|
|
50
|
+
const shown = (hasBoth && tab === 'published') ? page._pub : page;
|
|
51
|
+
const [rendered, setRendered] = useState(shown.markdown || '');
|
|
52
|
+
|
|
53
|
+
// Resolve private /v1/storage images to auth'd blob: URLs IN THE MARKDOWN TEXT (declarative), then
|
|
54
|
+
// render that. Doing it in the text — instead of mutating <img src> after render — means a
|
|
55
|
+
// re-render (toggling Draft/Published, a live-update refresh) can never leave a stale or revoked
|
|
56
|
+
// object URL on a reused <img> node, which previously showed a broken image. Re-runs per version.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false; const created = [];
|
|
59
|
+
const raw = shown.markdown || '';
|
|
60
|
+
setRendered(raw); // show text/structure at once; images swap in a moment later
|
|
61
|
+
(async () => {
|
|
62
|
+
const urls = [...new Set(raw.match(/\/v1\/storage\/[^\s)\]"'>]+/g) || [])];
|
|
63
|
+
if (!urls.length) return;
|
|
64
|
+
let out = raw;
|
|
65
|
+
for (const su of urls) {
|
|
66
|
+
try { const bu = await orgService.fetchStorageObjectUrl(su); created.push(bu); out = out.split(su).join(bu); }
|
|
67
|
+
catch (e) { /* leave the storage URL — renders broken but never throws */ }
|
|
68
|
+
}
|
|
69
|
+
if (!cancelled) setRendered(out);
|
|
70
|
+
})();
|
|
71
|
+
return () => { cancelled = true; created.forEach(u => { try { URL.revokeObjectURL(u); } catch (e) { /* noop */ } }); };
|
|
72
|
+
}, [shown.markdown]);
|
|
73
|
+
|
|
74
|
+
// created/saved/published timestamps come from the workspace read (record metadata on the value).
|
|
75
|
+
const created = page._createdAt || page._pub?._createdAt;
|
|
76
|
+
const savedAt = page._draft ? page._updatedAt : null; // draft = working copy → "last saved"
|
|
77
|
+
const publishedAt = page._pub?._updatedAt || (!page._draft && page._published ? page._updatedAt : null);
|
|
78
|
+
|
|
79
|
+
return html`
|
|
80
|
+
<div class="pj-doc-toolbar">
|
|
81
|
+
<span class="pj-doc-vtitle">${shown.title || shown.id || page.id}</span>
|
|
82
|
+
${hasBoth ? html`
|
|
83
|
+
<div class="seg" role="tablist">
|
|
84
|
+
<button class="seg-btn ${tab === 'draft' ? 'active' : ''}" onClick=${() => setTab('draft')}>${t('organisms.draftVersion') || 'Draft'}</button>
|
|
85
|
+
<button class="seg-btn ${tab === 'published' ? 'active' : ''}" onClick=${() => setTab('published')}>${t('organisms.publishedVersion') || 'Published'}</button>
|
|
86
|
+
</div>` : null}
|
|
87
|
+
<button class="btn-ghost btn-sm" onClick=${onEdit}>${t('organisms.edit') || 'Edit'}</button>
|
|
88
|
+
${page._draft ? html`<button class="btn-primary btn-sm" onClick=${onPublish} disabled=${busy}>${t('organisms.publish') || 'Publish'}</button>` : null}
|
|
89
|
+
${onPopOut ? html`<button class="btn-ghost btn-sm pj-doc-popout" title=${t('organisms.popOut') || 'Open in its own window'} onClick=${onPopOut}>${'⧉'}</button>` : null}
|
|
90
|
+
</div>
|
|
91
|
+
${(created || savedAt || publishedAt) ? html`
|
|
92
|
+
<div class="pj-doc-meta">
|
|
93
|
+
${created ? html`<${KeyValueRow} label=${t('organisms.createdAt') || 'Created'} value=${dt(created)} />` : null}
|
|
94
|
+
${savedAt ? html`<${KeyValueRow} label=${t('organisms.lastSaved') || 'Last saved'} value=${dt(savedAt)} />` : null}
|
|
95
|
+
${publishedAt ? html`<${KeyValueRow} label=${t('organisms.publishedAt') || 'Published'} value=${dt(publishedAt)} />` : null}
|
|
96
|
+
</div>` : null}
|
|
97
|
+
<div class="pj-doc-view"><${Markdown} text=${rendered} onWikiLink=${onWikiLink} /></div>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Document editor: a Toast UI Editor (WYSIWYG, with its own built-in Markdown⇄WYSIWYG toggle, so
|
|
101
|
+
* non-technical users type like a document). Falls back to a plain markdown textarea + live preview
|
|
102
|
+
* if the editor can't load. Title is a separate Preact-controlled field. */
|
|
103
|
+
export function DocumentEditor({ orgId, page, busy, onSave, onCancel }) {
|
|
104
|
+
const [title, setTitle] = useState((page && page.title) || '');
|
|
105
|
+
const [mode, setMode] = useState('rich'); // 'rich' = Toast UI; 'markdown' = fallback textarea
|
|
106
|
+
const [md, setMd] = useState((page && page.markdown) || '');
|
|
107
|
+
const containerRef = useRef(null);
|
|
108
|
+
const editorRef = useRef(null);
|
|
109
|
+
const imageMap = useRef({}); // data: URL (shown in editor) → /v1/storage URL (saved)
|
|
110
|
+
const displayMap = useRef({}); // blob: URL (shown in editor) → /v1/storage URL (saved) — for already-stored images
|
|
111
|
+
const pending = useRef([]); // in-flight image uploads — save() awaits these
|
|
112
|
+
const [saving, setSaving] = useState(false);
|
|
113
|
+
const [images, setImages] = useState([]); // embedded /v1/storage images: [{key, alt, visibility}]
|
|
114
|
+
const [imgBusy, setImgBusy] = useState(false);
|
|
115
|
+
|
|
116
|
+
// Load the visibility of the document's already-saved /v1/storage images, so the author can make
|
|
117
|
+
// them public (a private image won't load for other viewers of a shared/published document).
|
|
118
|
+
// Newly-pasted images appear here after the first save + reopen.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
let cancelled = false;
|
|
121
|
+
const embedded = orgService.extractStorageImages((page && page.markdown) || '');
|
|
122
|
+
if (!embedded.length) { setImages([]); return undefined; }
|
|
123
|
+
orgService.listStorageVisibilities().then((vis) => {
|
|
124
|
+
if (!cancelled) setImages(embedded.map(e => ({ ...e, visibility: vis[e.key] || 'private' })));
|
|
125
|
+
}).catch(() => { if (!cancelled) setImages(embedded.map(e => ({ ...e, visibility: 'private' }))); });
|
|
126
|
+
return () => { cancelled = true; };
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const changeImageVisibility = async (key, visibility) => {
|
|
130
|
+
setImgBusy(true);
|
|
131
|
+
try {
|
|
132
|
+
const r = await orgService.setImageVisibility(key, visibility);
|
|
133
|
+
if (r?.ok === false) throw new Error(r?.error?.message || 'Failed');
|
|
134
|
+
setImages(imgs => imgs.map(i => i.key === key ? { ...i, visibility } : i));
|
|
135
|
+
} catch (e) { /* leave as-is; a failed toggle just doesn't change the pill */ }
|
|
136
|
+
finally { setImgBusy(false); }
|
|
137
|
+
};
|
|
138
|
+
const makeAllImagesPublic = async () => {
|
|
139
|
+
setImgBusy(true);
|
|
140
|
+
try {
|
|
141
|
+
const targets = images.filter(i => i.visibility !== 'public');
|
|
142
|
+
await Promise.all(targets.map(i => orgService.setImageVisibility(i.key, 'public').catch(() => {})));
|
|
143
|
+
setImages(imgs => imgs.map(i => ({ ...i, visibility: 'public' })));
|
|
144
|
+
} finally { setImgBusy(false); }
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Show the image instantly via a data URL, upload to storage in the background, and remember the
|
|
148
|
+
// mapping so save() rewrites the data URL to the storage URL. If the upload fails, the image stays
|
|
149
|
+
// inline (still renders + saves, just larger) — so an image NEVER silently disappears.
|
|
150
|
+
const uploadAndMap = (blob, dataUrl) => {
|
|
151
|
+
pending.current.push(
|
|
152
|
+
orgService.uploadImage(orgId, blob, blob.type || 'image/png')
|
|
153
|
+
.then((url) => { imageMap.current[dataUrl] = url; })
|
|
154
|
+
.catch(() => { /* keep the inline data URL */ }),
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
const insertFromFile = async (file) => {
|
|
158
|
+
if (!file) return;
|
|
159
|
+
const dataUrl = await orgService.blobToDataUrl(file);
|
|
160
|
+
if (mode === 'rich' && editorRef.current) editorRef.current.exec('addImage', { imageUrl: dataUrl, altText: file.name || 'image' });
|
|
161
|
+
else setMd((m) => m + `\n\n\n`);
|
|
162
|
+
uploadAndMap(file, dataUrl);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (mode !== 'rich') return undefined;
|
|
167
|
+
let inst = null, cancelled = false; const blobUrls = [];
|
|
168
|
+
(async () => {
|
|
169
|
+
const Editor = await loadToastUI().catch(() => null);
|
|
170
|
+
if (cancelled) return;
|
|
171
|
+
if (!Editor) { setMode('markdown'); return; }
|
|
172
|
+
// Already-stored images embed a private /v1/storage URL, which a plain <img> in the editor
|
|
173
|
+
// can't load (the GET needs the session token). Fetch each with auth, show it as a blob: URL,
|
|
174
|
+
// and remember blob→storage so save() rewrites it back to the canonical storage URL.
|
|
175
|
+
let initial = (page && page.markdown) || '';
|
|
176
|
+
const urls = [...new Set(initial.match(/\/v1\/storage\/[^\s)\]"'>]+/g) || [])];
|
|
177
|
+
for (const su of urls) {
|
|
178
|
+
try {
|
|
179
|
+
const bu = await orgService.fetchStorageObjectUrl(su);
|
|
180
|
+
displayMap.current[bu] = su; blobUrls.push(bu);
|
|
181
|
+
initial = initial.split(su).join(bu);
|
|
182
|
+
} catch (e) { /* leave the storage URL — it renders broken but saves intact */ }
|
|
183
|
+
}
|
|
184
|
+
if (cancelled || !containerRef.current) return;
|
|
185
|
+
inst = new Editor({
|
|
186
|
+
el: containerRef.current,
|
|
187
|
+
height: '440px',
|
|
188
|
+
initialEditType: 'wysiwyg',
|
|
189
|
+
previewStyle: 'tab',
|
|
190
|
+
initialValue: initial,
|
|
191
|
+
usageStatistics: false,
|
|
192
|
+
// Drop Toast UI's own image button — its file popup is unreliable; the "📷 Insert image"
|
|
193
|
+
// button above is the image path (and paste/drag still work via the hook below).
|
|
194
|
+
toolbarItems: [
|
|
195
|
+
['heading', 'bold', 'italic', 'strike'],
|
|
196
|
+
['hr', 'quote'],
|
|
197
|
+
['ul', 'ol', 'task', 'indent', 'outdent'],
|
|
198
|
+
['table', 'link'],
|
|
199
|
+
['code', 'codeblock'],
|
|
200
|
+
],
|
|
201
|
+
hooks: {
|
|
202
|
+
// Paste/drag an image → insert it as a data URL (shows at once) + upload in the background.
|
|
203
|
+
addImageBlobHook: async (blob, callback) => {
|
|
204
|
+
const dataUrl = await orgService.blobToDataUrl(blob);
|
|
205
|
+
callback(dataUrl, '');
|
|
206
|
+
uploadAndMap(blob, dataUrl);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
editorRef.current = inst;
|
|
211
|
+
})();
|
|
212
|
+
return () => {
|
|
213
|
+
cancelled = true;
|
|
214
|
+
if (inst) { try { inst.destroy(); } catch (e) { /* noop */ } }
|
|
215
|
+
editorRef.current = null;
|
|
216
|
+
blobUrls.forEach(u => { try { URL.revokeObjectURL(u); } catch (e) { /* noop */ } });
|
|
217
|
+
};
|
|
218
|
+
}, [mode]);
|
|
219
|
+
|
|
220
|
+
const save = async () => {
|
|
221
|
+
setSaving(true);
|
|
222
|
+
try {
|
|
223
|
+
if (pending.current.length) { await Promise.all(pending.current); pending.current = []; } // finish uploads first
|
|
224
|
+
let markdown = (mode === 'rich' && editorRef.current) ? editorRef.current.getMarkdown() : md;
|
|
225
|
+
for (const [dataUrl, storageUrl] of Object.entries(imageMap.current)) markdown = markdown.split(dataUrl).join(storageUrl);
|
|
226
|
+
for (const [blobUrl, storageUrl] of Object.entries(displayMap.current)) markdown = markdown.split(blobUrl).join(storageUrl);
|
|
227
|
+
// Point each image at the URL form its visibility needs: public → /v1/pub/<ghii>/<key> (loads
|
|
228
|
+
// for any viewer), private → /v1/storage/<key> (owner-only). So a public document's images render.
|
|
229
|
+
const visByKey = {}; for (const im of images) visByKey[im.key] = im.visibility;
|
|
230
|
+
markdown = orgService.applyImageVisibilityUrls(markdown, visByKey, orgService.currentGhii());
|
|
231
|
+
onSave({ ...page, title: title.trim(), markdown });
|
|
232
|
+
} finally { setSaving(false); }
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return html`
|
|
236
|
+
<div class="pj-doc-editor">
|
|
237
|
+
<input type="text" class="input-field input-sm" placeholder=${t('organisms.pageTitle') || 'Document title'}
|
|
238
|
+
value=${title} onInput=${e => setTitle(e.target.value)} />
|
|
239
|
+
<div class="pj-doc-imgbar">
|
|
240
|
+
<label class="btn-outline btn-sm pj-file-btn">
|
|
241
|
+
<span class="pj-file-btn-icon">📷</span> ${t('organisms.insertImage') || 'Upload image from file'}
|
|
242
|
+
<input type="file" accept="image/*" hidden onChange=${e => { insertFromFile(e.target.files && e.target.files[0]); e.target.value = ''; }} />
|
|
243
|
+
</label>
|
|
244
|
+
<span class="pj-imgbar-hint">${t('organisms.orPaste') || '…or paste / drag an image into the editor'}</span>
|
|
245
|
+
</div>
|
|
246
|
+
${images.length ? html`
|
|
247
|
+
<div class="pj-img-vis">
|
|
248
|
+
<div class="pj-img-vis-head">
|
|
249
|
+
<span class="pj-img-vis-title">${t('organisms.fileVisibility') || 'File visibility'}</span>
|
|
250
|
+
<span class="pj-img-vis-note">${t('organisms.fileVisibilityNote') || 'Private files only load for you — make them public to share the document.'}</span>
|
|
251
|
+
${images.some(i => i.visibility !== 'public') ? html`<button class="btn-ghost btn-sm" disabled=${imgBusy} onClick=${makeAllImagesPublic}>${t('organisms.makeAllPublic') || 'Make all public'}</button>` : null}
|
|
252
|
+
</div>
|
|
253
|
+
${images.map(i => html`
|
|
254
|
+
<div class="pj-img-vis-row" key=${i.key}>
|
|
255
|
+
<span class="pj-img-vis-name" title=${i.key}>${(i.alt)}</span>
|
|
256
|
+
<${VisibilityPill} visibility=${i.visibility} onClick=${() => { if (!imgBusy) changeImageVisibility(i.key, i.visibility === 'public' ? 'private' : 'public'); }} />
|
|
257
|
+
</div>`)}
|
|
258
|
+
</div>` : null}
|
|
259
|
+
${mode === 'rich'
|
|
260
|
+
? html`<div ref=${containerRef} class="pj-tui"></div>`
|
|
261
|
+
: html`<div class="pj-doc-grid">
|
|
262
|
+
<textarea class="input-field pj-doc-md" rows="14" placeholder=${t('organisms.writeMarkdown') || 'Write markdown…'}
|
|
263
|
+
value=${md} onInput=${e => setMd(e.target.value)}></textarea>
|
|
264
|
+
<div class="pj-doc-preview"><${Markdown} text=${md} /></div>
|
|
265
|
+
</div>`}
|
|
266
|
+
<div class="form-actions">
|
|
267
|
+
<button class="btn-primary btn-sm" onClick=${save} disabled=${busy || saving || !title.trim()}>
|
|
268
|
+
${saving ? html`<span class="spinner"></span> ${t('organisms.saving') || 'Saving…'}` : (t('organisms.saveDraft') || 'Save draft')}
|
|
269
|
+
</button>
|
|
270
|
+
<button class="btn-ghost btn-sm" onClick=${onCancel}>${t('organisms.cancel') || 'Cancel'}</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file helpers.js
|
|
3
|
+
* @description Shared pure helpers for the organisms tab modules — locale-aware date
|
|
4
|
+
* formatting, relative-time labels, avatar monograms, and the whole-organism ZIP export.
|
|
5
|
+
* Extracted from organisms-tab.js (no behaviour change) so every organism module shares
|
|
6
|
+
* one copy instead of re-implementing them.
|
|
7
|
+
* @structure fmtDate, relTime, orgInitials, exportOrganismZip
|
|
8
|
+
* @usage import { fmtDate, relTime, orgInitials, exportOrganismZip } from '/views/profile/organisms/helpers.js';
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
*/
|
|
12
|
+
import { t, getLocale } from '/js/i18n.js';
|
|
13
|
+
|
|
14
|
+
/** Date-only, formatted in the APP locale (browser locale may differ — fi must show 10.6.2026, not 6/10/2026).
|
|
15
|
+
* @param {string} s ISO date string
|
|
16
|
+
* @returns {string} */
|
|
17
|
+
export function fmtDate(s) {
|
|
18
|
+
return new Date(s).toLocaleDateString(getLocale() === 'fi' ? 'fi-FI' : undefined);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Relative time for list metadata ("2 h ago" / "2 h sitten"); falls back to a locale date past 7 days.
|
|
22
|
+
* @param {string} s ISO date string
|
|
23
|
+
* @returns {string} */
|
|
24
|
+
export function relTime(s) {
|
|
25
|
+
const ts = new Date(s).getTime();
|
|
26
|
+
if (!Number.isFinite(ts)) return '';
|
|
27
|
+
const mins = Math.max(0, Math.round((Date.now() - ts) / 60000));
|
|
28
|
+
if (mins < 60) return (t('organisms.relMin') || '{n} min ago').replace('{n}', String(mins));
|
|
29
|
+
const hours = Math.round(mins / 60);
|
|
30
|
+
if (hours < 24) return (t('organisms.relHours') || '{n} h ago').replace('{n}', String(hours));
|
|
31
|
+
const days = Math.round(hours / 24);
|
|
32
|
+
if (days <= 7) return (t('organisms.relDays') || '{n} d ago').replace('{n}', String(days));
|
|
33
|
+
return fmtDate(s);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Two-letter monogram for the list/home avatar (initials of the first two words, else first two chars).
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @returns {string} */
|
|
39
|
+
export function orgInitials(name) {
|
|
40
|
+
const s = String(name || '?').trim();
|
|
41
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
42
|
+
const ini = words.length >= 2 ? words[0][0] + words[1][0] : s.slice(0, 2);
|
|
43
|
+
return ini.charAt(0).toUpperCase() + (ini.charAt(1) || '').toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Download a whole-organism ZIP backup (used from the list "…" menu and the home header).
|
|
47
|
+
* @param {{ id: string, name?: string }} org
|
|
48
|
+
* @param {(msg: string) => void} showToast
|
|
49
|
+
* @returns {Promise<void>} */
|
|
50
|
+
export async function exportOrganismZip(org, showToast) {
|
|
51
|
+
try {
|
|
52
|
+
const jwt = window.AIMEAT?.auth?.getSession?.()?.jwt || '';
|
|
53
|
+
const res = await fetch(`/v1/organisms/${encodeURIComponent(org.id)}/export`, { headers: { Authorization: 'Bearer ' + jwt } });
|
|
54
|
+
if (!res.ok) throw new Error('Export failed');
|
|
55
|
+
const blob = await res.blob();
|
|
56
|
+
const url = URL.createObjectURL(blob);
|
|
57
|
+
const a = document.createElement('a');
|
|
58
|
+
a.href = url; a.download = `organism-${String(org.name || org.id).replace(/[^a-z0-9_-]+/gi, '-').slice(0, 40)}.zip`;
|
|
59
|
+
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
|
|
60
|
+
} catch (e) { showToast((e && e.message) || 'Export failed'); }
|
|
61
|
+
}
|