@unson/brainbase-mcp 0.6.0 → 0.8.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/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +63 -0
- package/dist/company-os-objectives.js +234 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +104 -2
- package/dist/server.js +186 -4
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local owner review surface for judgment value proofs.
|
|
3
|
+
*
|
|
4
|
+
* The host supplies a same-origin request function and a review token. A host
|
|
5
|
+
* that cannot read a local journal may replace the unavailable notice. This
|
|
6
|
+
* module owns presentation only: it never treats an unavailable journal as an
|
|
7
|
+
* empty list, never edits the judgment journal, and keeps internal IDs inside
|
|
8
|
+
* the audit details.
|
|
9
|
+
*
|
|
10
|
+
* The screen follows the organization edition's pattern (workspace-kit): page
|
|
11
|
+
* head, the 記録の範囲 notice, filters, metrics, and the 委任の地図 as a ledger
|
|
12
|
+
* of rows the owner selects. A host that gives a `rail` element gets the
|
|
13
|
+
* selected row or judgment there; without one (the organization edition, the
|
|
14
|
+
* standalone review host) the same detail follows the workspace content.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
workspaceActions,
|
|
19
|
+
workspaceButton,
|
|
20
|
+
workspaceDefinition,
|
|
21
|
+
workspaceDetailEmpty,
|
|
22
|
+
workspaceLedger,
|
|
23
|
+
workspaceMetrics,
|
|
24
|
+
workspaceNotice,
|
|
25
|
+
workspacePageHeader,
|
|
26
|
+
workspaceRailBlock,
|
|
27
|
+
workspaceRailHead,
|
|
28
|
+
workspaceSectionTitle,
|
|
29
|
+
} from './workspace-kit.js';
|
|
30
|
+
|
|
31
|
+
export const VALUE_PROOF_REVIEW_UI_CONTRACT_VERSION = 'value-proof-review-ui.v2';
|
|
32
|
+
|
|
33
|
+
const SECTION_ORDER = Object.freeze(['needs_human', 'blocked', 'continued']);
|
|
34
|
+
const SECTION_LABELS = Object.freeze({
|
|
35
|
+
needs_human: 'あなたの判断が必要',
|
|
36
|
+
blocked: '止まっている',
|
|
37
|
+
continued: '聞かずに進めた',
|
|
38
|
+
other: 'その他',
|
|
39
|
+
});
|
|
40
|
+
const DELEGATION_STATE_LABELS = Object.freeze({
|
|
41
|
+
delegated: '任せている',
|
|
42
|
+
verifying: '確かめ中',
|
|
43
|
+
returned: '戻している',
|
|
44
|
+
});
|
|
45
|
+
const DELEGATION_STATES = Object.freeze(Object.keys(DELEGATION_STATE_LABELS));
|
|
46
|
+
const DELEGATION_SOURCES = Object.freeze(['judgment_kind', 'reason_code', 'unrecorded']);
|
|
47
|
+
const RESOLUTION_LABELS = Object.freeze({
|
|
48
|
+
continued_without_human: '聞かずに続行',
|
|
49
|
+
human_required: 'あなたに戻した',
|
|
50
|
+
not_applicable: '判断の代行なし',
|
|
51
|
+
});
|
|
52
|
+
const OUTCOME_LABELS = Object.freeze({
|
|
53
|
+
outcome_verified: '成果確認済み',
|
|
54
|
+
unconfirmed: '成果未確認',
|
|
55
|
+
not_applicable: '成果確認の対象外',
|
|
56
|
+
});
|
|
57
|
+
const BASIS_LAYER_LABELS = Object.freeze({
|
|
58
|
+
philosophy: '大切にすること',
|
|
59
|
+
objective: '目的',
|
|
60
|
+
world_model: '現状と見通し',
|
|
61
|
+
method: '判断方法',
|
|
62
|
+
constraint: '守る条件',
|
|
63
|
+
other: 'その他',
|
|
64
|
+
});
|
|
65
|
+
const EXECUTION_LABELS = Object.freeze({ not_started: '未着手', executing: '実行中', completed: '完了', blocked: '停止' });
|
|
66
|
+
const FEEDBACK_LABELS = Object.freeze({
|
|
67
|
+
none: '未評価',
|
|
68
|
+
pending: '評価待ち',
|
|
69
|
+
accepted: '採用',
|
|
70
|
+
corrected: '訂正',
|
|
71
|
+
next_time_ask: '次回は聞く',
|
|
72
|
+
reverted: '取り消し',
|
|
73
|
+
});
|
|
74
|
+
const FEEDBACK_LAYER_LABELS = Object.freeze({
|
|
75
|
+
delegation: '任せる範囲',
|
|
76
|
+
method: '判断方法',
|
|
77
|
+
objective: '目的',
|
|
78
|
+
world_model: '現状と見通し',
|
|
79
|
+
philosophy: '大切にすること',
|
|
80
|
+
other: 'その他',
|
|
81
|
+
});
|
|
82
|
+
/** Layers the owner can pick for a correction. `delegation` comes from 「次回は聞く」. */
|
|
83
|
+
export const VALUE_PROOF_CORRECTION_LAYERS = Object.freeze(['method', 'objective', 'world_model', 'philosophy', 'other']);
|
|
84
|
+
export const VALUE_PROOF_FEEDBACK_OPTIONS = Object.freeze([
|
|
85
|
+
Object.freeze({ value: 'accepted', label: '採用', hint: '聞かずに進めてよかった', summary: 'none', placeholder: '' }),
|
|
86
|
+
Object.freeze({ value: 'corrected', label: '訂正', hint: '判断の中身が違った', summary: 'required', placeholder: '何が違ったか(必須)' }),
|
|
87
|
+
Object.freeze({ value: 'next_time_ask', label: '次回は聞く', hint: 'この種の判断は、次は聞いてほしい。任せる範囲の訂正として記録します', summary: 'optional', placeholder: 'どの条件なら聞くべきか(任意)' }),
|
|
88
|
+
Object.freeze({ value: 'reverted', label: '取り消し', hint: 'この判断を元に戻した(記録のみ。外部の操作は戻さない)', summary: 'required', placeholder: '何を戻したか(必須)' }),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
function getDocument(explicit) {
|
|
92
|
+
const value = explicit ?? (typeof document === 'undefined' ? null : document);
|
|
93
|
+
if (!value || typeof value.createElement !== 'function') throw new Error('document_unavailable');
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeElement(doc, tag, { className, text, attrs = {} } = {}) {
|
|
98
|
+
const element = doc.createElement(tag);
|
|
99
|
+
if (className) element.className = className;
|
|
100
|
+
if (text !== undefined && text !== null) element.textContent = String(text);
|
|
101
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
102
|
+
if (value === undefined || value === null || value === false) continue;
|
|
103
|
+
element.setAttribute(name, value === true ? '' : String(value));
|
|
104
|
+
}
|
|
105
|
+
return element;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isRecord(value) {
|
|
109
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function text(value) {
|
|
113
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatDate(value) {
|
|
117
|
+
const time = typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
118
|
+
if (Number.isNaN(time)) return '日時不明';
|
|
119
|
+
const date = new Date(time);
|
|
120
|
+
const pad = (number) => String(number).padStart(2, '0');
|
|
121
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function itemKey(proof) {
|
|
125
|
+
return `${proof.intent_id}\u0000${proof.decision_attempt_id}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isProof(value) {
|
|
129
|
+
return isRecord(value)
|
|
130
|
+
&& value.schema_version === 'brainbase-judgment-value-proof-v1'
|
|
131
|
+
&& typeof value.intent_id === 'string'
|
|
132
|
+
&& typeof value.decision_attempt_id === 'string'
|
|
133
|
+
&& isRecord(value.interruption)
|
|
134
|
+
&& isRecord(value.decision)
|
|
135
|
+
&& isRecord(value.execution)
|
|
136
|
+
&& isRecord(value.outcome)
|
|
137
|
+
&& isRecord(value.feedback);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Normalize the host response. Anything unexpected becomes `invalid`, never an empty success. */
|
|
141
|
+
export function normalizeValueProofReviewHome(payload) {
|
|
142
|
+
if (!isRecord(payload)) return { status: 'invalid', reason: '応答の形式が不正です' };
|
|
143
|
+
if (payload.status === 'unavailable') {
|
|
144
|
+
return { status: 'unavailable', root: text(payload.root), reason: text(payload.reason) ?? 'unknown' };
|
|
145
|
+
}
|
|
146
|
+
if (payload.status !== 'available' || !isRecord(payload.coverage) || !isRecord(payload.sections)) {
|
|
147
|
+
return { status: 'invalid', reason: '応答の形式が不正です' };
|
|
148
|
+
}
|
|
149
|
+
const sections = {};
|
|
150
|
+
for (const section of [...SECTION_ORDER, 'other']) {
|
|
151
|
+
const items = payload.sections[section];
|
|
152
|
+
if (!Array.isArray(items) || !items.every((item) => isRecord(item) && isProof(item.proof))) {
|
|
153
|
+
return { status: 'invalid', reason: `区分「${SECTION_LABELS[section]}」の形式が不正です` };
|
|
154
|
+
}
|
|
155
|
+
sections[section] = items.map((item) => ({
|
|
156
|
+
section,
|
|
157
|
+
proof: item.proof,
|
|
158
|
+
feedbackHistory: Array.isArray(item.feedback_history) ? item.feedback_history.filter(isRecord) : [],
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
const itemsByKey = new Map(SECTION_ORDER.flatMap((section) => sections[section].map((item) => [itemKey(item.proof), item])));
|
|
162
|
+
const delegationMap = normalizeDelegationMap(payload.delegation_map, itemsByKey);
|
|
163
|
+
if (!delegationMap) return { status: 'invalid', reason: '委任の地図の形式が不正です' };
|
|
164
|
+
return {
|
|
165
|
+
status: 'available',
|
|
166
|
+
root: text(payload.root),
|
|
167
|
+
coverage: {
|
|
168
|
+
saved: Number.isInteger(payload.coverage.saved) ? payload.coverage.saved : null,
|
|
169
|
+
rejected: Number.isInteger(payload.coverage.rejected) ? payload.coverage.rejected : null,
|
|
170
|
+
latestRecordedAt: text(payload.coverage.latest_recorded_at),
|
|
171
|
+
possiblyStalled: payload.coverage.possibly_stalled === true,
|
|
172
|
+
},
|
|
173
|
+
sections,
|
|
174
|
+
itemsByKey,
|
|
175
|
+
delegationMap,
|
|
176
|
+
rejected: Array.isArray(payload.rejected) ? payload.rejected.filter(isRecord) : [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function refKey(ref) {
|
|
181
|
+
return isRecord(ref) && typeof ref.intent_id === 'string' && typeof ref.decision_attempt_id === 'string'
|
|
182
|
+
? `${ref.intent_id}\u0000${ref.decision_attempt_id}`
|
|
183
|
+
: null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function count(value) {
|
|
187
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeStateBasis(basis) {
|
|
191
|
+
if (!isRecord(basis)) return { reason: 'no_feedback' };
|
|
192
|
+
if (basis.reason === 'latest_judgment_returned') return { reason: basis.reason, at: text(basis.at) };
|
|
193
|
+
if (basis.reason === 'latest_feedback') {
|
|
194
|
+
return { reason: basis.reason, status: text(basis.status), targetLayer: text(basis.target_layer), at: text(basis.at) };
|
|
195
|
+
}
|
|
196
|
+
return { reason: 'no_feedback' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The server decides rows and states; this only checks the shape and resolves item references. */
|
|
200
|
+
function normalizeDelegationMap(map, itemsByKey) {
|
|
201
|
+
if (!isRecord(map) || !Array.isArray(map.rows)) return null;
|
|
202
|
+
const refs = (list) => (Array.isArray(list) ? list.map(refKey).filter((key) => key && itemsByKey.has(key)) : []);
|
|
203
|
+
const rows = [];
|
|
204
|
+
for (const row of map.rows) {
|
|
205
|
+
if (!isRecord(row) || !text(row.key) || !DELEGATION_SOURCES.includes(row.source)
|
|
206
|
+
|| !DELEGATION_STATES.includes(row.state) || !Array.isArray(row.items)) return null;
|
|
207
|
+
const counts = isRecord(row.counts) ? row.counts : {};
|
|
208
|
+
const byFeedback = isRecord(counts.by_feedback) ? counts.by_feedback : {};
|
|
209
|
+
rows.push({
|
|
210
|
+
key: row.key,
|
|
211
|
+
source: row.source,
|
|
212
|
+
label: text(row.label),
|
|
213
|
+
state: row.state,
|
|
214
|
+
stateBasis: normalizeStateBasis(row.state_basis),
|
|
215
|
+
counts: {
|
|
216
|
+
continued: count(counts.continued),
|
|
217
|
+
returned: count(counts.returned),
|
|
218
|
+
rated: count(counts.rated),
|
|
219
|
+
byFeedback: {
|
|
220
|
+
accepted: count(byFeedback.accepted),
|
|
221
|
+
corrected: count(byFeedback.corrected),
|
|
222
|
+
next_time_ask: count(byFeedback.next_time_ask),
|
|
223
|
+
reverted: count(byFeedback.reverted),
|
|
224
|
+
},
|
|
225
|
+
correctedOrReverted: count(counts.corrected_or_reverted),
|
|
226
|
+
inherited: count(counts.inherited),
|
|
227
|
+
},
|
|
228
|
+
latestRecordedAt: text(row.latest_recorded_at),
|
|
229
|
+
itemKeys: refs(row.items),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const rowLabel = new Map(rows.map((row) => [row.key, row]));
|
|
233
|
+
const highlight = (list) => (Array.isArray(list) ? list : [])
|
|
234
|
+
.map((ref) => ({ key: refKey(ref), row: rowLabel.get(ref?.row_key) ?? null }))
|
|
235
|
+
.filter((entry) => entry.key && itemsByKey.has(entry.key));
|
|
236
|
+
return {
|
|
237
|
+
judged: count(map.judged),
|
|
238
|
+
kindRecorded: count(map.kind_recorded),
|
|
239
|
+
rated: count(map.rated),
|
|
240
|
+
rows,
|
|
241
|
+
correctedAfterContinue: highlight(map.corrected_after_continue),
|
|
242
|
+
continuedAfterAsk: highlight(map.continued_after_ask),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isUnrated(proof) {
|
|
247
|
+
return proof.feedback.status === 'none' || proof.feedback.status === 'pending';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function itemVisible(state, item) {
|
|
251
|
+
if (!item) return false;
|
|
252
|
+
if (state.unratedOnly && !isUnrated(item.proof)) return false;
|
|
253
|
+
if (state.needsHumanOnly && item.section !== 'needs_human') return false;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rowItems(state, row) {
|
|
258
|
+
return row.itemKeys.map((key) => state.home.itemsByKey.get(key)).filter((item) => itemVisible(state, item));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function visibleRows(state) {
|
|
262
|
+
const rows = state.home?.delegationMap?.rows ?? [];
|
|
263
|
+
return state.needsHumanOnly ? rows.filter((row) => rowItems(state, row).length > 0) : rows;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function rowOfItem(state, key) {
|
|
267
|
+
return state.home?.delegationMap?.rows.find((row) => row.itemKeys.includes(key)) ?? null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function findItem(state, key) {
|
|
271
|
+
if (state.home?.status !== 'available' || !key) return null;
|
|
272
|
+
for (const section of [...SECTION_ORDER, 'other']) {
|
|
273
|
+
const found = state.home.sections[section].find((item) => itemKey(item.proof) === key);
|
|
274
|
+
if (found) return found;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
function firstVisibleKey(state) {
|
|
281
|
+
for (const row of visibleRows(state)) {
|
|
282
|
+
const item = rowItems(state, row)[0];
|
|
283
|
+
if (item) return itemKey(item.proof);
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Keep the selection on something the filters still show: a hidden judgment
|
|
290
|
+
* moves to the first visible one (in the selected row when it has one), and
|
|
291
|
+
* the selected row follows it; a hidden row falls back to the first visible row.
|
|
292
|
+
*/
|
|
293
|
+
function revealSelection(state) {
|
|
294
|
+
if (state.home?.status !== 'available') return;
|
|
295
|
+
if (!itemVisible(state, findItem(state, state.selectedKey))) {
|
|
296
|
+
const selectedRow = visibleRows(state).find((row) => row.key === state.selectedRowKey);
|
|
297
|
+
const inRow = selectedRow ? rowItems(state, selectedRow)[0] : null;
|
|
298
|
+
state.selectedKey = inRow ? itemKey(inRow.proof) : firstVisibleKey(state);
|
|
299
|
+
const row = rowOfItem(state, state.selectedKey);
|
|
300
|
+
if (row) state.selectedRowKey = row.key;
|
|
301
|
+
}
|
|
302
|
+
const rows = visibleRows(state);
|
|
303
|
+
if (!rows.some((row) => row.key === state.selectedRowKey)) {
|
|
304
|
+
state.selectedRowKey = rowOfItem(state, state.selectedKey)?.key ?? rows[0]?.key ?? null;
|
|
305
|
+
}
|
|
306
|
+
if (state.railView === 'judgment' && !findItem(state, state.selectedKey)) state.railView = 'row';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function notice(doc, className, message, role = 'status') {
|
|
310
|
+
return makeElement(doc, 'p', { className: `vpr-notice ${className}`, text: message, attrs: { role } });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const PAGE_LEAD = 'Brainbaseに、どの種類の判断をどこまで任せていて、そのうち何をあなたが直したか。';
|
|
314
|
+
|
|
315
|
+
function shortDate(value) {
|
|
316
|
+
const time = typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
317
|
+
if (Number.isNaN(time)) return '日時不明';
|
|
318
|
+
const date = new Date(time);
|
|
319
|
+
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderPageHead(doc, page) {
|
|
323
|
+
return workspacePageHeader(doc, {
|
|
324
|
+
crumbs: Array.isArray(page?.crumbs) ? page.crumbs : [],
|
|
325
|
+
title: '判断の見返し',
|
|
326
|
+
lead: PAGE_LEAD,
|
|
327
|
+
source: text(page?.source),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** One element for a notice body, so the kit's label column stays on the left. */
|
|
332
|
+
function noticeBody(doc, ...parts) {
|
|
333
|
+
const body = makeElement(doc, 'div', { className: 'bb-vpr-notice-body' });
|
|
334
|
+
for (const part of parts) {
|
|
335
|
+
if (part === null || part === undefined || part === false) continue;
|
|
336
|
+
body.append(typeof part === 'string' ? makeElement(doc, 'p', { text: part }) : part);
|
|
337
|
+
}
|
|
338
|
+
return body;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function renderCoverageNotice(doc, home) {
|
|
342
|
+
const saved = home.coverage.saved === null ? '保存済み 未確認' : `保存済み ${home.coverage.saved}件`;
|
|
343
|
+
const latest = home.coverage.latestRecordedAt ? `最終記録 ${formatDate(home.coverage.latestRecordedAt)}` : '最終記録 なし';
|
|
344
|
+
let rejected = null;
|
|
345
|
+
if (home.coverage.rejected) {
|
|
346
|
+
rejected = makeElement(doc, 'details', { className: 'bb-vpr-rejected' });
|
|
347
|
+
rejected.append(makeElement(doc, 'summary', { text: `読めない記録 ${home.coverage.rejected}件` }));
|
|
348
|
+
const list = makeElement(doc, 'ul');
|
|
349
|
+
for (const entry of home.rejected) {
|
|
350
|
+
list.append(makeElement(doc, 'li', { text: `${text(entry.file) ?? '不明なファイル'}: ${text(entry.reason) ?? '理由不明'}` }));
|
|
351
|
+
}
|
|
352
|
+
rejected.append(list);
|
|
353
|
+
}
|
|
354
|
+
return workspaceNotice(doc, {
|
|
355
|
+
label: '記録の範囲',
|
|
356
|
+
text: noticeBody(doc, `判断journal(${home.root ?? '場所不明'})を読んでいます。${saved}・${latest}。`, rejected),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function toggleButton(doc, { label, pressed, disabled = false, onClick }) {
|
|
361
|
+
const button = workspaceButton(doc, {
|
|
362
|
+
text: label,
|
|
363
|
+
onClick,
|
|
364
|
+
disabled,
|
|
365
|
+
attrs: { 'aria-pressed': pressed ? 'true' : 'false' },
|
|
366
|
+
});
|
|
367
|
+
button.className = `${button.className} bb-vpr-toggle${pressed ? ' is-pressed' : ''}`;
|
|
368
|
+
return button;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function renderFilters(doc, home, state, callbacks) {
|
|
372
|
+
const needsHuman = home.sections.needs_human.length;
|
|
373
|
+
const actions = workspaceActions(doc, [
|
|
374
|
+
toggleButton(doc, {
|
|
375
|
+
label: state.needsHumanOnly ? `あなたの判断が必要 ${needsHuman}件(絞り込み中・解除)` : `あなたの判断が必要 ${needsHuman}件`,
|
|
376
|
+
pressed: state.needsHumanOnly,
|
|
377
|
+
disabled: needsHuman === 0 && !state.needsHumanOnly,
|
|
378
|
+
onClick: () => callbacks.onToggleNeedsHuman?.(!state.needsHumanOnly),
|
|
379
|
+
}),
|
|
380
|
+
toggleButton(doc, {
|
|
381
|
+
label: state.unratedOnly ? '未評価のみ(絞り込み中・解除)' : '未評価のみ',
|
|
382
|
+
pressed: state.unratedOnly,
|
|
383
|
+
onClick: () => callbacks.onToggleUnrated?.(!state.unratedOnly),
|
|
384
|
+
}),
|
|
385
|
+
]);
|
|
386
|
+
actions.className = `${actions.className} bb-vpr-filters`;
|
|
387
|
+
actions.setAttribute('aria-label', '絞り込み');
|
|
388
|
+
return actions;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderMetrics(doc, home) {
|
|
392
|
+
const map = home.delegationMap;
|
|
393
|
+
return workspaceMetrics(doc, [
|
|
394
|
+
{ label: '保存済み', value: home.coverage.saved, note: '判断journalの記録' },
|
|
395
|
+
{ label: 'あなたの判断が必要', value: home.sections.needs_human.length, note: 'あなたに戻した判断' },
|
|
396
|
+
{ label: '聞かずに進めた', value: home.sections.continued.length, note: '聞かずに続行した判断' },
|
|
397
|
+
{ label: '評価済み', value: map.rated, note: `判断 ${map.judged}件のうち` },
|
|
398
|
+
], { ariaLabel: '判断の集計' });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function itemTitle(proof) {
|
|
402
|
+
return text(proof.interruption.question_display_text)
|
|
403
|
+
?? text(proof.human_decision?.question)
|
|
404
|
+
?? '質問の記録なし';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function rowName(row) {
|
|
408
|
+
if (row.source === 'judgment_kind') return row.label ?? '種類名なし';
|
|
409
|
+
if (row.source === 'reason_code') return `種類の記録なし・理由: ${row.label ?? '不明'}`;
|
|
410
|
+
return '種類の記録なし';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function stateBasisText(basis) {
|
|
414
|
+
if (basis.reason === 'latest_judgment_returned') return `最新の判断をあなたに戻した(${formatDate(basis.at)})`;
|
|
415
|
+
if (basis.reason === 'latest_feedback') {
|
|
416
|
+
const layer = basis.status === 'corrected' ? FEEDBACK_LAYER_LABELS[basis.targetLayer] : null;
|
|
417
|
+
return `最新の評価が${FEEDBACK_LABELS[basis.status] ?? '不明'}${layer ? `(${layer})` : ''}(${formatDate(basis.at)})`;
|
|
418
|
+
}
|
|
419
|
+
return 'まだ評価がありません';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function ratedText(row) {
|
|
423
|
+
const { counts } = row;
|
|
424
|
+
if (counts.rated === 0) return '未評価';
|
|
425
|
+
const byFeedback = ['accepted', 'corrected', 'next_time_ask', 'reverted']
|
|
426
|
+
.filter((status) => counts.byFeedback[status] > 0)
|
|
427
|
+
.map((status) => `${FEEDBACK_LABELS[status]} ${counts.byFeedback[status]}`);
|
|
428
|
+
return `${counts.rated}件${byFeedback.length > 0 ? `(${byFeedback.join('・')})` : ''}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function feedbackText(status, layerKey, summary) {
|
|
432
|
+
const layer = FEEDBACK_LAYER_LABELS[layerKey];
|
|
433
|
+
return `${FEEDBACK_LABELS[status] ?? status ?? '評価不明'}${layer ? `(${layer})` : ''}${text(summary) ? `: ${text(summary)}` : ''}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The latest feedback of a judgment: the history entry when there is one, else the proof's own. */
|
|
437
|
+
function latestFeedbackText(item) {
|
|
438
|
+
const latest = item.feedbackHistory.at(-1);
|
|
439
|
+
if (latest) return feedbackText(latest.status, latest.target_layer, latest.summary);
|
|
440
|
+
return feedbackText(item.proof.feedback.status, null, item.proof.feedback.summary);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function judgmentLedgerRow(state, item, row, callbacks) {
|
|
444
|
+
const key = itemKey(item.proof);
|
|
445
|
+
return {
|
|
446
|
+
key,
|
|
447
|
+
selected: key === state.selectedKey && state.railView === 'judgment',
|
|
448
|
+
onSelect: (selected) => callbacks.onSelect?.(selected),
|
|
449
|
+
cells: [
|
|
450
|
+
{ text: itemTitle(item.proof), className: 'bb-vpr-primary' },
|
|
451
|
+
row ? rowName(row) : { text: '種類の記録なし', className: 'is-unresolved' },
|
|
452
|
+
latestFeedbackText(item),
|
|
453
|
+
shortDate(item.proof.recorded_at),
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderHighlight(doc, state, entries, { className, title, hint }, callbacks) {
|
|
459
|
+
const visible = entries
|
|
460
|
+
.map((entry) => ({ entry, item: state.home.itemsByKey.get(entry.key) }))
|
|
461
|
+
.filter(({ item }) => itemVisible(state, item));
|
|
462
|
+
if (visible.length === 0) return null;
|
|
463
|
+
const block = makeElement(doc, 'section', { className: `bb-vpr-highlight ${className}`, attrs: { 'aria-label': title } });
|
|
464
|
+
const heading = makeElement(doc, 'h2');
|
|
465
|
+
heading.append(makeElement(doc, 'span', { text: title }), makeElement(doc, 'span', { className: 'bb-vpr-count', text: `${visible.length}件` }));
|
|
466
|
+
block.append(heading, makeElement(doc, 'p', { className: 'bb-vpr-hint', text: hint }));
|
|
467
|
+
block.append(workspaceLedger(doc, {
|
|
468
|
+
className: 'bb-vpr-judgment-ledger',
|
|
469
|
+
ariaLabel: title,
|
|
470
|
+
columns: ['判断', '判断の種類', '評価', '記録'],
|
|
471
|
+
rows: visible.map(({ entry, item }) => judgmentLedgerRow(state, item, entry.row, callbacks)),
|
|
472
|
+
}));
|
|
473
|
+
return block;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The name in the map ledger: rows without a kind sit under their own heading, so the reason code is enough. */
|
|
477
|
+
function ledgerRowName(row) {
|
|
478
|
+
if (row.source === 'reason_code') return `理由: ${row.label ?? '不明'}`;
|
|
479
|
+
if (row.source === 'unrecorded') return '理由の記録もなし';
|
|
480
|
+
return rowName(row);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function mapLedgerRow(state, row, callbacks) {
|
|
484
|
+
const corrected = row.counts.correctedOrReverted;
|
|
485
|
+
return {
|
|
486
|
+
key: row.key,
|
|
487
|
+
selected: row.key === state.selectedRowKey,
|
|
488
|
+
onSelect: (key) => callbacks.onSelectRow?.(key),
|
|
489
|
+
className: `is-${row.state}`,
|
|
490
|
+
cells: [
|
|
491
|
+
{ primary: ledgerRowName(row) },
|
|
492
|
+
{ text: DELEGATION_STATE_LABELS[row.state], className: `bb-vpr-state is-${row.state}` },
|
|
493
|
+
`続行 ${row.counts.continued}・戻した ${row.counts.returned}`,
|
|
494
|
+
ratedText(row),
|
|
495
|
+
{ text: `${corrected}件`, className: corrected > 0 ? 'bb-vpr-alert' : 'is-unresolved' },
|
|
496
|
+
row.latestRecordedAt ? shortDate(row.latestRecordedAt) : { text: '日時不明', className: 'is-unresolved' },
|
|
497
|
+
],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const MAP_COLUMNS = Object.freeze(['判断の種類', '状態', '判断', '評価', '訂正・取り消し', '最新']);
|
|
502
|
+
|
|
503
|
+
function renderDelegationMap(doc, state, callbacks) {
|
|
504
|
+
const map = state.home.delegationMap;
|
|
505
|
+
const block = makeElement(doc, 'section', { className: 'bb-vpr-map', attrs: { 'aria-label': '委任の地図' } });
|
|
506
|
+
block.append(workspaceSectionTitle(doc, {
|
|
507
|
+
title: '委任の地図',
|
|
508
|
+
lead: '判断の種類ごとに、任せている・確かめ中・戻しているを、最新の判断と最新の評価から示します。行を選ぶと、その種類の判断が出ます。',
|
|
509
|
+
}));
|
|
510
|
+
if (map.judged > 0 && map.kindRecorded === 0) {
|
|
511
|
+
block.append(workspaceNotice(doc, { label: '判断の種類', text: '判断の種類はまだ記録されていません。理由コードで分けています。' }));
|
|
512
|
+
}
|
|
513
|
+
const rows = visibleRows(state);
|
|
514
|
+
if (rows.length === 0) {
|
|
515
|
+
block.append(workspaceLedger(doc, {
|
|
516
|
+
className: 'bb-vpr-map-ledger',
|
|
517
|
+
ariaLabel: '委任の地図',
|
|
518
|
+
columns: MAP_COLUMNS,
|
|
519
|
+
rows: [],
|
|
520
|
+
empty: state.needsHumanOnly ? 'あなたの判断が必要な記録はありません。' : '判断の記録はありません。',
|
|
521
|
+
}));
|
|
522
|
+
return block;
|
|
523
|
+
}
|
|
524
|
+
const kinds = rows.filter((row) => row.source === 'judgment_kind');
|
|
525
|
+
const unrecorded = rows.filter((row) => row.source !== 'judgment_kind');
|
|
526
|
+
if (kinds.length > 0) {
|
|
527
|
+
block.append(workspaceLedger(doc, {
|
|
528
|
+
className: 'bb-vpr-map-ledger',
|
|
529
|
+
ariaLabel: '判断の種類ごとの委任',
|
|
530
|
+
columns: MAP_COLUMNS,
|
|
531
|
+
rows: kinds.map((row) => mapLedgerRow(state, row, callbacks)),
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
if (unrecorded.length > 0) {
|
|
535
|
+
block.append(
|
|
536
|
+
makeElement(doc, 'h3', { className: 'bb-vpr-subheading', text: '判断の種類が記録されていない判断' }),
|
|
537
|
+
makeElement(doc, 'p', { className: 'bb-vpr-hint', text: '理由コードは、聞いた・聞かなかった理由であって、判断の種類ではありません。' }),
|
|
538
|
+
workspaceLedger(doc, {
|
|
539
|
+
className: 'bb-vpr-map-ledger is-unrecorded',
|
|
540
|
+
ariaLabel: '判断の種類が記録されていない判断',
|
|
541
|
+
columns: MAP_COLUMNS,
|
|
542
|
+
rows: unrecorded.map((row) => mapLedgerRow(state, row, callbacks)),
|
|
543
|
+
}),
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return block;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function basisText(entry) {
|
|
550
|
+
const application = text(entry.application) ?? '適用内容なし';
|
|
551
|
+
const layer = BASIS_LAYER_LABELS[entry.layer];
|
|
552
|
+
return layer ? `[${layer}] ${application}` : application;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function inheritanceText(inheritance) {
|
|
556
|
+
const sources = Array.isArray(inheritance?.sources) ? inheritance.sources : [];
|
|
557
|
+
if (sources.length === 0) return '引き継ぎの記録なし';
|
|
558
|
+
const parts = [sources.map((source) => text(source.label) ?? text(source.ref) ?? '不明').join(' / ')];
|
|
559
|
+
const same = Array.isArray(inheritance.same_conditions) ? inheritance.same_conditions.filter(text) : [];
|
|
560
|
+
const rechecked = Array.isArray(inheritance.rechecked_conditions) ? inheritance.rechecked_conditions.filter(text) : [];
|
|
561
|
+
if (same.length > 0) parts.push(`今回も同じ: ${same.join('、')}`);
|
|
562
|
+
if (rechecked.length > 0) parts.push(`今回だけ確認: ${rechecked.join('、')}`);
|
|
563
|
+
return parts.join('。');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function kindLabel(proof) {
|
|
567
|
+
return text(proof.decision?.judgment_kind?.label);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function renderItemButton(doc, state, item, callbacks) {
|
|
571
|
+
const key = itemKey(item.proof);
|
|
572
|
+
const selected = key === state.selectedKey;
|
|
573
|
+
const button = makeElement(doc, 'button', {
|
|
574
|
+
className: `bb-vpr-item${selected ? ' is-selected' : ''}`,
|
|
575
|
+
attrs: { type: 'button', 'aria-pressed': selected ? 'true' : 'false' },
|
|
576
|
+
});
|
|
577
|
+
button.append(
|
|
578
|
+
makeElement(doc, 'span', { className: 'bb-vpr-item-title', text: itemTitle(item.proof) }),
|
|
579
|
+
makeElement(doc, 'span', { className: 'bb-vpr-item-summary', text: text(item.proof.decision.summary) ?? text(item.proof.human_decision?.why_human) ?? '判断の記録なし' }),
|
|
580
|
+
makeElement(doc, 'span', {
|
|
581
|
+
className: 'bb-vpr-item-meta',
|
|
582
|
+
text: [
|
|
583
|
+
SECTION_LABELS[item.section],
|
|
584
|
+
formatDate(item.proof.recorded_at),
|
|
585
|
+
OUTCOME_LABELS[item.proof.outcome.status] ?? '成果不明',
|
|
586
|
+
FEEDBACK_LABELS[item.proof.feedback.status] ?? '評価不明',
|
|
587
|
+
].join(' · '),
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
590
|
+
button.addEventListener('click', () => callbacks.onSelect?.(key));
|
|
591
|
+
const entry = makeElement(doc, 'li');
|
|
592
|
+
entry.append(button);
|
|
593
|
+
return entry;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** The selected map row: its state and reason, its counts and its judgments. */
|
|
597
|
+
function renderRowDetail(doc, state, row, callbacks) {
|
|
598
|
+
const detail = [workspaceRailHead(doc, {
|
|
599
|
+
kicker: '判断の種類',
|
|
600
|
+
title: rowName(row),
|
|
601
|
+
sub: `${DELEGATION_STATE_LABELS[row.state]}:${stateBasisText(row.stateBasis)}`,
|
|
602
|
+
})];
|
|
603
|
+
detail.push(workspaceRailBlock(doc, {
|
|
604
|
+
title: '記録と評価',
|
|
605
|
+
content: workspaceDefinition(doc, [
|
|
606
|
+
['聞かずに続行', `${row.counts.continued}件`],
|
|
607
|
+
['あなたに戻した', `${row.counts.returned}件`],
|
|
608
|
+
['評価', ratedText(row)],
|
|
609
|
+
['訂正・取り消し', `${row.counts.correctedOrReverted}件`],
|
|
610
|
+
['引き継ぎあり', `${row.counts.inherited}件`],
|
|
611
|
+
['最新', row.latestRecordedAt ? formatDate(row.latestRecordedAt) : '日時不明'],
|
|
612
|
+
]),
|
|
613
|
+
}));
|
|
614
|
+
const items = rowItems(state, row);
|
|
615
|
+
const list = items.length === 0
|
|
616
|
+
? makeElement(doc, 'p', { className: 'bb-vpr-empty', text: state.unratedOnly ? '未評価の判断はありません。' : 'この行に表示できる判断はありません。' })
|
|
617
|
+
: makeElement(doc, 'ul', { className: 'bb-vpr-list' });
|
|
618
|
+
for (const item of items) list.append(renderItemButton(doc, state, item, callbacks));
|
|
619
|
+
detail.push(workspaceRailBlock(doc, { title: `判断 ${items.length}件`, className: 'bb-vpr-row-judgments', content: list }));
|
|
620
|
+
return detail;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function renderFeedbackForm(doc, item, state, callbacks) {
|
|
624
|
+
const form = makeElement(doc, 'form', { className: 'vpr-feedback', attrs: { 'aria-label': 'この判断を評価する' } });
|
|
625
|
+
const fieldset = makeElement(doc, 'fieldset');
|
|
626
|
+
fieldset.append(makeElement(doc, 'legend', { text: 'この判断を評価する' }));
|
|
627
|
+
const draft = state.draft;
|
|
628
|
+
const summary = makeElement(doc, 'textarea', { attrs: { rows: '3', maxlength: '500', 'aria-label': '評価の理由' } });
|
|
629
|
+
summary.value = draft.summary;
|
|
630
|
+
const summaryHint = makeElement(doc, 'p', { className: 'bb-vpr-hint' });
|
|
631
|
+
const syncSummary = (value) => {
|
|
632
|
+
const option = VALUE_PROOF_FEEDBACK_OPTIONS.find((entry) => entry.value === value);
|
|
633
|
+
summary.setAttribute('placeholder', option?.placeholder || '理由(任意)');
|
|
634
|
+
summaryHint.textContent = option ? option.hint : '評価を選んでください。';
|
|
635
|
+
if (option?.summary === 'required') summary.setAttribute('required', '');
|
|
636
|
+
else if (typeof summary.removeAttribute === 'function') summary.removeAttribute('required');
|
|
637
|
+
};
|
|
638
|
+
const options = makeElement(doc, 'div', { className: 'vpr-options' });
|
|
639
|
+
for (const option of VALUE_PROOF_FEEDBACK_OPTIONS) {
|
|
640
|
+
const label = makeElement(doc, 'label', { className: 'vpr-option' });
|
|
641
|
+
const radio = makeElement(doc, 'input', { attrs: { type: 'radio', name: 'vpr-feedback-status', value: option.value } });
|
|
642
|
+
radio.checked = draft.status === option.value;
|
|
643
|
+
radio.addEventListener('change', () => {
|
|
644
|
+
callbacks.onDraft?.({ status: option.value });
|
|
645
|
+
syncSummary(option.value);
|
|
646
|
+
syncLayers(option.value);
|
|
647
|
+
});
|
|
648
|
+
label.append(radio, makeElement(doc, 'span', { text: option.label }));
|
|
649
|
+
options.append(label);
|
|
650
|
+
}
|
|
651
|
+
const layerGroup = makeElement(doc, 'div', { className: 'vpr-layers', attrs: { role: 'radiogroup', 'aria-label': '何を直すか' } });
|
|
652
|
+
layerGroup.append(makeElement(doc, 'p', { className: 'bb-vpr-hint', text: '何を直すか(必須)' }));
|
|
653
|
+
for (const layer of VALUE_PROOF_CORRECTION_LAYERS) {
|
|
654
|
+
const label = makeElement(doc, 'label', { className: 'vpr-option' });
|
|
655
|
+
const radio = makeElement(doc, 'input', { attrs: { type: 'radio', name: 'vpr-feedback-layer', value: layer } });
|
|
656
|
+
radio.checked = draft.targetLayer === layer;
|
|
657
|
+
radio.addEventListener('change', () => callbacks.onDraft?.({ targetLayer: layer }));
|
|
658
|
+
label.append(radio, makeElement(doc, 'span', { text: FEEDBACK_LAYER_LABELS[layer] }));
|
|
659
|
+
layerGroup.append(label);
|
|
660
|
+
}
|
|
661
|
+
const syncLayers = (value) => {
|
|
662
|
+
if (value === 'corrected') {
|
|
663
|
+
if (typeof layerGroup.removeAttribute === 'function') layerGroup.removeAttribute('hidden');
|
|
664
|
+
} else {
|
|
665
|
+
layerGroup.setAttribute('hidden', '');
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
syncSummary(draft.status);
|
|
669
|
+
syncLayers(draft.status);
|
|
670
|
+
summary.addEventListener('input', () => callbacks.onDraft?.({ summary: summary.value }));
|
|
671
|
+
const submit = makeElement(doc, 'button', {
|
|
672
|
+
className: 'bb-ws-button is-primary vpr-submit',
|
|
673
|
+
text: state.save.state === 'saving' ? '保存中…' : '評価を保存',
|
|
674
|
+
attrs: { type: 'submit', disabled: state.save.state === 'saving' },
|
|
675
|
+
});
|
|
676
|
+
form.addEventListener('submit', (event) => {
|
|
677
|
+
event?.preventDefault?.();
|
|
678
|
+
void callbacks.onSubmit?.(item);
|
|
679
|
+
});
|
|
680
|
+
fieldset.append(options, summaryHint, layerGroup, summary);
|
|
681
|
+
form.append(fieldset, submit);
|
|
682
|
+
if (state.save.state === 'saved') form.append(notice(doc, 'is-success', state.save.message));
|
|
683
|
+
if (state.save.state === 'error') form.append(notice(doc, 'is-danger', state.save.message, 'alert'));
|
|
684
|
+
return form;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function consultText(proof) {
|
|
688
|
+
return [
|
|
689
|
+
'Brainbaseのこの判断について相談したい。',
|
|
690
|
+
`本来の質問: ${itemTitle(proof)}`,
|
|
691
|
+
`判断: ${text(proof.decision.summary) ?? '記録なし'}`,
|
|
692
|
+
`decision_attempt_id: ${proof.decision_attempt_id}`,
|
|
693
|
+
].join('\n');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function facts(doc, pairs) {
|
|
697
|
+
const list = workspaceDefinition(doc, pairs);
|
|
698
|
+
list.className = `${list.className} bb-vpr-facts`;
|
|
699
|
+
return list;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** The selected judgment as rail blocks, in the contract order. */
|
|
703
|
+
function renderJudgmentDetail(doc, state, item, callbacks) {
|
|
704
|
+
const { proof } = item;
|
|
705
|
+
const detail = [];
|
|
706
|
+
const row = rowOfItem(state, itemKey(proof));
|
|
707
|
+
if (row) {
|
|
708
|
+
const back = workspaceButton(doc, { text: `← ${rowName(row)}`, variant: 'quiet', onClick: () => callbacks.onShowRow?.() });
|
|
709
|
+
back.className = `${back.className} bb-vpr-back`;
|
|
710
|
+
detail.push(back);
|
|
711
|
+
}
|
|
712
|
+
const head = workspaceRailHead(doc, {
|
|
713
|
+
kicker: SECTION_LABELS[item.section],
|
|
714
|
+
title: itemTitle(proof),
|
|
715
|
+
sub: kindLabel(proof) ? `判断の種類: ${kindLabel(proof)}` : row ? rowName(row) : null,
|
|
716
|
+
});
|
|
717
|
+
const heading = Array.from(head.children ?? []).find((child) => child.tagName === 'H2');
|
|
718
|
+
heading?.setAttribute('tabindex', '-1');
|
|
719
|
+
detail.push(head);
|
|
720
|
+
|
|
721
|
+
const reason = text(proof.interruption.human_reason) ?? text(proof.interruption.reason_code) ?? '理由の記録なし';
|
|
722
|
+
detail.push(workspaceRailBlock(doc, {
|
|
723
|
+
title: '判断',
|
|
724
|
+
content: facts(doc, [
|
|
725
|
+
['扱い', `${RESOLUTION_LABELS[proof.interruption.resolution] ?? '不明'}(${reason})`],
|
|
726
|
+
['判断', text(proof.decision.summary) ?? '判断の記録なし'],
|
|
727
|
+
['仕事への影響', text(proof.decision.work_impact) ?? '影響の記録なし'],
|
|
728
|
+
]),
|
|
729
|
+
}));
|
|
730
|
+
const basis = Array.isArray(proof.decision.basis) ? proof.decision.basis : [];
|
|
731
|
+
const reuse = proof.decision.prior_learning_reused;
|
|
732
|
+
detail.push(workspaceRailBlock(doc, {
|
|
733
|
+
title: '根拠と引き継ぎ',
|
|
734
|
+
content: facts(doc, [
|
|
735
|
+
['根拠', basis.length > 0 ? basis.map(basisText).join(' / ') : '根拠の記録なし'],
|
|
736
|
+
['過去の学習の再利用', reuse === true ? 'あり' : reuse === false ? 'なし' : '未確認'],
|
|
737
|
+
['引き継ぎ', inheritanceText(proof.decision.inheritance)],
|
|
738
|
+
]),
|
|
739
|
+
}));
|
|
740
|
+
const evidence = Array.isArray(proof.outcome.evidence_refs) ? proof.outcome.evidence_refs : [];
|
|
741
|
+
const evidenceText = evidence.length > 0
|
|
742
|
+
? `(証拠: ${evidence.map((entry) => `${text(entry.label) ?? entry.kind} ${entry.status === 'verified' ? '確認済み' : '未確認'}`).join(' / ')})`
|
|
743
|
+
: '';
|
|
744
|
+
const latestLayer = item.feedbackHistory.at(-1)?.target_layer;
|
|
745
|
+
detail.push(workspaceRailBlock(doc, {
|
|
746
|
+
title: '実行と成果',
|
|
747
|
+
content: facts(doc, [
|
|
748
|
+
['実行', `${EXECUTION_LABELS[proof.execution.status] ?? '不明'}${text(proof.execution.summary) ? `: ${text(proof.execution.summary)}` : ''}`],
|
|
749
|
+
['成果の確認', {
|
|
750
|
+
text: `${OUTCOME_LABELS[proof.outcome.status] ?? '不明'}${text(proof.outcome.summary) ? `: ${text(proof.outcome.summary)}` : ''}${evidenceText}`,
|
|
751
|
+
className: `bb-vpr-outcome is-${proof.outcome.status}`,
|
|
752
|
+
}],
|
|
753
|
+
['評価', feedbackText(proof.feedback.status, latestLayer, proof.feedback.summary)],
|
|
754
|
+
]),
|
|
755
|
+
}));
|
|
756
|
+
|
|
757
|
+
if (proof.human_decision) {
|
|
758
|
+
const content = [makeElement(doc, 'p', { text: text(proof.human_decision.why_human) ?? '理由の記録なし' })];
|
|
759
|
+
const options = Array.isArray(proof.human_decision.options) ? proof.human_decision.options : [];
|
|
760
|
+
if (options.length > 0) {
|
|
761
|
+
const list = makeElement(doc, 'ul', { className: 'bb-vpr-options-list' });
|
|
762
|
+
for (const option of options) list.append(makeElement(doc, 'li', { text: `${text(option.label) ?? option.id}: ${text(option.impact) ?? '影響の記録なし'}` }));
|
|
763
|
+
content.push(makeElement(doc, 'h4', { text: '選択肢と影響' }), list);
|
|
764
|
+
}
|
|
765
|
+
detail.push(workspaceRailBlock(doc, { title: 'あなたに戻した理由', className: 'bb-vpr-human-decision', content }));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const feedback = [renderFeedbackForm(doc, item, state, callbacks)];
|
|
769
|
+
if (item.feedbackHistory.length > 1) {
|
|
770
|
+
const history = makeElement(doc, 'details', { className: 'bb-vpr-history' });
|
|
771
|
+
history.append(makeElement(doc, 'summary', { text: `評価の履歴 ${item.feedbackHistory.length}件` }));
|
|
772
|
+
const list = makeElement(doc, 'ol');
|
|
773
|
+
for (const entry of item.feedbackHistory) {
|
|
774
|
+
list.append(makeElement(doc, 'li', { text: `${formatDate(entry.recorded_at)} ${feedbackText(entry.status, entry.target_layer, entry.summary)}` }));
|
|
775
|
+
}
|
|
776
|
+
history.append(list);
|
|
777
|
+
feedback.push(history);
|
|
778
|
+
}
|
|
779
|
+
detail.push(workspaceRailBlock(doc, { title: '評価', className: 'bb-vpr-feedback-block', content: feedback }));
|
|
780
|
+
|
|
781
|
+
const consult = [workspaceButton(doc, { text: 'Codexで相談する(依頼文をコピー)', onClick: () => callbacks.onConsult?.(consultText(proof)) })];
|
|
782
|
+
if (state.consultMessage) consult.push(notice(doc, 'is-muted', state.consultMessage));
|
|
783
|
+
detail.push(workspaceRailBlock(doc, { title: 'Codexで相談', content: consult }));
|
|
784
|
+
|
|
785
|
+
const audit = makeElement(doc, 'details', { className: 'bb-vpr-audit' });
|
|
786
|
+
audit.append(makeElement(doc, 'summary', { text: '監査詳細' }));
|
|
787
|
+
const auditPairs = [
|
|
788
|
+
['intent_id', proof.intent_id],
|
|
789
|
+
['decision_attempt_id', proof.decision_attempt_id],
|
|
790
|
+
['記録日時', proof.recorded_at],
|
|
791
|
+
['状態', String(proof.state)],
|
|
792
|
+
];
|
|
793
|
+
if (text(proof.interruption.question_digest)) auditPairs.push(['question_digest', proof.interruption.question_digest]);
|
|
794
|
+
if (basis.length > 0) auditPairs.push(['根拠の対象ID', basis.map((entry) => entry.entity_id).join(', ')]);
|
|
795
|
+
if (evidence.length > 0) auditPairs.push(['証拠参照', evidence.map((entry) => `${entry.kind}:${entry.ref}`).join(', ')]);
|
|
796
|
+
const artifacts = Array.isArray(proof.execution.artifact_refs) ? proof.execution.artifact_refs : [];
|
|
797
|
+
if (artifacts.length > 0) auditPairs.push(['成果物参照', artifacts.map((entry) => `${entry.kind}:${entry.ref}`).join(', ')]);
|
|
798
|
+
audit.append(workspaceDefinition(doc, auditPairs));
|
|
799
|
+
detail.push(workspaceRailBlock(doc, { className: 'bb-vpr-audit-block', content: audit }));
|
|
800
|
+
|
|
801
|
+
if (state.focusCard && heading && typeof heading.focus === 'function') queueMicrotask(() => heading.focus());
|
|
802
|
+
return detail;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** The detail for the rail (or inline): the selected judgment, else the selected row. Null when nothing can be shown. */
|
|
806
|
+
function renderDetail(doc, state, callbacks) {
|
|
807
|
+
if (state.home?.status !== 'available') return null;
|
|
808
|
+
const item = state.railView === 'judgment' ? findItem(state, state.selectedKey) : null;
|
|
809
|
+
const row = item ? null : visibleRows(state).find((entry) => entry.key === state.selectedRowKey) ?? null;
|
|
810
|
+
if (!item && !row) return null;
|
|
811
|
+
const detail = makeElement(doc, 'div', {
|
|
812
|
+
className: `vpr bb-vpr-detail${item ? ' is-judgment' : ' is-row'}`,
|
|
813
|
+
attrs: { 'data-detail': item ? 'judgment' : 'row' },
|
|
814
|
+
});
|
|
815
|
+
detail.append(...(item ? renderJudgmentDetail(doc, state, item, callbacks) : renderRowDetail(doc, state, row, callbacks)));
|
|
816
|
+
return detail;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Render the whole surface into a host-owned root, and the detail into `options.rail` when given. */
|
|
820
|
+
export function renderValueProofReview(root, state, callbacks = {}, options = {}) {
|
|
821
|
+
const doc = getDocument(options.document);
|
|
822
|
+
const rail = options.rail ?? null;
|
|
823
|
+
root.replaceChildren();
|
|
824
|
+
const surface = makeElement(doc, 'div', {
|
|
825
|
+
className: 'vpr',
|
|
826
|
+
attrs: { 'data-contract-version': VALUE_PROOF_REVIEW_UI_CONTRACT_VERSION, 'data-layout': rail ? 'rail' : 'inline' },
|
|
827
|
+
});
|
|
828
|
+
surface.append(renderPageHead(doc, options.page));
|
|
829
|
+
const finish = (detail, empty) => {
|
|
830
|
+
if (rail) {
|
|
831
|
+
if (detail) rail.replaceChildren(detail);
|
|
832
|
+
else rail.replaceChildren(workspaceDetailEmpty(doc, empty));
|
|
833
|
+
} else if (detail) {
|
|
834
|
+
const inline = makeElement(doc, 'section', { className: 'bb-vpr-inline-detail', attrs: { 'aria-label': '選択中の項目' } });
|
|
835
|
+
inline.append(detail);
|
|
836
|
+
surface.append(inline);
|
|
837
|
+
}
|
|
838
|
+
root.append(surface);
|
|
839
|
+
return surface;
|
|
840
|
+
};
|
|
841
|
+
const unreadable = { mark: '!', title: '判断を表示できません', text: '記録の範囲の注記を確かめてください。' };
|
|
842
|
+
|
|
843
|
+
if (state.phase === 'loading' && !state.home) {
|
|
844
|
+
surface.append(workspaceNotice(doc, { label: '記録の範囲', text: '判断の記録を読み込んでいます。' }));
|
|
845
|
+
return finish(null, { mark: '…', title: '読み込んでいます', text: '判断の記録を読み込んでいます。' });
|
|
846
|
+
}
|
|
847
|
+
if (state.phase === 'error') {
|
|
848
|
+
const retry = workspaceButton(doc, { text: '再試行', onClick: () => void callbacks.onReload?.() });
|
|
849
|
+
surface.append(workspaceNotice(doc, {
|
|
850
|
+
label: '記録の範囲',
|
|
851
|
+
tone: 'danger',
|
|
852
|
+
text: noticeBody(doc, `判断の記録を取得できません(${state.error ?? 'request_failed'})。0件ではありません。`, retry),
|
|
853
|
+
}));
|
|
854
|
+
return finish(null, unreadable);
|
|
855
|
+
}
|
|
856
|
+
const home = state.home;
|
|
857
|
+
if (!home || home.status === 'invalid') {
|
|
858
|
+
surface.append(workspaceNotice(doc, { label: '記録の範囲', tone: 'danger', text: `判断の記録を表示できません(${home?.reason ?? '応答なし'})。0件ではありません。` }));
|
|
859
|
+
return finish(null, unreadable);
|
|
860
|
+
}
|
|
861
|
+
if (home.status === 'unavailable') {
|
|
862
|
+
const hostNotice = options.unavailableNotice;
|
|
863
|
+
const title = makeElement(doc, 'p', { className: 'bb-vpr-notice-title', text: text(hostNotice?.title) ?? '判断journalに接続できません' });
|
|
864
|
+
surface.append(workspaceNotice(doc, {
|
|
865
|
+
label: '記録の範囲',
|
|
866
|
+
tone: 'danger',
|
|
867
|
+
text: noticeBody(doc,
|
|
868
|
+
title,
|
|
869
|
+
home.root ? `場所: ${home.root}(${home.reason})` : `理由: ${home.reason}`,
|
|
870
|
+
`${text(hostNotice?.guidance) ?? '記録の場所は、起動時の --journal か環境変数 BRAINBASE_JUDGMENT_JOURNAL_DIR で指定できます。'}0件としては扱いません。`),
|
|
871
|
+
}));
|
|
872
|
+
return finish(null, unreadable);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
surface.append(renderCoverageNotice(doc, home));
|
|
876
|
+
if (home.coverage.possiblyStalled) {
|
|
877
|
+
surface.append(workspaceNotice(doc, {
|
|
878
|
+
label: '記録の停止',
|
|
879
|
+
tone: 'warning',
|
|
880
|
+
text: 'しばらく新しい記録がありません。記録が止まっている可能性があります。0件を「何も無かった」とは扱いません。',
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
surface.append(renderFilters(doc, home, state, callbacks), renderMetrics(doc, home));
|
|
884
|
+
if (home.delegationMap.rated === 0 && home.delegationMap.judged > 0) {
|
|
885
|
+
surface.append(workspaceNotice(doc, { label: '評価', text: 'まだ評価がありません。任せている・戻しているは評価から決まります。' }));
|
|
886
|
+
}
|
|
887
|
+
for (const highlight of [
|
|
888
|
+
renderHighlight(doc, state, home.delegationMap.correctedAfterContinue, {
|
|
889
|
+
className: 'is-corrected', title: '聞かずに進めたが直した判断', hint: 'あなたに聞かずに進めたあと、あなたが訂正・取り消しした判断',
|
|
890
|
+
}, callbacks),
|
|
891
|
+
renderHighlight(doc, state, home.delegationMap.continuedAfterAsk, {
|
|
892
|
+
className: 'is-after-ask', title: '評価の後も聞かずに進めた判断', hint: '「次回は聞く」と評価した種類で、その後も聞かずに進めた判断。評価がまだ次の判断に効いていません',
|
|
893
|
+
}, callbacks),
|
|
894
|
+
]) if (highlight) surface.append(highlight);
|
|
895
|
+
surface.append(renderDelegationMap(doc, state, callbacks));
|
|
896
|
+
if (home.sections.other.length > 0) {
|
|
897
|
+
surface.append(makeElement(doc, 'p', { className: 'bb-vpr-hint', text: `その他 ${home.sections.other.length}件(判断の代行が無い記録)` }));
|
|
898
|
+
}
|
|
899
|
+
return finish(renderDetail(doc, state, callbacks), {
|
|
900
|
+
mark: '判',
|
|
901
|
+
title: '判断の種類を選択',
|
|
902
|
+
text: '委任の地図から行を選ぶと、その種類の判断が出ます。判断を選ぶと、根拠と評価の欄が出ます。',
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function joinPath(basePath, suffix) {
|
|
907
|
+
return `${String(basePath || '/api/value-proofs').replace(/\/+$/u, '')}${suffix}`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function readErrorMessage(response) {
|
|
911
|
+
try {
|
|
912
|
+
const body = await response.json();
|
|
913
|
+
return text(body?.error?.message) ?? text(body?.error?.code) ?? `HTTP ${response.status}`;
|
|
914
|
+
} catch {
|
|
915
|
+
return `HTTP ${response.status}`;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export function createValueProofReviewUI({
|
|
920
|
+
root,
|
|
921
|
+
/** The host's right-rail element for the selected row or judgment. Without it, the detail follows the workspace content. */
|
|
922
|
+
rail,
|
|
923
|
+
/** Page context from the host: `{ crumbs, source }` for the page head. */
|
|
924
|
+
page,
|
|
925
|
+
document: explicitDocument,
|
|
926
|
+
fetcher,
|
|
927
|
+
basePath = '/api/value-proofs',
|
|
928
|
+
token,
|
|
929
|
+
clipboard,
|
|
930
|
+
/** Optional `{ title, guidance }` for hosts where the local journal hint does not apply. */
|
|
931
|
+
unavailableNotice,
|
|
932
|
+
autoLoad = true,
|
|
933
|
+
} = {}) {
|
|
934
|
+
if (!root) throw new TypeError('root is required');
|
|
935
|
+
const doc = getDocument(explicitDocument);
|
|
936
|
+
const request = typeof fetcher === 'function'
|
|
937
|
+
? fetcher
|
|
938
|
+
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
939
|
+
const writeClipboard = clipboard
|
|
940
|
+
?? (typeof navigator !== 'undefined' && navigator.clipboard ? (value) => navigator.clipboard.writeText(value) : null);
|
|
941
|
+
|
|
942
|
+
const state = {
|
|
943
|
+
phase: 'loading',
|
|
944
|
+
home: null,
|
|
945
|
+
error: null,
|
|
946
|
+
selectedKey: null,
|
|
947
|
+
selectedRowKey: null,
|
|
948
|
+
/** What the detail shows: the selected map row, or the selected judgment. */
|
|
949
|
+
railView: 'row',
|
|
950
|
+
unratedOnly: false,
|
|
951
|
+
needsHumanOnly: false,
|
|
952
|
+
draft: { status: '', summary: '', targetLayer: '' },
|
|
953
|
+
save: { state: 'idle', message: '' },
|
|
954
|
+
focusCard: false,
|
|
955
|
+
consultMessage: '',
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const resetJudgmentInput = () => {
|
|
959
|
+
state.draft = { status: '', summary: '', targetLayer: '' };
|
|
960
|
+
state.save = { state: 'idle', message: '' };
|
|
961
|
+
state.consultMessage = '';
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const applyFilter = () => {
|
|
965
|
+
const previous = state.selectedKey;
|
|
966
|
+
revealSelection(state);
|
|
967
|
+
if (state.selectedKey !== previous) resetJudgmentInput();
|
|
968
|
+
controller.render();
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
const callbacks = {
|
|
972
|
+
onReload: () => controller.load(),
|
|
973
|
+
onToggleUnrated(value) {
|
|
974
|
+
state.unratedOnly = value;
|
|
975
|
+
applyFilter();
|
|
976
|
+
},
|
|
977
|
+
onToggleNeedsHuman(value) {
|
|
978
|
+
state.needsHumanOnly = value;
|
|
979
|
+
applyFilter();
|
|
980
|
+
},
|
|
981
|
+
onSelectRow(key) {
|
|
982
|
+
state.selectedRowKey = key;
|
|
983
|
+
state.railView = 'row';
|
|
984
|
+
controller.render();
|
|
985
|
+
},
|
|
986
|
+
onShowRow() {
|
|
987
|
+
state.railView = 'row';
|
|
988
|
+
controller.render();
|
|
989
|
+
},
|
|
990
|
+
onSelect(key) {
|
|
991
|
+
if (key !== state.selectedKey) resetJudgmentInput();
|
|
992
|
+
state.selectedKey = key;
|
|
993
|
+
state.selectedRowKey = rowOfItem(state, key)?.key ?? state.selectedRowKey;
|
|
994
|
+
state.railView = 'judgment';
|
|
995
|
+
state.focusCard = true;
|
|
996
|
+
controller.render();
|
|
997
|
+
state.focusCard = false;
|
|
998
|
+
},
|
|
999
|
+
onDraft(patch) {
|
|
1000
|
+
state.draft = { ...state.draft, ...patch };
|
|
1001
|
+
},
|
|
1002
|
+
async onSubmit(item) {
|
|
1003
|
+
const option = VALUE_PROOF_FEEDBACK_OPTIONS.find((entry) => entry.value === state.draft.status);
|
|
1004
|
+
if (!option) {
|
|
1005
|
+
state.save = { state: 'error', message: '評価を選んでください。' };
|
|
1006
|
+
controller.render();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (option.value === 'corrected' && !VALUE_PROOF_CORRECTION_LAYERS.includes(state.draft.targetLayer)) {
|
|
1010
|
+
state.save = { state: 'error', message: '「訂正」では、何を直すかを選んでください。' };
|
|
1011
|
+
controller.render();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (option.summary === 'required' && !text(state.draft.summary)) {
|
|
1015
|
+
state.save = { state: 'error', message: `「${option.label}」には理由の1文が必要です。` };
|
|
1016
|
+
controller.render();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (!request) {
|
|
1020
|
+
state.save = { state: 'error', message: '保存先のAPIが未設定です。' };
|
|
1021
|
+
controller.render();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
state.save = { state: 'saving', message: '' };
|
|
1025
|
+
controller.render();
|
|
1026
|
+
try {
|
|
1027
|
+
const response = await request(joinPath(basePath, '/feedback'), {
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
headers: { 'Content-Type': 'application/json', 'X-Brainbase-Review-Token': token ?? '' },
|
|
1030
|
+
body: JSON.stringify({
|
|
1031
|
+
intent_id: item.proof.intent_id,
|
|
1032
|
+
decision_attempt_id: item.proof.decision_attempt_id,
|
|
1033
|
+
status: option.value,
|
|
1034
|
+
summary: text(state.draft.summary),
|
|
1035
|
+
target_layer: option.value === 'corrected' ? state.draft.targetLayer : null,
|
|
1036
|
+
}),
|
|
1037
|
+
});
|
|
1038
|
+
if (!response.ok) throw new Error(await readErrorMessage(response));
|
|
1039
|
+
await controller.load();
|
|
1040
|
+
const key = itemKey(item.proof);
|
|
1041
|
+
const saved = findItem(state, key);
|
|
1042
|
+
if (saved?.proof.feedback.status !== option.value) throw new Error('保存後の読み戻しで評価を確認できません');
|
|
1043
|
+
// Keep the read-back on the judgment just rated, even when a filter now hides it from the lists.
|
|
1044
|
+
state.selectedKey = key;
|
|
1045
|
+
state.selectedRowKey = rowOfItem(state, key)?.key ?? state.selectedRowKey;
|
|
1046
|
+
state.railView = 'judgment';
|
|
1047
|
+
state.draft = { status: '', summary: '', targetLayer: '' };
|
|
1048
|
+
state.save = { state: 'saved', message: `「${option.label}」を保存しました(読み戻し済み)。` };
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
state.save = { state: 'error', message: `保存できませんでした: ${error instanceof Error ? error.message : 'request_failed'}。入力は残しています。` };
|
|
1051
|
+
}
|
|
1052
|
+
controller.render();
|
|
1053
|
+
},
|
|
1054
|
+
async onConsult(value) {
|
|
1055
|
+
if (!writeClipboard) {
|
|
1056
|
+
state.consultMessage = `コピーできない環境です。次の文をCodexへ貼ってください。\n${value}`;
|
|
1057
|
+
} else {
|
|
1058
|
+
try {
|
|
1059
|
+
await writeClipboard(value);
|
|
1060
|
+
state.consultMessage = '依頼文をコピーしました。Codexに貼ると、この判断の相談から始められます。';
|
|
1061
|
+
} catch {
|
|
1062
|
+
state.consultMessage = `コピーできませんでした。次の文をCodexへ貼ってください。\n${value}`;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
controller.render();
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const controller = {
|
|
1070
|
+
get state() { return state; },
|
|
1071
|
+
render() {
|
|
1072
|
+
renderValueProofReview(root, state, callbacks, { document: doc, unavailableNotice, rail, page });
|
|
1073
|
+
return controller;
|
|
1074
|
+
},
|
|
1075
|
+
async load() {
|
|
1076
|
+
if (!request) {
|
|
1077
|
+
state.phase = 'error';
|
|
1078
|
+
state.error = 'api_unavailable';
|
|
1079
|
+
controller.render();
|
|
1080
|
+
return state.home;
|
|
1081
|
+
}
|
|
1082
|
+
state.phase = 'loading';
|
|
1083
|
+
try {
|
|
1084
|
+
const response = await request(joinPath(basePath, '/home'));
|
|
1085
|
+
if (!response.ok) throw new Error(await readErrorMessage(response));
|
|
1086
|
+
state.home = normalizeValueProofReviewHome(await response.json());
|
|
1087
|
+
state.phase = 'ready';
|
|
1088
|
+
state.error = null;
|
|
1089
|
+
revealSelection(state);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
state.phase = 'error';
|
|
1092
|
+
state.error = error instanceof Error ? error.message : 'request_failed';
|
|
1093
|
+
}
|
|
1094
|
+
controller.render();
|
|
1095
|
+
return state.home;
|
|
1096
|
+
},
|
|
1097
|
+
callbacks,
|
|
1098
|
+
};
|
|
1099
|
+
controller.render();
|
|
1100
|
+
if (autoLoad) void controller.load();
|
|
1101
|
+
return controller;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export default renderValueProofReview;
|