@arcfold/dsh-method-ui 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/lib/client.js +4237 -0
- package/package.json +35 -4
- package/src/Brand.mjs +17 -0
- package/src/CollaborationView.mjs +56 -0
- package/src/DesktopModels.mjs +40 -0
- package/src/DesktopSetup.mjs +48 -0
- package/src/GoalDock.mjs +130 -0
- package/src/GoalWorkList.mjs +52 -0
- package/src/ImportDock.mjs +172 -0
- package/src/MethodDock.mjs +145 -0
- package/src/ModelSettings.mjs +144 -0
- package/src/OpportunityDock.mjs +66 -0
- package/src/UnderstandingDock.mjs +117 -0
- package/src/browse-scope.mjs +6 -0
- package/src/browse-source.mjs +31 -0
- package/src/client.mjs +138 -0
- package/src/desktop-setup-host.mjs +30 -0
- package/src/desktop-workspace.mjs +8 -0
- package/src/goal-host.mjs +133 -0
- package/src/goal-locales.mjs +78 -0
- package/src/goal-source.mjs +72 -0
- package/src/import-host.mjs +98 -0
- package/src/import-locales.mjs +126 -0
- package/src/import-source.mjs +83 -0
- package/src/index.mjs +127 -0
- package/src/locales.mjs +153 -0
- package/src/main-session.mjs +7 -0
- package/src/method-source.mjs +82 -0
- package/src/model-host.mjs +56 -0
- package/src/model-settings-locales.mjs +91 -0
- package/src/model-settings-source.mjs +119 -0
- package/src/opportunity-host.mjs +118 -0
- package/src/opportunity-locales.mjs +34 -0
- package/src/opportunity-source.mjs +56 -0
- package/src/style.css +99 -0
- package/src/tree-client.mjs +15 -0
- package/src/tree-host.mjs +39 -0
- package/src/tree-source.mjs +65 -0
- package/src/tree-style.css +19 -0
- package/src/tree-view.mjs +278 -0
- package/src/understanding-host.mjs +123 -0
- package/src/understanding-locales.mjs +38 -0
- package/src/understanding-source.mjs +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcfold/dsh-method-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.mjs",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.mjs",
|
|
8
|
+
"./client": "./lib/client.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"dsh": {
|
|
12
|
+
"client": {
|
|
13
|
+
"platform": "web",
|
|
14
|
+
"inject": [
|
|
15
|
+
"@deepseek-ai/dsh-client-connection",
|
|
16
|
+
"@deepseek-ai/dsh-client-locale",
|
|
17
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
18
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
19
|
+
"@deepseek-ai/dsh-client-file-upload",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
26
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.2-rc.1",
|
|
27
|
+
"react": "18.3.1"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"lib",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"THIRD_PARTY_NOTICES.md"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
package/src/Brand.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Visuals only: the native parent owns navigation, permissions and focus.
|
|
4
|
+
export function BrandMark({ size = 24, className }) {
|
|
5
|
+
return React.createElement('svg', { width: size, height: size, viewBox: '0 0 32 32',
|
|
6
|
+
className, fill: 'none', 'aria-hidden': true, focusable: false,
|
|
7
|
+
style: { flexShrink: 0, color: 'inherit' } },
|
|
8
|
+
React.createElement('path', { d: 'M5 26 16 5l11 21M10 19h12M16 5v21',
|
|
9
|
+
stroke: 'currentColor', strokeWidth: 2.4, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
10
|
+
}
|
|
11
|
+
export function BrandName() { return React.createElement('span', null, 'Arcfold'); }
|
|
12
|
+
export function installBrand(ctx) {
|
|
13
|
+
for (const [name, component] of [
|
|
14
|
+
['sidebar.brand.mark', BrandMark], ['sidebar.brand.name', BrandName],
|
|
15
|
+
['conversation.hero.brand.mark', BrandMark],
|
|
16
|
+
]) ctx.slots.inject(name, () => ctx.slots.register({ name }, component));
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { OpportunityDock } from './OpportunityDock.mjs';
|
|
3
|
+
import { MethodDock } from './MethodDock.mjs';
|
|
4
|
+
import { GoalDock } from './GoalDock.mjs';
|
|
5
|
+
import { UnderstandingDock } from './UnderstandingDock.mjs';
|
|
6
|
+
import { ImportDock } from './ImportDock.mjs';
|
|
7
|
+
import { ModelSettings } from './ModelSettings.mjs';
|
|
8
|
+
|
|
9
|
+
const h = React.createElement;
|
|
10
|
+
function Panels(props) {
|
|
11
|
+
return h(React.Fragment, null, h(OpportunityDock, props), h(MethodDock, props),
|
|
12
|
+
h(GoalDock, props), h(UnderstandingDock, props), h(ImportDock, props));
|
|
13
|
+
}
|
|
14
|
+
function Models(props) {
|
|
15
|
+
return h(ModelSettings, { ...props, onRefresh: props.onModelsRefresh });
|
|
16
|
+
}
|
|
17
|
+
function Dialog({ title, onClose, children, t }) {
|
|
18
|
+
const ref = useRef(null), titleId = useId();
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const dialog = ref.current;
|
|
21
|
+
dialog.showModal();
|
|
22
|
+
return () => { dialog.close(); };
|
|
23
|
+
}, []);
|
|
24
|
+
return h('dialog', { ref, className: 'arcfold-dialog', 'aria-labelledby': titleId,
|
|
25
|
+
onCancel: event => { event.preventDefault(); onClose(); } },
|
|
26
|
+
h('header', { className: 'arcfold-toolbar' },
|
|
27
|
+
h('h2', { id: titleId }, title),
|
|
28
|
+
h('button', { type: 'button', onClick: onClose, autoFocus: true }, t('collaborationClose'))),
|
|
29
|
+
children);
|
|
30
|
+
}
|
|
31
|
+
export function CollaborationView(props) {
|
|
32
|
+
const scope = props.useBrowseScope ? props.useBrowseScope(value => value) : { viewScope: 'all', epoch: 0 };
|
|
33
|
+
const [modelsOpen, setModelsOpen] = useState(false);
|
|
34
|
+
useEffect(() => { setModelsOpen(false); }, [props.opportunitySessionId]);
|
|
35
|
+
return h('section', { className: 'arcfold-view', 'aria-label': props.t('collaborationTitle') },
|
|
36
|
+
h('header', { className: 'arcfold-toolbar' },
|
|
37
|
+
h('h2', null, props.t('collaborationTitle')),
|
|
38
|
+
h('button', { type: 'button', onClick: () => props.onOpenModels ? props.onOpenModels() : setModelsOpen(true) }, props.t('modelsTitle'))),
|
|
39
|
+
h('div', { className: 'arcfold-view-scope', role: 'group', 'aria-label': props.t('collaborationViewScope') },
|
|
40
|
+
['all', 'current'].map(viewScope => h('button', { key: viewScope, type: 'button',
|
|
41
|
+
'aria-pressed': scope.viewScope === viewScope,
|
|
42
|
+
onClick: () => props.onViewScopeChange?.(viewScope) }, props.t(viewScope === 'all' ? 'collaborationAll' : 'collaborationCurrent')))),
|
|
43
|
+
h(Panels, { ...props, viewScope: scope.viewScope, key: `${props.opportunitySessionId}:${scope.epoch}` }),
|
|
44
|
+
modelsOpen && h(Dialog, { title: props.t('modelsTitle'), t: props.t, onClose: () => setModelsOpen(false) }, h(Models, props)));
|
|
45
|
+
}
|
|
46
|
+
export function CollaborationHero(props) {
|
|
47
|
+
const [panel, setPanel] = useState(null);
|
|
48
|
+
// A session switch must not carry an open dialog or unfinished draft forward.
|
|
49
|
+
useEffect(() => { setPanel(null); }, [props.opportunitySessionId]);
|
|
50
|
+
return h('div', { className: 'arcfold-hero-entry' },
|
|
51
|
+
h('button', { type: 'button', onClick: () => setPanel('collaboration') }, props.t('collaborationTitle')),
|
|
52
|
+
h('button', { type: 'button', onClick: () => setPanel('models') }, props.t('modelsTitle')),
|
|
53
|
+
panel && h(Dialog, { title: props.t(panel === 'models' ? 'modelsTitle' : 'collaborationTitle'),
|
|
54
|
+
t: props.t, onClose: () => setPanel(null) },
|
|
55
|
+
panel === 'models' ? h(Models, props) : h(CollaborationView, { ...props, onOpenModels: () => setPanel('models') })));
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
const h = React.createElement, REF = 'DEEPSEEK_API_KEY';
|
|
3
|
+
|
|
4
|
+
/** Secrets stay in this local input until submitted to DSH's credential API. */
|
|
5
|
+
export function DesktopModels({ rpc }) {
|
|
6
|
+
const [info, setInfo] = useState(null), [secret, setSecret] = useState('');
|
|
7
|
+
const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
|
|
8
|
+
async function refresh() {
|
|
9
|
+
const result = await rpc.call('/api', 'credentials/describe', { args: { refs: [REF] } });
|
|
10
|
+
if (result?.ok !== true) throw new Error('无法读取密钥状态。');
|
|
11
|
+
const value = result.value?.[REF];
|
|
12
|
+
setInfo({ configured: value?.configured === true, writable: value?.writable === true });
|
|
13
|
+
}
|
|
14
|
+
useEffect(() => { let active = true;
|
|
15
|
+
void rpc.call('/api', 'credentials/describe', { args: { refs: [REF] } }).then(result => {
|
|
16
|
+
if (!active) return;
|
|
17
|
+
if (result?.ok !== true) throw new Error('unavailable');
|
|
18
|
+
const value = result.value?.[REF]; setInfo({ configured: value?.configured === true, writable: value?.writable === true });
|
|
19
|
+
}).catch(() => { if (active) setMessage('无法读取密钥状态,请稍后重试。'); });
|
|
20
|
+
return () => { active = false; };
|
|
21
|
+
}, [rpc]);
|
|
22
|
+
async function save() {
|
|
23
|
+
if (!info?.writable || !secret.trim() || busy) return;
|
|
24
|
+
setBusy(true); setMessage('正在保存…');
|
|
25
|
+
try {
|
|
26
|
+
const result = await rpc.call('/api', 'credentials/set', { args: { ref: REF, value: secret } });
|
|
27
|
+
if (result?.ok !== true) throw new Error('unconfirmed');
|
|
28
|
+
await refresh(); setMessage('已保存到 Desktop 的凭据服务。');
|
|
29
|
+
} catch { setMessage('保存结果尚未确认,请刷新状态后再决定。'); }
|
|
30
|
+
finally { setSecret(''); setBusy(false); }
|
|
31
|
+
}
|
|
32
|
+
return h('section', { className: 'arcfold-desktop-setup' }, h('h2', null, 'DeepSeek'),
|
|
33
|
+
h('p', null, '密钥由 DSH 统一保存和使用;Arcfold 不把密钥写进自身数据或费用记录。'),
|
|
34
|
+
h('p', null, !info ? '正在读取密钥状态…' : info.configured ? 'DeepSeek 密钥已配置。' : '尚未配置 DeepSeek 密钥。'),
|
|
35
|
+
h('label', null, 'DeepSeek API 密钥', h('input', { type: 'password', autoComplete: 'off', value: secret,
|
|
36
|
+
disabled: !info?.writable || busy, onChange: event => setSecret(event.target.value) })),
|
|
37
|
+
h('button', { type: 'button', disabled: !info?.writable || !secret.trim() || busy, onClick: () => void save() }, '保存密钥'),
|
|
38
|
+
h('button', { type: 'button', disabled: busy, onClick: () => void refresh().catch(() => setMessage('无法读取密钥状态。')) }, '刷新状态'),
|
|
39
|
+
message && h('p', { role: 'status' }, message));
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const h = React.createElement;
|
|
4
|
+
export function DesktopSetup({ initial, rpc, openWorkspace }) {
|
|
5
|
+
const [info, setInfo] = useState(initial), [checked, setChecked] = useState(false);
|
|
6
|
+
const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
|
|
7
|
+
async function refresh() {
|
|
8
|
+
const reply = await rpc.call('/api', 'arcfoldDesktop/status', { args: { request: {} } });
|
|
9
|
+
if (reply?.ok !== true) throw new Error('无法读取设置状态。');
|
|
10
|
+
setInfo(reply.value); return reply.value;
|
|
11
|
+
}
|
|
12
|
+
async function open(path) {
|
|
13
|
+
if (typeof openWorkspace !== 'function') throw new Error('DSH 工作区服务尚未就绪。');
|
|
14
|
+
await openWorkspace(path); globalThis.location.reload();
|
|
15
|
+
}
|
|
16
|
+
async function start() {
|
|
17
|
+
if (!checked || busy) return;
|
|
18
|
+
setBusy(true); setMessage('正在创建独立数据并启动 Arcfold…');
|
|
19
|
+
let initialized = false;
|
|
20
|
+
try {
|
|
21
|
+
const result = await rpc.call('/api', 'arcfoldDesktop/start', { args: { request: { confirmed: true } } });
|
|
22
|
+
if (result?.ok !== true) throw new Error('设置结果尚未确认。');
|
|
23
|
+
setInfo(result.value);
|
|
24
|
+
if (result.value?.state === 'ready') { initialized = true; setMessage('设置完成,正在打开独立工作区。'); await open(result.value.workspace); return; }
|
|
25
|
+
setMessage('设置尚未完成,请查看当前状态。');
|
|
26
|
+
} catch { setMessage(initialized ? '设置已完成,但工作区未能打开。请重试打开工作区。' : '设置结果尚未确认,请刷新状态,不要重复提交。'); try { await refresh(); } catch {} }
|
|
27
|
+
finally { setBusy(false); }
|
|
28
|
+
}
|
|
29
|
+
const refreshButton = h('button', { type: 'button', onClick: () => void refresh().catch(() => setMessage('无法读取设置状态。')) }, '刷新状态');
|
|
30
|
+
if (info.state !== 'needs_setup') return h('section', { className: 'arcfold-desktop-setup' },
|
|
31
|
+
h('h2', null, 'Arcfold 设置'), h('p', null, info.state === 'configured' ? '正在读取已保存的设置。' :
|
|
32
|
+
info.state === 'ready' ? 'Arcfold 已就绪。' : 'Arcfold 尚未完成启动,请保留已有数据并检查设置。'),
|
|
33
|
+
info.activationError && h('p', { role: 'alert' }, info.activationError), message && h('p', { role: 'status' }, message),
|
|
34
|
+
info.state === 'ready' && h('button', { type: 'button', disabled: busy, onClick: () => { setBusy(true); void open(info.workspace).catch(() => setMessage('无法打开工作区,请稍后重试。')).finally(() => setBusy(false)); } }, '打开 Arcfold 工作区'), refreshButton);
|
|
35
|
+
return h('section', { className: 'arcfold-desktop-setup' },
|
|
36
|
+
h('h2', null, '开始使用 Arcfold'),
|
|
37
|
+
h('p', null, 'Arcfold 会在 Desktop 中保存独立的目标、经历和费用记录。其他版本已有的数据不会自动导入。'),
|
|
38
|
+
h('p', null, '工作区:', h('code', null, info.workspace)),
|
|
39
|
+
h('p', null, '数据位置:', h('code', null, info.dataRoot)),
|
|
40
|
+
h('p', null, '主对话和与其相关的后台处理使用当前对话选择的模型;不同后台任务保留各自的思考设置。完成后可在“设置 → 模型”中配置 DeepSeek 密钥。'),
|
|
41
|
+
h('p', null, '跨多段对话自动整理时,使用最新相关对话的模型;没有关联对话时使用默认模型。'),
|
|
42
|
+
h('p', null, '模型服务会产生费用。这里记录请求和估算费用,但不设本地消费上限;实际扣费以服务商为准。'),
|
|
43
|
+
h('p', null, '当前估算价格资料核对日期:', info.priceReviewedAt, '。'),
|
|
44
|
+
h('button', { type: 'button', 'aria-pressed': checked, onClick: () => setChecked(value => !value) },
|
|
45
|
+
checked ? '已确认:模型调用可能产生费用,本机不设消费上限' : '确认:模型调用可能产生费用,本机不设消费上限'),
|
|
46
|
+
h('div', null, h('button', { type: 'button', disabled: !checked || busy, onClick: () => void start() }, busy ? '正在设置…' : '创建并开始'), ' ', refreshButton),
|
|
47
|
+
message && h('p', { role: 'status' }, message));
|
|
48
|
+
}
|
package/src/GoalDock.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { presentationLabel } from './locales.mjs';
|
|
2
|
+
import { GoalWorkList } from './GoalWorkList.mjs';
|
|
3
|
+
import React, { useRef, useState } from 'react';
|
|
4
|
+
const h = React.createElement;
|
|
5
|
+
const content = value => typeof value === 'string' ? value : value?.content ?? '';
|
|
6
|
+
const version = goal => JSON.stringify([goal.id, goal.revision, goal.definitionVersion]);
|
|
7
|
+
|
|
8
|
+
function GoalCard({ detail, works, canDiscuss, onGoalDiscuss, onGoalRefresh, onAction, workBusy, t }) {
|
|
9
|
+
const goal = detail.goal, allowed = detail.availability?.allowed === true;
|
|
10
|
+
const [draft, setDraft] = useState(''), [draftVersion, setDraftVersion] = useState(null);
|
|
11
|
+
const [busy, setBusy] = useState(false), [error, setError] = useState(null), [notice, setNotice] = useState(null);
|
|
12
|
+
const pending = useRef(false), retryIds = useRef(new Map());
|
|
13
|
+
const staleDraft = Boolean(draft && draftVersion !== version(goal));
|
|
14
|
+
async function discuss() {
|
|
15
|
+
if (pending.current || !draft.trim() || staleDraft || !canDiscuss || !(allowed || detail.repairAllowed)) return;
|
|
16
|
+
const request = { goalId: goal.id, expectedRevision: goal.revision, definitionVersion: goal.definitionVersion,
|
|
17
|
+
mode: allowed ? 'discuss' : 'repair', text: draft };
|
|
18
|
+
const key = JSON.stringify(request);
|
|
19
|
+
if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
|
|
20
|
+
request.requestId = retryIds.current.get(key);
|
|
21
|
+
pending.current = true; setBusy(true); setError(null); setNotice(null);
|
|
22
|
+
try {
|
|
23
|
+
const result = await onGoalDiscuss(request), admission = result.binding?.admission?.state;
|
|
24
|
+
setNotice(t(admission === 'accepted' ? 'goalAccepted' : admission === 'failed' ? 'goalFailed' : 'goalUnknown'));
|
|
25
|
+
await onGoalRefresh();
|
|
26
|
+
} catch (failure) {
|
|
27
|
+
setError(failure.remote ? failure.message : `${failure.message || t('failed')} ${t('goalUnknown')}`);
|
|
28
|
+
if (['revision_conflict', 'goal_source_invalid', 'goal_unavailable'].includes(failure.code)) await onGoalRefresh();
|
|
29
|
+
} finally { pending.current = false; setBusy(false); }
|
|
30
|
+
}
|
|
31
|
+
const text = (key, value) => h('div', { key, className: 'text' }, content(value));
|
|
32
|
+
const label = (kind, value) => presentationLabel(kind, value, t);
|
|
33
|
+
return h('article', { className: 'goal-card', id: `arcfold-goal-${goal.id}`, 'aria-label': goal.title || t('goalUnavailable') },
|
|
34
|
+
h('header', { className: 'goal-card-heading' }, h('h3', null, goal.title || t('goalUnavailable')),
|
|
35
|
+
h('span', { className: 'goal-status-badge' }, label('goal', goal.status))),
|
|
36
|
+
detail.browse?.related === false && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
|
|
37
|
+
!allowed && h('p', { role: 'alert' }, detail.availability?.message || t('goalUnavailable')),
|
|
38
|
+
allowed && h('div', { className: 'goal-outcome text' }, content(goal.outcome)),
|
|
39
|
+
h('div', { className: 'goal-card-section' },
|
|
40
|
+
works.length ? h(GoalWorkList, { works, title: t('goalCurrentWork'), onAction, busy: workBusy, t })
|
|
41
|
+
: h(React.Fragment, null, h('h4', null, t('goalCurrentWork')), h('p', { className: 'meta' }, t('goalNoCurrentWork')))),
|
|
42
|
+
allowed && h('div', { className: 'goal-card-section goal-progress' },
|
|
43
|
+
h('h4', null, t('goalProgress')),
|
|
44
|
+
text('progress', detail.coverage?.progressUnavailable > 0 ? t('goalProgressUnavailable') : content(goal.progress) || t('goalNoProgress'))),
|
|
45
|
+
h('details', { className: 'goal-card-evidence' }, h('summary', null, t('goalEvidence')),
|
|
46
|
+
h('p', { className: 'meta' }, `${t('goalDefinition')} ${goal.definitionVersion} · ${t('revision')} ${goal.revision}`),
|
|
47
|
+
allowed && h(React.Fragment, null,
|
|
48
|
+
h('h4', null, t('goalRelations')),
|
|
49
|
+
!(detail.relations || []).length && h('p', { className: 'meta' }, t('goalNoRelations')),
|
|
50
|
+
(detail.relations || []).map((item, index) => h('p', { className: 'meta', key: index },
|
|
51
|
+
`${label('relation', item.type)} · ${item.targetId} · ${item.state === 'needs_review' ? t('goalRelationReview') : item.state === 'unavailable' ? t('goalRelationUnavailable') : ''}`)),
|
|
52
|
+
h('h4', null, t('sources')),
|
|
53
|
+
(detail.sources || []).map(source => h('div', { className: 'source', key: source.id },
|
|
54
|
+
h('p', { className: 'meta' }, `${source.id} · ${label('role', source.role)} · ${t('revision')} ${source.revision} · ${source.occurredAt || ''}`),
|
|
55
|
+
text('content', source.content), h('p', { className: 'meta' }, JSON.stringify(source.provenance || null))))),
|
|
56
|
+
(detail.links || []).map(link => h('div', { className: 'source', key: link.id },
|
|
57
|
+
h('p', { className: 'meta' }, `${link.sessionId} · ${t('turn')} ${link.turn} · ${t('goalDefinition')} ${link.definitionVersion} · ${label('outcome', link.outcome?.outcome ?? 'pending')} · ${t('goalUnassessed')}`),
|
|
58
|
+
h('p', { className: 'meta' }, t('goalContribution')),
|
|
59
|
+
text('contribution', link.contribution?.available === true ? link.contribution.reason : t('goalContributionUnavailable')))),
|
|
60
|
+
!(detail.links || []).length && !works.length && h('p', { className: 'meta' }, t('goalNoLinks')),
|
|
61
|
+
(detail.discussions || []).length > 0 && h('div', null,
|
|
62
|
+
h('h4', null, `${t('goalHistory')} · ${detail.discussions.length}/${detail.discussionsTotal ?? detail.discussions.length}`),
|
|
63
|
+
detail.discussions.map(item => h('div', { className: 'source', key: item.id },
|
|
64
|
+
h('p', { className: 'meta' }, `${item.requestId} · ${t('goalAdmission')}: ${label('admission', item.admission?.state ?? 'unknown')}`),
|
|
65
|
+
text('submitted', item.content?.map(part => part.text || '').join('') || ''),
|
|
66
|
+
item.admission?.state !== 'accepted' && h('p', null, t('goalUnknown')))))),
|
|
67
|
+
(allowed || detail.repairAllowed) && h('details', { className: 'goal-discussion' },
|
|
68
|
+
h('summary', null, t('goalCorrection')),
|
|
69
|
+
h('p', { className: 'meta' }, t(allowed ? 'goalDiscussHelp' : 'goalRepairHelp')),
|
|
70
|
+
!canDiscuss && h('p', { className: 'meta' }, t('goalMainRequired')),
|
|
71
|
+
staleDraft && h('p', { role: 'alert' }, t('goalDraftChanged')),
|
|
72
|
+
error && h('p', { role: 'alert' }, error), notice && h('p', { role: 'status' }, notice),
|
|
73
|
+
h('label', null, t('goalDiscussLabel'), h('textarea', { value: draft, maxLength: 24000, disabled: busy,
|
|
74
|
+
onChange: event => { setDraft(event.target.value); setDraftVersion(version(goal)); } })),
|
|
75
|
+
h('button', { type: 'button', onClick: () => { void discuss(); }, disabled: busy || staleDraft || !draft.trim() || !canDiscuss },
|
|
76
|
+
t(allowed ? 'goalDiscuss' : 'goalRepair'))));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function GoalDock({ useGoals, onGoalDiscuss, onGoalStopWork, onGoalResumeWork, onGoalRefresh, onGoalPage, t }) {
|
|
80
|
+
const state = useGoals(value => value);
|
|
81
|
+
const [expanded, setExpanded] = useState(false), [busy, setBusy] = useState(false);
|
|
82
|
+
const [error, setError] = useState(null), [notice, setNotice] = useState(null);
|
|
83
|
+
const pending = useRef(false), retryIds = useRef(new Map());
|
|
84
|
+
async function controlWork(action, work) {
|
|
85
|
+
const control = action === 'resume' ? onGoalResumeWork : onGoalStopWork;
|
|
86
|
+
if (pending.current || !control) return;
|
|
87
|
+
const request = { workId: work.id, nativeGoalId: work.nativeGoalId, nativeRevision: work.nativeRevision };
|
|
88
|
+
const key = JSON.stringify({ action, ...request });
|
|
89
|
+
if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
|
|
90
|
+
request.requestId = retryIds.current.get(key);
|
|
91
|
+
pending.current = true; setBusy(true); setError(null); setNotice(null);
|
|
92
|
+
try {
|
|
93
|
+
const result = await control(request);
|
|
94
|
+
if (result?.state === 'applied') {
|
|
95
|
+
retryIds.current.delete(key); setNotice(t(action === 'resume' ? 'goalWorkResumed' : 'goalWorkStopped'));
|
|
96
|
+
} else setNotice(t('goalWorkUnknown'));
|
|
97
|
+
} catch (failure) {
|
|
98
|
+
if (failure.remote) retryIds.current.delete(key);
|
|
99
|
+
setError(`${failure.message || t('failed')}${failure.remote ? '' : ` ${t('goalWorkUnknown')}`}`);
|
|
100
|
+
await onGoalRefresh();
|
|
101
|
+
} finally { pending.current = false; setBusy(false); }
|
|
102
|
+
}
|
|
103
|
+
// The existing list read already includes full goal details and related work.
|
|
104
|
+
// Keep one card per goal; no second detail fetch or duplicate goal navigation.
|
|
105
|
+
const works = [...new Map([...state.items.flatMap(item => item.works || []), ...(state.works || [])]
|
|
106
|
+
.map(work => [work.id, work])).values()];
|
|
107
|
+
const belongs = (work, id) => work.relation?.state === 'current' && work.relation.goalId === id;
|
|
108
|
+
const others = works.filter(work => !state.items.some(item => belongs(work, item.goal.id)));
|
|
109
|
+
const button = (label, action, disabled = false, props = {}) => h('button', { type: 'button', disabled,
|
|
110
|
+
onClick: () => { void action(); }, ...props }, label);
|
|
111
|
+
return h('section', { className: 'arcfold-methods arcfold-goals', 'aria-label': t('goalTitle') },
|
|
112
|
+
h('header', null, h('strong', null, `${t('goalTitle')} · ${state.total}`),
|
|
113
|
+
button(expanded ? t('goalHide') : t('goalOpen'), () => setExpanded(!expanded), false, { 'aria-expanded': expanded }),
|
|
114
|
+
button(t('refresh'), onGoalRefresh)),
|
|
115
|
+
state.loading && h('p', { role: 'status' }, t('goalLoading')),
|
|
116
|
+
state.error && h('p', { role: 'alert' }, state.error),
|
|
117
|
+
// Collapsing the panel preserves unsent drafts and each card's request identity.
|
|
118
|
+
h('div', { className: 'detail', hidden: !expanded },
|
|
119
|
+
h('p', { className: 'meta' }, t('goalBoundary')),
|
|
120
|
+
error && h('p', { role: 'alert' }, error), notice && h('p', { role: 'status' }, notice),
|
|
121
|
+
h('div', { className: 'goal-cards' }, state.items.map(item => h(GoalCard, { key: item.goal.id, detail: item,
|
|
122
|
+
works: works.filter(work => belongs(work, item.goal.id)), canDiscuss: state.canDiscuss === true && item.browse?.related !== false,
|
|
123
|
+
onGoalDiscuss, onGoalRefresh, onAction: controlWork, workBusy: busy, t }))),
|
|
124
|
+
!state.loading && !state.error && !state.items.length && h('p', null, t('goalEmpty')),
|
|
125
|
+
others.length > 0 && h('details', { className: 'goal-other-work' },
|
|
126
|
+
h('summary', null, `${t('goalOtherWork')} · ${others.length}`), h(GoalWorkList, { works: others, onAction: controlWork, busy, t })),
|
|
127
|
+
(state.offset > 0 || state.nextOffset != null) && h('nav', null,
|
|
128
|
+
button(t('previous'), () => onGoalPage(Math.max(0, state.offset - 20)), state.offset === 0),
|
|
129
|
+
button(t('next'), () => onGoalPage(state.nextOffset), state.nextOffset == null))));
|
|
130
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const h = React.createElement;
|
|
3
|
+
|
|
4
|
+
function workStatus(work, t) {
|
|
5
|
+
if (work.stage) {
|
|
6
|
+
const keys = { waiting: 'goalStageWaiting', completed: 'goalStageComplete', paused: 'goalWorkPaused', needs_review: 'goalStageReview' };
|
|
7
|
+
if (Object.hasOwn(keys, work.stage.status)) return t(keys[work.stage.status]);
|
|
8
|
+
} else if (work.phase === 'active') return t('goalStageReview');
|
|
9
|
+
if (work.phase === 'active') return t(work.activation === 'armed' ? 'goalWorkEnabled' :
|
|
10
|
+
work.activation === 'disarmed' ? 'goalWorkDisarmed' : 'presentationUnknown');
|
|
11
|
+
const keys = { paused: 'goalWorkPaused', blocked: 'goalWorkBlocked', complete: 'goalWorkComplete', cleared: 'goalWorkCleared' };
|
|
12
|
+
return t(Object.hasOwn(keys, work.phase) ? keys[work.phase] : 'presentationUnknown');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Arcfold stage decisions and the corresponding native execution are one view.
|
|
16
|
+
export function GoalWorkList({ works = [], title, onAction, busy, t }) {
|
|
17
|
+
if (!works.length) return null;
|
|
18
|
+
return h('section', { className: 'goal-work-list', 'aria-label': t('goalWorkTitle') },
|
|
19
|
+
title && h('h4', null, title),
|
|
20
|
+
works.map(work => {
|
|
21
|
+
const relation = work.relation;
|
|
22
|
+
const linked = relation?.state === 'current';
|
|
23
|
+
const blocker = work.blockedReason;
|
|
24
|
+
const reasonKey = ({ 'round-limit': 'goalBlockRoundLimit', 'queue-failed': 'goalBlockQueueFailed',
|
|
25
|
+
'prompt-rejected': 'goalBlockPromptRejected' })[blocker?.code];
|
|
26
|
+
return h('div', { className: 'goal-work', key: work.id },
|
|
27
|
+
h('span', { className: 'goal-status-badge' }, workStatus(work, t)),
|
|
28
|
+
work.otherSession && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
|
|
29
|
+
h('p', { className: 'text' }, work.stage?.available === false ? t('goalStageReview') :
|
|
30
|
+
work.stage?.objective || (linked ? relation.reason : work.objective)),
|
|
31
|
+
work.stage?.reason && h('p', { className: 'meta' }, work.stage.reason),
|
|
32
|
+
!relation && h('p', { className: 'meta' }, t('goalWorkUnlinked')),
|
|
33
|
+
relation && !linked && h('p', { className: 'meta' },
|
|
34
|
+
t(relation.state === 'needs_review' ? 'goalWorkRelationReview' : 'goalRelationUnavailable')),
|
|
35
|
+
work.phase === 'blocked' && h('div', { className: 'goal-work-blocker' }, h('h5', null, t('goalBlockReason')),
|
|
36
|
+
h('p', { className: 'text' }, reasonKey ? t(reasonKey) : blocker?.message || t('goalBlockUnknown'))),
|
|
37
|
+
work.canStop && onAction && h('button', { type: 'button', disabled: busy,
|
|
38
|
+
onClick: () => { void onAction('stop', work); } }, t('goalWorkStop')),
|
|
39
|
+
work.canResume && onAction && h('button', { type: 'button', disabled: busy,
|
|
40
|
+
onClick: () => { void onAction('resume', work); } }, t('goalWorkResume')),
|
|
41
|
+
h('details', { className: 'goal-work-details' }, h('summary', null, t('goalWorkOriginal')),
|
|
42
|
+
h('p', { className: 'text' }, work.stage?.available === false ? t('goalStageReview') : work.stage?.objective || work.objective),
|
|
43
|
+
work.stage?.doneWhen && h('p', { className: 'text' }, `${t('goalStageDoneWhen')} ${work.stage.doneWhen}`),
|
|
44
|
+
work.stage?.boundaries && h('p', { className: 'text' }, `${t('goalStageBoundaries')} ${work.stage.boundaries}`),
|
|
45
|
+
work.phase === 'blocked' && blocker?.message && h('p', { className: 'text' }, blocker.message),
|
|
46
|
+
h('p', { className: 'meta' }, `${t('goalWorkRounds')} ${work.roundsStarted} / ${work.maxGoalRounds}`),
|
|
47
|
+
h('p', { className: 'meta' }, t(work.provenance === 'verified-native-delegation' ? 'goalWorkVerified' :
|
|
48
|
+
work.provenance === 'source-unavailable' ? 'goalWorkSourceUnavailable' : 'goalWorkLegacy')),
|
|
49
|
+
h('p', { className: 'meta' }, `${work.sessionId} · ${work.nativeGoalId} · ${t('revision')} ${work.nativeRevision}`),
|
|
50
|
+
relation && h('p', { className: 'meta' }, `${relation.goalId} · ${t('goalDefinition')} ${relation.definitionVersion}`)));
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { importReasonKeys } from './import-locales.mjs';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { IMPORT_MAX_FILE_BYTES } from './import-source.mjs';
|
|
4
|
+
const h = React.createElement;
|
|
5
|
+
const statusKeys = { preparing:'importPreparingStatus', ready:'importReady', failed:'importFailed', confirmed:'importConfirmed', withdrawn:'importWithdrawn' };
|
|
6
|
+
const eligibilityKeys = { enabled:'importEnabled', trial:'importTrial', paused:'importPaused', history:'importHistory', needs_review:'importNeedsReview' };
|
|
7
|
+
const scopeKeys = { global:'importGlobal', mapped:'importMapped', historical:'importHistoricalScope' };
|
|
8
|
+
export function ImportDock({ useImports, importSessionId, onImportUpload, onImportPrepare, onImportDetails,
|
|
9
|
+
onImportConfirm, onImportWithdraw, onImportRefresh, onImportPage, t }) {
|
|
10
|
+
const state = useImports(value => value);
|
|
11
|
+
const [expanded, expand] = useState(false), [file, setFile] = useState(null), [receipt, setReceipt] = useState(null);
|
|
12
|
+
const [detail, setDetail] = useState(null), [scope, setScope] = useState(''), [text, setText] = useState(null);
|
|
13
|
+
const [busy, setBusy] = useState(false), [loading, setLoading] = useState(false), [progress, setProgress] = useState(null);
|
|
14
|
+
const [error, setError] = useState(''), [notice, setNotice] = useState('');
|
|
15
|
+
const mounted = useRef(true), generation = useRef(0), pending = useRef(false), identities = useRef(new Map());
|
|
16
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current++; }; }, []);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
generation.current++; setDetail(null); setText(null); setReceipt(null); setScope(''); setLoading(false); setProgress(null);
|
|
19
|
+
setError(''); if (pending.current) setNotice(t('importUnknown'));
|
|
20
|
+
}, [state.readEpoch]);
|
|
21
|
+
const current = value => mounted.current && value === generation.current;
|
|
22
|
+
async function show(batchId, view = 'records', offset = 0, retainNotice = false) {
|
|
23
|
+
const version = ++generation.current; setLoading(true); setText(null); setDetail(null);
|
|
24
|
+
if (!retainNotice) { setError(''); setNotice(''); }
|
|
25
|
+
try {
|
|
26
|
+
const result = await onImportDetails({ batchId, view, offset, limit: 20 });
|
|
27
|
+
if (current(version)) { setDetail({ ...result, offset }); setScope(result.batch.scopeMap[0]?.sourceConversationId || ''); }
|
|
28
|
+
} catch (failure) { if (current(version)) setError(failure.message || t('failed')); }
|
|
29
|
+
finally { if (current(version)) setLoading(false); }
|
|
30
|
+
}
|
|
31
|
+
async function readText(textKey, offset = 0) {
|
|
32
|
+
if (!detail || pending.current) return;
|
|
33
|
+
const version = generation.current, batchId = detail.batch.id; setLoading(true); setText(null);
|
|
34
|
+
try {
|
|
35
|
+
const result = await onImportDetails({ batchId, view:'text', textKey, offset, limit:16000 });
|
|
36
|
+
if (current(version)) setText(result);
|
|
37
|
+
} catch (failure) { if (current(version)) setError(failure.message || t('failed')); }
|
|
38
|
+
finally { if (current(version)) setLoading(false); }
|
|
39
|
+
}
|
|
40
|
+
async function refresh() {
|
|
41
|
+
const version = generation.current, selected = detail;
|
|
42
|
+
await onImportRefresh();
|
|
43
|
+
if (current(version) && selected) await show(selected.batch.id, selected.view, selected.offset, true);
|
|
44
|
+
}
|
|
45
|
+
async function prepare(reuse = false) {
|
|
46
|
+
if (state.canImport === false || pending.current || loading || (!reuse && !file) || (reuse && !receipt)) return;
|
|
47
|
+
const version = ++generation.current;
|
|
48
|
+
pending.current = true; setBusy(true); setError(''); setNotice(''); setText(null); setProgress(null);
|
|
49
|
+
try {
|
|
50
|
+
let staged = receipt;
|
|
51
|
+
if (!reuse) {
|
|
52
|
+
staged = await onImportUpload(file, value => { if (current(version)) setProgress(value); });
|
|
53
|
+
if (!current(version)) return;
|
|
54
|
+
setReceipt(staged);
|
|
55
|
+
}
|
|
56
|
+
const value = await onImportPrepare({ receiptId: staged.receiptId, requestId:globalThis.crypto.randomUUID(),
|
|
57
|
+
scopeMap:reuse && scope ? [{ sourceConversationId:scope, targetSessionId:importSessionId }] : [] });
|
|
58
|
+
if (!current(version)) return;
|
|
59
|
+
setReceipt({ ...staged, batchId:value.id });
|
|
60
|
+
await onImportRefresh();
|
|
61
|
+
if (current(version)) await show(value.id, 'records', 0, true);
|
|
62
|
+
} catch (failure) {
|
|
63
|
+
if (current(version)) setError((failure.message || t('failed')) + (failure.remote ? '' : ' ' + t('importUnknown')));
|
|
64
|
+
} finally { pending.current = false; if (mounted.current) { setBusy(false); setProgress(null); } }
|
|
65
|
+
}
|
|
66
|
+
async function decide(action) {
|
|
67
|
+
const batch = detail?.batch;
|
|
68
|
+
if (state.canImport === false || detail?.canImport === false || !batch || pending.current || loading) return;
|
|
69
|
+
const changed = scope !== (batch.scopeMap[0]?.sourceConversationId || '');
|
|
70
|
+
if (action === 'confirm' && (batch.status !== 'ready' || batch.sessionId !== importSessionId || changed)) return;
|
|
71
|
+
if (action === 'withdraw' && batch.status !== 'confirmed') return;
|
|
72
|
+
const version = generation.current;
|
|
73
|
+
const request = { batchId:batch.id, expectedRevision:batch.revision, previewHash:batch.previewHash };
|
|
74
|
+
const key = JSON.stringify([action,request]);
|
|
75
|
+
if (!identities.current.has(key)) identities.current.set(key,globalThis.crypto.randomUUID());
|
|
76
|
+
request.requestId = identities.current.get(key);
|
|
77
|
+
pending.current = true; setBusy(true); setError(''); setNotice('');
|
|
78
|
+
try {
|
|
79
|
+
const value = await (action === 'confirm' ? onImportConfirm(request) : onImportWithdraw(request));
|
|
80
|
+
if (!current(version)) return;
|
|
81
|
+
setNotice(t('importSaved'));
|
|
82
|
+
await onImportRefresh();
|
|
83
|
+
if (current(version)) await show(value.id, 'records', 0, true);
|
|
84
|
+
} catch (failure) {
|
|
85
|
+
if (current(version)) {
|
|
86
|
+
setError((failure.message || t('failed')) + (failure.remote ? '' : ' ' + t('importUnknown')));
|
|
87
|
+
// Only read back. A lost response must never trigger a second mutation.
|
|
88
|
+
await onImportRefresh();
|
|
89
|
+
if (current(version)) await show(batch.id, 'records', 0, true);
|
|
90
|
+
}
|
|
91
|
+
} finally { pending.current = false; if (mounted.current) setBusy(false); }
|
|
92
|
+
}
|
|
93
|
+
const button = (label, action, disabled = false, props = {}) => h('button', { type:'button', disabled,
|
|
94
|
+
onClick:() => { void action(); }, ...props }, t(label));
|
|
95
|
+
const body = value => h('div', { className:'text' }, String(value ?? ''));
|
|
96
|
+
const reason = code => t(importReasonKeys.includes(code) ? 'importReason_' + code : 'importReasonUnrecognized');
|
|
97
|
+
const role = value => t(['user','assistant','tool','system','material','program'].includes(value) ? 'importRole_' + value : 'importRoleUnknown');
|
|
98
|
+
const batch = detail?.batch, locked = busy || loading;
|
|
99
|
+
const actionLocked = locked || state.canImport === false || detail?.canImport === false;
|
|
100
|
+
const scopeChanged = batch && scope !== (batch.scopeMap[0]?.sourceConversationId || '');
|
|
101
|
+
const receiptMatches = receipt && batch && receipt.batchId === batch.id;
|
|
102
|
+
return h('section', { className:'arcfold-methods arcfold-imports', 'aria-label':t('importTitle') },
|
|
103
|
+
h('header', null, h('strong', null, `${t('importTitle')} · ${state.total}`),
|
|
104
|
+
button(expanded ? 'importHide' : 'importOpen', () => expand(!expanded), busy, { 'aria-expanded':expanded }),
|
|
105
|
+
button('refresh', refresh, locked)),
|
|
106
|
+
expanded && h('div', { className:'detail' },
|
|
107
|
+
h('p', null, t('importBoundary')),
|
|
108
|
+
h('label', null, t('importFile'), h('input', { type:'file', accept:'.json,.jsonl,.ndjson,application/json', disabled:actionLocked,
|
|
109
|
+
onChange:event => {
|
|
110
|
+
const chosen = event.target.files?.[0] || null; generation.current++; setDetail(null); setText(null); setReceipt(null); setScope(''); setNotice('');
|
|
111
|
+
const valid = chosen && chosen.size > 0 && chosen.size <= IMPORT_MAX_FILE_BYTES;
|
|
112
|
+
setFile(valid ? chosen : null); setError(chosen && !valid ? t('importFileTooLarge') : '');
|
|
113
|
+
} })),
|
|
114
|
+
button('importPrepare', () => prepare(), actionLocked || !file),
|
|
115
|
+
busy && h('p', { role:'status' }, t('importWorking'), progress && ` ${progress.loaded.toLocaleString()} / ${file?.size.toLocaleString()} bytes`),
|
|
116
|
+
h('p', { className:'meta' }, t('importStorage')),
|
|
117
|
+
(state.loading || loading) && h('p', { role:'status' }, t('importLoading')),
|
|
118
|
+
(error || state.error) && h('p', { role:'alert' }, error || state.error), notice && h('p', { role:'status' }, notice),
|
|
119
|
+
h('ul', null, state.items.map(row => h('li', { key:row.id },
|
|
120
|
+
body(row.file.name), h('p', { className:'meta' }, `${t(statusKeys[row.status])} · ${row.createdAt}`),
|
|
121
|
+
button('importPreview', () => show(row.id), locked)))),
|
|
122
|
+
!state.loading && !state.error && !state.items.length && h('p', null, t('importEmpty')),
|
|
123
|
+
h('nav', null, button('previous', () => onImportPage(Math.max(0,state.offset-20)), locked || !state.offset),
|
|
124
|
+
button('next', () => onImportPage(state.nextOffset), locked || state.nextOffset == null)),
|
|
125
|
+
batch && h('article', { 'aria-label':t('importTitle') + ' ' + batch.id },
|
|
126
|
+
h('h3', null, batch.file.name), h('p', null, t(statusKeys[batch.status])),
|
|
127
|
+
h('p', { className:'meta' }, t('importSnapshot')),
|
|
128
|
+
batch.status === 'withdrawn' && h('p', { role:'status' }, t('importWithdrawnNotice')),
|
|
129
|
+
h('p', { className:'meta' }, `${t('importRevision')} ${batch.revision} · ${batch.file.bytes.toLocaleString()} bytes`),
|
|
130
|
+
h('details', null, h('summary', null, t('importFileHash')), body(batch.file.sha256)),
|
|
131
|
+
batch.error && h('p', { role:'alert' }, batch.error.message),
|
|
132
|
+
h('h4', null, t('importCountsAtPreview')),
|
|
133
|
+
h('dl', { className:'import-counts' }, Object.entries({ sources:'importCountSources',records:'importCountRecords',enabled:'importEnabled',trial:'importTrial',paused:'importPaused',history:'importHistory',needsReview:'importNeedsReview' }).map(([name,key]) =>
|
|
134
|
+
h('div', { key:name }, h('dt', null, t(key)), h('dd', null, String(batch.counts[name] ?? 0))))),
|
|
135
|
+
h('h4', null, t('importScopeTitle')), h('p', { className:'meta' }, t('importScopeHelp')),
|
|
136
|
+
h('label', null, t('importMapScope'), h('select', { value:scope, disabled:actionLocked || batch.status !== 'ready' || !receiptMatches || batch.sessionId !== importSessionId,
|
|
137
|
+
onChange:event => setScope(event.target.value) },
|
|
138
|
+
h('option', { value:'' }, t('importKeepScope')),
|
|
139
|
+
batch.sourceScopes.map(source => h('option', { key:source.sourceConversationId, value:source.sourceConversationId }, `${source.title || source.sourceConversationId} · ${source.messageCount}`)))),
|
|
140
|
+
scope && h('p', { className:'text' }, batch.sourceScopes.find(source => source.sourceConversationId === scope)?.preview),
|
|
141
|
+
scopeChanged && h('p', { role:'status' }, t('importScopeChanged')),
|
|
142
|
+
batch.status === 'ready' && (receiptMatches ? button('importReprepare', () => prepare(true), actionLocked || !scopeChanged)
|
|
143
|
+
: h('p', { className:'meta' }, t('importNoReceipt'))),
|
|
144
|
+
h('nav', null, button('importRecords', () => show(batch.id,'records'), locked), button('importSources', () => show(batch.id,'sources'), locked)),
|
|
145
|
+
h('p', { className:'meta' }, t('importExcerpt')),
|
|
146
|
+
h('ul', null, detail.items.map(row => h('li', { key:row.id, className:'source' },
|
|
147
|
+
row.title && h('h4', null, row.title), body(row.textPreview),
|
|
148
|
+
row.eligibility && h('p', null, `${t('importEligibilityAtPreview')}:${t(eligibilityKeys[row.eligibility])}`),
|
|
149
|
+
row.claimType && h('p', null, t({ user_statement:'importClaimLiteral', inference:'importClaimInference', open_question:'importClaimQuestion' }[row.claimType] || 'importClaimInference')),
|
|
150
|
+
row.kind === 'methods' && h(React.Fragment, null,
|
|
151
|
+
h('h4', null, t('importAppliesTo')), body(row.appliesTo ?? t('importAttributeUnavailable')),
|
|
152
|
+
h('h4', null, t('importDoesNotApplyTo')), body(row.doesNotApplyTo ?? t('importAttributeUnavailable'))),
|
|
153
|
+
row.scope && h('p', { className:'meta' }, t(scopeKeys[row.scope.kind])),
|
|
154
|
+
row.scope?.topics?.length > 0 && h('p', { className:'meta' }, t('importTopics') + ': ' + row.scope.topics.join(' · ')),
|
|
155
|
+
row.role && h('p', { className:'meta' }, `${role(row.role)} · ${row.occurredAt || ''} · ${t('importSourceAtPreview')}:${t(row.available ? 'importAvailable' : 'importUnavailable')}`),
|
|
156
|
+
row.methodUsage && h('p', null, `${t('importUsageAtPreview')}:${t('importUsed')} ${row.methodUsage.trialUses ?? '—'} / ${t('importMaximum')} ${row.methodUsage.maxTrialUses ?? '—'} · ${t('importRemaining')} ${row.methodUsage.remaining ?? '—'}`),
|
|
157
|
+
row.reasonCodes?.length > 0 && h('details', null, h('summary', null, t('importReason')), body(row.reasonCodes.map(reason).join('\n'))),
|
|
158
|
+
row.existingRecordId && h('details', null, h('summary', null, t('importExistingRecord')), body(row.existingRecordId)),
|
|
159
|
+
row.originalTextKey && button('importOriginalRecord', () => readText(row.originalTextKey), locked),
|
|
160
|
+
row.textKey && button(detail.view === 'sources' ? 'importText' : 'importContent', () => readText(row.textKey), locked)))),
|
|
161
|
+
h('nav', null, button('previous', () => show(batch.id,detail.view,Math.max(0,detail.offset-20)), locked || !detail.offset),
|
|
162
|
+
button('next', () => show(batch.id,detail.view,detail.nextOffset), locked || detail.nextOffset == null)),
|
|
163
|
+
text && h('section', { 'aria-label':t('importTextPage') },
|
|
164
|
+
h('h4', null, t('importTextPage')), h('p', { className:'meta' }, `${text.offset}–${text.offset+text.text.length} / ${text.total}`), body(text.text),
|
|
165
|
+
button('importReadPrevious', () => readText(text.textKey,Math.max(0,text.offset-16000)), locked || !text.offset),
|
|
166
|
+
button('importReadMore', () => readText(text.textKey,text.nextOffset), locked || text.nextOffset == null)),
|
|
167
|
+
batch.status === 'ready' && h('div', { className:'import-decision' }, h('p', null, t('importConfirmHelp')),
|
|
168
|
+
batch.sessionId !== importSessionId && h('p', null, t('importOtherSession')),
|
|
169
|
+
button('importConfirm', () => decide('confirm'), actionLocked || scopeChanged || batch.sessionId !== importSessionId)),
|
|
170
|
+
batch.status === 'confirmed' && h('div', { className:'import-decision' }, h('p', null, t('importWithdrawHelp')),
|
|
171
|
+
button('importWithdraw', () => decide('withdraw'), actionLocked)))));
|
|
172
|
+
}
|