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.
Files changed (252) hide show
  1. package/README.md +8 -1
  2. package/dist/locales/en.json +144 -0
  3. package/dist/locales/fi.json +144 -0
  4. package/dist/public/components/ImageDeliverable.js +2 -1
  5. package/dist/public/components/Modal.js +3 -2
  6. package/dist/public/components/NotificationBell.js +2 -1
  7. package/dist/public/components/PresenceDot.js +53 -0
  8. package/dist/public/css/components/markdown.css +67 -0
  9. package/dist/public/css/theme.css +30 -0
  10. package/dist/public/css/views/notebook.css +195 -0
  11. package/dist/public/css/views/profile.css +15 -0
  12. package/dist/public/js/api.js +5 -3
  13. package/dist/public/js/components/auth-image.js +3 -2
  14. package/dist/public/js/components/tag-cloud.js +1 -1
  15. package/dist/public/js/components/tag-editor.js +1 -1
  16. package/dist/public/js/hooks.js +1 -2
  17. package/dist/public/js/presence-store.js +97 -0
  18. package/dist/public/js/services/admin.js +14 -5
  19. package/dist/public/js/services/boards.js +1 -1
  20. package/dist/public/js/services/ecosystem.js +1 -1
  21. package/dist/public/js/services/foundry-prompts-base.js +2 -2
  22. package/dist/public/js/services/foundry-smoke.js +2 -1
  23. package/dist/public/js/services/foundry-validate.js +6 -8
  24. package/dist/public/js/services/foundry.js +4 -3
  25. package/dist/public/js/services/generator-prompts-base.js +2 -2
  26. package/dist/public/js/services/generator-smoke.js +2 -1
  27. package/dist/public/js/services/generator-validate.js +5 -7
  28. package/dist/public/js/services/generator.js +2 -1
  29. package/dist/public/js/services/memory.js +13 -0
  30. package/dist/public/js/services/messages.js +2 -1
  31. package/dist/public/js/services/notebook.js +124 -0
  32. package/dist/public/js/services/packages.js +3 -2
  33. package/dist/public/js/services/presence.js +35 -0
  34. package/dist/public/js/style-testbed.js +6 -5
  35. package/dist/public/lib/minidenticons.min.js +1 -0
  36. package/dist/public/lib/yaml.mjs +1 -0
  37. package/dist/public/spa.html +90 -8
  38. package/dist/public/types/aimeat-globals.d.ts +52 -0
  39. package/dist/public/types/vendored-libs.d.ts +56 -0
  40. package/dist/public/views/_template.js +3 -1
  41. package/dist/public/views/admin/agent-integration-tab.js +4 -3
  42. package/dist/public/views/admin/agents-tab.js +3 -2
  43. package/dist/public/views/admin/config-tab.js +4 -3
  44. package/dist/public/views/admin/economy-tab.js +4 -3
  45. package/dist/public/views/admin/federation-tab.js +138 -1
  46. package/dist/public/views/admin/maintenance-tab.js +1 -1
  47. package/dist/public/views/admin/messages-tab.js +115 -0
  48. package/dist/public/views/admin/overview-tab.js +4 -3
  49. package/dist/public/views/admin/portal-tab.js +90 -2
  50. package/dist/public/views/admin/push-tab.js +8 -6
  51. package/dist/public/views/admin/security-tab.js +7 -7
  52. package/dist/public/views/admin.js +29 -9
  53. package/dist/public/views/doc-solo.js +3 -1
  54. package/dist/public/views/guides.js +8 -4
  55. package/dist/public/views/hobbies.js +4 -2
  56. package/dist/public/views/marketplace.js +10 -9
  57. package/dist/public/views/openclaw.js +1 -1
  58. package/dist/public/views/portal-dev.js +5 -2
  59. package/dist/public/views/portal.js +3 -2
  60. package/dist/public/views/profile/agents/shared-board.js +4 -2
  61. package/dist/public/views/profile/agents/tab-messages.js +13 -11
  62. package/dist/public/views/profile/agents-messages-subtab.js +2 -1
  63. package/dist/public/views/profile/apps-tab.js +2 -1
  64. package/dist/public/views/profile/boards-tab.js +3 -2
  65. package/dist/public/views/profile/calibrator-batch.js +3 -2
  66. package/dist/public/views/profile/data-wallet-tab.js +2 -1
  67. package/dist/public/views/profile/extensions-tab.js +4 -3
  68. package/dist/public/views/profile/federation-tab.js +4 -0
  69. package/dist/public/views/profile/foundry-dashboard/use-autopilot.js +3 -2
  70. package/dist/public/views/profile/foundry-detail.js +1 -1
  71. package/dist/public/views/profile/generator-detail.js +1 -1
  72. package/dist/public/views/profile/inbox-tab.js +7 -3
  73. package/dist/public/views/profile/inline-panels.js +3 -1
  74. package/dist/public/views/profile/knowledge-tab.js +2 -1
  75. package/dist/public/views/profile/landing-page.js +88 -0
  76. package/dist/public/views/profile/memory-tab.js +6 -5
  77. package/dist/public/views/profile/notebook-tab.js +500 -0
  78. package/dist/public/views/profile/organisms/activity-panel.js +135 -0
  79. package/dist/public/views/profile/organisms/agents.js +141 -0
  80. package/dist/public/views/profile/organisms/document.js +274 -0
  81. package/dist/public/views/profile/organisms/helpers.js +61 -0
  82. package/dist/public/views/profile/organisms/home.js +315 -0
  83. package/dist/public/views/profile/organisms/members.js +220 -0
  84. package/dist/public/views/profile/organisms/panels.js +173 -0
  85. package/dist/public/views/profile/organisms/participants-panel.js +160 -0
  86. package/dist/public/views/profile/organisms/schema-form.js +127 -0
  87. package/dist/public/views/profile/organisms/sources-panel.js +164 -0
  88. package/dist/public/views/profile/organisms/widgets.js +58 -0
  89. package/dist/public/views/profile/organisms/workspace-comments.js +99 -0
  90. package/dist/public/views/profile/organisms/workspace-list.js +268 -0
  91. package/dist/public/views/profile/organisms/workspace.js +1272 -0
  92. package/dist/public/views/profile/organisms-tab.js +456 -3819
  93. package/dist/public/views/profile/shared.js +64 -2
  94. package/dist/public/views/profile/workflows-form.js +1 -1
  95. package/dist/public/views/profile.js +16 -14
  96. package/dist/public/views/public-knowledge-viewer.js +3 -2
  97. package/dist/public/views/public-workspace-viewer.js +2 -1
  98. package/dist/scripts/check-importmap.d.ts +2 -0
  99. package/dist/scripts/check-importmap.d.ts.map +1 -0
  100. package/dist/scripts/check-importmap.js +102 -0
  101. package/dist/scripts/check-importmap.js.map +1 -0
  102. package/dist/src/cli/scaffold.d.ts +26 -3
  103. package/dist/src/cli/scaffold.d.ts.map +1 -1
  104. package/dist/src/cli/scaffold.js +33 -3
  105. package/dist/src/cli/scaffold.js.map +1 -1
  106. package/dist/src/config.d.ts +10 -0
  107. package/dist/src/config.d.ts.map +1 -1
  108. package/dist/src/config.js +5 -0
  109. package/dist/src/config.js.map +1 -1
  110. package/dist/src/generated/api-types.d.ts +927 -11
  111. package/dist/src/generated/api-types.d.ts.map +1 -1
  112. package/dist/src/generated/prisma-postgres/edge.js +32 -6
  113. package/dist/src/generated/prisma-postgres/index-browser.js +29 -3
  114. package/dist/src/generated/prisma-postgres/index.d.ts +1970 -160
  115. package/dist/src/generated/prisma-postgres/index.js +32 -6
  116. package/dist/src/generated/prisma-postgres/package.json +1 -1
  117. package/dist/src/generated/prisma-postgres/schema.prisma +45 -12
  118. package/dist/src/generated/prisma-postgres/wasm.js +32 -6
  119. package/dist/src/index.js +36 -4
  120. package/dist/src/index.js.map +1 -1
  121. package/dist/src/routes/admin-monitoring.d.ts.map +1 -1
  122. package/dist/src/routes/admin-monitoring.js +8 -0
  123. package/dist/src/routes/admin-monitoring.js.map +1 -1
  124. package/dist/src/routes/federation-peer.d.ts.map +1 -1
  125. package/dist/src/routes/federation-peer.js +327 -43
  126. package/dist/src/routes/federation-peer.js.map +1 -1
  127. package/dist/src/routes/federation-sync.d.ts +2 -0
  128. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  129. package/dist/src/routes/federation-sync.js +20 -1
  130. package/dist/src/routes/federation-sync.js.map +1 -1
  131. package/dist/src/routes/librarian.d.ts +18 -0
  132. package/dist/src/routes/librarian.d.ts.map +1 -0
  133. package/dist/src/routes/librarian.js +88 -0
  134. package/dist/src/routes/librarian.js.map +1 -0
  135. package/dist/src/routes/messages.d.ts.map +1 -1
  136. package/dist/src/routes/messages.js +2 -1
  137. package/dist/src/routes/messages.js.map +1 -1
  138. package/dist/src/routes/portal.d.ts.map +1 -1
  139. package/dist/src/routes/portal.js +9 -1
  140. package/dist/src/routes/portal.js.map +1 -1
  141. package/dist/src/routes/presence.d.ts +23 -0
  142. package/dist/src/routes/presence.d.ts.map +1 -0
  143. package/dist/src/routes/presence.js +88 -0
  144. package/dist/src/routes/presence.js.map +1 -0
  145. package/dist/src/routes/site.d.ts +12 -0
  146. package/dist/src/routes/site.d.ts.map +1 -1
  147. package/dist/src/routes/site.js +45 -1
  148. package/dist/src/routes/site.js.map +1 -1
  149. package/dist/src/routes/sse.d.ts +2 -0
  150. package/dist/src/routes/sse.d.ts.map +1 -1
  151. package/dist/src/routes/sse.js +9 -0
  152. package/dist/src/routes/sse.js.map +1 -1
  153. package/dist/src/routes/wellknown.d.ts.map +1 -1
  154. package/dist/src/routes/wellknown.js +5 -0
  155. package/dist/src/routes/wellknown.js.map +1 -1
  156. package/dist/src/routes/work.d.ts.map +1 -1
  157. package/dist/src/routes/work.js +6 -0
  158. package/dist/src/routes/work.js.map +1 -1
  159. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  160. package/dist/src/server-bootstrap/routes-loader.js +13 -0
  161. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  162. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  163. package/dist/src/server-bootstrap/service-init.js +34 -0
  164. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  165. package/dist/src/services/config-schema.d.ts.map +1 -1
  166. package/dist/src/services/config-schema.js +2 -0
  167. package/dist/src/services/config-schema.js.map +1 -1
  168. package/dist/src/services/federation-availability.d.ts +28 -0
  169. package/dist/src/services/federation-availability.d.ts.map +1 -0
  170. package/dist/src/services/federation-availability.js +58 -0
  171. package/dist/src/services/federation-availability.js.map +1 -0
  172. package/dist/src/services/federation-book.d.ts +70 -0
  173. package/dist/src/services/federation-book.d.ts.map +1 -0
  174. package/dist/src/services/federation-book.js +198 -0
  175. package/dist/src/services/federation-book.js.map +1 -0
  176. package/dist/src/services/federation-tiers.d.ts +45 -0
  177. package/dist/src/services/federation-tiers.d.ts.map +1 -0
  178. package/dist/src/services/federation-tiers.js +58 -0
  179. package/dist/src/services/federation-tiers.js.map +1 -0
  180. package/dist/src/services/federation.d.ts +15 -0
  181. package/dist/src/services/federation.d.ts.map +1 -1
  182. package/dist/src/services/federation.js +12 -0
  183. package/dist/src/services/federation.js.map +1 -1
  184. package/dist/src/services/librarian.d.ts +62 -0
  185. package/dist/src/services/librarian.d.ts.map +1 -0
  186. package/dist/src/services/librarian.js +135 -0
  187. package/dist/src/services/librarian.js.map +1 -0
  188. package/dist/src/services/message-delivery.d.ts +3 -1
  189. package/dist/src/services/message-delivery.d.ts.map +1 -1
  190. package/dist/src/services/message-delivery.js +23 -2
  191. package/dist/src/services/message-delivery.js.map +1 -1
  192. package/dist/src/services/network-policy.d.ts +88 -0
  193. package/dist/src/services/network-policy.d.ts.map +1 -0
  194. package/dist/src/services/network-policy.js +154 -0
  195. package/dist/src/services/network-policy.js.map +1 -0
  196. package/dist/src/services/notebook-classify-prompt.d.ts +15 -0
  197. package/dist/src/services/notebook-classify-prompt.d.ts.map +1 -0
  198. package/dist/src/services/notebook-classify-prompt.js +39 -0
  199. package/dist/src/services/notebook-classify-prompt.js.map +1 -0
  200. package/dist/src/services/notebook-classify.d.ts +80 -0
  201. package/dist/src/services/notebook-classify.d.ts.map +1 -0
  202. package/dist/src/services/notebook-classify.js +150 -0
  203. package/dist/src/services/notebook-classify.js.map +1 -0
  204. package/dist/src/services/presence.d.ts +108 -0
  205. package/dist/src/services/presence.d.ts.map +1 -0
  206. package/dist/src/services/presence.js +304 -0
  207. package/dist/src/services/presence.js.map +1 -0
  208. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  209. package/dist/src/services/prompt-defaults.js +13 -0
  210. package/dist/src/services/prompt-defaults.js.map +1 -1
  211. package/dist/src/services/site.d.ts +20 -0
  212. package/dist/src/services/site.d.ts.map +1 -1
  213. package/dist/src/services/site.js +70 -0
  214. package/dist/src/services/site.js.map +1 -1
  215. package/dist/src/storage/interface.d.ts +48 -0
  216. package/dist/src/storage/interface.d.ts.map +1 -1
  217. package/dist/src/storage/interface.js.map +1 -1
  218. package/dist/src/storage/providers/mongodb/index.d.ts +11 -1
  219. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  220. package/dist/src/storage/providers/mongodb/index.js +128 -2
  221. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  222. package/dist/src/storage/providers/sqlite/index.d.ts +7 -1
  223. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  224. package/dist/src/storage/providers/sqlite/index.js +27 -2
  225. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  226. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +5 -1
  227. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
  228. package/dist/src/storage/providers/sqlite/repos/direct-message.js +48 -0
  229. package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
  230. package/dist/src/storage/providers/sqlite/repos/memory.d.ts +2 -0
  231. package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
  232. package/dist/src/storage/providers/sqlite/repos/memory.js +55 -0
  233. package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
  234. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  235. package/dist/src/storage/providers/sqlite/schema.js +61 -0
  236. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  237. package/dist/src/storage/repositories/direct-message.repository.d.ts +5 -1
  238. package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
  239. package/dist/src/storage/repositories/memory.repository.d.ts +25 -0
  240. package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
  241. package/dist/src/utils/node-descriptor.d.ts +34 -0
  242. package/dist/src/utils/node-descriptor.d.ts.map +1 -0
  243. package/dist/src/utils/node-descriptor.js +42 -0
  244. package/dist/src/utils/node-descriptor.js.map +1 -0
  245. package/dist/src/utils/version.d.ts +3 -0
  246. package/dist/src/utils/version.d.ts.map +1 -0
  247. package/dist/src/utils/version.js +40 -0
  248. package/dist/src/utils/version.js.map +1 -0
  249. package/dist/static/app-catalog.html +643 -4
  250. package/package.json +5 -2
  251. package/prisma/schema.postgres.prisma +51 -18
  252. 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
+ }