@unson/brainbase-mcp 0.41.3 → 0.41.4
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/judgment-history-http.js +52 -11
- package/dist/judgment-history.d.ts +70 -2
- package/dist/judgment-history.js +381 -122
- package/package.json +2 -2
- package/ui/judgment-history.css +215 -22
- package/ui/judgment-history.js +398 -104
package/ui/judgment-history.js
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
workspaceDefinition,
|
|
15
15
|
workspaceDetailEmpty,
|
|
16
16
|
workspaceLedger,
|
|
17
|
-
workspaceNotice,
|
|
18
17
|
workspacePageHeader,
|
|
19
18
|
workspaceRailBlock,
|
|
20
19
|
workspaceRailHead,
|
|
@@ -198,7 +197,7 @@ function normalizeNormalReference(value) {
|
|
|
198
197
|
why: text(value.why) || null,
|
|
199
198
|
usage: text(value.usage) || null,
|
|
200
199
|
availability: text(value.availability).toLowerCase() || 'unknown',
|
|
201
|
-
label: text(value.label ?? value.name) ||
|
|
200
|
+
label: text(value.label ?? value.name) || null,
|
|
202
201
|
};
|
|
203
202
|
}
|
|
204
203
|
|
|
@@ -806,23 +805,50 @@ function formatDate(value) {
|
|
|
806
805
|
}
|
|
807
806
|
}
|
|
808
807
|
|
|
808
|
+
function referenceLabel(ref) {
|
|
809
|
+
const label = text(ref?.label);
|
|
810
|
+
const id = text(ref?.id ?? ref?.ref);
|
|
811
|
+
if (!label || (id && label === id)) return '';
|
|
812
|
+
return label;
|
|
813
|
+
}
|
|
814
|
+
|
|
809
815
|
function makeReferenceSummary(doc, references) {
|
|
810
816
|
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
|
|
811
817
|
if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
|
|
818
|
+
const labeled = [];
|
|
819
|
+
let unlabeled = 0;
|
|
820
|
+
for (const ref of references) {
|
|
821
|
+
const label = referenceLabel(ref);
|
|
822
|
+
if (label) labeled.push(label);
|
|
823
|
+
else unlabeled += 1;
|
|
824
|
+
}
|
|
812
825
|
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-summary' });
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
826
|
+
if (labeled.length) {
|
|
827
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: labeled.slice(0, 2).join('、') }));
|
|
828
|
+
if (labeled.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${labeled.length - 2}件` }));
|
|
829
|
+
}
|
|
830
|
+
if (unlabeled > 0) wrap.append(makeWorkspaceElement(doc, 'strong', { text: `名前のない参照 ${unlabeled}件` }));
|
|
816
831
|
return wrap;
|
|
817
832
|
}
|
|
818
833
|
|
|
834
|
+
function judgmentStatusLabel(status) {
|
|
835
|
+
const labels = {
|
|
836
|
+
resolved: '解決済み',
|
|
837
|
+
needs_clarification: '確認が必要',
|
|
838
|
+
needs_classification: '分類待ち',
|
|
839
|
+
needs_policy_resolution: '方針の確認待ち',
|
|
840
|
+
unknown: '状態は未記録',
|
|
841
|
+
};
|
|
842
|
+
const key = text(status);
|
|
843
|
+
if (!key) return '状態は未記録';
|
|
844
|
+
return labels[key] ?? '状態は未記録';
|
|
845
|
+
}
|
|
846
|
+
|
|
819
847
|
function makeDecisionSummary(doc, row) {
|
|
820
848
|
if (row.mode === 'normal') {
|
|
821
849
|
const summary = text(row.judgment?.summary);
|
|
822
|
-
const status = text(row.judgment?.status);
|
|
823
850
|
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
|
|
824
851
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
|
|
825
|
-
wrap.append(makeWorkspaceElement(doc, 'small', { text: `${NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明'}${status ? `・${status}` : ''}` }));
|
|
826
852
|
return wrap;
|
|
827
853
|
}
|
|
828
854
|
const summary = text(row.proof?.decision?.summary);
|
|
@@ -833,19 +859,28 @@ function makeDecisionSummary(doc, row) {
|
|
|
833
859
|
return wrap;
|
|
834
860
|
}
|
|
835
861
|
|
|
862
|
+
const REFERENCE_AVAILABILITY_LABELS = Object.freeze({
|
|
863
|
+
recorded: '今の権限で開けます',
|
|
864
|
+
permission_denied: '開く権限がありません',
|
|
865
|
+
unavailable: '本文はもうありません',
|
|
866
|
+
unknown: '未確認',
|
|
867
|
+
});
|
|
868
|
+
|
|
836
869
|
function appendReferenceList(doc, references) {
|
|
837
870
|
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
|
|
838
871
|
if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
|
|
839
872
|
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
|
|
840
873
|
for (const ref of references) {
|
|
841
874
|
const item = makeWorkspaceElement(doc, 'li');
|
|
842
|
-
|
|
843
|
-
item.append(makeWorkspaceElement(doc, '
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
|
|
875
|
+
const label = referenceLabel(ref);
|
|
876
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: label || '名前のない参照' }));
|
|
877
|
+
const recordId = text(ref.ref ?? ref.id);
|
|
878
|
+
if (recordId) {
|
|
879
|
+
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-record-id', text: `記録ID:${recordId}(コピー)` }));
|
|
848
880
|
}
|
|
881
|
+
if (ref.application && ref.application !== ref.label && ref.application !== label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
|
|
882
|
+
const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)] ?? REFERENCE_AVAILABILITY_LABELS.unknown;
|
|
883
|
+
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
|
|
849
884
|
if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
|
|
850
885
|
if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
|
|
851
886
|
const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
|
|
@@ -1147,7 +1182,9 @@ function renderRail(doc, rail, row, detail, onClose, options = {}) {
|
|
|
1147
1182
|
rail.append(workspaceDetailEmpty(doc, {
|
|
1148
1183
|
mark: '↗',
|
|
1149
1184
|
title: '履歴を選択',
|
|
1150
|
-
text:
|
|
1185
|
+
text: options.loading
|
|
1186
|
+
? '行を選ぶと、使った参照と行った判断を確認できます。読み込み中の行も、届いたものから選べます。'
|
|
1187
|
+
: '行を選ぶと、使った参照と行った判断を確認できます。',
|
|
1151
1188
|
}));
|
|
1152
1189
|
return;
|
|
1153
1190
|
}
|
|
@@ -1189,43 +1226,129 @@ function safeDocument(explicit) {
|
|
|
1189
1226
|
return doc;
|
|
1190
1227
|
}
|
|
1191
1228
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
if (home?.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1229
|
+
const RESPONSE_ERROR_CODES = new Set([
|
|
1230
|
+
'response_not_object', 'records_missing', 'sections_missing', 'unsupported_status',
|
|
1231
|
+
'coverage_incomplete', 'record_rejected', 'record_invalid',
|
|
1232
|
+
]);
|
|
1233
|
+
|
|
1234
|
+
function publicErrorText(error) {
|
|
1235
|
+
const code = text(error);
|
|
1236
|
+
if (!code) return '';
|
|
1237
|
+
if (RESPONSE_ERROR_CODES.has(code)) return '応答を読み取れませんでした';
|
|
1238
|
+
return '';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function prefersReducedMotion() {
|
|
1242
|
+
try {
|
|
1243
|
+
return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function sourceDetail(doc, home) {
|
|
1250
|
+
const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-source-detail' });
|
|
1251
|
+
details.append(makeWorkspaceElement(doc, 'summary', { text: '取得元の詳細' }));
|
|
1252
|
+
const body = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-source-detail-body' });
|
|
1253
|
+
const storage = home?.coverage?.storage;
|
|
1254
|
+
if (storage) body.append(makeWorkspaceElement(doc, 'p', { text: `保存先: ${normalStorageLabel(storage)}` }));
|
|
1255
|
+
const sources = Array.isArray(home?.coverage?.sources) ? home.coverage.sources : [];
|
|
1256
|
+
for (const source of sources) {
|
|
1257
|
+
if (!source?.reason) continue;
|
|
1258
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${source.reason}` }));
|
|
1259
|
+
}
|
|
1260
|
+
if (home?.coverage?.reason && !sources.some((source) => source?.reason === home.coverage.reason)) {
|
|
1261
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${home.coverage.reason}` }));
|
|
1262
|
+
}
|
|
1263
|
+
const rejected = home?.coverage?.rejected;
|
|
1264
|
+
if (Number.isInteger(rejected) && rejected > 0) {
|
|
1265
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `読めなかった件数: ${rejected}` }));
|
|
1266
|
+
}
|
|
1267
|
+
if (text(home?.coverage?.reason) === 'journal_scan_capped') {
|
|
1268
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: '記録が多いため、新しい順に上限まで読みました。古い判断が抜けている可能性があります。' }));
|
|
1269
|
+
}
|
|
1270
|
+
details.append(body);
|
|
1271
|
+
return details;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function sourceChips(doc, sources) {
|
|
1275
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-banner-sources' });
|
|
1276
|
+
for (const source of sources) wrap.append(makeWorkspaceElement(doc, 'span', { text: sourceChipText(source) }));
|
|
1277
|
+
return wrap;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function historyBanner(doc, { label, tone, role, sources, home, note, reload }) {
|
|
1281
|
+
const banner = makeWorkspaceElement(doc, 'div', {
|
|
1282
|
+
className: `bb-jh-banner is-${tone}`,
|
|
1283
|
+
attrs: { role: role ?? 'status' },
|
|
1228
1284
|
});
|
|
1285
|
+
banner.append(makeWorkspaceElement(doc, 'strong', { text: label }));
|
|
1286
|
+
if (sources?.length) banner.append(sourceChips(doc, sources));
|
|
1287
|
+
if (note) banner.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-banner-note', text: note }));
|
|
1288
|
+
if (home) banner.append(sourceDetail(doc, home));
|
|
1289
|
+
if (reload) banner.append(reload);
|
|
1290
|
+
return banner;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow = false, arrived = null } = {}) {
|
|
1294
|
+
const reload = workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry, attrs: { 'data-action': 'reload' } });
|
|
1295
|
+
if (phase === 'error' || home?.status === 'error' || home?.status === 'invalid') {
|
|
1296
|
+
const detail = publicErrorText(error);
|
|
1297
|
+
return historyBanner(doc, {
|
|
1298
|
+
label: '取得できません',
|
|
1299
|
+
tone: 'danger',
|
|
1300
|
+
role: 'alert',
|
|
1301
|
+
note: `判断履歴を取得できませんでした。データが0件とは確認できていません。${detail ? ` ${detail}` : ''}`,
|
|
1302
|
+
home: error ? { coverage: { reason: text(error) } } : home,
|
|
1303
|
+
reload,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
if (home?.status === 'unavailable') {
|
|
1307
|
+
return historyBanner(doc, {
|
|
1308
|
+
label: '取得できません',
|
|
1309
|
+
tone: 'danger',
|
|
1310
|
+
role: 'alert',
|
|
1311
|
+
sources: home.coverage?.sources,
|
|
1312
|
+
home,
|
|
1313
|
+
note: '判断の記録に接続できません。データが0件とは確認できていません。',
|
|
1314
|
+
reload,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (loading) {
|
|
1318
|
+
const progressHome = arrived ?? home;
|
|
1319
|
+
const counts = new Map();
|
|
1320
|
+
for (const record of progressHome?.records ?? []) {
|
|
1321
|
+
const key = normalEntrypoint(record.entrypoint);
|
|
1322
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1323
|
+
}
|
|
1324
|
+
const known = Array.isArray(progressHome?.coverage?.sources) && progressHome.coverage.sources.length
|
|
1325
|
+
? progressHome.coverage.sources
|
|
1326
|
+
: NORMAL_ENTRYPOINTS.filter((entrypoint) => entrypoint !== 'unknown').map((entrypoint) => ({ entrypoint, status: 'pending', reason: null }));
|
|
1327
|
+
const sources = known.map((source) => {
|
|
1328
|
+
const count = counts.get(normalEntrypoint(source.entrypoint)) ?? 0;
|
|
1329
|
+
if (count > 0) return { entrypoint: source.entrypoint, status: 'partial', retrieved: count, reason: null };
|
|
1330
|
+
if (source.status === 'unavailable') return { entrypoint: source.entrypoint, status: 'unavailable', reason: source.reason ?? null };
|
|
1331
|
+
return { entrypoint: source.entrypoint, status: 'pending', reason: null };
|
|
1332
|
+
});
|
|
1333
|
+
return historyBanner(doc, {
|
|
1334
|
+
label: '読み込み中',
|
|
1335
|
+
tone: 'info',
|
|
1336
|
+
role: 'status',
|
|
1337
|
+
sources,
|
|
1338
|
+
note: slow
|
|
1339
|
+
? '時間がかかっています。届いた分だけ先に見られます。'
|
|
1340
|
+
: '取得元ごとに届いた順に表示します',
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (home?.status === 'partial') {
|
|
1344
|
+
return historyBanner(doc, {
|
|
1345
|
+
label: '一部の記録だけを表示中',
|
|
1346
|
+
tone: 'warning',
|
|
1347
|
+
role: 'status',
|
|
1348
|
+
sources: home.coverage?.sources,
|
|
1349
|
+
home,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1229
1352
|
return null;
|
|
1230
1353
|
}
|
|
1231
1354
|
|
|
@@ -1255,38 +1378,51 @@ function createSearch(doc, value, onInput) {
|
|
|
1255
1378
|
}
|
|
1256
1379
|
|
|
1257
1380
|
function normalStorageLabel(storage) {
|
|
1258
|
-
if (storage === 'local') return '
|
|
1259
|
-
if (storage === 'server') return '
|
|
1260
|
-
return
|
|
1381
|
+
if (storage === 'local') return 'この端末';
|
|
1382
|
+
if (storage === 'server') return '組織のサーバー';
|
|
1383
|
+
return '接続先不明';
|
|
1261
1384
|
}
|
|
1262
1385
|
|
|
1263
1386
|
function normalReasonLabel(reason) {
|
|
1264
1387
|
const labels = {
|
|
1388
|
+
journal_scan_capped: '読み込み上限まで',
|
|
1389
|
+
source_not_connected: '未接続',
|
|
1265
1390
|
unconnected: '未接続',
|
|
1266
|
-
mana_not_connected: '
|
|
1267
|
-
company_os_native_source_not_connected: '
|
|
1268
|
-
journal_unavailable: '
|
|
1269
|
-
journal_unreadable: '
|
|
1391
|
+
mana_not_connected: '未接続',
|
|
1392
|
+
company_os_native_source_not_connected: '実行履歴は未接続',
|
|
1393
|
+
journal_unavailable: '判断の記録が見つかりません',
|
|
1394
|
+
journal_unreadable: '判断の記録を読み取れません',
|
|
1395
|
+
some_journal_records_unreadable: '一部の記録を読み取れません',
|
|
1396
|
+
project_scope_unverified: 'プロジェクトの範囲を確かめられていません',
|
|
1397
|
+
project_scope_forbidden: 'このプロジェクトを見る権限がありません',
|
|
1398
|
+
journal_not_directory: '記録の保存場所が正しくありません',
|
|
1399
|
+
journal_symlink_rejected: '記録の保存場所が正しくありません',
|
|
1400
|
+
permission_denied: '見る権限がありません',
|
|
1401
|
+
judgment_history_partial: '一部だけ取得',
|
|
1402
|
+
judgment_history_unavailable: '取得できません',
|
|
1403
|
+
judgment_journal_unavailable: '判断の記録に接続できません',
|
|
1270
1404
|
};
|
|
1271
|
-
|
|
1405
|
+
if (!reason) return '';
|
|
1406
|
+
return labels[reason] ?? '理由を確かめられません';
|
|
1272
1407
|
}
|
|
1273
1408
|
|
|
1274
1409
|
function normalSourceStatusLabel(status) {
|
|
1275
1410
|
const labels = {
|
|
1276
|
-
available: '
|
|
1277
|
-
partial: '
|
|
1411
|
+
available: '取得済み',
|
|
1412
|
+
partial: '一部',
|
|
1278
1413
|
unavailable: '未接続',
|
|
1279
1414
|
};
|
|
1280
1415
|
return labels[status] ?? '未確認';
|
|
1281
1416
|
}
|
|
1282
1417
|
|
|
1283
|
-
function
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1418
|
+
function sourceChipText(source) {
|
|
1419
|
+
const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
|
|
1420
|
+
if (source?.status === 'pending' || source?.status === 'loading') return `${name}:確認中…`;
|
|
1421
|
+
if (Number.isInteger(source?.retrieved)) return `${name}:${source.retrieved}件まで届きました`;
|
|
1422
|
+
const statusLabel = normalSourceStatusLabel(source.status);
|
|
1423
|
+
const reasonLabel = source.reason ? normalReasonLabel(source.reason) : '';
|
|
1424
|
+
const reason = reasonLabel && reasonLabel !== statusLabel ? `(${reasonLabel})` : '';
|
|
1425
|
+
return `${name}:${statusLabel}${reason}`;
|
|
1290
1426
|
}
|
|
1291
1427
|
|
|
1292
1428
|
function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
|
|
@@ -1309,12 +1445,12 @@ function createFilterSelect(doc, { label, value, options, onChange, className =
|
|
|
1309
1445
|
function normalFilterControls(doc, home, state, onChange) {
|
|
1310
1446
|
const filters = home?.filters ?? {};
|
|
1311
1447
|
const projects = [...(filters.projects ?? [])];
|
|
1312
|
-
const knownProjects = (home
|
|
1448
|
+
const knownProjects = (home?.records ?? []).map((record) => record.projectCode).filter(Boolean);
|
|
1313
1449
|
for (const project of knownProjects) {
|
|
1314
1450
|
if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
|
|
1315
1451
|
}
|
|
1316
1452
|
const entrypoints = [...(filters.entrypoints ?? [])];
|
|
1317
|
-
const knownEntrypoints = (home
|
|
1453
|
+
const knownEntrypoints = (home?.records ?? []).map((record) => record.entrypoint).filter(Boolean);
|
|
1318
1454
|
for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
|
|
1319
1455
|
if (!entrypoints.some((option) => option.value === entrypoint)) {
|
|
1320
1456
|
entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
|
|
@@ -1350,6 +1486,71 @@ function paginationControls(doc, aggregate, onNext) {
|
|
|
1350
1486
|
return wrap;
|
|
1351
1487
|
}
|
|
1352
1488
|
|
|
1489
|
+
function makeEntrypointStatus(doc, row) {
|
|
1490
|
+
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-entrypoint' });
|
|
1491
|
+
if (row.mode !== 'normal') {
|
|
1492
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: row.delegated ? 'Brainbase' : '本人' }));
|
|
1493
|
+
return wrap;
|
|
1494
|
+
}
|
|
1495
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明' }));
|
|
1496
|
+
wrap.append(makeWorkspaceElement(doc, 'small', { text: judgmentStatusLabel(row.judgment?.status) }));
|
|
1497
|
+
return wrap;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function skeletonRows(doc, count = 5) {
|
|
1501
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
1502
|
+
key: `skeleton-${index}`,
|
|
1503
|
+
className: 'bb-jh-skeleton-row',
|
|
1504
|
+
cells: [
|
|
1505
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-date' }),
|
|
1506
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-decision' }),
|
|
1507
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-reference' }),
|
|
1508
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-status' }),
|
|
1509
|
+
],
|
|
1510
|
+
}));
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function summaryLine(doc, aggregate, { loading = false } = {}) {
|
|
1514
|
+
const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
|
|
1515
|
+
const retrieved = Number.isInteger(aggregate?.retrievedCount) ? aggregate.retrievedCount : (aggregate?.rows?.length ?? 0);
|
|
1516
|
+
const complete = aggregate?.countComplete === true && Number.isInteger(aggregate?.totalCount);
|
|
1517
|
+
const showCount = complete || retrieved > 0;
|
|
1518
|
+
if (showCount) {
|
|
1519
|
+
line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
|
|
1520
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
|
|
1521
|
+
} else {
|
|
1522
|
+
line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1523
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
|
|
1524
|
+
}
|
|
1525
|
+
if (!complete) {
|
|
1526
|
+
line.append(makeWorkspaceElement(doc, 'span', {
|
|
1527
|
+
className: 'bb-jh-summary-note',
|
|
1528
|
+
text: loading ? '(読み込み中・全件数は未確認)' : '(全件数は未確認)',
|
|
1529
|
+
}));
|
|
1530
|
+
}
|
|
1531
|
+
const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
|
|
1532
|
+
const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
|
|
1533
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
|
|
1534
|
+
if (Number.isInteger(references)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${references}件` }));
|
|
1535
|
+
else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1536
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確認済み ' }));
|
|
1537
|
+
if (Number.isInteger(confirmed)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${confirmed}件` }));
|
|
1538
|
+
else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1539
|
+
return line;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function formatCheckedAt(value) {
|
|
1543
|
+
const date = new Date(value);
|
|
1544
|
+
if (!Number.isFinite(date.getTime())) return '';
|
|
1545
|
+
try {
|
|
1546
|
+
return new Intl.DateTimeFormat('ja-JP', {
|
|
1547
|
+
timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit', hour12: false,
|
|
1548
|
+
}).format(date);
|
|
1549
|
+
} catch {
|
|
1550
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1353
1554
|
function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
1354
1555
|
return aggregate.rows.map((row) => ({
|
|
1355
1556
|
key: row.key,
|
|
@@ -1359,8 +1560,9 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
|
1359
1560
|
onSelect,
|
|
1360
1561
|
cells: [
|
|
1361
1562
|
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
|
|
1362
|
-
makeReferenceSummary(doc, row.references),
|
|
1363
1563
|
makeDecisionSummary(doc, row),
|
|
1564
|
+
makeReferenceSummary(doc, row.references),
|
|
1565
|
+
makeEntrypointStatus(doc, row),
|
|
1364
1566
|
],
|
|
1365
1567
|
}));
|
|
1366
1568
|
}
|
|
@@ -1377,6 +1579,8 @@ export function createJudgmentHistoryUI({
|
|
|
1377
1579
|
autoLoad = true,
|
|
1378
1580
|
pageHeader = true,
|
|
1379
1581
|
now = new Date(),
|
|
1582
|
+
skeletonDelayMs = 300,
|
|
1583
|
+
slowDelayMs = 10000,
|
|
1380
1584
|
} = {}) {
|
|
1381
1585
|
if (!root) throw new TypeError('root is required');
|
|
1382
1586
|
const doc = safeDocument(explicitDocument);
|
|
@@ -1409,8 +1613,51 @@ export function createJudgmentHistoryUI({
|
|
|
1409
1613
|
focusSearch: false,
|
|
1410
1614
|
lastFocusedKey: null,
|
|
1411
1615
|
pendingDecisionId: null,
|
|
1616
|
+
showSkeleton: false,
|
|
1617
|
+
slow: false,
|
|
1618
|
+
arrived: null,
|
|
1619
|
+
checkedAt: null,
|
|
1620
|
+
skeletonTimer: null,
|
|
1621
|
+
slowTimer: null,
|
|
1412
1622
|
};
|
|
1413
1623
|
|
|
1624
|
+
function clearLoadTimers() {
|
|
1625
|
+
if (state.skeletonTimer !== null) {
|
|
1626
|
+
clearTimeout(state.skeletonTimer);
|
|
1627
|
+
state.skeletonTimer = null;
|
|
1628
|
+
}
|
|
1629
|
+
if (state.slowTimer !== null) {
|
|
1630
|
+
clearTimeout(state.slowTimer);
|
|
1631
|
+
state.slowTimer = null;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function scheduleLoadTimers(generation) {
|
|
1636
|
+
clearLoadTimers();
|
|
1637
|
+
const skeletonDelay = Number.isFinite(skeletonDelayMs) ? Math.max(0, skeletonDelayMs) : 300;
|
|
1638
|
+
const slowDelay = Number.isFinite(slowDelayMs) ? Math.max(0, slowDelayMs) : 10000;
|
|
1639
|
+
const stillLoading = () => generation === state.generation && !state.disposed && state.phase === 'loading';
|
|
1640
|
+
if (skeletonDelay === 0) state.showSkeleton = true;
|
|
1641
|
+
else {
|
|
1642
|
+
state.skeletonTimer = setTimeout(() => {
|
|
1643
|
+
if (!stillLoading()) return;
|
|
1644
|
+
state.showSkeleton = true;
|
|
1645
|
+
render();
|
|
1646
|
+
}, skeletonDelay);
|
|
1647
|
+
}
|
|
1648
|
+
if (slowDelay === 0) {
|
|
1649
|
+
state.slow = true;
|
|
1650
|
+
state.showSkeleton = true;
|
|
1651
|
+
} else {
|
|
1652
|
+
state.slowTimer = setTimeout(() => {
|
|
1653
|
+
if (!stillLoading()) return;
|
|
1654
|
+
state.slow = true;
|
|
1655
|
+
state.showSkeleton = true;
|
|
1656
|
+
render();
|
|
1657
|
+
}, slowDelay);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1414
1661
|
function closeSelected() {
|
|
1415
1662
|
if (!state.selectedKey || state.disposed) return false;
|
|
1416
1663
|
state.lastFocusedKey = state.selectedKey;
|
|
@@ -1661,28 +1908,43 @@ export function createJudgmentHistoryUI({
|
|
|
1661
1908
|
|
|
1662
1909
|
function render() {
|
|
1663
1910
|
if (state.disposed) return;
|
|
1911
|
+
if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) return;
|
|
1664
1912
|
const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
|
|
1665
1913
|
const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
|
|
1666
1914
|
? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
|
|
1667
1915
|
: null;
|
|
1668
1916
|
const previousSearchFocused = previousSearchInput && doc.activeElement === previousSearchInput;
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1917
|
+
const loading = state.phase === 'loading';
|
|
1918
|
+
const sourceHome = loading ? state.arrived : state.home;
|
|
1919
|
+
const aggregate = loading
|
|
1920
|
+
? (sourceHome
|
|
1921
|
+
? aggregateJudgmentHistory(sourceHome, { now: readNow(), period: state.period, query: state.query })
|
|
1922
|
+
: { status: 'partial', rows: [], visibleCount: 0, stats: {}, coverage: null, countComplete: false, retrievedCount: 0, mode: 'normal' })
|
|
1923
|
+
: (state.aggregate ?? {
|
|
1924
|
+
status: state.home?.status ?? 'unavailable', rows: [], visibleCount: 0, stats: {}, coverage: state.home?.coverage ?? null,
|
|
1925
|
+
});
|
|
1926
|
+
const wrapper = makeWorkspaceElement(doc, 'div', {
|
|
1927
|
+
className: `bb-jh${prefersReducedMotion() ? ' is-reduced-motion' : ''}`,
|
|
1928
|
+
attrs: { 'data-contract-version': JUDGMENT_HISTORY_UI_CONTRACT_VERSION, 'aria-busy': loading ? 'true' : 'false' },
|
|
1929
|
+
});
|
|
1673
1930
|
if (pageHeader) {
|
|
1674
1931
|
const context = currentPage();
|
|
1675
1932
|
wrapper.append(workspacePageHeader(doc, {
|
|
1676
1933
|
crumbs: context.crumbs,
|
|
1677
1934
|
title: '判断の履歴',
|
|
1678
|
-
lead: 'Brainbase
|
|
1935
|
+
lead: '参照した内容と、Brainbaseが代わりに行った判断を見返す',
|
|
1679
1936
|
source: context.source,
|
|
1680
1937
|
}));
|
|
1681
1938
|
}
|
|
1682
|
-
const notice = statusNotice(doc, state.phase, state.error, state.home, () => void load()
|
|
1939
|
+
const notice = statusNotice(doc, state.phase, state.error, loading ? (state.arrived ?? state.home) : state.home, () => void load(), {
|
|
1940
|
+
loading,
|
|
1941
|
+
slow: state.slow,
|
|
1942
|
+
arrived: state.arrived,
|
|
1943
|
+
});
|
|
1683
1944
|
if (notice) wrapper.append(notice);
|
|
1684
|
-
|
|
1685
|
-
|
|
1945
|
+
const failed = state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid';
|
|
1946
|
+
if (failed) {
|
|
1947
|
+
wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
|
|
1686
1948
|
root.replaceChildren(wrapper);
|
|
1687
1949
|
renderRail(doc, rail, null, null);
|
|
1688
1950
|
return;
|
|
@@ -1692,53 +1954,63 @@ export function createJudgmentHistoryUI({
|
|
|
1692
1954
|
state.period = period;
|
|
1693
1955
|
state.selectedKey = null;
|
|
1694
1956
|
state.cursor = null;
|
|
1695
|
-
if (normalMode()) void load({ append: false });
|
|
1957
|
+
if (loading || normalMode()) void load({ append: false });
|
|
1696
1958
|
else {
|
|
1697
1959
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1698
1960
|
render();
|
|
1699
1961
|
}
|
|
1700
1962
|
}));
|
|
1701
|
-
|
|
1963
|
+
const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
|
|
1964
|
+
if (showNormalFilters) controls.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
|
|
1702
1965
|
controls.append(createSearch(doc, state.query, (query) => {
|
|
1703
1966
|
state.query = String(query);
|
|
1704
1967
|
state.focusSearch = true;
|
|
1705
1968
|
state.selectedKey = null;
|
|
1706
|
-
|
|
1969
|
+
const searchable = loading ? sourceHome : state.home;
|
|
1970
|
+
if (searchable) state.aggregate = aggregateJudgmentHistory(searchable, { now: readNow(), period: state.period, query: state.query });
|
|
1707
1971
|
render();
|
|
1708
1972
|
}));
|
|
1709
|
-
|
|
1973
|
+
const toolbarStatus = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-toolbar-status' });
|
|
1974
|
+
if (loading) {
|
|
1975
|
+
toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
|
|
1976
|
+
className: 'bb-jh-checked',
|
|
1977
|
+
text: '確認しています…',
|
|
1978
|
+
attrs: { role: 'status' },
|
|
1979
|
+
}));
|
|
1980
|
+
} else if (state.checkedAt) {
|
|
1981
|
+
const stamp = formatCheckedAt(state.checkedAt);
|
|
1982
|
+
if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-checked', text: `${stamp}に確認` }));
|
|
1983
|
+
}
|
|
1984
|
+
toolbarStatus.append(workspaceButton(doc, {
|
|
1710
1985
|
text: '再読み込み',
|
|
1711
1986
|
variant: 'quiet',
|
|
1987
|
+
disabled: loading,
|
|
1712
1988
|
onClick: () => void load(),
|
|
1713
1989
|
attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
|
|
1714
1990
|
}));
|
|
1991
|
+
controls.append(toolbarStatus);
|
|
1715
1992
|
wrapper.append(controls);
|
|
1716
|
-
|
|
1717
|
-
wrapper.append(makeWorkspaceElement(doc, 'p', {
|
|
1718
|
-
className: 'bb-jh-reference-note',
|
|
1719
|
-
text: normalMode()
|
|
1720
|
-
? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
|
|
1721
|
-
: '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
|
|
1722
|
-
}));
|
|
1993
|
+
wrapper.append(summaryLine(doc, aggregate, { loading }));
|
|
1723
1994
|
const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
|
|
1724
1995
|
? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
|
|
1725
1996
|
: 'この期間の判断履歴はありません。';
|
|
1997
|
+
const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
|
|
1998
|
+
state.lastFocusedKey = key;
|
|
1999
|
+
state.selectedKey = key;
|
|
2000
|
+
state.detailPhase = 'idle';
|
|
2001
|
+
state.detailError = null;
|
|
2002
|
+
render();
|
|
2003
|
+
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
2004
|
+
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
2005
|
+
});
|
|
1726
2006
|
wrapper.append(workspaceLedger(doc, {
|
|
1727
2007
|
className: 'bb-jh-ledger',
|
|
1728
2008
|
ariaLabel: '判断履歴',
|
|
1729
|
-
columns: ['日時', '使った参照', '
|
|
1730
|
-
rows:
|
|
1731
|
-
state.lastFocusedKey = key;
|
|
1732
|
-
state.selectedKey = key;
|
|
1733
|
-
state.detailPhase = 'idle';
|
|
1734
|
-
state.detailError = null;
|
|
1735
|
-
render();
|
|
1736
|
-
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
1737
|
-
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
1738
|
-
}),
|
|
2009
|
+
columns: ['日時', '行った判断', '使った参照', '入口・状態'],
|
|
2010
|
+
rows: loading ? [...dataRows, ...skeletonRows(doc)] : dataRows,
|
|
1739
2011
|
empty: emptyText,
|
|
1740
2012
|
}));
|
|
1741
|
-
const pagination = paginationControls(doc, aggregate, () => {
|
|
2013
|
+
const pagination = loading ? null : paginationControls(doc, aggregate, () => {
|
|
1742
2014
|
if (aggregate.pagination?.nextCursor) {
|
|
1743
2015
|
state.cursor = aggregate.pagination.nextCursor;
|
|
1744
2016
|
void load({ cursor: state.cursor, append: true });
|
|
@@ -1776,7 +2048,7 @@ export function createJudgmentHistoryUI({
|
|
|
1776
2048
|
onCopyReference: (payload) => void copyReference(payload),
|
|
1777
2049
|
}
|
|
1778
2050
|
: {};
|
|
1779
|
-
renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, railOptions);
|
|
2051
|
+
renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
|
|
1780
2052
|
if (state.lastFocusedKey) {
|
|
1781
2053
|
const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
|
|
1782
2054
|
row?.focus?.();
|
|
@@ -1787,26 +2059,39 @@ export function createJudgmentHistoryUI({
|
|
|
1787
2059
|
async function load({ cursor = null, append = false } = {}) {
|
|
1788
2060
|
if (state.disposed) return null;
|
|
1789
2061
|
const generation = ++state.generation;
|
|
1790
|
-
state.phase = 'loading';
|
|
1791
2062
|
state.error = null;
|
|
1792
2063
|
if (!append) {
|
|
2064
|
+
state.phase = 'loading';
|
|
1793
2065
|
state.home = null;
|
|
1794
2066
|
state.aggregate = null;
|
|
2067
|
+
state.arrived = null;
|
|
2068
|
+
state.showSkeleton = false;
|
|
2069
|
+
state.slow = false;
|
|
2070
|
+
scheduleLoadTimers(generation);
|
|
2071
|
+
render();
|
|
1795
2072
|
}
|
|
1796
|
-
render();
|
|
1797
2073
|
if (!request) {
|
|
1798
2074
|
if (generation !== state.generation || state.disposed) return null;
|
|
2075
|
+
clearLoadTimers();
|
|
2076
|
+
state.showSkeleton = false;
|
|
1799
2077
|
state.phase = 'error';
|
|
1800
2078
|
state.error = 'fetch_unavailable';
|
|
1801
2079
|
render();
|
|
1802
2080
|
return null;
|
|
1803
2081
|
}
|
|
1804
2082
|
try {
|
|
2083
|
+
const report = (payload) => {
|
|
2084
|
+
if (generation !== state.generation || state.disposed || state.phase !== 'loading') return;
|
|
2085
|
+
const normalized = normalizeJudgmentHistoryHome(payload);
|
|
2086
|
+
if (!normalized || normalized.status === 'invalid') return;
|
|
2087
|
+
state.arrived = normalized;
|
|
2088
|
+
render();
|
|
2089
|
+
};
|
|
1805
2090
|
let activePath = normalizedBasePath;
|
|
1806
2091
|
let response = await request(historyPath(normalizedBasePath, cursor), {
|
|
1807
2092
|
method: 'GET',
|
|
1808
2093
|
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1809
|
-
});
|
|
2094
|
+
}, { report });
|
|
1810
2095
|
if (generation !== state.generation || state.disposed) return null;
|
|
1811
2096
|
if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
|
|
1812
2097
|
activePath = legacyBasePath;
|
|
@@ -1837,6 +2122,11 @@ export function createJudgmentHistoryUI({
|
|
|
1837
2122
|
state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
|
|
1838
2123
|
state.cursor = state.home.pagination?.nextCursor ?? null;
|
|
1839
2124
|
}
|
|
2125
|
+
clearLoadTimers();
|
|
2126
|
+
state.showSkeleton = false;
|
|
2127
|
+
state.slow = false;
|
|
2128
|
+
state.arrived = null;
|
|
2129
|
+
state.checkedAt = readNow();
|
|
1840
2130
|
state.phase = 'ready';
|
|
1841
2131
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1842
2132
|
if (state.pendingDecisionId) {
|
|
@@ -1848,6 +2138,9 @@ export function createJudgmentHistoryUI({
|
|
|
1848
2138
|
return state.aggregate;
|
|
1849
2139
|
} catch (error) {
|
|
1850
2140
|
if (generation !== state.generation || state.disposed) return null;
|
|
2141
|
+
clearLoadTimers();
|
|
2142
|
+
state.showSkeleton = false;
|
|
2143
|
+
state.slow = false;
|
|
1851
2144
|
state.phase = 'error';
|
|
1852
2145
|
state.error = error instanceof Error ? error.message : String(error);
|
|
1853
2146
|
render();
|
|
@@ -1875,6 +2168,7 @@ export function createJudgmentHistoryUI({
|
|
|
1875
2168
|
if (state.disposed) return;
|
|
1876
2169
|
state.disposed = true;
|
|
1877
2170
|
state.generation += 1;
|
|
2171
|
+
clearLoadTimers();
|
|
1878
2172
|
root.removeEventListener?.('keydown', handleKeyDown);
|
|
1879
2173
|
rail?.removeEventListener?.('keydown', handleKeyDown);
|
|
1880
2174
|
root.replaceChildren();
|