@unson/brainbase-mcp 0.19.0 → 0.20.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.js +1 -0
- package/package.json +3 -2
- package/ui/project-overview.js +263 -0
- package/ui/project-workspace.css +175 -0
- package/ui/project-workspace.js +38 -3
package/dist/local-web-host.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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",
|
|
@@ -289,6 +289,7 @@
|
|
|
289
289
|
},
|
|
290
290
|
"./contracts/foundation/graph-history.sql": "./contracts/foundation/graph-history.sql",
|
|
291
291
|
"./ui/project-workspace": "./ui/project-workspace.js",
|
|
292
|
+
"./ui/project-overview": "./ui/project-overview.js",
|
|
292
293
|
"./ui/project-graph": "./ui/project-graph.js",
|
|
293
294
|
"./ui/project-workspace.css": "./ui/project-workspace.css",
|
|
294
295
|
"./ui/project-graph-vendor.js": "./ui/project-graph-vendor.js"
|
|
@@ -363,5 +364,5 @@
|
|
|
363
364
|
"engines": {
|
|
364
365
|
"node": ">=20"
|
|
365
366
|
},
|
|
366
|
-
"gitHead": "
|
|
367
|
+
"gitHead": "eae9755b000357391dc4f1244537e8b6bfd7925c"
|
|
367
368
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Optional host projection for the first project overview surface.
|
|
3
|
+
*
|
|
4
|
+
* This module only renders values supplied by an authorized host. It does not
|
|
5
|
+
* derive relationships, counts, next actions, or material membership from the
|
|
6
|
+
* shared Graph payload.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatDateTime, makeElement, textOrNull } from './graph-view-shared.js';
|
|
10
|
+
import { workspaceNotice } from './workspace-kit.js';
|
|
11
|
+
|
|
12
|
+
const MATERIAL_STATES = new Set(['ok', 'partial', 'unknown', 'failed', 'unavailable', 'loading']);
|
|
13
|
+
const BODY_STATES = new Set(['retrieved', 'unretrieved', 'unavailable', 'failed', 'unknown']);
|
|
14
|
+
|
|
15
|
+
const MATERIAL_STATE_LABELS = Object.freeze({
|
|
16
|
+
ok: '取得済み',
|
|
17
|
+
partial: '一部取得',
|
|
18
|
+
unknown: '未確認',
|
|
19
|
+
failed: '読み取り失敗',
|
|
20
|
+
unavailable: '未接続',
|
|
21
|
+
loading: '読み込み中',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const BODY_STATE_LABELS = Object.freeze({
|
|
25
|
+
retrieved: '本文取得済み',
|
|
26
|
+
unretrieved: '本文未取得',
|
|
27
|
+
unavailable: '本文未接続',
|
|
28
|
+
failed: '本文読取失敗',
|
|
29
|
+
unknown: '本文状態未確認',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function cleanText(value) {
|
|
33
|
+
return textOrNull(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeTotal(value) {
|
|
37
|
+
if (Number.isInteger(value) && value >= 0) return value;
|
|
38
|
+
if (typeof value === 'string' && /^\d+$/u.test(value)) return Number(value);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeDate(value) {
|
|
43
|
+
return cleanText(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeMaterialItem(item) {
|
|
47
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
48
|
+
const id = cleanText(item.id);
|
|
49
|
+
const title = cleanText(item.title) ?? (id ? id : '資料名未確認');
|
|
50
|
+
const relation = cleanText(item.relation);
|
|
51
|
+
const source = cleanText(item.source);
|
|
52
|
+
const bodyState = BODY_STATES.has(item.bodyState) ? item.bodyState : 'unknown';
|
|
53
|
+
const contentAsOf = normalizeDate(item.contentAsOf);
|
|
54
|
+
const updatedAt = normalizeDate(item.updatedAt);
|
|
55
|
+
if (!id && !relation && !source && !contentAsOf && !updatedAt && bodyState === 'unknown' && title === '資料名未確認') return null;
|
|
56
|
+
return { id, title, relation, source, bodyState, contentAsOf, updatedAt };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeMaterials(value) {
|
|
60
|
+
const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
61
|
+
const state = MATERIAL_STATES.has(input.state) ? input.state : 'unknown';
|
|
62
|
+
return {
|
|
63
|
+
state,
|
|
64
|
+
items: Array.isArray(input.items) ? input.items.map(normalizeMaterialItem).filter(Boolean) : [],
|
|
65
|
+
total: normalizeTotal(input.total),
|
|
66
|
+
note: cleanText(input.note),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeCheck(item) {
|
|
71
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
72
|
+
const id = cleanText(item.id);
|
|
73
|
+
const title = cleanText(item.title) ?? (id ? id : '確認事項未確認');
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
title,
|
|
77
|
+
summary: cleanText(item.summary),
|
|
78
|
+
recordId: cleanText(item.recordId),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeAbout(value) {
|
|
83
|
+
const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
84
|
+
return {
|
|
85
|
+
definition: cleanText(input.definition),
|
|
86
|
+
owner: cleanText(input.owner),
|
|
87
|
+
finalApprover: cleanText(input.finalApprover),
|
|
88
|
+
goal: cleanText(input.goal),
|
|
89
|
+
source: cleanText(input.source),
|
|
90
|
+
asOf: normalizeDate(input.asOf),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize the optional host projection without turning malformed or missing
|
|
96
|
+
* fields into successful empty data.
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeProjectOverview(value) {
|
|
99
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
100
|
+
return {
|
|
101
|
+
about: normalizeAbout(value.about),
|
|
102
|
+
checks: Array.isArray(value.checks) ? value.checks.map(normalizeCheck).filter(Boolean) : [],
|
|
103
|
+
directMaterials: normalizeMaterials(value.directMaterials),
|
|
104
|
+
relatedMaterials: normalizeMaterials(value.relatedMaterials),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function displayDate(value) {
|
|
109
|
+
if (!value) return '未確認';
|
|
110
|
+
return Number.isNaN(Date.parse(value)) ? '日時未確認' : formatDateTime(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stateLabel(state) {
|
|
114
|
+
return MATERIAL_STATE_LABELS[state] ?? '未確認';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderAbout(doc, about) {
|
|
118
|
+
const section = makeElement(doc, 'section', {
|
|
119
|
+
className: 'bb-pkw-overview-section bb-pkw-overview-about',
|
|
120
|
+
attrs: { 'aria-label': 'このプロジェクトについて' },
|
|
121
|
+
});
|
|
122
|
+
section.append(makeElement(doc, 'h3', { text: 'このプロジェクトについて' }));
|
|
123
|
+
const facts = makeElement(doc, 'dl', { className: 'bb-pkw-overview-facts' });
|
|
124
|
+
for (const [label, value] of [
|
|
125
|
+
['何のプロジェクトか', about.definition],
|
|
126
|
+
['責任者', about.owner],
|
|
127
|
+
['最終決裁', about.finalApprover],
|
|
128
|
+
['現在の目標', about.goal],
|
|
129
|
+
]) {
|
|
130
|
+
const row = makeElement(doc, 'div', { className: 'bb-pkw-overview-fact' });
|
|
131
|
+
row.append(makeElement(doc, 'dt', { text: label }), makeElement(doc, 'dd', { text: value ?? '未確認' }));
|
|
132
|
+
facts.append(row);
|
|
133
|
+
}
|
|
134
|
+
section.append(facts);
|
|
135
|
+
const metadata = makeElement(doc, 'p', { className: 'bb-pkw-overview-meta' });
|
|
136
|
+
metadata.append(
|
|
137
|
+
makeElement(doc, 'span', { text: `出典: ${about.source ?? '未確認'}` }),
|
|
138
|
+
makeElement(doc, 'span', { text: `読み取った時点: ${displayDate(about.asOf)}` }),
|
|
139
|
+
);
|
|
140
|
+
section.append(metadata);
|
|
141
|
+
return section;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderChecks(doc, checks, actions = {}) {
|
|
145
|
+
const section = makeElement(doc, 'section', {
|
|
146
|
+
className: 'bb-pkw-overview-section bb-pkw-overview-checks',
|
|
147
|
+
attrs: { 'aria-label': '次に確認すること' },
|
|
148
|
+
});
|
|
149
|
+
section.append(makeElement(doc, 'h3', { text: '次に確認すること' }));
|
|
150
|
+
if (checks.length === 0) {
|
|
151
|
+
section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '確認事項は未確認です。' }));
|
|
152
|
+
return section;
|
|
153
|
+
}
|
|
154
|
+
const list = makeElement(doc, 'ol', { className: 'bb-pkw-overview-check-list' });
|
|
155
|
+
for (const check of checks) {
|
|
156
|
+
const item = makeElement(doc, 'li', { className: 'bb-pkw-overview-check' });
|
|
157
|
+
// `id` identifies the host-side check; it is not necessarily a Graph ID.
|
|
158
|
+
// Only an explicit canonical recordId may open the existing Graph detail.
|
|
159
|
+
const targetId = check.recordId;
|
|
160
|
+
const title = typeof actions.openGraphEntity === 'function' && targetId
|
|
161
|
+
? makeElement(doc, 'button', { className: 'bb-pkw-overview-link', text: check.title, attrs: { type: 'button', 'data-record-id': targetId } })
|
|
162
|
+
: makeElement(doc, 'strong', { text: check.title });
|
|
163
|
+
if (typeof actions.openGraphEntity === 'function' && targetId) title.addEventListener('click', () => actions.openGraphEntity(targetId));
|
|
164
|
+
item.append(title);
|
|
165
|
+
if (check.summary) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: check.summary }));
|
|
166
|
+
list.append(item);
|
|
167
|
+
}
|
|
168
|
+
section.append(list);
|
|
169
|
+
return section;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderMaterialState(doc, materials) {
|
|
173
|
+
const state = materials.state;
|
|
174
|
+
const total = materials.total;
|
|
175
|
+
let text;
|
|
176
|
+
if (state === 'ok') {
|
|
177
|
+
if (total === 0 && materials.items.length === 0) text = '確認済み 0件';
|
|
178
|
+
else if (total === null) text = materials.items.length > 0 ? `表示 ${materials.items.length}件 · 総数未確認` : '件数未確認';
|
|
179
|
+
else text = `表示 ${materials.items.length}件 · 総数 ${total}件${total > materials.items.length ? ' · 続き未取得' : ''}`;
|
|
180
|
+
} else if (total !== null) {
|
|
181
|
+
text = `表示 ${materials.items.length}件 · 総数 ${total}件 · ${stateLabel(state)}`;
|
|
182
|
+
} else {
|
|
183
|
+
text = materials.items.length > 0 ? `表示 ${materials.items.length}件 · 総数未確認 · ${stateLabel(state)}` : stateLabel(state);
|
|
184
|
+
}
|
|
185
|
+
return makeElement(doc, 'p', { className: 'bb-pkw-overview-material-state', text });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function materialNotice(doc, materials) {
|
|
189
|
+
if (materials.state === 'ok') return null;
|
|
190
|
+
const tone = materials.state === 'failed' ? 'danger' : materials.state === 'unknown' || materials.state === 'partial' ? 'warning' : 'info';
|
|
191
|
+
return workspaceNotice(doc, {
|
|
192
|
+
label: stateLabel(materials.state),
|
|
193
|
+
text: materials.note ?? (materials.state === 'partial' ? '一部の資料だけ取得されています。' : '資料の取得状態を確認できません。'),
|
|
194
|
+
tone,
|
|
195
|
+
role: materials.state === 'failed' ? 'alert' : 'status',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderMaterialTable(doc, materials, actions = {}) {
|
|
200
|
+
const table = makeElement(doc, 'div', {
|
|
201
|
+
className: 'bb-pkw-overview-material-table',
|
|
202
|
+
attrs: { role: 'table', 'aria-label': '資料一覧' },
|
|
203
|
+
});
|
|
204
|
+
const columns = ['資料', '関係', '出典', '本文', '内容時点', '記録更新'];
|
|
205
|
+
const header = makeElement(doc, 'div', { className: 'bb-pkw-overview-material-row bb-pkw-overview-material-header', attrs: { role: 'row' } });
|
|
206
|
+
for (const column of columns) header.append(makeElement(doc, 'span', { text: column, attrs: { role: 'columnheader' } }));
|
|
207
|
+
table.append(header);
|
|
208
|
+
if (materials.items.length === 0) {
|
|
209
|
+
const empty = materials.state === 'ok' && materials.total === 0
|
|
210
|
+
? '確認済みの資料はありません。'
|
|
211
|
+
: '資料の行は未確認です。';
|
|
212
|
+
table.append(makeElement(doc, 'p', { className: 'bb-pkw-muted bb-pkw-overview-material-empty', text: empty }));
|
|
213
|
+
return table;
|
|
214
|
+
}
|
|
215
|
+
for (const material of materials.items) {
|
|
216
|
+
const row = makeElement(doc, 'div', { className: 'bb-pkw-overview-material-row', attrs: { role: 'row' } });
|
|
217
|
+
const title = typeof actions.openGraphEntity === 'function' && material.id
|
|
218
|
+
? makeElement(doc, 'button', { className: 'bb-pkw-overview-link', text: material.title, attrs: { type: 'button', 'data-record-id': material.id } })
|
|
219
|
+
: makeElement(doc, 'strong', { text: material.title });
|
|
220
|
+
if (typeof actions.openGraphEntity === 'function' && material.id) title.addEventListener('click', () => actions.openGraphEntity(material.id));
|
|
221
|
+
row.append(
|
|
222
|
+
makeElement(doc, 'span', { attrs: { role: 'cell' } }),
|
|
223
|
+
makeElement(doc, 'span', { text: material.relation ?? '関係未確認', attrs: { role: 'cell' } }),
|
|
224
|
+
makeElement(doc, 'span', { text: material.source ?? '出典未確認', attrs: { role: 'cell' } }),
|
|
225
|
+
makeElement(doc, 'span', { text: BODY_STATE_LABELS[material.bodyState] ?? BODY_STATE_LABELS.unknown, attrs: { role: 'cell' } }),
|
|
226
|
+
makeElement(doc, 'time', { text: displayDate(material.contentAsOf), attrs: { role: 'cell', ...(material.contentAsOf ? { datetime: material.contentAsOf } : {}) } }),
|
|
227
|
+
makeElement(doc, 'time', { text: displayDate(material.updatedAt), attrs: { role: 'cell', ...(material.updatedAt ? { datetime: material.updatedAt } : {}) } }),
|
|
228
|
+
);
|
|
229
|
+
row.children[0].append(title);
|
|
230
|
+
table.append(row);
|
|
231
|
+
}
|
|
232
|
+
return table;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderMaterials(doc, title, materials, actions = {}) {
|
|
236
|
+
const section = makeElement(doc, 'section', {
|
|
237
|
+
className: 'bb-pkw-overview-section bb-pkw-overview-materials',
|
|
238
|
+
attrs: { 'aria-label': title },
|
|
239
|
+
});
|
|
240
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-overview-section-heading' });
|
|
241
|
+
heading.append(makeElement(doc, 'h3', { text: title }), renderMaterialState(doc, materials));
|
|
242
|
+
section.append(heading);
|
|
243
|
+
const notice = materialNotice(doc, materials);
|
|
244
|
+
if (notice) section.append(notice);
|
|
245
|
+
if (materials.note && materials.state === 'ok') section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: materials.note }));
|
|
246
|
+
section.append(renderMaterialTable(doc, materials, actions));
|
|
247
|
+
return section;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Render the host-provided overview in the required cognitive order. */
|
|
251
|
+
export function renderProjectOverview(doc, value, { projectName = 'プロジェクト', actions = {} } = {}) {
|
|
252
|
+
const overview = normalizeProjectOverview(value);
|
|
253
|
+
if (!overview) return null;
|
|
254
|
+
const name = cleanText(projectName) ?? 'プロジェクト';
|
|
255
|
+
const root = makeElement(doc, 'div', { className: 'bb-pkw-overview-projection', attrs: { 'aria-label': 'プロジェクト概要' } });
|
|
256
|
+
root.append(
|
|
257
|
+
renderAbout(doc, overview.about),
|
|
258
|
+
renderChecks(doc, overview.checks, actions),
|
|
259
|
+
renderMaterials(doc, `${name}に直接紐づく資料`, overview.directMaterials, actions),
|
|
260
|
+
renderMaterials(doc, '関連資料', overview.relatedMaterials, actions),
|
|
261
|
+
);
|
|
262
|
+
return root;
|
|
263
|
+
}
|
package/ui/project-workspace.css
CHANGED
|
@@ -1187,3 +1187,178 @@ button.bb-pkw-context-task-title:focus-visible { outline: 3px solid var(--bb-col
|
|
|
1187
1187
|
border-radius: 0;
|
|
1188
1188
|
padding: 14px 16px;
|
|
1189
1189
|
}
|
|
1190
|
+
|
|
1191
|
+
/* Optional host projection: context first, then the evidence ledgers. */
|
|
1192
|
+
.bb-pkw-overview-projection {
|
|
1193
|
+
display: grid;
|
|
1194
|
+
gap: var(--bb-space-5);
|
|
1195
|
+
min-width: 0;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.bb-pkw-overview-section {
|
|
1199
|
+
display: grid;
|
|
1200
|
+
gap: var(--bb-space-3);
|
|
1201
|
+
min-width: 0;
|
|
1202
|
+
border-top: 1px solid var(--bb-color-border-strong);
|
|
1203
|
+
padding-top: var(--bb-space-4);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.bb-pkw-overview-section h3 {
|
|
1207
|
+
margin: 0;
|
|
1208
|
+
color: var(--bb-color-ink);
|
|
1209
|
+
font-size: 16px;
|
|
1210
|
+
font-weight: 760;
|
|
1211
|
+
letter-spacing: -.02em;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.bb-pkw-overview-facts {
|
|
1215
|
+
display: grid;
|
|
1216
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1217
|
+
gap: var(--bb-space-3);
|
|
1218
|
+
margin: 0;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.bb-pkw-overview-fact {
|
|
1222
|
+
display: grid;
|
|
1223
|
+
gap: 4px;
|
|
1224
|
+
min-width: 0;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.bb-pkw-overview-fact dt,
|
|
1228
|
+
.bb-pkw-overview-fact dd {
|
|
1229
|
+
margin: 0;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.bb-pkw-overview-fact dt {
|
|
1233
|
+
color: var(--bb-color-muted);
|
|
1234
|
+
font-size: 11px;
|
|
1235
|
+
font-weight: 720;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.bb-pkw-overview-fact dd {
|
|
1239
|
+
overflow-wrap: anywhere;
|
|
1240
|
+
color: var(--bb-color-ink);
|
|
1241
|
+
font-size: 13px;
|
|
1242
|
+
line-height: 1.6;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
.bb-pkw-overview-meta {
|
|
1246
|
+
display: flex;
|
|
1247
|
+
flex-wrap: wrap;
|
|
1248
|
+
gap: 4px var(--bb-space-4);
|
|
1249
|
+
margin: 0;
|
|
1250
|
+
color: var(--bb-color-muted);
|
|
1251
|
+
font-size: 11px;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.bb-pkw-overview-check-list {
|
|
1255
|
+
display: grid;
|
|
1256
|
+
gap: 0;
|
|
1257
|
+
margin: 0;
|
|
1258
|
+
padding: 0 0 0 20px;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.bb-pkw-overview-check {
|
|
1262
|
+
min-width: 0;
|
|
1263
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
1264
|
+
padding: var(--bb-space-3) 0;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.bb-pkw-overview-check:first-child { padding-top: 0; }
|
|
1268
|
+
.bb-pkw-overview-check:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
1269
|
+
|
|
1270
|
+
.bb-pkw-overview-check p {
|
|
1271
|
+
margin: 4px 0 0;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.bb-pkw-overview-link {
|
|
1275
|
+
max-width: 100%;
|
|
1276
|
+
overflow-wrap: anywhere;
|
|
1277
|
+
border: 0;
|
|
1278
|
+
padding: 0;
|
|
1279
|
+
color: var(--bb-color-accent-strong);
|
|
1280
|
+
background: transparent;
|
|
1281
|
+
font: inherit;
|
|
1282
|
+
font-weight: 720;
|
|
1283
|
+
text-align: left;
|
|
1284
|
+
text-decoration: none;
|
|
1285
|
+
cursor: pointer;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.bb-pkw-overview-link:hover {
|
|
1289
|
+
color: var(--bb-color-accent);
|
|
1290
|
+
text-decoration: underline;
|
|
1291
|
+
text-underline-offset: 3px;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.bb-pkw-overview-link:focus-visible {
|
|
1295
|
+
outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 45%, var(--bb-color-panel));
|
|
1296
|
+
outline-offset: 2px;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.bb-pkw-overview-section-heading {
|
|
1300
|
+
display: flex;
|
|
1301
|
+
flex-wrap: wrap;
|
|
1302
|
+
gap: var(--bb-space-2);
|
|
1303
|
+
align-items: baseline;
|
|
1304
|
+
justify-content: space-between;
|
|
1305
|
+
min-width: 0;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.bb-pkw-overview-material-state {
|
|
1309
|
+
margin: 0;
|
|
1310
|
+
color: var(--bb-color-muted);
|
|
1311
|
+
font-size: 11px;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.bb-pkw-overview-material-table {
|
|
1315
|
+
min-width: 0;
|
|
1316
|
+
overflow-x: auto;
|
|
1317
|
+
border-top: 1px solid var(--bb-color-border-strong);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.bb-pkw-overview-material-row {
|
|
1321
|
+
display: grid;
|
|
1322
|
+
grid-template-columns: minmax(220px, 2fr) minmax(110px, 1fr) minmax(130px, 1.15fr) minmax(120px, 1fr) minmax(135px, 1fr) minmax(135px, 1fr);
|
|
1323
|
+
gap: var(--bb-space-2);
|
|
1324
|
+
align-items: center;
|
|
1325
|
+
min-width: 900px;
|
|
1326
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
1327
|
+
padding: var(--bb-space-3) 0;
|
|
1328
|
+
color: var(--bb-color-muted);
|
|
1329
|
+
font-size: 11px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.bb-pkw-overview-material-header {
|
|
1333
|
+
border-bottom-color: var(--bb-color-border-strong);
|
|
1334
|
+
padding-block: var(--bb-space-2);
|
|
1335
|
+
font-weight: 720;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.bb-pkw-overview-material-row > span,
|
|
1339
|
+
.bb-pkw-overview-material-row > time {
|
|
1340
|
+
min-width: 0;
|
|
1341
|
+
overflow-wrap: anywhere;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.bb-pkw-overview-material-row > span:first-child {
|
|
1345
|
+
color: var(--bb-color-ink);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.bb-pkw-overview-material-row:last-child { border-bottom: 0; }
|
|
1349
|
+
|
|
1350
|
+
.bb-pkw-overview-material-empty {
|
|
1351
|
+
margin: 0;
|
|
1352
|
+
padding: var(--bb-space-3) 0;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
@media (max-width: 760px) {
|
|
1356
|
+
.bb-pkw-overview-facts {
|
|
1357
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.bb-pkw-overview-section-heading {
|
|
1361
|
+
align-items: flex-start;
|
|
1362
|
+
flex-direction: column;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
package/ui/project-workspace.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
validityText,
|
|
26
26
|
} from './graph-view-shared.js';
|
|
27
27
|
import { workspaceDefinition, workspaceNotice } from './workspace-kit.js';
|
|
28
|
+
import { normalizeProjectOverview, renderProjectOverview } from './project-overview.js';
|
|
28
29
|
|
|
29
30
|
export const PROJECT_WORKSPACE_CONTRACT_VERSION = 'brainbase.project-workspace.v1';
|
|
30
31
|
|
|
@@ -243,6 +244,31 @@ function graphEvidenceEdges(detail) {
|
|
|
243
244
|
return projectKnowledgeEdges(detail);
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Keep overview-only canonical IDs available for the existing detail action.
|
|
249
|
+
* These are loading placeholders, not Graph entities or inferred relations;
|
|
250
|
+
* the host must still provide readEntity before a projection-only ID can be
|
|
251
|
+
* selected.
|
|
252
|
+
*/
|
|
253
|
+
function projectOverviewProjectionRecords(overview) {
|
|
254
|
+
const records = new Map();
|
|
255
|
+
if (!overview) return records;
|
|
256
|
+
const add = (id, title) => {
|
|
257
|
+
const target = cleanId(id);
|
|
258
|
+
if (!target || records.has(target)) return;
|
|
259
|
+
records.set(target, {
|
|
260
|
+
id: target,
|
|
261
|
+
type: 'unknown',
|
|
262
|
+
name: cleanName(title, target),
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
for (const check of overview.checks) add(check.recordId, check.title);
|
|
266
|
+
for (const material of [...overview.directMaterials.items, ...overview.relatedMaterials.items]) {
|
|
267
|
+
add(material.id, material.title);
|
|
268
|
+
}
|
|
269
|
+
return records;
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
function resultEntity(result) {
|
|
247
273
|
if (!isRecord(result)) return null;
|
|
248
274
|
const payload = result.state === 'ok' && isRecord(result.payload) ? result.payload : result;
|
|
@@ -966,6 +992,12 @@ function renderOverview(doc, detail, context, entities, actions = {}) {
|
|
|
966
992
|
const payload = projectDetailPayload(detail);
|
|
967
993
|
const project = payload?.project;
|
|
968
994
|
const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
|
|
995
|
+
const overview = normalizeProjectOverview(payload?.overview);
|
|
996
|
+
if (overview) {
|
|
997
|
+
panel.append(renderProjectOverview(doc, overview, { projectName: project?.name, actions }));
|
|
998
|
+
panel.append(renderContextDirectory(doc, context, actions));
|
|
999
|
+
return panel;
|
|
1000
|
+
}
|
|
969
1001
|
panel.append(renderDenseOverview(doc, detail, context, entities, actions));
|
|
970
1002
|
panel.append(renderContextDirectory(doc, context, actions));
|
|
971
1003
|
const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
|
|
@@ -1011,6 +1043,7 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1011
1043
|
// but those records are not evidence that they belong to this project.
|
|
1012
1044
|
const overviewEntities = graphEntityMap(payload);
|
|
1013
1045
|
const overviewEdges = graphEvidenceEdges(payload);
|
|
1046
|
+
const overviewProjectionRecords = projectOverviewProjectionRecords(normalizeProjectOverview(payload?.overview));
|
|
1014
1047
|
const entities = new Map(overviewEntities);
|
|
1015
1048
|
const edges = [...overviewEdges];
|
|
1016
1049
|
let destroyed = false;
|
|
@@ -1137,7 +1170,8 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1137
1170
|
|
|
1138
1171
|
function select(id, { fromGraph = false } = {}) {
|
|
1139
1172
|
const target = cleanId(id);
|
|
1140
|
-
|
|
1173
|
+
const canReadOverviewProjection = overviewProjectionRecords.has(target) && typeof readEntity === 'function';
|
|
1174
|
+
if (!target || (!entities.has(target) && !canReadOverviewProjection) || destroyed) return;
|
|
1141
1175
|
// A list click synchronizes Sigma by calling its `select`, which notifies
|
|
1142
1176
|
// this workspace again. Keep the second notification from starting a
|
|
1143
1177
|
// duplicate entity read (or resetting a completed detail read).
|
|
@@ -1155,7 +1189,8 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1155
1189
|
|
|
1156
1190
|
function selectAndExplore(id) {
|
|
1157
1191
|
const target = cleanId(id);
|
|
1158
|
-
|
|
1192
|
+
const canReadOverviewProjection = overviewProjectionRecords.has(target) && typeof readEntity === 'function';
|
|
1193
|
+
if (!target || (!entities.has(target) && !canReadOverviewProjection) || destroyed) return;
|
|
1159
1194
|
activeTab = 'graph';
|
|
1160
1195
|
select(target);
|
|
1161
1196
|
render();
|
|
@@ -1201,7 +1236,7 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1201
1236
|
const panel = root.querySelector?.('.bb-pkw-selection') ?? selectedPanel;
|
|
1202
1237
|
if (!panel) return;
|
|
1203
1238
|
panel.replaceChildren();
|
|
1204
|
-
const entity = entities.get(selectedId);
|
|
1239
|
+
const entity = entities.get(selectedId) ?? overviewProjectionRecords.get(selectedId);
|
|
1205
1240
|
if (!entity) return;
|
|
1206
1241
|
const read = entityRead?.id === selectedId ? entityRead : null;
|
|
1207
1242
|
const fetched = selectedId === project.id ? { entity: project, incoming: [], outgoing: [] } : read?.entity ? read : null;
|