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
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* AppStrip — home/section sub-components
|
|
16
16
|
* - LandingPage — main orchestrator (default export)
|
|
17
17
|
* @version-history
|
|
18
|
+
* v3.3.0 — 2026-06-19 — Add the PresenceControl card to the home dashboard (own availability:
|
|
19
|
+
* auto/manual status + who-can-see visibility), previewing the shared <PresenceDot>.
|
|
18
20
|
* v3.2.0 — 2026-06-10 — Sidebar reorg: groups follow the information-refinement pipeline
|
|
19
21
|
* (Information / Automation / Activity / Build & Share / Account / Infrastructure) —
|
|
20
22
|
* Daily/Personal/Technical are gone. Pinned section under Home (pin-on-hover 📌, max 5,
|
|
@@ -65,7 +67,9 @@ import * as orgService from '/js/services/organisms.js';
|
|
|
65
67
|
import { listRecents } from '/js/recents.js';
|
|
66
68
|
import { getMemory, createMemory } from '/js/services/memory.js';
|
|
67
69
|
import { listInbox } from '/js/services/messages.js';
|
|
70
|
+
import { getMyPresence, setMyPresence } from '/js/services/presence.js';
|
|
68
71
|
import { Spinner } from './shared.js';
|
|
72
|
+
import { PresenceDot } from '/components/PresenceDot.js';
|
|
69
73
|
import { minidenticon } from '/lib/minidenticons.min.js';
|
|
70
74
|
|
|
71
75
|
/* ───── Small time helpers (reuse the organisms rel-time keys) ───── */
|
|
@@ -515,6 +519,88 @@ function AgentsCard({ owner }) {
|
|
|
515
519
|
`;
|
|
516
520
|
}
|
|
517
521
|
|
|
522
|
+
/* "Presence" — the owner's own availability control: status source (auto/manual),
|
|
523
|
+
* chosen status, and who-can-see visibility. One self-contained card; the dot it
|
|
524
|
+
* previews is the same <PresenceDot> rendered next to people everywhere else. */
|
|
525
|
+
function PresenceControl() {
|
|
526
|
+
const [cfg, setCfg] = useState(null);
|
|
527
|
+
const [status, setStatus] = useState('unknown');
|
|
528
|
+
const [saving, setSaving] = useState(false);
|
|
529
|
+
|
|
530
|
+
const load = useCallback(async () => {
|
|
531
|
+
try {
|
|
532
|
+
const r = await getMyPresence();
|
|
533
|
+
if (r?.data) { setCfg(r.data.config); setStatus(r.data.status || 'unknown'); }
|
|
534
|
+
} catch { /* control stays hidden until it loads */ }
|
|
535
|
+
}, []);
|
|
536
|
+
useEffect(() => { load(); }, [load]);
|
|
537
|
+
|
|
538
|
+
const save = async (partial) => {
|
|
539
|
+
if (!cfg) return;
|
|
540
|
+
const optimistic = { ...cfg, ...partial };
|
|
541
|
+
setCfg(optimistic);
|
|
542
|
+
setSaving(true);
|
|
543
|
+
try {
|
|
544
|
+
const r = await setMyPresence(partial);
|
|
545
|
+
if (r?.data) { setCfg(r.data.config); setStatus(r.data.status || 'unknown'); }
|
|
546
|
+
} catch { setCfg(cfg); /* revert */ }
|
|
547
|
+
setSaving(false);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
if (!cfg) return null;
|
|
551
|
+
|
|
552
|
+
return html`
|
|
553
|
+
<div class="pf-presence-card">
|
|
554
|
+
<div class="pf-presence-head">
|
|
555
|
+
<div>
|
|
556
|
+
<div class="section-title">${t('presence.control.title')}</div>
|
|
557
|
+
<div class="section-desc">${t('presence.control.desc')}</div>
|
|
558
|
+
</div>
|
|
559
|
+
<${PresenceDot} status=${status} size="md" label=${true} />
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="pf-presence-row">
|
|
563
|
+
<label class="pf-presence-label">${t('presence.control.modeLabel')}</label>
|
|
564
|
+
<div class="pf-presence-modes">
|
|
565
|
+
<button class=${'pf-presence-pill' + (cfg.mode === 'auto' ? ' pf-presence-pill--on' : '')}
|
|
566
|
+
disabled=${saving} onClick=${() => save({ mode: 'auto' })}>${t('presence.control.modeAuto')}</button>
|
|
567
|
+
<button class=${'pf-presence-pill' + (cfg.mode === 'manual' ? ' pf-presence-pill--on' : '')}
|
|
568
|
+
disabled=${saving} onClick=${() => save({ mode: 'manual' })}>${t('presence.control.modeManual')}</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
${cfg.mode === 'auto' ? html`
|
|
573
|
+
<div class="pf-presence-hint">${t('presence.control.modeAutoHint')}</div>
|
|
574
|
+
` : html`
|
|
575
|
+
<div class="pf-presence-row">
|
|
576
|
+
<label class="pf-presence-label" for="pf-presence-status">${t('presence.control.statusLabel')}</label>
|
|
577
|
+
<select id="pf-presence-status" class="pf-edit-select" value=${cfg.status} disabled=${saving}
|
|
578
|
+
onChange=${(e) => save({ status: e.target.value })}>
|
|
579
|
+
<option value="available">${t('presence.status.available')}</option>
|
|
580
|
+
<option value="busy">${t('presence.status.busy')}</option>
|
|
581
|
+
<option value="away">${t('presence.status.away')}</option>
|
|
582
|
+
<option value="invisible">${t('presence.status.invisible')}</option>
|
|
583
|
+
</select>
|
|
584
|
+
</div>
|
|
585
|
+
`}
|
|
586
|
+
|
|
587
|
+
<div class="pf-presence-row">
|
|
588
|
+
<label class="pf-presence-label" for="pf-presence-vis">${t('presence.control.visibilityLabel')}</label>
|
|
589
|
+
<select id="pf-presence-vis" class="pf-edit-select" value=${cfg.visibility} disabled=${saving}
|
|
590
|
+
onChange=${(e) => save({ visibility: e.target.value })}>
|
|
591
|
+
<option value="everyone">${t('presence.control.visEveryone')}</option>
|
|
592
|
+
<option value="contacts">${t('presence.control.visContacts')}</option>
|
|
593
|
+
<option value="nobody">${t('presence.control.visNobody')}</option>
|
|
594
|
+
</select>
|
|
595
|
+
</div>
|
|
596
|
+
<div class="pf-presence-hint">
|
|
597
|
+
${cfg.visibility === 'everyone' ? t('presence.control.visEveryoneHint')
|
|
598
|
+
: cfg.visibility === 'contacts' ? t('presence.control.visContactsHint') : ''}
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
603
|
+
|
|
518
604
|
/* ───── Sub-components ───── */
|
|
519
605
|
|
|
520
606
|
function ProfileCard({ tier, stats, session, onEditProfile, switchTab }) {
|
|
@@ -771,6 +857,7 @@ const SIDEBAR_GROUPS = [
|
|
|
771
857
|
{ titleKey: 'profile.landing.menuInformation', items: [ // raw → curated → governed
|
|
772
858
|
{ id: 'organisms', icon: '\u{1F3E2}', labelKey: 'profile.tabs.organisms' },
|
|
773
859
|
{ id: 'memory', icon: '\u{1F9E0}', labelKey: 'profile.tabs.memory' },
|
|
860
|
+
{ id: 'notebook', icon: '\u{1F4D3}', labelKey: 'profile.tabs.notebook' },
|
|
774
861
|
{ id: 'knowledge', icon: '\u{1F4DA}', labelKey: 'knowledge.tabLabel' },
|
|
775
862
|
{ id: 'boards', icon: '\u{1F4CB}', labelKey: 'profile.tabs.boards' },
|
|
776
863
|
] },
|
|
@@ -1035,6 +1122,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
1035
1122
|
<${ProfileCard} tier=${tier} stats=${stats} session=${session}
|
|
1036
1123
|
onEditProfile=${() => setEditOpen(true)}
|
|
1037
1124
|
switchTab=${(id) => open(id, 'main')} />
|
|
1125
|
+
<${PresenceControl} />
|
|
1038
1126
|
${isNew ? html`<${HeroOnboarding} switchTab=${(id) => open(id, 'main')} />` : null}
|
|
1039
1127
|
${(isNew || isActive) ? html`<${KnowledgeCallout} switchTab=${() => open('knowledge', 'main')} />` : null}
|
|
1040
1128
|
${isNew ? html`<${GhostTiles} switchTab=${() => open('packages', 'main')} />` : null}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* group-share brought into Memory; "Create a group →" when none exist). Plus bulk edit:
|
|
34
34
|
* per-row checkboxes + a bulk bar (change visibility incl. group / delete with confirm /
|
|
35
35
|
* clear). The detail-row select and edit-modal select remain as alternate paths.
|
|
36
|
+
* v2.2.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
|
|
36
37
|
*/
|
|
37
38
|
import { h } from 'preact';
|
|
38
39
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
@@ -279,7 +280,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
279
280
|
function readFileAsBase64(file) {
|
|
280
281
|
return new Promise((resolve, reject) => {
|
|
281
282
|
const reader = new FileReader();
|
|
282
|
-
reader.onload = () => resolve(reader.result.split(',')[1]);
|
|
283
|
+
reader.onload = () => resolve(/** @type {string} */ (reader.result).split(',')[1]);
|
|
283
284
|
reader.onerror = reject;
|
|
284
285
|
reader.readAsDataURL(file);
|
|
285
286
|
});
|
|
@@ -287,13 +288,13 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
287
288
|
|
|
288
289
|
async function handleDownloadFile(key) {
|
|
289
290
|
try {
|
|
290
|
-
const headers = {};
|
|
291
|
+
const headers = /** @type {Record<string,string>} */ ({});
|
|
291
292
|
if (window.AIMEAT?.auth?.hasSession) {
|
|
292
293
|
const session = window.AIMEAT.auth.getSession();
|
|
293
294
|
if (session?.jwt) headers['Authorization'] = 'Bearer ' + session.jwt;
|
|
294
295
|
}
|
|
295
296
|
const resp = await fetch(`${NODE_URL}/v1/memory/files/${encodeURIComponent(key)}`, { headers });
|
|
296
|
-
if (!resp.ok) throw new Error(resp.status);
|
|
297
|
+
if (!resp.ok) throw new Error(String(resp.status));
|
|
297
298
|
const blob = await resp.blob();
|
|
298
299
|
const url = URL.createObjectURL(blob);
|
|
299
300
|
const a = document.createElement('a');
|
|
@@ -433,9 +434,9 @@ export default function MemoryTab({ session, showToast, onStats }) {
|
|
|
433
434
|
const sorted = [...entries];
|
|
434
435
|
switch (sortBy) {
|
|
435
436
|
case 'updated':
|
|
436
|
-
return sorted.sort((a, b) => new Date(b.updated_at || 0) - new Date(a.updated_at || 0));
|
|
437
|
+
return sorted.sort((a, b) => +new Date(b.updated_at || 0) - +new Date(a.updated_at || 0));
|
|
437
438
|
case 'created':
|
|
438
|
-
return sorted.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
|
439
|
+
return sorted.sort((a, b) => +new Date(b.created_at || 0) - +new Date(a.created_at || 0));
|
|
439
440
|
case 'alpha':
|
|
440
441
|
return sorted.sort((a, b) => a.key.localeCompare(b.key));
|
|
441
442
|
default:
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file notebook-tab.js
|
|
3
|
+
* @description Profile "Notebook" tab — slice A: free-text CAPTURE (saved to the notebook inbox) +
|
|
4
|
+
* the LIBRARIAN read-head (GET /v1/librarian/search, ranked full-text across every organism you
|
|
5
|
+
* have contributed to plus your personal notes). Classify → disambiguate → materialize-document
|
|
6
|
+
* is slice B. See docs/internal/design-organism-notebook-and-librarian.md.
|
|
7
|
+
* @structure
|
|
8
|
+
* - NotebookTab (default export) — capture box, inbox list, librarian search
|
|
9
|
+
* @usage html`<${NotebookTab} session=${session} showToast=${showToast} onStats=${onStats} />`
|
|
10
|
+
* @version-history
|
|
11
|
+
* v1.0.0 — 2026-06-19 — Initial: capture + librarian search (slice A).
|
|
12
|
+
*/
|
|
13
|
+
import { h } from 'preact';
|
|
14
|
+
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
15
|
+
import htm from 'htm';
|
|
16
|
+
const html = htm.bind(h);
|
|
17
|
+
import { t } from '/js/i18n.js';
|
|
18
|
+
import { escHtml } from '/js/utils.js';
|
|
19
|
+
import { Spinner } from './shared.js';
|
|
20
|
+
import * as memoryService from '/js/services/memory.js';
|
|
21
|
+
import { classifyNote, materializeDocument } from '/js/services/notebook.js';
|
|
22
|
+
import { listOrganisms } from '/js/services/organisms.js';
|
|
23
|
+
import { useConfirm } from '/components/Modal.js';
|
|
24
|
+
import { Markdown } from '/components/Markdown.js';
|
|
25
|
+
import { OpenRouterSettings } from './generator-settings.js';
|
|
26
|
+
|
|
27
|
+
const NEW = '__new__';
|
|
28
|
+
|
|
29
|
+
// Stages shown while the (slow) AI classify call runs, so the user sees what is happening. The
|
|
30
|
+
// timer advances to (and dwells on) the AI step — that is genuinely where the wait is spent.
|
|
31
|
+
const NB_STEPS = ['profile.notebook.step1', 'profile.notebook.step2'];
|
|
32
|
+
|
|
33
|
+
const INBOX_PREFIX = 'notebook.inbox.';
|
|
34
|
+
|
|
35
|
+
function relTime(iso) {
|
|
36
|
+
if (!iso) return '';
|
|
37
|
+
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
38
|
+
if (mins < 1) return t('profile.memory.timeJustNow') || 'just now';
|
|
39
|
+
if (mins < 60) return (t('profile.memory.timeMinsAgo') || '{n}m ago').replace('{n}', String(mins));
|
|
40
|
+
const hrs = Math.floor(mins / 60);
|
|
41
|
+
if (hrs < 24) return (t('profile.memory.timeHoursAgo') || '{n}h ago').replace('{n}', String(hrs));
|
|
42
|
+
const days = Math.floor(hrs / 24);
|
|
43
|
+
if (days < 30) return (t('profile.memory.timeDaysAgo') || '{n}d ago').replace('{n}', String(days));
|
|
44
|
+
return new Date(iso).toLocaleDateString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** First non-empty line of a note (markdown heading marks stripped), for the collapsed one-line view. */
|
|
48
|
+
function firstLine(text) {
|
|
49
|
+
const line = (text || '').split('\n').map(l => l.trim()).find(Boolean) || '';
|
|
50
|
+
return line.replace(/^#{1,6}\s+/, '').replace(/[*_`>#]/g, '').slice(0, 160);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Best-effort plain text of a note value for the inbox preview. */
|
|
54
|
+
function noteText(value) {
|
|
55
|
+
if (typeof value === 'string') return value;
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
if (typeof value.text === 'string') return value.text;
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
return String(value ?? '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function NotebookTab({ session, showToast, onStats }) {
|
|
64
|
+
const { confirm, ConfirmUI } = useConfirm();
|
|
65
|
+
const [draft, setDraft] = useState('');
|
|
66
|
+
const [saving, setSaving] = useState(false);
|
|
67
|
+
const [inbox, setInbox] = useState(null);
|
|
68
|
+
const [orgNames, setOrgNames] = useState({});
|
|
69
|
+
const [inboxFilter, setInboxFilter] = useState('');
|
|
70
|
+
const [inboxSort, setInboxSort] = useState('new'); // 'new' | 'old'
|
|
71
|
+
const [noteView, setNoteView] = useState({}); // noteKey → 'line' | 'peek' | 'full' (default 'peek')
|
|
72
|
+
|
|
73
|
+
const [query, setQuery] = useState('');
|
|
74
|
+
const [hits, setHits] = useState(null); // null = not searched yet
|
|
75
|
+
const [searching, setSearching] = useState(false);
|
|
76
|
+
const [searchScope, setSearchScope] = useState('own'); // 'own' | 'public'
|
|
77
|
+
|
|
78
|
+
// Slice B — placement suggestion for one note at a time.
|
|
79
|
+
const [sortingKey, setSortingKey] = useState(null); // note key being classified (loading)
|
|
80
|
+
const [sortStep, setSortStep] = useState(0); // progress stage index during classify
|
|
81
|
+
const [sortError, setSortError] = useState(null); // { noteKey, message } — persistent inline error
|
|
82
|
+
const [suggest, setSuggest] = useState(null); // { noteKey, result, edit }
|
|
83
|
+
const [materializing, setMaterializing] = useState(false);
|
|
84
|
+
const stepTimer = useRef(null);
|
|
85
|
+
function stopStepTimer() { if (stepTimer.current) { clearInterval(stepTimer.current); stepTimer.current = null; } }
|
|
86
|
+
useEffect(() => stopStepTimer, []);
|
|
87
|
+
|
|
88
|
+
useEffect(() => { if (session) { loadInbox(); loadOrgNames(); } }, [session]);
|
|
89
|
+
|
|
90
|
+
// Re-fetch the inbox on live updates (a capture elsewhere, a sync) — Rule from the frontend guide.
|
|
91
|
+
const liveRef = useRef(() => loadInbox());
|
|
92
|
+
liveRef.current = () => loadInbox();
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const handler = () => liveRef.current();
|
|
95
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
96
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
async function loadInbox() {
|
|
100
|
+
try {
|
|
101
|
+
const all = await memoryService.listMemories();
|
|
102
|
+
const notes = (Array.isArray(all) ? all : [])
|
|
103
|
+
.filter(m => typeof m.key === 'string' && m.key.startsWith(INBOX_PREFIX))
|
|
104
|
+
.sort((a, b) => +new Date(b.updated_at || b.created_at || 0) - +new Date(a.updated_at || a.created_at || 0));
|
|
105
|
+
setInbox(notes);
|
|
106
|
+
onStats?.({ notebook: notes.length });
|
|
107
|
+
} catch { setInbox([]); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadOrgNames() {
|
|
111
|
+
try {
|
|
112
|
+
const resp = await listOrganisms({ member: session.owner });
|
|
113
|
+
const map = {};
|
|
114
|
+
for (const o of (resp?.data?.organisms || [])) map[o.id] = o.name;
|
|
115
|
+
setOrgNames(map);
|
|
116
|
+
} catch { /* names are a nicety — ids still render */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleCapture() {
|
|
120
|
+
const text = draft.trim();
|
|
121
|
+
if (!text || saving) return;
|
|
122
|
+
setSaving(true);
|
|
123
|
+
try {
|
|
124
|
+
const key = INBOX_PREFIX + Date.now();
|
|
125
|
+
const resp = await memoryService.createMemory(key, { text, capturedAt: new Date().toISOString() }, 'private');
|
|
126
|
+
if (resp?.ok === false) { showToast(resp.error?.message || t('profile.notebook.saveFailed'), true); return; }
|
|
127
|
+
showToast(t('profile.notebook.saved'));
|
|
128
|
+
setDraft('');
|
|
129
|
+
await loadInbox();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
showToast(e.message || t('profile.notebook.saveFailed'), true);
|
|
132
|
+
} finally { setSaving(false); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleDelete(key) {
|
|
136
|
+
confirm(t('profile.notebook.deleteConfirm'), async () => {
|
|
137
|
+
const resp = await memoryService.deleteMemory(key);
|
|
138
|
+
if (resp?.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
|
|
139
|
+
showToast(t('profile.notebook.deleted'));
|
|
140
|
+
loadInbox();
|
|
141
|
+
}, { danger: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function runSearch(q, scope) {
|
|
145
|
+
if (!q) { setHits(null); return; }
|
|
146
|
+
setSearching(true);
|
|
147
|
+
try {
|
|
148
|
+
setHits(await memoryService.librarianSearch(q, 50, scope));
|
|
149
|
+
} catch (e) {
|
|
150
|
+
showToast(e.message || t('profile.error'), true);
|
|
151
|
+
setHits([]);
|
|
152
|
+
} finally { setSearching(false); }
|
|
153
|
+
}
|
|
154
|
+
function handleSearch() { runSearch(query.trim(), searchScope); }
|
|
155
|
+
function pickScope(scope) {
|
|
156
|
+
setSearchScope(scope);
|
|
157
|
+
if (query.trim()) runSearch(query.trim(), scope); // re-run so the toggle shows immediately
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hitDocId(hit) {
|
|
161
|
+
const prefix = `organism.${hit.organismId}.w.${hit.workspaceId}.${hit.space}.`;
|
|
162
|
+
return hit.space && hit.key.startsWith(prefix) ? hit.key.slice(prefix.length).split('.')[0] : null;
|
|
163
|
+
}
|
|
164
|
+
/** Whether a hit has a place to open (so the button is shown). */
|
|
165
|
+
function canOpen(hit) {
|
|
166
|
+
if (hit.kind === 'knowledge') return !!hit.packageId;
|
|
167
|
+
if (hit.organismId && hit.workspaceId) return true;
|
|
168
|
+
return searchScope !== 'public'; // personal memory only has an in-app home in your own scope
|
|
169
|
+
}
|
|
170
|
+
// Open a hit at its real home. Knowledge → the public knowledge viewer; an organism document →
|
|
171
|
+
// the in-app workspace doc (your own) or the public no-auth workspace viewer (public scope); a
|
|
172
|
+
// personal entry → the Memory tab.
|
|
173
|
+
function openHit(hit) {
|
|
174
|
+
if (hit.kind === 'knowledge' && hit.packageId) {
|
|
175
|
+
window.open(`/v1/publicknowledgeviewer?id=${encodeURIComponent(hit.packageId)}`, '_blank', 'noopener');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (hit.organismId && hit.workspaceId) {
|
|
179
|
+
const docId = hitDocId(hit);
|
|
180
|
+
if (searchScope === 'public') {
|
|
181
|
+
let u = `/v1/publicworkspaceviewer?org=${encodeURIComponent(hit.organismId)}&ws=${encodeURIComponent(hit.workspaceId)}`;
|
|
182
|
+
if (hit.space && docId) u += `&type=${encodeURIComponent(hit.space)}&id=${encodeURIComponent(docId)}`;
|
|
183
|
+
window.open(u, '_blank', 'noopener');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
sessionStorage.setItem('aimeat.ws.openId', hit.organismId);
|
|
188
|
+
sessionStorage.setItem('aimeat.ws.openWs', hit.workspaceId);
|
|
189
|
+
if (docId) sessionStorage.setItem(`aimeat.ws.${hit.organismId}.${hit.workspaceId}.openDoc`, JSON.stringify({ namespace: hit.space, id: docId }));
|
|
190
|
+
} catch { /* noop */ }
|
|
191
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'organisms' } }));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (searchScope !== 'public') window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'memory' } }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Short producer label for a public hit: agent#owner or owner (drop the @node). */
|
|
198
|
+
function producerLabel(gaii) {
|
|
199
|
+
return String(gaii || '').split('@')[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Collapse cycle for an inbox note: one line → peek → full → one line.
|
|
203
|
+
const cycleView = (key) => setNoteView(v => {
|
|
204
|
+
const cur = v[key] || 'peek';
|
|
205
|
+
const next = cur === 'line' ? 'peek' : cur === 'peek' ? 'full' : 'line';
|
|
206
|
+
return { ...v, [key]: next };
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Filter (text) + sort (date) the inbox for display.
|
|
210
|
+
function visibleInbox() {
|
|
211
|
+
const ft = inboxFilter.trim().toLowerCase();
|
|
212
|
+
const filtered = (inbox || []).filter(n => !ft || noteText(n.value).toLowerCase().includes(ft));
|
|
213
|
+
return filtered.sort((a, b) => {
|
|
214
|
+
const da = +new Date(a.updated_at || a.created_at || 0), db = +new Date(b.updated_at || b.created_at || 0);
|
|
215
|
+
return inboxSort === 'old' ? da - db : db - da;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Slice B: classify → suggestion → materialize ──
|
|
220
|
+
|
|
221
|
+
/** Build the initial editable plan from a classify result. */
|
|
222
|
+
function initEdit(result) {
|
|
223
|
+
const s = result.suggestion || {};
|
|
224
|
+
const cn = result.createNew || {};
|
|
225
|
+
const organismId = s.organismId || (cn.suggest ? NEW : NEW);
|
|
226
|
+
const workspaceId = organismId === NEW ? NEW : (s.workspaceId || NEW);
|
|
227
|
+
return {
|
|
228
|
+
organismId,
|
|
229
|
+
organismName: cn.organismName || '',
|
|
230
|
+
workspaceId,
|
|
231
|
+
workspaceName: cn.workspaceName || '',
|
|
232
|
+
space: workspaceId === NEW ? '' : (s.space || ''),
|
|
233
|
+
title: s.title || '',
|
|
234
|
+
markdown: s.markdown || '',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function handleSuggest(note) {
|
|
239
|
+
setSuggest(null);
|
|
240
|
+
setSortError(null);
|
|
241
|
+
setSortingKey(note.key);
|
|
242
|
+
setSortStep(0);
|
|
243
|
+
stopStepTimer();
|
|
244
|
+
// Walk the user through the expected stages while the AI thinks (the call can take ~30s).
|
|
245
|
+
stepTimer.current = setInterval(() => setSortStep(s => Math.min(s + 1, NB_STEPS.length - 1)), 2500);
|
|
246
|
+
try {
|
|
247
|
+
const result = await classifyNote(noteText(note.value));
|
|
248
|
+
if (!result) throw new Error(t('profile.error'));
|
|
249
|
+
setSuggest({ noteKey: note.key, result, edit: initEdit(result) });
|
|
250
|
+
} catch (e) {
|
|
251
|
+
setSortError({ noteKey: note.key, message: e.message || t('profile.error'), code: e.code });
|
|
252
|
+
} finally { stopStepTimer(); setSortingKey(null); }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function patchEdit(patch) {
|
|
256
|
+
setSuggest(s => s ? { ...s, edit: { ...s.edit, ...patch } } : s);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onOrganismChange(organismId) {
|
|
260
|
+
if (organismId === NEW) { patchEdit({ organismId: NEW, workspaceId: NEW, space: '' }); return; }
|
|
261
|
+
const org = suggest.result.context.organisms.find(o => o.id === organismId);
|
|
262
|
+
const firstWs = org?.workspaces?.[0];
|
|
263
|
+
patchEdit({ organismId, workspaceId: firstWs ? firstWs.id : NEW, space: firstWs?.documentSpaces?.[0]?.namespace || '' });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function onWorkspaceChange(workspaceId) {
|
|
267
|
+
if (workspaceId === NEW) { patchEdit({ workspaceId: NEW, space: '' }); return; }
|
|
268
|
+
const org = suggest.result.context.organisms.find(o => o.id === suggest.edit.organismId);
|
|
269
|
+
const ws = org?.workspaces?.find(w => w.id === workspaceId);
|
|
270
|
+
patchEdit({ workspaceId, space: ws?.documentSpaces?.[0]?.namespace || '' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function applyAlternative(alt) {
|
|
274
|
+
const org = suggest.result.context.organisms.find(o => o.id === alt.organismId);
|
|
275
|
+
const ws = org?.workspaces?.find(w => w.id === alt.workspaceId);
|
|
276
|
+
patchEdit({
|
|
277
|
+
organismId: alt.organismId || NEW,
|
|
278
|
+
workspaceId: alt.workspaceId || NEW,
|
|
279
|
+
space: alt.space || ws?.documentSpaces?.[0]?.namespace || '',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function handleMaterialize() {
|
|
284
|
+
if (!suggest) return;
|
|
285
|
+
const e = suggest.edit;
|
|
286
|
+
if (!e.title.trim()) { showToast(t('profile.notebook.titleRequired'), true); return; }
|
|
287
|
+
if (e.organismId === NEW && !e.organismName.trim()) { showToast(t('profile.notebook.orgNameRequired'), true); return; }
|
|
288
|
+
setMaterializing(true);
|
|
289
|
+
try {
|
|
290
|
+
await materializeDocument({
|
|
291
|
+
organismId: e.organismId === NEW ? null : e.organismId,
|
|
292
|
+
organismName: e.organismName,
|
|
293
|
+
workspaceId: (e.organismId === NEW || e.workspaceId === NEW) ? null : e.workspaceId,
|
|
294
|
+
workspaceName: e.workspaceName,
|
|
295
|
+
space: (e.organismId === NEW || e.workspaceId === NEW || !e.space) ? null : e.space,
|
|
296
|
+
title: e.title.trim(),
|
|
297
|
+
markdown: e.markdown,
|
|
298
|
+
sourceKey: suggest.noteKey,
|
|
299
|
+
});
|
|
300
|
+
showToast(t('profile.notebook.materialized'));
|
|
301
|
+
setSuggest(null);
|
|
302
|
+
await loadInbox();
|
|
303
|
+
loadOrgNames(); // a just-created organism's name should resolve in subsequent search badges
|
|
304
|
+
} catch (err) {
|
|
305
|
+
showToast(err.message || t('profile.error'), true);
|
|
306
|
+
} finally { setMaterializing(false); }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const renderSuggestPanel = () => {
|
|
310
|
+
const { result, edit } = suggest;
|
|
311
|
+
const orgs = result.context?.organisms || [];
|
|
312
|
+
const org = orgs.find(o => o.id === edit.organismId);
|
|
313
|
+
const workspaces = org?.workspaces || [];
|
|
314
|
+
const ws = workspaces.find(w => w.id === edit.workspaceId);
|
|
315
|
+
const docSpaces = ws?.documentSpaces || [];
|
|
316
|
+
const conf = result.suggestion ? Math.round((result.suggestion.confidence || 0) * 100) : null;
|
|
317
|
+
return html`
|
|
318
|
+
<div class="pf-nb-suggest">
|
|
319
|
+
${result.suggestion?.reason && html`<div class="text-meta-sm pf-nb-suggest-reason">${escHtml(result.suggestion.reason)}${conf !== null ? ` · ${conf}%` : ''}</div>`}
|
|
320
|
+
|
|
321
|
+
<label class="pf-nb-suggest-label">${t('profile.notebook.fieldOrganism')}</label>
|
|
322
|
+
<select class="input-field" value=${edit.organismId} onChange=${e => onOrganismChange(e.target.value)}>
|
|
323
|
+
${orgs.map(o => html`<option key=${o.id} value=${o.id}>${escHtml(o.name)}</option>`)}
|
|
324
|
+
<option value=${NEW}>➕ ${t('profile.notebook.newOrganism')}</option>
|
|
325
|
+
</select>
|
|
326
|
+
${edit.organismId === NEW && html`
|
|
327
|
+
<input type="text" class="input-field" placeholder=${t('profile.notebook.newOrgNamePlaceholder')}
|
|
328
|
+
value=${edit.organismName} onInput=${e => patchEdit({ organismName: e.target.value })} />`}
|
|
329
|
+
|
|
330
|
+
${edit.organismId !== NEW && html`
|
|
331
|
+
<label class="pf-nb-suggest-label">${t('profile.notebook.fieldWorkspace')}</label>
|
|
332
|
+
<select class="input-field" value=${edit.workspaceId} onChange=${e => onWorkspaceChange(e.target.value)}>
|
|
333
|
+
${workspaces.map(w => html`<option key=${w.id} value=${w.id}>${escHtml(w.name)}</option>`)}
|
|
334
|
+
<option value=${NEW}>➕ ${t('profile.notebook.newWorkspace')}</option>
|
|
335
|
+
</select>`}
|
|
336
|
+
${(edit.organismId === NEW || edit.workspaceId === NEW) && html`
|
|
337
|
+
<input type="text" class="input-field" placeholder=${t('profile.notebook.newWsNamePlaceholder')}
|
|
338
|
+
value=${edit.workspaceName} onInput=${e => patchEdit({ workspaceName: e.target.value })} />`}
|
|
339
|
+
|
|
340
|
+
${edit.organismId !== NEW && edit.workspaceId !== NEW && docSpaces.length > 0 && html`
|
|
341
|
+
<label class="pf-nb-suggest-label">${t('profile.notebook.fieldSpace')}</label>
|
|
342
|
+
<select class="input-field" value=${edit.space} onChange=${e => patchEdit({ space: e.target.value })}>
|
|
343
|
+
${docSpaces.map(s => html`<option key=${s.namespace} value=${s.namespace}>${escHtml(s.name)}</option>`)}
|
|
344
|
+
</select>`}
|
|
345
|
+
|
|
346
|
+
${result.alternatives?.length > 0 && html`
|
|
347
|
+
<div class="pf-nb-suggest-alts">
|
|
348
|
+
<span class="text-meta-sm">${t('profile.notebook.alternatives')}</span>
|
|
349
|
+
${result.alternatives.map((alt, i) => html`
|
|
350
|
+
<button key=${i} class="btn-ghost btn-sm" onClick=${() => applyAlternative(alt)}>
|
|
351
|
+
${escHtml(alt.organismName || '?')}${alt.workspaceName ? ` ▸ ${escHtml(alt.workspaceName)}` : ''}
|
|
352
|
+
</button>`)}
|
|
353
|
+
</div>`}
|
|
354
|
+
|
|
355
|
+
<label class="pf-nb-suggest-label">${t('profile.notebook.fieldTitle')}</label>
|
|
356
|
+
<input type="text" class="input-field" value=${edit.title} onInput=${e => patchEdit({ title: e.target.value })} />
|
|
357
|
+
<label class="pf-nb-suggest-label">${t('profile.notebook.fieldBody')}</label>
|
|
358
|
+
<textarea class="input-field pf-nb-suggest-body" rows="6" value=${edit.markdown}
|
|
359
|
+
onInput=${e => patchEdit({ markdown: e.target.value })}></textarea>
|
|
360
|
+
|
|
361
|
+
<div class="pf-nb-suggest-actions">
|
|
362
|
+
<button class="btn-primary" disabled=${materializing} onClick=${handleMaterialize}>
|
|
363
|
+
${materializing ? '…' : t('profile.notebook.materializeBtn')}
|
|
364
|
+
</button>
|
|
365
|
+
<button class="btn-ghost btn-sm" onClick=${() => setSuggest(null)}>${t('profile.notebook.cancelBtn')}</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
`;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const renderHit = (hit) => html`
|
|
372
|
+
<div class="pf-nb-hit" key=${hit.key}>
|
|
373
|
+
<div class="pf-nb-hit-head">
|
|
374
|
+
<span class="pf-nb-hit-title">${escHtml(hit.title || hit.key)}</span>
|
|
375
|
+
${hit.kind === 'knowledge'
|
|
376
|
+
? html`<span class="badge badge-success">${t('profile.notebook.kindKnowledge')}${hit.contentType ? ` · ${escHtml(hit.contentType)}` : ''}</span>`
|
|
377
|
+
: hit.organismId
|
|
378
|
+
? html`<span class="badge badge-info">${escHtml(orgNames[hit.organismId] || hit.organismId)}</span>`
|
|
379
|
+
: html`<span class="badge">${t('profile.notebook.personalNote')}</span>`}
|
|
380
|
+
</div>
|
|
381
|
+
${searchScope === 'public' && html`<div class="text-meta-sm pf-nb-hit-producer">${t('profile.notebook.producer')}: ${escHtml(producerLabel(hit.producer))}</div>`}
|
|
382
|
+
${hit.snippet && html`<div class="pf-nb-hit-snippet">${escHtml(hit.snippet)}</div>`}
|
|
383
|
+
<div class="pf-nb-hit-foot">
|
|
384
|
+
<span class="text-meta-sm pf-nb-hit-key" title=${hit.key}>${escHtml(hit.key)}</span>
|
|
385
|
+
${canOpen(hit) && html`<button class="btn-ghost btn-sm" onClick=${() => openHit(hit)}>${t('profile.notebook.openInMemory')}</button>`}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
return html`
|
|
391
|
+
${ConfirmUI}
|
|
392
|
+
<div class="section-title">${t('profile.notebook.title')}</div>
|
|
393
|
+
<div class="section-desc">${t('profile.notebook.desc')}</div>
|
|
394
|
+
|
|
395
|
+
<div class="pf-nb-capture">
|
|
396
|
+
<textarea class="input-field pf-nb-textarea" rows="4"
|
|
397
|
+
placeholder=${t('profile.notebook.capturePlaceholder')}
|
|
398
|
+
value=${draft} onInput=${e => setDraft(e.target.value)}></textarea>
|
|
399
|
+
<div class="pf-nb-capture-actions">
|
|
400
|
+
<button class="btn-primary" disabled=${!draft.trim() || saving} onClick=${handleCapture}>
|
|
401
|
+
${saving ? '…' : t('profile.notebook.captureBtn')}
|
|
402
|
+
</button>
|
|
403
|
+
<span class="text-meta-sm">${t('profile.notebook.captureHint')}</span>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div class="section-title pf-nb-section">${t('profile.notebook.librarianTitle')}</div>
|
|
408
|
+
<div class="section-desc">${t('profile.notebook.librarianDesc')}</div>
|
|
409
|
+
<div class="sub-tabs pf-nb-scope">
|
|
410
|
+
<button class="sub-tab ${searchScope === 'own' ? 'active' : ''}" onClick=${() => pickScope('own')}>${t('profile.notebook.scopeOwn')}</button>
|
|
411
|
+
<button class="sub-tab ${searchScope === 'public' ? 'active' : ''}" onClick=${() => pickScope('public')}>${t('profile.notebook.scopePublic')}</button>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="action-bar">
|
|
414
|
+
<div class="search-bar pf-nb-search">
|
|
415
|
+
<input type="text" class="input-field" placeholder=${t('profile.notebook.searchPlaceholder')}
|
|
416
|
+
value=${query} onInput=${e => setQuery(e.target.value)}
|
|
417
|
+
onKeyDown=${e => e.key === 'Enter' && handleSearch()} />
|
|
418
|
+
<button class="btn-primary" onClick=${handleSearch}>${t('profile.notebook.searchBtn')}</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
${searching && html`<${Spinner} text=${t('profile.notebook.searching')} />`}
|
|
422
|
+
${!searching && hits !== null && html`
|
|
423
|
+
${hits.length === 0
|
|
424
|
+
? html`<div class="empty">${t('profile.notebook.noHits')}</div>`
|
|
425
|
+
: html`
|
|
426
|
+
<div class="text-meta-sm mb-half">${(t('profile.notebook.hitsCount') || '{n} results').replace('{n}', String(hits.length))}</div>
|
|
427
|
+
<div class="pf-nb-hits">${hits.map(renderHit)}</div>
|
|
428
|
+
`}
|
|
429
|
+
`}
|
|
430
|
+
|
|
431
|
+
<div class="section-title pf-nb-section">${t('profile.notebook.inboxTitle')}</div>
|
|
432
|
+
${inbox === null
|
|
433
|
+
? html`<${Spinner} text=${t('profile.notebook.inboxLoading')} />`
|
|
434
|
+
: inbox.length === 0
|
|
435
|
+
? html`<div class="empty">${t('profile.notebook.inboxEmpty')}</div>`
|
|
436
|
+
: html`
|
|
437
|
+
<div class="action-bar pf-nb-inbox-bar">
|
|
438
|
+
<div class="search-bar pf-nb-search">
|
|
439
|
+
<input type="text" class="input-field" placeholder=${t('profile.notebook.filterPlaceholder')}
|
|
440
|
+
value=${inboxFilter} onInput=${e => setInboxFilter(e.target.value)} />
|
|
441
|
+
${inboxFilter && html`<button class="btn-ghost btn-sm" onClick=${() => setInboxFilter('')}>✕</button>`}
|
|
442
|
+
</div>
|
|
443
|
+
<select class="input-field pf-nb-sort" value=${inboxSort} onChange=${e => setInboxSort(e.target.value)}>
|
|
444
|
+
<option value="new">${t('profile.notebook.sortNew')}</option>
|
|
445
|
+
<option value="old">${t('profile.notebook.sortOld')}</option>
|
|
446
|
+
</select>
|
|
447
|
+
</div>
|
|
448
|
+
${visibleInbox().length === 0
|
|
449
|
+
? html`<div class="empty">${t('profile.notebook.noMatch')}</div>`
|
|
450
|
+
: html`<div class="pf-nb-inbox">
|
|
451
|
+
${visibleInbox().map(note => { const view = noteView[note.key] || 'peek'; return html`
|
|
452
|
+
<div class="pf-nb-note" key=${note.key}>
|
|
453
|
+
<div class="pf-nb-note-text pf-nb-note-text--${view}">
|
|
454
|
+
${view === 'line'
|
|
455
|
+
? html`<span class="pf-nb-note-line">${escHtml(firstLine(noteText(note.value)))}</span>`
|
|
456
|
+
: html`<${Markdown} text=${noteText(note.value)} />`}
|
|
457
|
+
</div>
|
|
458
|
+
<div class="pf-nb-note-foot">
|
|
459
|
+
<div class="pf-nb-note-meta">
|
|
460
|
+
<button class="btn-ghost btn-sm pf-nb-view-btn" title=${t('profile.notebook.toggleView')}
|
|
461
|
+
onClick=${() => cycleView(note.key)}>${view === 'full' ? '⌃' : '⌄'}</button>
|
|
462
|
+
<span class="text-meta-sm">${relTime(note.updated_at || note.created_at)}</span>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="pf-nb-note-btns">
|
|
465
|
+
<button class="btn-primary btn-sm" disabled=${sortingKey === note.key}
|
|
466
|
+
onClick=${() => handleSuggest(note)}>
|
|
467
|
+
${sortingKey === note.key ? t('profile.notebook.sorting') : t('profile.notebook.suggestBtn')}
|
|
468
|
+
</button>
|
|
469
|
+
<button class="btn-danger btn-sm" onClick=${() => handleDelete(note.key)}>${t('profile.notebook.deleteBtn')}</button>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
${sortingKey === note.key && html`
|
|
473
|
+
<div class="pf-nb-suggest pf-nb-progress">
|
|
474
|
+
<${Spinner} text=${t(NB_STEPS[sortStep])} />
|
|
475
|
+
<ol class="pf-nb-steps">
|
|
476
|
+
${NB_STEPS.map((s, i) => html`
|
|
477
|
+
<li key=${i} class="pf-nb-step ${i < sortStep ? 'done' : i === sortStep ? 'active' : ''}">
|
|
478
|
+
${i < sortStep ? '✓' : i === sortStep ? '→' : '·'} ${t(s)}
|
|
479
|
+
</li>`)}
|
|
480
|
+
</ol>
|
|
481
|
+
</div>`}
|
|
482
|
+
${sortError?.noteKey === note.key && html`
|
|
483
|
+
<div class="pf-nb-suggest pf-nb-progress">
|
|
484
|
+
<div class="alert alert-warning"><span class="alert-msg">${t('profile.notebook.sortErrorTitle')}: ${escHtml(sortError.message)}</span></div>
|
|
485
|
+
${sortError.code === 'NO_OPENROUTER_KEY' && html`
|
|
486
|
+
<div class="text-meta-sm">${t('profile.notebook.needKey')}</div>
|
|
487
|
+
<${OpenRouterSettings} onSettingsChange=${() => {}} />`}
|
|
488
|
+
<div class="pf-nb-suggest-actions">
|
|
489
|
+
<button class="btn-primary btn-sm" onClick=${() => handleSuggest(note)}>${t('profile.notebook.tryAgain')}</button>
|
|
490
|
+
<button class="btn-ghost btn-sm" onClick=${() => setSortError(null)}>${t('profile.notebook.dismiss')}</button>
|
|
491
|
+
</div>
|
|
492
|
+
</div>`}
|
|
493
|
+
${suggest?.noteKey === note.key && renderSuggestPanel()}
|
|
494
|
+
</div>
|
|
495
|
+
`; })}
|
|
496
|
+
</div>`}
|
|
497
|
+
`
|
|
498
|
+
}
|
|
499
|
+
`;
|
|
500
|
+
}
|