@unson/brainbase-mcp 0.41.13 → 0.43.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/dist/local-web-host.d.ts +6 -0
- package/dist/local-web-host.js +35 -1
- package/dist/personal-memory.d.ts +101 -0
- package/dist/personal-memory.js +204 -0
- package/dist/ssot.d.ts +3 -1
- package/dist/ssot.js +4 -0
- package/package.json +11 -2
- package/ui/area-nav.css +56 -4
- package/ui/area-nav.js +130 -10
- package/ui/inbox.css +189 -0
- package/ui/inbox.js +578 -0
- package/ui/judgment-history.js +74 -3
- package/ui/local-web-shell.css +4 -2
- package/ui/local-web-shell.js +107 -51
- package/ui/personal-memory-http-port.js +72 -0
- package/ui/personal-memory.css +254 -0
- package/ui/personal-memory.js +677 -0
package/ui/judgment-history.js
CHANGED
|
@@ -1148,12 +1148,45 @@ function appendJudgmentBasis(doc, references, labels) {
|
|
|
1148
1148
|
return wrap;
|
|
1149
1149
|
}
|
|
1150
1150
|
|
|
1151
|
-
function
|
|
1151
|
+
function personalMemoryExcerpt(value) {
|
|
1152
|
+
const body = String(value ?? '').replace(/\s+/gu, ' ').trim();
|
|
1153
|
+
return body.length > 60 ? `${body.slice(0, 60)}…` : body;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* The used memories the host resolved for this viewer: an opening and a link
|
|
1158
|
+
* to あなたの記憶. Only the owner's host resolves them; without that, or when
|
|
1159
|
+
* the lookup failed, this stays the count only.
|
|
1160
|
+
*/
|
|
1161
|
+
function ownerMemoryLinks(doc, references, memories) {
|
|
1162
|
+
if (!(memories instanceof Map)) return null;
|
|
1163
|
+
const found = [];
|
|
1164
|
+
for (const reference of Array.isArray(references) ? references : []) {
|
|
1165
|
+
if (judgmentReferenceKind(reference) !== PERSONAL_KNOWLEDGE_KIND) continue;
|
|
1166
|
+
const entry = memories.get(text(reference?.ref));
|
|
1167
|
+
if (entry?.status === 'found') found.push(entry);
|
|
1168
|
+
}
|
|
1169
|
+
if (found.length === 0) return null;
|
|
1170
|
+
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-owner-memories', attrs: { 'aria-label': '使った記憶' } });
|
|
1171
|
+
for (const entry of found) {
|
|
1172
|
+
const item = makeWorkspaceElement(doc, 'li');
|
|
1173
|
+
item.append(makeWorkspaceElement(doc, 'a', { text: entry.excerpt, attrs: { href: entry.href } }));
|
|
1174
|
+
list.append(item);
|
|
1175
|
+
}
|
|
1176
|
+
return list;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function appendOwnerJudgment(doc, references, memories = null) {
|
|
1152
1180
|
const { owner } = describeJudgmentBasis(references);
|
|
1153
1181
|
const wrap = makeWorkspaceElement(doc, 'div', { className: `bb-jh-owner is-${owner.state}` });
|
|
1154
1182
|
if (owner.state === 'applied') {
|
|
1155
1183
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: `あなたの判断の材料(個人KG)を${owner.count}件使いました。` }));
|
|
1156
|
-
|
|
1184
|
+
const links = ownerMemoryLinks(doc, references, memories);
|
|
1185
|
+
if (links) {
|
|
1186
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。使った記憶はあなたにだけ見えています。' }), links);
|
|
1187
|
+
} else {
|
|
1188
|
+
wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。個人KGの中身はここには出しません。' }));
|
|
1189
|
+
}
|
|
1157
1190
|
} else if (owner.state === 'not_applied') {
|
|
1158
1191
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: 'あなたの判断の材料(個人KG)は使っていません。' }));
|
|
1159
1192
|
wrap.append(makeWorkspaceElement(doc, 'p', { text: 'Graphの哲学・目的・世界モデルと、AIの判断で進めました。' }));
|
|
@@ -1467,7 +1500,7 @@ function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
|
|
|
1467
1500
|
}));
|
|
1468
1501
|
rail.append(workspaceRailBlock(doc, {
|
|
1469
1502
|
title: '本人の考え',
|
|
1470
|
-
content: appendOwnerJudgment(doc, record.selectedReferences),
|
|
1503
|
+
content: appendOwnerJudgment(doc, record.selectedReferences, options.personalMemories),
|
|
1471
1504
|
}));
|
|
1472
1505
|
rail.append(workspaceRailBlock(doc, {
|
|
1473
1506
|
title: '判断の理由',
|
|
@@ -1907,6 +1940,10 @@ export function createJudgmentHistoryUI({
|
|
|
1907
1940
|
// Optional host port: (references: {ref, kind}[]) => Promise<Map|object of ref -> name>.
|
|
1908
1941
|
// The host resolves names under the viewer's current permission.
|
|
1909
1942
|
resolveReferenceLabels = null,
|
|
1943
|
+
// Optional owner-only port: (refs: string[]) => Promise<Map|object of ref -> { excerpt, href }>.
|
|
1944
|
+
// A host passes it only to the owner; the 本人の考え block then shows the
|
|
1945
|
+
// opening of each used memory with a link to あなたの記憶.
|
|
1946
|
+
resolvePersonalMemory = null,
|
|
1910
1947
|
} = {}) {
|
|
1911
1948
|
if (!root) throw new TypeError('root is required');
|
|
1912
1949
|
const doc = safeDocument(explicitDocument);
|
|
@@ -1946,6 +1983,7 @@ export function createJudgmentHistoryUI({
|
|
|
1946
1983
|
skeletonTimer: null,
|
|
1947
1984
|
slowTimer: null,
|
|
1948
1985
|
labels: new Map(),
|
|
1986
|
+
personalMemories: new Map(),
|
|
1949
1987
|
};
|
|
1950
1988
|
|
|
1951
1989
|
let filterPopover = null;
|
|
@@ -2254,6 +2292,37 @@ export function createJudgmentHistoryUI({
|
|
|
2254
2292
|
void load({ append: false });
|
|
2255
2293
|
}
|
|
2256
2294
|
|
|
2295
|
+
function requestPersonalMemories(rows) {
|
|
2296
|
+
if (typeof resolvePersonalMemory !== 'function') return;
|
|
2297
|
+
const pending = [];
|
|
2298
|
+
for (const row of rows) {
|
|
2299
|
+
if (row.mode !== 'normal' || !Array.isArray(row.references)) continue;
|
|
2300
|
+
for (const reference of row.references) {
|
|
2301
|
+
const ref = text(reference?.ref);
|
|
2302
|
+
if (!ref || judgmentReferenceKind(reference) !== PERSONAL_KNOWLEDGE_KIND || state.personalMemories.has(ref)) continue;
|
|
2303
|
+
state.personalMemories.set(ref, { status: 'loading' });
|
|
2304
|
+
pending.push(ref);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (!pending.length) return;
|
|
2308
|
+
const settle = (resolved) => {
|
|
2309
|
+
if (state.disposed) return;
|
|
2310
|
+
const lookup = resolved instanceof Map
|
|
2311
|
+
? (ref) => resolved.get(ref)
|
|
2312
|
+
: isRecord(resolved) ? (ref) => resolved[ref] : () => null;
|
|
2313
|
+
for (const ref of pending) {
|
|
2314
|
+
const entry = lookup(ref);
|
|
2315
|
+
const excerpt = isRecord(entry) ? personalMemoryExcerpt(entry.excerpt) : '';
|
|
2316
|
+
const href = isRecord(entry) ? text(entry.href) : '';
|
|
2317
|
+
state.personalMemories.set(ref, excerpt && href ? { status: 'found', excerpt, href } : { status: 'missing' });
|
|
2318
|
+
}
|
|
2319
|
+
render();
|
|
2320
|
+
};
|
|
2321
|
+
Promise.resolve()
|
|
2322
|
+
.then(() => resolvePersonalMemory([...pending]))
|
|
2323
|
+
.then(settle, () => settle(null));
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2257
2326
|
function requestReferenceLabels(rows) {
|
|
2258
2327
|
const pending = [];
|
|
2259
2328
|
for (const row of rows) {
|
|
@@ -2440,6 +2509,7 @@ export function createJudgmentHistoryUI({
|
|
|
2440
2509
|
? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
|
|
2441
2510
|
: 'この期間の判断履歴はありません。';
|
|
2442
2511
|
requestReferenceLabels(aggregate.rows);
|
|
2512
|
+
requestPersonalMemories(aggregate.rows);
|
|
2443
2513
|
const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
|
|
2444
2514
|
state.lastFocusedKey = key;
|
|
2445
2515
|
state.selectedKey = key;
|
|
@@ -2507,6 +2577,7 @@ export function createJudgmentHistoryUI({
|
|
|
2507
2577
|
onSubmitFeedback: (controls) => void submitFeedback(selected, controls),
|
|
2508
2578
|
onCopyReference: (payload) => void copyReference(payload),
|
|
2509
2579
|
labels: state.labels,
|
|
2580
|
+
personalMemories: state.personalMemories,
|
|
2510
2581
|
}
|
|
2511
2582
|
: {};
|
|
2512
2583
|
if (nativeDrawer) {
|
package/ui/local-web-shell.css
CHANGED
|
@@ -67,9 +67,11 @@ body {
|
|
|
67
67
|
letter-spacing: -.035em;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* The menu fills the rail so that 設定 sits at its bottom (ledger D10). */
|
|
70
71
|
.bb-shell-nav {
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
flex: 1 1 auto;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
.bb-shell-nav-link {
|
package/ui/local-web-shell.js
CHANGED
|
@@ -26,6 +26,8 @@ import { createMeetingMinutesUI } from './meeting-minutes.js';
|
|
|
26
26
|
import { createMeetingMinutesStorageExtension } from './meeting-minutes-storage.js';
|
|
27
27
|
import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-http.js';
|
|
28
28
|
import { createWorldModelView } from './world-model-view.js';
|
|
29
|
+
import { createPersonalMemoryUI, parsePersonalMemoryRoute, personalMemoryRouteTarget } from './personal-memory.js';
|
|
30
|
+
import { createPersonalMemoryHttpPort } from './personal-memory-http-port.js';
|
|
29
31
|
import { createGraphClient } from './graph-view-shared.js';
|
|
30
32
|
import { installNativeScrollbars } from './native-scrollbars.js';
|
|
31
33
|
import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
|
|
@@ -53,66 +55,69 @@ const NAV_ICON_PATHS = Object.freeze({
|
|
|
53
55
|
minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
|
|
54
56
|
// Two people (the organization edition's 人と会社 uses the same shape).
|
|
55
57
|
people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
|
|
58
|
+
// あなたの記憶 (ledger D15): one person and a lock, seen only by the owner.
|
|
59
|
+
memory: ['M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M3.5 20a6.5 6.5 0 0 1 10-5.5', 'M15 15h6v5h-6z', 'M16.5 15v-1.5a1.5 1.5 0 0 1 3 0V15'],
|
|
56
60
|
// Kinds of information and how they connect (settings, ledger D14): three linked boxes.
|
|
57
61
|
ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
|
|
58
62
|
// World view (extension id `world`, ledger P17).
|
|
59
63
|
world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
|
|
60
|
-
//
|
|
61
|
-
// the organization edition's gear.
|
|
62
|
-
'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
|
|
64
|
+
// 設定 (ledger D10): the organization edition's gear.
|
|
63
65
|
'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
|
|
64
66
|
});
|
|
65
67
|
// Screens without their own icon (for example a private product's extension).
|
|
66
68
|
const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
|
|
67
69
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
68
70
|
|
|
69
|
-
// The menu's
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Object.freeze({ id: '
|
|
77
|
-
Object.freeze({ id: '
|
|
78
|
-
Object.freeze({ id: '
|
|
79
|
-
Object.freeze({ id: 'settings', label: '
|
|
71
|
+
// The menu's sections (brainbase-project ledger D10, 2026-10-17 revision): the
|
|
72
|
+
// destinations themselves under small headings, in the organization edition's
|
|
73
|
+
// order, with 設定 at the bottom. Screens OSS Personal does not have are not
|
|
74
|
+
// listed. `screenId` makes the heading open that screen (プロジェクトと関係者);
|
|
75
|
+
// `area` turns the section's screens into one line with tabs (設定). Inside a
|
|
76
|
+
// section, `order` lists the screens first in that order.
|
|
77
|
+
const NAV_SECTIONS = Object.freeze([
|
|
78
|
+
Object.freeze({ id: 'meetings', label: '会議', order: ['minutes'] }),
|
|
79
|
+
Object.freeze({ id: 'project', label: 'プロジェクト', screenId: 'projects', order: ['objectives'] }),
|
|
80
|
+
Object.freeze({ id: 'explore', label: '調べる', order: ['value-proofs', 'graph', 'people', 'memory'] }),
|
|
81
|
+
Object.freeze({ id: 'settings', label: null, placement: 'footer', order: ['ontology'], area: Object.freeze({ label: '設定', defaultScreen: 'ontology', icon: 'area:settings' }) }),
|
|
80
82
|
]);
|
|
81
83
|
|
|
84
|
+
const sectionOf = (screen) => NAV_SECTIONS.find((section) => section.id === screen.section) ?? null;
|
|
85
|
+
|
|
82
86
|
// Menu order only: the screens' own order (and so the first screen) stays as given.
|
|
83
|
-
// Screens without
|
|
84
|
-
// screen come first (a leading extension such as the world), the rest come
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
87
|
+
// Screens without a section keep their place — those before the first sectioned
|
|
88
|
+
// screen come first (a leading extension such as the world), the rest come after
|
|
89
|
+
// the sections and before 設定.
|
|
90
|
+
function navAreas(screens) {
|
|
91
|
+
const own = (screen, section = null) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id, ...(section ? { section } : {}) });
|
|
92
|
+
const firstSectioned = screens.findIndex((screen) => sectionOf(screen));
|
|
93
|
+
const leading = firstSectioned === -1 ? [...screens] : screens.slice(0, firstSectioned);
|
|
94
|
+
const trailing = firstSectioned === -1 ? [] : screens.slice(firstSectioned).filter((screen) => !sectionOf(screen));
|
|
95
|
+
const sectioned = NAV_SECTIONS.map((section) => {
|
|
96
|
+
const members = screens.filter((screen) => screen.section === section.id && screen.id !== section.screenId);
|
|
92
97
|
const rank = (screen) => {
|
|
93
|
-
const index =
|
|
94
|
-
return index === -1 ?
|
|
98
|
+
const index = section.order.indexOf(screen.id);
|
|
99
|
+
return index === -1 ? section.order.length + members.indexOf(screen) : index;
|
|
95
100
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
const ordered = [...members].sort((a, b) => rank(a) - rank(b));
|
|
102
|
+
if (ordered.length === 0) return { section, areas: [] };
|
|
103
|
+
if (section.area) {
|
|
104
|
+
return { section, areas: [{ id: section.id, label: section.area.label, defaultScreen: section.area.defaultScreen, screenIds: ordered.map((screen) => screen.id), icon: section.area.icon, section: section.id }] };
|
|
105
|
+
}
|
|
106
|
+
return { section, areas: ordered.map((screen) => own(screen, section.id)) };
|
|
107
|
+
});
|
|
108
|
+
const main = sectioned.filter(({ section }) => section.placement !== 'footer').flatMap(({ areas }) => areas);
|
|
109
|
+
const footer = sectioned.filter(({ section }) => section.placement === 'footer').flatMap(({ areas }) => areas);
|
|
110
|
+
return [...leading.map((screen) => own(screen)), ...main, ...trailing.map((screen) => own(screen)), ...footer];
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
//
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
...
|
|
108
|
-
|
|
109
|
-
label: area.label,
|
|
110
|
-
defaultScreen: area.defaultScreen,
|
|
111
|
-
screenIds: members.map((screen) => screen.id),
|
|
112
|
-
icon: area.icon,
|
|
113
|
-
})),
|
|
114
|
-
...layout.trailing.map(own),
|
|
115
|
-
];
|
|
113
|
+
// The sections handed to the menu part; a heading opens its screen only when that screen exists.
|
|
114
|
+
function navSections(screens) {
|
|
115
|
+
return NAV_SECTIONS.map((section) => ({
|
|
116
|
+
id: section.id,
|
|
117
|
+
label: section.label,
|
|
118
|
+
placement: section.placement ?? 'main',
|
|
119
|
+
...(section.screenId && screens.some((screen) => screen.id === section.screenId) ? { screenId: section.screenId } : {}),
|
|
120
|
+
}));
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
function makeIcon(doc, id) {
|
|
@@ -163,11 +168,14 @@ export function parseLocalWebTarget(target) {
|
|
|
163
168
|
// プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
|
|
164
169
|
const decisionId = screenId === 'value-proofs' ? params.get('decision')?.trim() || null : null;
|
|
165
170
|
const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
|
|
171
|
+
// あなたの記憶 keeps its filters and the opened memory (never the search words).
|
|
172
|
+
const memoryRoute = screenId === 'memory' ? parsePersonalMemoryRoute(params) : null;
|
|
166
173
|
return {
|
|
167
174
|
screenId,
|
|
168
175
|
entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
|
|
169
176
|
...(decisionId ? { decisionId } : {}),
|
|
170
177
|
...(projectId ? { projectId } : {}),
|
|
178
|
+
...(memoryRoute ? { memoryRoute } : {}),
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -228,6 +236,26 @@ function judgmentReferenceLabelReader(context) {
|
|
|
228
236
|
};
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
function personalMemoryPort(context) {
|
|
240
|
+
return createPersonalMemoryHttpPort({ fetcher: context.fetcher, token: context.token });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The opening of each used memory, for 判断の履歴's 本人の考え; the local host has one owner. */
|
|
244
|
+
function personalMemoryResolver(context) {
|
|
245
|
+
const port = personalMemoryPort(context);
|
|
246
|
+
return async (refs) => {
|
|
247
|
+
const list = await port.listMemories({});
|
|
248
|
+
if (list.status !== 'ok') throw new Error(list.reason);
|
|
249
|
+
const wanted = new Set(refs);
|
|
250
|
+
const found = new Map();
|
|
251
|
+
for (const item of list.items) {
|
|
252
|
+
if (!wanted.has(item.id)) continue;
|
|
253
|
+
found.set(item.id, { excerpt: item.text, href: `#${personalMemoryRouteTarget({ memoryId: item.id })}` });
|
|
254
|
+
}
|
|
255
|
+
return found;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
function mountToday(container, context) {
|
|
232
260
|
return createJudgmentHistoryUI({
|
|
233
261
|
root: makePage(container, context),
|
|
@@ -238,6 +266,23 @@ function mountToday(container, context) {
|
|
|
238
266
|
token: context.token,
|
|
239
267
|
basePath: '/api/judgment-history',
|
|
240
268
|
resolveReferenceLabels: judgmentReferenceLabelReader(context),
|
|
269
|
+
resolvePersonalMemory: personalMemoryResolver(context),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function mountPersonalMemory(container, context) {
|
|
274
|
+
return createPersonalMemoryUI({
|
|
275
|
+
root: makePage(container, context),
|
|
276
|
+
rail: context.rail,
|
|
277
|
+
page: context.page,
|
|
278
|
+
document: context.document,
|
|
279
|
+
port: personalMemoryPort(context),
|
|
280
|
+
judgmentHref: (decisionId) => `#value-proofs?decision=${encodeURIComponent(decisionId)}`,
|
|
281
|
+
candidatesCommand: 'brainbase onboard:candidates',
|
|
282
|
+
route: context.initialEntityId ?? undefined,
|
|
283
|
+
onRouteChange: context.navigate
|
|
284
|
+
? (route, options) => context.navigate(personalMemoryRouteTarget(route), options)
|
|
285
|
+
: undefined,
|
|
241
286
|
});
|
|
242
287
|
}
|
|
243
288
|
|
|
@@ -332,17 +377,19 @@ function mountGraphScreen(screenId, createView, extra = {}) {
|
|
|
332
377
|
}
|
|
333
378
|
|
|
334
379
|
export const LOCAL_WEB_SCREENS = Object.freeze([
|
|
335
|
-
Object.freeze({ id: 'value-proofs', label: '判断の履歴',
|
|
336
|
-
Object.freeze({ id: 'minutes', label: '議事録',
|
|
337
|
-
Object.freeze({ id: 'objectives', label: '目的と現状',
|
|
338
|
-
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者',
|
|
380
|
+
Object.freeze({ id: 'value-proofs', label: '判断の履歴', section: 'explore', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
|
|
381
|
+
Object.freeze({ id: 'minutes', label: '議事録', section: 'meetings', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
|
|
382
|
+
Object.freeze({ id: 'objectives', label: '目的と現状', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
|
|
383
|
+
Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
|
|
339
384
|
// A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
|
|
340
|
-
Object.freeze({ id: 'graph', label: '情報と関係',
|
|
385
|
+
Object.freeze({ id: 'graph', label: '情報と関係', section: 'explore', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
|
|
341
386
|
onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
|
|
342
387
|
})) }),
|
|
343
|
-
Object.freeze({ id: 'people', label: '人と会社',
|
|
388
|
+
Object.freeze({ id: 'people', label: '人と会社', section: 'explore', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
|
|
389
|
+
// Only the owner sees it (ledger D15); the local host has one owner, so it is always listed here.
|
|
390
|
+
Object.freeze({ id: 'memory', label: 'あなたの記憶', section: 'explore', usesGraph: false, rail: true, ownerOnly: true, source: '手元の個人KG', mount: mountPersonalMemory }),
|
|
344
391
|
// The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
|
|
345
|
-
Object.freeze({ id: 'ontology', label: '情報の種類とつながり方',
|
|
392
|
+
Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', section: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
|
|
346
393
|
]);
|
|
347
394
|
|
|
348
395
|
function renderGraphGate(doc, slot, status, onRecheck, label) {
|
|
@@ -428,10 +475,14 @@ export function createLocalWebShell({
|
|
|
428
475
|
meetingMinutesLineage = null,
|
|
429
476
|
areaMemory = createAreaMemory(browserAreaStorage()),
|
|
430
477
|
onNavigate,
|
|
478
|
+
// A host that serves more than one person passes false for anyone but the owner:
|
|
479
|
+
// owner-only screens (あなたの記憶) are then neither listed nor mounted.
|
|
480
|
+
isOwner = true,
|
|
431
481
|
} = {}) {
|
|
432
482
|
if (!root) throw new TypeError('root is required');
|
|
433
483
|
const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
|
|
434
484
|
if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
|
|
485
|
+
screens = screens.filter((screen) => isOwner === true || screen.ownerOnly !== true);
|
|
435
486
|
const request = typeof fetcher === 'function'
|
|
436
487
|
? fetcher
|
|
437
488
|
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
@@ -466,6 +517,7 @@ export function createLocalWebShell({
|
|
|
466
517
|
const areaNav = createAreaNav({
|
|
467
518
|
document: doc,
|
|
468
519
|
areas: areaList,
|
|
520
|
+
sections: navSections(screens),
|
|
469
521
|
screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
|
|
470
522
|
memory: areaMemory,
|
|
471
523
|
linkClassName: 'bb-shell-nav-link',
|
|
@@ -522,6 +574,7 @@ export function createLocalWebShell({
|
|
|
522
574
|
if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
|
|
523
575
|
if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
|
|
524
576
|
if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
|
|
577
|
+
if (entityId && screen.id === 'memory' && typeof view?.applyRoute === 'function') view.applyRoute(entityId);
|
|
525
578
|
return;
|
|
526
579
|
}
|
|
527
580
|
const slot = slots.get(screen.id);
|
|
@@ -535,6 +588,7 @@ export function createLocalWebShell({
|
|
|
535
588
|
const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
|
|
536
589
|
const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true;
|
|
537
590
|
mounted.set(screen.id, view);
|
|
591
|
+
if (screen.id === 'memory' && typeof view?.load === 'function') void view.load();
|
|
538
592
|
if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
|
|
539
593
|
if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
|
|
540
594
|
updateRail();
|
|
@@ -550,7 +604,9 @@ export function createLocalWebShell({
|
|
|
550
604
|
const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
|
|
551
605
|
pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
|
|
552
606
|
: screen.id === 'value-proofs' ? parsed.decisionId ?? null
|
|
553
|
-
: screen.id === 'projects' ? parsed.projectId ?? null
|
|
607
|
+
: screen.id === 'projects' ? parsed.projectId ?? null
|
|
608
|
+
// あなたの記憶 receives its whole route (filters and the opened memory).
|
|
609
|
+
: screen.id === 'memory' ? parsed.memoryRoute ?? null : null);
|
|
554
610
|
active = screen.id;
|
|
555
611
|
for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
|
|
556
612
|
areaNav.show(screen.id);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The local Web host's port for 「あなたの記憶」 (ui/personal-memory.js).
|
|
3
|
+
*
|
|
4
|
+
* Every read carries the launch token: the memory bodies are private, so the
|
|
5
|
+
* host guards these reads as it guards writes. The list is read whole and
|
|
6
|
+
* filtered here, so the words being searched for never go into a request URL.
|
|
7
|
+
* The local host has a single owner, so `isOwner` is always true.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { filterPersonalMemories } from './personal-memory.js';
|
|
11
|
+
|
|
12
|
+
export function createPersonalMemoryHttpPort({
|
|
13
|
+
fetcher,
|
|
14
|
+
token,
|
|
15
|
+
basePath = '/api/personal-memory',
|
|
16
|
+
tokenHeader = 'x-brainbase-review-token',
|
|
17
|
+
} = {}) {
|
|
18
|
+
const request = typeof fetcher === 'function'
|
|
19
|
+
? fetcher
|
|
20
|
+
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
21
|
+
const base = String(basePath).replace(/\/+$/u, '');
|
|
22
|
+
|
|
23
|
+
async function read(path) {
|
|
24
|
+
if (!request) return { status: 0, body: null, failure: 'fetch_unavailable' };
|
|
25
|
+
try {
|
|
26
|
+
const response = await request(path, { method: 'GET', headers: token ? { [tokenHeader]: token } : {} });
|
|
27
|
+
let body = null;
|
|
28
|
+
try {
|
|
29
|
+
body = await response.json();
|
|
30
|
+
} catch {
|
|
31
|
+
body = null;
|
|
32
|
+
}
|
|
33
|
+
return { status: response.status, ok: response.ok === true, body };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { status: 0, body: null, failure: error instanceof Error ? error.message : 'request_failed' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unavailable(result) {
|
|
40
|
+
return {
|
|
41
|
+
status: 'unavailable',
|
|
42
|
+
reason: result.failure ?? result.body?.reason ?? result.body?.error?.code ?? `http_${result.status}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
isOwner: () => true,
|
|
48
|
+
async listMemories(filter = {}) {
|
|
49
|
+
const result = await read(base);
|
|
50
|
+
if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
|
|
51
|
+
return { status: 'ok', items: filterPersonalMemories(result.body.items, filter), nextCursor: null };
|
|
52
|
+
},
|
|
53
|
+
async readMemory(id) {
|
|
54
|
+
const result = await read(`${base}/${encodeURIComponent(id)}`);
|
|
55
|
+
if (result.status === 404 && result.body?.status === 'not_found') return { status: 'not_found' };
|
|
56
|
+
if (!result.ok || result.body?.status !== 'ok' || !result.body.item) return unavailable(result);
|
|
57
|
+
return { status: 'ok', item: result.body.item };
|
|
58
|
+
},
|
|
59
|
+
async listJudgmentsUsing(id, limit) {
|
|
60
|
+
const result = await read(`${base}/${encodeURIComponent(id)}/judgments?limit=${encodeURIComponent(String(limit))}`);
|
|
61
|
+
if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
|
|
62
|
+
return { status: 'ok', items: result.body.items, total: result.body.total };
|
|
63
|
+
},
|
|
64
|
+
async countJudgmentsUsing() {
|
|
65
|
+
const result = await read(`${base}?view=judgment-counts`);
|
|
66
|
+
if (!result.ok || result.body?.status !== 'ok' || !result.body.counts) return unavailable(result);
|
|
67
|
+
return { status: 'ok', counts: result.body.counts };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default createPersonalMemoryHttpPort;
|