@unson/brainbase-mcp 0.10.1 → 0.10.3
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/package.json +2 -2
- package/ui/graph-projects-view.js +116 -12
- package/ui/project-workspace.css +38 -0
- package/ui/project-workspace.js +123 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -336,5 +336,5 @@
|
|
|
336
336
|
"engines": {
|
|
337
337
|
"node": ">=20"
|
|
338
338
|
},
|
|
339
|
-
"gitHead": "
|
|
339
|
+
"gitHead": "8abcea403a82666c8c4616d4023fb28a10af761a"
|
|
340
340
|
}
|
|
@@ -54,7 +54,12 @@ import {
|
|
|
54
54
|
graphHostEmptyNotice,
|
|
55
55
|
} from './graph-view-shared.js';
|
|
56
56
|
import { graphOwnShareSource, ownProjectDetail, ownShareSection, ownShareStateNotice } from './graph-own-share.js';
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
createProjectKnowledgeWorkspace,
|
|
59
|
+
normalizeProjectContext,
|
|
60
|
+
renderProjectContext,
|
|
61
|
+
renderProjectKnowledgeSkeleton,
|
|
62
|
+
} from './project-workspace.js';
|
|
58
63
|
import { mountProjectGraph as defaultMountProjectGraph } from './project-graph.js';
|
|
59
64
|
import {
|
|
60
65
|
workspaceActions,
|
|
@@ -148,6 +153,19 @@ function failureText(error) {
|
|
|
148
153
|
return textOrNull(error instanceof Error ? error.message : typeof error === 'string' ? error : null) ?? '理由不明';
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
/**
|
|
157
|
+
* A host may know the selected project's identity before its Graph list is
|
|
158
|
+
* available. Keep this boundary deliberately small: the early shell only
|
|
159
|
+
* accepts an id and display name, and never treats a host summary as Graph
|
|
160
|
+
* data such as goal, status or participant counts.
|
|
161
|
+
*/
|
|
162
|
+
function normalizeProjectSummary(value) {
|
|
163
|
+
if (!isRecord(value)) return null;
|
|
164
|
+
const id = textOrNull(value.id ?? value.code);
|
|
165
|
+
if (!id) return null;
|
|
166
|
+
return { id, name: textOrNull(value.name) ?? id };
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
/**
|
|
152
170
|
* Mounts 「プロジェクトと関係者」.
|
|
153
171
|
*
|
|
@@ -198,9 +216,14 @@ function failureText(error) {
|
|
|
198
216
|
* labelled 「引き継いだ自分の分(組織には未反映)」 unless `label` says otherwise. They are never
|
|
199
217
|
* added to the ledger, the metrics or the rail, and a failed read shows only in that section.
|
|
200
218
|
* Without it nothing of the kind is drawn or read.
|
|
201
|
-
* @param {(projectId: string) => Promise<object>} [options.loadProjectContext]
|
|
202
|
-
* Optional read-only supplementary sections (tasks/knowledge).
|
|
203
|
-
*
|
|
219
|
+
* @param {(projectId: string, options?: { onProgress?: (snapshot: object) => void }) => Promise<object>} [options.loadProjectContext]
|
|
220
|
+
* Optional read-only supplementary sections (tasks/knowledge). The host may
|
|
221
|
+
* call `onProgress` with a complete `{ sections: [...] }` snapshot as each
|
|
222
|
+
* section resolves; a failed context read is kept separate from the Graph
|
|
223
|
+
* detail read.
|
|
224
|
+
* @param {Array<{ id: string, name?: string }> | (() => Array<{ id: string, name?: string }>)} [options.projectSummaries]
|
|
225
|
+
* Optional host-known project identities. These are used only to draw the
|
|
226
|
+
* selected project's early loading shell before the Graph list is read.
|
|
204
227
|
* @param {(container: Element, options: object) => { destroy?: Function }} [options.mountProjectGraph]
|
|
205
228
|
* Host-injected Sigma.js graph mount. When omitted, the shared browser
|
|
206
229
|
* entry is used. The workspace keeps an accessible relation list when
|
|
@@ -232,6 +255,7 @@ export function createGraphProjectsView({
|
|
|
232
255
|
onSelect,
|
|
233
256
|
ownShare,
|
|
234
257
|
loadProjectContext,
|
|
258
|
+
projectSummaries,
|
|
235
259
|
mountProjectGraph,
|
|
236
260
|
} = {}) {
|
|
237
261
|
if (!root) throw new TypeError('root is required');
|
|
@@ -267,6 +291,7 @@ export function createGraphProjectsView({
|
|
|
267
291
|
let knowledgeWorkspace = null;
|
|
268
292
|
let contextRequest = 0;
|
|
269
293
|
let detailRequest = 0;
|
|
294
|
+
let hostListRequest = 0;
|
|
270
295
|
let viewDestroyed = false;
|
|
271
296
|
|
|
272
297
|
const correction = createGraphCorrection({
|
|
@@ -289,6 +314,55 @@ export function createGraphProjectsView({
|
|
|
289
314
|
return state.list.state === 'ok' ? state.list.payload.projects.find((project) => project.id === id) ?? null : null;
|
|
290
315
|
}
|
|
291
316
|
|
|
317
|
+
function projectSummaryFor(id) {
|
|
318
|
+
const target = textOrNull(id);
|
|
319
|
+
if (!target) return null;
|
|
320
|
+
let summaries = projectSummaries;
|
|
321
|
+
try {
|
|
322
|
+
if (typeof summaries === 'function') summaries = summaries();
|
|
323
|
+
} catch {
|
|
324
|
+
summaries = null;
|
|
325
|
+
}
|
|
326
|
+
if (Array.isArray(summaries)) {
|
|
327
|
+
const summary = summaries.map(normalizeProjectSummary).find((item) => item?.id === target);
|
|
328
|
+
if (summary) return summary;
|
|
329
|
+
}
|
|
330
|
+
const listed = listItem(target);
|
|
331
|
+
return listed ? { id: listed.id, name: listed.name } : { id: target, name: target };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderProjectKnowledgeLoading(id, readState, { onRetry } = {}) {
|
|
335
|
+
const target = textOrNull(id);
|
|
336
|
+
if (!target) return null;
|
|
337
|
+
const project = projectSummaryFor(target);
|
|
338
|
+
const loading = makeElement(doc, 'section', {
|
|
339
|
+
className: 'bb-pkw bb-pkw-loading',
|
|
340
|
+
attrs: {
|
|
341
|
+
'aria-label': 'プロジェクトの知識',
|
|
342
|
+
'data-project-id': target,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
loading.append(makeElement(doc, 'div', { className: 'bb-pkw-kicker', text: 'プロジェクトの記録' }));
|
|
346
|
+
const header = makeElement(doc, 'header', { className: 'bb-pkw-header' });
|
|
347
|
+
header.append(
|
|
348
|
+
makeElement(doc, 'h2', { text: project.name }),
|
|
349
|
+
makeElement(doc, 'p', { className: 'bb-pkw-lead', text: '目的・関係・判断の根拠を、実際に記録された情報からたどれます。' }),
|
|
350
|
+
);
|
|
351
|
+
loading.append(header);
|
|
352
|
+
const notice = graphStateNotice(doc, readState, {
|
|
353
|
+
onRetry,
|
|
354
|
+
loadingText: 'プロジェクトの記録を読み込んでいます。',
|
|
355
|
+
});
|
|
356
|
+
if (notice) loading.append(notice);
|
|
357
|
+
if (readState?.state === 'loading') {
|
|
358
|
+
loading.append(renderProjectKnowledgeSkeleton(doc, { context: state.context?.projectId === target ? state.context : null }));
|
|
359
|
+
} else if (state.context?.projectId === target) {
|
|
360
|
+
const contextBlock = renderProjectContext(doc, state.context);
|
|
361
|
+
if (contextBlock) loading.append(contextBlock);
|
|
362
|
+
}
|
|
363
|
+
return loading;
|
|
364
|
+
}
|
|
365
|
+
|
|
292
366
|
function openCorrection(at, open) {
|
|
293
367
|
state.panelAt = at;
|
|
294
368
|
open();
|
|
@@ -345,16 +419,27 @@ export function createGraphProjectsView({
|
|
|
345
419
|
const request = ++contextRequest;
|
|
346
420
|
state.context = normalizeProjectContext(id, { state: 'loading' });
|
|
347
421
|
controller.render();
|
|
422
|
+
const applyContext = (snapshot) => {
|
|
423
|
+
const normalized = normalizeProjectContext(id, snapshot);
|
|
424
|
+
if (viewDestroyed || request !== contextRequest || state.selectedId !== id) return false;
|
|
425
|
+
state.context = normalized;
|
|
426
|
+
// Context progress must not rebuild the workspace: rebuilding destroys
|
|
427
|
+
// Sigma's viewport and resets the user's active tab/selection. Detail
|
|
428
|
+
// loading still uses the shell render so the project name is immediate.
|
|
429
|
+
if (knowledgeWorkspace && state.detail?.id === id && state.detail.state === 'ok') {
|
|
430
|
+
knowledgeWorkspace.setContext?.(normalized);
|
|
431
|
+
} else {
|
|
432
|
+
controller.render();
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
};
|
|
348
436
|
let result;
|
|
349
437
|
try {
|
|
350
|
-
result = await loadProjectContext(id);
|
|
438
|
+
result = await loadProjectContext(id, { onProgress: (snapshot) => { applyContext(snapshot); } });
|
|
351
439
|
} catch (error) {
|
|
352
440
|
result = { state: 'failed', message: `補足情報を読み取れませんでした(${failureText(error)})。` };
|
|
353
441
|
}
|
|
354
|
-
|
|
355
|
-
if (viewDestroyed || request !== contextRequest || state.selectedId !== id) return state.context;
|
|
356
|
-
state.context = normalized;
|
|
357
|
-
controller.render();
|
|
442
|
+
applyContext(result);
|
|
358
443
|
return state.context;
|
|
359
444
|
}
|
|
360
445
|
|
|
@@ -456,6 +541,9 @@ export function createGraphProjectsView({
|
|
|
456
541
|
const stateNotice = graphStateNotice(doc, state.list, { onRetry: () => controller.load(), loadingText: 'プロジェクトを読み込んでいます。' });
|
|
457
542
|
if (stateNotice) {
|
|
458
543
|
children.push(stateNotice);
|
|
544
|
+
if (state.selectedId) {
|
|
545
|
+
children.push(renderProjectKnowledgeLoading(state.selectedId, state.list, { onRetry: () => controller.load() }));
|
|
546
|
+
}
|
|
459
547
|
return children;
|
|
460
548
|
}
|
|
461
549
|
const { payload } = state.list;
|
|
@@ -486,6 +574,10 @@ export function createGraphProjectsView({
|
|
|
486
574
|
onTabChange: setKnowledgeTabChrome,
|
|
487
575
|
});
|
|
488
576
|
children.push(knowledgeWorkspace.element);
|
|
577
|
+
} else if (state.selectedId) {
|
|
578
|
+
children.push(renderProjectKnowledgeLoading(state.selectedId, state.detail ?? { state: 'loading' }, {
|
|
579
|
+
onRetry: () => controller.loadDetail(state.selectedId),
|
|
580
|
+
}));
|
|
489
581
|
}
|
|
490
582
|
|
|
491
583
|
if (payload.projects.length === 0) {
|
|
@@ -623,7 +715,10 @@ export function createGraphProjectsView({
|
|
|
623
715
|
const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
|
|
624
716
|
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
|
|
625
717
|
if (!loaded) {
|
|
626
|
-
|
|
718
|
+
const readState = detail?.id === state.selectedId
|
|
719
|
+
? detail
|
|
720
|
+
: state.list.state === 'ok' ? { state: 'loading' } : state.list;
|
|
721
|
+
children.push(graphStateNotice(doc, readState, {
|
|
627
722
|
onRetry: () => controller.loadDetail(state.selectedId),
|
|
628
723
|
loadingText: 'このプロジェクトの関係者を読み込んでいます。',
|
|
629
724
|
}));
|
|
@@ -692,13 +787,16 @@ export function createGraphProjectsView({
|
|
|
692
787
|
|
|
693
788
|
/** Reads the host's project list (see `load`). */
|
|
694
789
|
async function loadHostList({ keep = false } = {}) {
|
|
790
|
+
const request = ++hostListRequest;
|
|
791
|
+
if (viewDestroyed) return state.list;
|
|
695
792
|
if (!(keep && state.list.state === 'ok')) {
|
|
696
793
|
state.list = { state: 'loading' };
|
|
697
794
|
controller.render();
|
|
698
795
|
}
|
|
699
|
-
|
|
796
|
+
const result = await client.read('/projects');
|
|
797
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
798
|
+
state.list = normalizeProjectList(result);
|
|
700
799
|
if (state.list.state !== 'ok') {
|
|
701
|
-
if (!keep) state.selectedId = null;
|
|
702
800
|
controller.render();
|
|
703
801
|
return state.list;
|
|
704
802
|
}
|
|
@@ -711,16 +809,21 @@ export function createGraphProjectsView({
|
|
|
711
809
|
contextRequest += 1;
|
|
712
810
|
state.selectedId = projects[0]?.id ?? null;
|
|
713
811
|
if (state.selectedId) {
|
|
812
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
714
813
|
const reading = controller.loadDetail(state.selectedId);
|
|
715
814
|
notifySelect(state.selectedId);
|
|
716
815
|
await reading;
|
|
816
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
717
817
|
return state.list;
|
|
718
818
|
}
|
|
719
819
|
} else if (state.detail?.id !== state.selectedId) {
|
|
720
820
|
// The project the host asked for is listed; read it for the rail.
|
|
821
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
721
822
|
await controller.loadDetail(state.selectedId);
|
|
823
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
722
824
|
return state.list;
|
|
723
825
|
}
|
|
826
|
+
if (viewDestroyed || request !== hostListRequest) return state.list;
|
|
724
827
|
controller.render();
|
|
725
828
|
return state.list;
|
|
726
829
|
}
|
|
@@ -744,6 +847,7 @@ export function createGraphProjectsView({
|
|
|
744
847
|
destroy() {
|
|
745
848
|
if (viewDestroyed) return;
|
|
746
849
|
viewDestroyed = true;
|
|
850
|
+
hostListRequest += 1;
|
|
747
851
|
detailRequest += 1;
|
|
748
852
|
contextRequest += 1;
|
|
749
853
|
knowledgeWorkspace?.destroy();
|
package/ui/project-workspace.css
CHANGED
|
@@ -267,6 +267,44 @@
|
|
|
267
267
|
padding-top: var(--bb-space-3);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
.bb-pkw-skeleton-section {
|
|
271
|
+
min-height: 118px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.bb-pkw-skeleton-stack {
|
|
275
|
+
display: grid;
|
|
276
|
+
gap: var(--bb-space-2);
|
|
277
|
+
width: min(100%, 360px);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.bb-pkw-skeleton-bar {
|
|
281
|
+
display: block;
|
|
282
|
+
width: 100%;
|
|
283
|
+
height: 10px;
|
|
284
|
+
border-radius: 999px;
|
|
285
|
+
background: color-mix(in srgb, var(--bb-color-border-strong) 65%, var(--bb-color-panel));
|
|
286
|
+
animation: bb-pkw-skeleton-pulse 1.35s ease-in-out infinite;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.bb-pkw-skeleton-bar-1 { width: 92%; }
|
|
290
|
+
.bb-pkw-skeleton-bar-2 { width: 68%; }
|
|
291
|
+
.bb-pkw-skeleton-bar-3 { width: 82%; }
|
|
292
|
+
|
|
293
|
+
.bb-pkw-context-group-loading {
|
|
294
|
+
min-height: 72px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes bb-pkw-skeleton-pulse {
|
|
298
|
+
0%, 100% { opacity: .48; }
|
|
299
|
+
50% { opacity: 1; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@media (prefers-reduced-motion: reduce) {
|
|
303
|
+
.bb-pkw-skeleton-bar {
|
|
304
|
+
animation: none;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
270
308
|
.bb-pkw-context-heading,
|
|
271
309
|
.bb-pkw-graph-intro,
|
|
272
310
|
.bb-pkw-selected-head,
|
package/ui/project-workspace.js
CHANGED
|
@@ -95,7 +95,18 @@ export function normalizeProjectContext(projectId, result) {
|
|
|
95
95
|
if (result?.state === 'failed' || result?.state === 'error') {
|
|
96
96
|
return { projectId: id, state: 'failed', message: firstText(result.message, result.reason) ?? '補足情報を読み取れませんでした。', sections: [] };
|
|
97
97
|
}
|
|
98
|
-
if (result?.state === 'loading')
|
|
98
|
+
if (result?.state === 'loading') {
|
|
99
|
+
const partial = result?.payload && isRecord(result.payload) ? result.payload : result;
|
|
100
|
+
const rawSections = Array.isArray(partial.sections) ? partial.sections : [];
|
|
101
|
+
const normalized = {
|
|
102
|
+
projectId: id,
|
|
103
|
+
state: 'loading',
|
|
104
|
+
sections: rawSections.map(normalizeContextSection).filter(Boolean),
|
|
105
|
+
};
|
|
106
|
+
const message = firstText(result.message, result.reason);
|
|
107
|
+
if (message) normalized.message = message;
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
99
110
|
if (result?.state === 'unavailable') {
|
|
100
111
|
return { projectId: id, state: 'unavailable', message: firstText(result.message, result.reason) ?? '補足情報は接続されていません。', sections: [] };
|
|
101
112
|
}
|
|
@@ -107,7 +118,7 @@ export function normalizeProjectContext(projectId, result) {
|
|
|
107
118
|
if (sections.length === 0 && payload.sections.length > 0) {
|
|
108
119
|
return { projectId: id, state: 'unavailable', message: '補足情報の形式を確認できません。', sections: [] };
|
|
109
120
|
}
|
|
110
|
-
return { projectId: id, state: 'ok', sections };
|
|
121
|
+
return { projectId: id, state: sections.some((section) => section.state === 'loading') ? 'loading' : 'ok', sections };
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function addEntity(map, candidate) {
|
|
@@ -292,30 +303,66 @@ function renderEntityRelations(doc, edges, direction, entity, asOf, onSelect) {
|
|
|
292
303
|
return list.children.length > 0 ? list : null;
|
|
293
304
|
}
|
|
294
305
|
|
|
295
|
-
function
|
|
306
|
+
function skeletonBars(doc, count = 3) {
|
|
307
|
+
const stack = makeElement(doc, 'div', { className: 'bb-pkw-skeleton-stack', attrs: { 'aria-hidden': 'true' } });
|
|
308
|
+
for (let index = 0; index < count; index += 1) {
|
|
309
|
+
stack.append(makeElement(doc, 'span', {
|
|
310
|
+
className: `bb-pkw-skeleton-bar bb-pkw-skeleton-bar-${(index % 3) + 1}`,
|
|
311
|
+
attrs: { 'aria-hidden': 'true' },
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
return stack;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function contextSectionSkeleton(doc, { title = null, message = null } = {}) {
|
|
318
|
+
const group = makeElement(doc, 'section', {
|
|
319
|
+
className: 'bb-pkw-context-group bb-pkw-context-group-loading',
|
|
320
|
+
attrs: { 'aria-label': title ?? '補足情報', 'aria-busy': 'true' },
|
|
321
|
+
});
|
|
322
|
+
if (title) group.append(makeElement(doc, 'h4', { text: title }));
|
|
323
|
+
if (message) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: message }));
|
|
324
|
+
group.append(skeletonBars(doc));
|
|
325
|
+
return group;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Renders supplementary context while preserving each section's own state. */
|
|
329
|
+
export function renderProjectContext(doc, context) {
|
|
296
330
|
if (!context) return null;
|
|
297
|
-
const
|
|
331
|
+
const sections = Array.isArray(context.sections) ? context.sections : [];
|
|
332
|
+
const section = makeElement(doc, 'section', {
|
|
333
|
+
className: 'bb-pkw-section bb-pkw-context',
|
|
334
|
+
attrs: { 'aria-label': '補足情報' },
|
|
335
|
+
});
|
|
298
336
|
section.append(makeElement(doc, 'h3', { text: '補足情報' }));
|
|
299
|
-
if (context.state === 'loading') {
|
|
300
|
-
section.append(workspaceNotice(doc, { label: '読み込み中', text: 'タスクや知識の補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
|
|
301
|
-
return section;
|
|
302
|
-
}
|
|
303
337
|
if (context.state !== 'ok') {
|
|
304
|
-
|
|
305
|
-
|
|
338
|
+
if (context.state === 'loading') {
|
|
339
|
+
section.append(workspaceNotice(doc, { label: '読み込み中', text: context.message ?? '補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
|
|
340
|
+
if (sections.length === 0) section.append(contextSectionSkeleton(doc));
|
|
341
|
+
} else {
|
|
342
|
+
section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
|
|
343
|
+
}
|
|
306
344
|
}
|
|
307
|
-
if (context.sections.length === 0) {
|
|
345
|
+
if (context.state === 'ok' && sections.length === 0) {
|
|
308
346
|
section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '補足情報のセクションはありません。' }));
|
|
309
347
|
return section;
|
|
310
348
|
}
|
|
311
|
-
for (const item of
|
|
312
|
-
const group = makeElement(doc, 'section', {
|
|
349
|
+
for (const item of sections) {
|
|
350
|
+
const group = makeElement(doc, 'section', {
|
|
351
|
+
className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
|
|
352
|
+
attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
|
|
353
|
+
});
|
|
313
354
|
const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
|
|
314
355
|
heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : 'muted'));
|
|
315
356
|
group.append(heading);
|
|
316
|
-
if (item.message) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
|
|
357
|
+
if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
|
|
317
358
|
if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
|
|
318
|
-
if (item.
|
|
359
|
+
if (item.state === 'loading') {
|
|
360
|
+
group.append(skeletonBars(doc));
|
|
361
|
+
} else if (item.state === 'failed') {
|
|
362
|
+
group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
|
|
363
|
+
} else if (item.state === 'unavailable') {
|
|
364
|
+
group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
|
|
365
|
+
} else if (item.items.length > 0) {
|
|
319
366
|
const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
|
|
320
367
|
for (const entry of item.items) {
|
|
321
368
|
const row = makeElement(doc, 'li');
|
|
@@ -334,7 +381,30 @@ function renderContext(doc, context) {
|
|
|
334
381
|
return section;
|
|
335
382
|
}
|
|
336
383
|
|
|
337
|
-
|
|
384
|
+
/** Structural shell used while the Graph detail is loading; it contains no inferred values. */
|
|
385
|
+
export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
386
|
+
const panel = makeElement(doc, 'div', {
|
|
387
|
+
className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
|
|
388
|
+
attrs: { 'aria-label': '概要' },
|
|
389
|
+
});
|
|
390
|
+
const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
|
|
391
|
+
for (const title of ['目的と現在地', '関係者と役割']) {
|
|
392
|
+
const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
|
|
393
|
+
item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc));
|
|
394
|
+
columns.append(item);
|
|
395
|
+
}
|
|
396
|
+
panel.append(columns);
|
|
397
|
+
for (const title of ['記録された判断', '関連する情報']) {
|
|
398
|
+
const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
|
|
399
|
+
item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
|
|
400
|
+
panel.append(item);
|
|
401
|
+
}
|
|
402
|
+
const contextBlock = renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
|
|
403
|
+
if (contextBlock) panel.append(contextBlock);
|
|
404
|
+
return panel;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function renderOverview(doc, detail, context, edges, entities, onSelect, onContext) {
|
|
338
408
|
const payload = projectDetailPayload(detail);
|
|
339
409
|
const project = payload?.project;
|
|
340
410
|
const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
|
|
@@ -423,8 +493,11 @@ function renderOverview(doc, detail, context, edges, entities, onSelect) {
|
|
|
423
493
|
missing.append(list);
|
|
424
494
|
panel.append(missing);
|
|
425
495
|
}
|
|
426
|
-
const contextBlock =
|
|
427
|
-
if (contextBlock)
|
|
496
|
+
const contextBlock = renderProjectContext(doc, context);
|
|
497
|
+
if (contextBlock) {
|
|
498
|
+
panel.append(contextBlock);
|
|
499
|
+
if (typeof onContext === 'function') onContext(contextBlock);
|
|
500
|
+
}
|
|
428
501
|
return panel;
|
|
429
502
|
}
|
|
430
503
|
|
|
@@ -463,10 +536,13 @@ export function createProjectKnowledgeWorkspace({
|
|
|
463
536
|
let entityReadRequest = 0;
|
|
464
537
|
let graphSearch = '';
|
|
465
538
|
let graphType = '';
|
|
539
|
+
let currentContext = context;
|
|
466
540
|
// Keep direct references as well as the DOM lookup. The local test harness
|
|
467
541
|
// intentionally has no querySelector implementation, and a host can replace
|
|
468
542
|
// the panel while a detail read is still in flight.
|
|
469
543
|
let selectedPanel = null;
|
|
544
|
+
let overviewPanel = null;
|
|
545
|
+
let contextBlock = null;
|
|
470
546
|
|
|
471
547
|
const root = makeElement(doc, 'section', { className: 'bb-pkw', attrs: { 'data-contract-version': PROJECT_WORKSPACE_CONTRACT_VERSION, 'aria-label': 'プロジェクトの知識' } });
|
|
472
548
|
const body = makeElement(doc, 'div', { className: 'bb-pkw-body' });
|
|
@@ -698,15 +774,42 @@ export function createProjectKnowledgeWorkspace({
|
|
|
698
774
|
graphHandle = null;
|
|
699
775
|
}
|
|
700
776
|
|
|
777
|
+
function replaceContextBlock(nextContext) {
|
|
778
|
+
currentContext = nextContext;
|
|
779
|
+
if (destroyed) return;
|
|
780
|
+
const nextBlock = renderProjectContext(doc, currentContext);
|
|
781
|
+
if (!nextBlock) {
|
|
782
|
+
if (contextBlock?.parentNode?.removeChild) contextBlock.parentNode.removeChild(contextBlock);
|
|
783
|
+
contextBlock = null;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
|
|
787
|
+
contextBlock.parentNode.replaceChild(nextBlock, contextBlock);
|
|
788
|
+
contextBlock = nextBlock;
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (contextBlock?.replaceWith) {
|
|
792
|
+
contextBlock.replaceWith(nextBlock);
|
|
793
|
+
contextBlock = nextBlock;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (overviewPanel && !contextBlock) {
|
|
797
|
+
overviewPanel.append(nextBlock);
|
|
798
|
+
contextBlock = nextBlock;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
701
802
|
function render() {
|
|
702
803
|
if (destroyed) return;
|
|
703
804
|
if (typeof onTabChange === 'function') onTabChange(activeTab);
|
|
704
805
|
teardownGraph();
|
|
705
806
|
tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
|
|
706
807
|
panels.replaceChildren();
|
|
707
|
-
|
|
808
|
+
contextBlock = null;
|
|
809
|
+
const overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node) => { contextBlock = node; });
|
|
708
810
|
overview.id = 'bb-pkw-panel-overview';
|
|
709
811
|
overview.hidden = activeTab !== 'overview';
|
|
812
|
+
overviewPanel = overview;
|
|
710
813
|
panels.append(overview);
|
|
711
814
|
const graph = renderGraphPanel();
|
|
712
815
|
graph.hidden = activeTab !== 'graph';
|
|
@@ -721,7 +824,7 @@ export function createProjectKnowledgeWorkspace({
|
|
|
721
824
|
}
|
|
722
825
|
|
|
723
826
|
render();
|
|
724
|
-
return Object.freeze({ element: root, destroy, select, setTab });
|
|
827
|
+
return Object.freeze({ element: root, destroy, select, setTab, setContext: replaceContextBlock });
|
|
725
828
|
}
|
|
726
829
|
|
|
727
830
|
export default createProjectKnowledgeWorkspace;
|