@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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { presentationLabel } from './locales.mjs';
|
|
2
|
+
import React, { useRef, useState } from 'react';
|
|
3
|
+
const h = React.createElement;
|
|
4
|
+
const text = (key, value) => h('div', { className: 'text', key }, value ?? '');
|
|
5
|
+
|
|
6
|
+
export function MethodDock({ useMethods, onDetails, onDecide, onAcknowledge, onRefresh, onPage, t }) {
|
|
7
|
+
const state = useMethods(value => value);
|
|
8
|
+
const [expanded, setExpanded] = useState(false);
|
|
9
|
+
const [detail, setDetail] = useState(null);
|
|
10
|
+
const [detailLoading, setDetailLoading] = useState(false);
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [notice, setNotice] = useState(null);
|
|
14
|
+
const actionPending = useRef(false), detailGeneration = useRef(0), retryIds = useRef(new Map());
|
|
15
|
+
|
|
16
|
+
async function show(methodId, keepError = false) {
|
|
17
|
+
const generation = ++detailGeneration.current;
|
|
18
|
+
setDetailLoading(true); setExpanded(true);
|
|
19
|
+
if (!keepError) { setError(null); setNotice(null); }
|
|
20
|
+
try {
|
|
21
|
+
const value = await onDetails(methodId);
|
|
22
|
+
if (generation === detailGeneration.current) setDetail(value);
|
|
23
|
+
} catch (failure) {
|
|
24
|
+
if (generation === detailGeneration.current) { setDetail(null); setError(failure.message || t('failed')); }
|
|
25
|
+
} finally {
|
|
26
|
+
if (generation === detailGeneration.current) setDetailLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function closeDetail() {
|
|
30
|
+
detailGeneration.current++; setDetail(null); setDetailLoading(false);
|
|
31
|
+
}
|
|
32
|
+
async function historyPage(offset) {
|
|
33
|
+
if (!detail) return;
|
|
34
|
+
const methodId = detail.method.id, generation = ++detailGeneration.current;
|
|
35
|
+
setDetailLoading(true); setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const value = await onDetails(methodId, offset);
|
|
38
|
+
if (generation === detailGeneration.current) setDetail(current => current?.method.id === methodId
|
|
39
|
+
? { ...current, notifications: value.notifications } : current);
|
|
40
|
+
// History reads do not silently replace the full method/version the user
|
|
41
|
+
// is deciding on. A deliberate reload or conflict refresh does that.
|
|
42
|
+
} catch (failure) {
|
|
43
|
+
if (generation === detailGeneration.current) setError(failure.message || t('failed'));
|
|
44
|
+
} finally { if (generation === detailGeneration.current) setDetailLoading(false); }
|
|
45
|
+
}
|
|
46
|
+
async function decide(action) {
|
|
47
|
+
if (actionPending.current || detailLoading || !detail) return;
|
|
48
|
+
actionPending.current = true; setBusy(true); setError(null); setNotice(null);
|
|
49
|
+
// The request comes from the displayed detail, never a newer unseen list item.
|
|
50
|
+
const request = { methodId: detail.method.id, expectedRevision: detail.method.revision,
|
|
51
|
+
contentHash: detail.contentHash, action };
|
|
52
|
+
const key = JSON.stringify(request);
|
|
53
|
+
if (!retryIds.current.has(key)) retryIds.current.set(key, globalThis.crypto.randomUUID());
|
|
54
|
+
request.requestId = retryIds.current.get(key);
|
|
55
|
+
try {
|
|
56
|
+
await onDecide(request);
|
|
57
|
+
retryIds.current.delete(key);
|
|
58
|
+
setNotice(t('saved'));
|
|
59
|
+
await show(request.methodId, true);
|
|
60
|
+
} catch (failure) {
|
|
61
|
+
if (failure.remote) retryIds.current.delete(key);
|
|
62
|
+
setError(failure.remote ? failure.message : `${failure.message || t('failed')} ${t('unknown')}`);
|
|
63
|
+
if (failure.code === 'revision_conflict' || failure.code === 'method_superseded' || failure.code === 'method_source_invalid') {
|
|
64
|
+
setError(`${failure.message || ''} ${t('conflict')}`);
|
|
65
|
+
await show(request.methodId, true);
|
|
66
|
+
}
|
|
67
|
+
} finally { actionPending.current = false; setBusy(false); }
|
|
68
|
+
}
|
|
69
|
+
async function acknowledge(id) {
|
|
70
|
+
if (actionPending.current) return;
|
|
71
|
+
actionPending.current = true; setBusy(true); setError(null);
|
|
72
|
+
try { await onAcknowledge(id); await historyPage(detail?.notifications?.offset ?? 0); }
|
|
73
|
+
catch (failure) { setError(failure.message || t('failed')); }
|
|
74
|
+
finally { actionPending.current = false; setBusy(false); }
|
|
75
|
+
}
|
|
76
|
+
const button = (label, action, disabled = false, extra = {}) => h('button', { type: 'button', disabled,
|
|
77
|
+
onClick: () => { void action(); }, ...extra }, label);
|
|
78
|
+
const method = detail?.method;
|
|
79
|
+
const history = detail?.notifications;
|
|
80
|
+
return h('section', { className: 'arcfold-methods', 'aria-label': t('title') },
|
|
81
|
+
h('header', null,
|
|
82
|
+
h('strong', null, `${t('title')} · ${t('pending')} ${state.decisionCount} · ${t('unread')} ${state.unreadCount}`),
|
|
83
|
+
button(expanded ? t('close') : t('open'), () => setExpanded(!expanded), false, { 'aria-expanded': expanded }),
|
|
84
|
+
button(t('refresh'), onRefresh)),
|
|
85
|
+
state.loading && h('p', { role: 'status' }, t('loading')),
|
|
86
|
+
state.error && h('p', { role: 'alert' }, state.error),
|
|
87
|
+
expanded && h('div', { className: 'detail' },
|
|
88
|
+
error && h('p', { role: 'alert' }, error),
|
|
89
|
+
notice && h('p', { role: 'status' }, notice),
|
|
90
|
+
!method && !detailLoading && h('ul', null, state.items.map(item => h('li', { key: item.method.id },
|
|
91
|
+
h('strong', null, item.method.title),
|
|
92
|
+
h('p', { className: 'meta' }, `${presentationLabel('method', item.method.status, t)} · ${t('version')} ${item.method.version ?? 1}`),
|
|
93
|
+
text('current-reason', item.reason),
|
|
94
|
+
item.needsDecision && h('p', null, t('decisionPrompt')),
|
|
95
|
+
item.unreadCount > 0 && h('p', { className: 'meta' }, `${t('unread')} ${item.unreadCount}`),
|
|
96
|
+
button(t(item.needsDecision ? 'reviewDecision' : 'viewDetails'), () => show(item.method.id), busy)))),
|
|
97
|
+
!method && !detailLoading && !state.loading && !state.error && !state.items.length && h('p', null, t('empty')),
|
|
98
|
+
!method && !detailLoading && (state.offset > 0 || state.nextOffset != null) && h('nav', { 'aria-label': t('title') },
|
|
99
|
+
button(t('previous'), () => onPage(Math.max(0, state.offset - 20)), state.offset === 0),
|
|
100
|
+
button(t('next'), () => onPage(state.nextOffset), state.nextOffset === null || state.nextOffset === undefined)),
|
|
101
|
+
detailLoading && h('p', { role: 'status' }, t('loading')),
|
|
102
|
+
method && h('ul', null, h('li', null, h('article', { 'aria-label': method.title },
|
|
103
|
+
button(t('backToMethods'), closeDetail, busy),
|
|
104
|
+
h('h3', null, method.title),
|
|
105
|
+
h('p', { className: 'meta' }, [`${t('version')} ${method.version ?? 1}`,
|
|
106
|
+
method.revision != null ? `${t('revision')} ${method.revision}` : null,
|
|
107
|
+
`${t('status')} ${presentationLabel('method', method.status, t)}`].filter(Boolean).join(' · ')),
|
|
108
|
+
h('h4', null, t('reason')), text('reason', detail.reason),
|
|
109
|
+
detail.needsDecision && h('p', null, t('decisionPrompt')),
|
|
110
|
+
detail.usage && h('p', null, `${t('usage')} ${detail.usage.trialUses}/${detail.usage.maxTrialUses} · ${t('remaining')} ${detail.usage.remaining}`),
|
|
111
|
+
detail.pendingAdoptionCount > 0 && h('div', null,
|
|
112
|
+
h('h4', null, `${t('pendingAdoptions')} ${detail.pendingAdoptionCount}`),
|
|
113
|
+
(detail.pendingAdoptions || []).map(adoption => h('p', { className: 'meta', key: adoption.id },
|
|
114
|
+
`${adoption.id} · ${t('conversation')} ${adoption.sessionId} · ${t('turn')} ${adoption.turn} · ${presentationLabel('adoption', adoption.state, t)}`))),
|
|
115
|
+
method.guidance !== undefined && h('div', null,
|
|
116
|
+
h('h4', null, t('guidance')), text('guidance', method.guidance),
|
|
117
|
+
h('h4', null, t('applies')), text('applies', method.appliesTo),
|
|
118
|
+
h('h4', null, t('excludes')), text('excludes', method.doesNotApplyTo)),
|
|
119
|
+
h('h4', null, t('sources')),
|
|
120
|
+
detail.sources.length ? detail.sources.map(source => h('div', { className: 'source', key: source.id },
|
|
121
|
+
h('p', { className: 'meta' }, `${t('sourceIdentity')}: ${source.id} · ${t('revision')} ${source.revision} · ${t('author')}: ${presentationLabel('role', source.role, t)} · ${t('sourceTime')}: ${source.occurredAt ?? ''}`),
|
|
122
|
+
text('content', source.content),
|
|
123
|
+
h('p', { className: 'meta' }, `${t('provenance')}: ${JSON.stringify(source.provenance ?? null)}`))) : h('p', null, t('noSources')),
|
|
124
|
+
detail.successors.length > 0 && h('div', null, h('h4', null, t('successors')),
|
|
125
|
+
detail.successors.map(next => h('p', { key: next.id },
|
|
126
|
+
`${next.title} · ${t('version')} ${next.version} · ${presentationLabel('method', next.status, t)} `,
|
|
127
|
+
button(t('open'), () => show(next.id), busy)))),
|
|
128
|
+
!detail.activation.allowed && method.status !== 'active' && h('p', { role: 'alert' }, detail.activation.message),
|
|
129
|
+
h('p', { className: 'meta' }, t('boundary')),
|
|
130
|
+
h('div', { className: 'actions' },
|
|
131
|
+
button(busy ? t('busy') : t('continue'), () => decide('continue'), busy || detailLoading || !detail.activation.allowed),
|
|
132
|
+
button(t(method.status === 'paused' ? 'keepPaused' : 'pause'), () => decide('pause'),
|
|
133
|
+
busy || detailLoading || !(['trial', 'active'].includes(method.status) || method.status === 'paused' && detail.needsDecision)),
|
|
134
|
+
button(t('retry'), () => show(method.id), busy)),
|
|
135
|
+
h('details', { key: method.id, 'aria-label': t('history') },
|
|
136
|
+
h('summary', null, `${t('history')} ${history?.total ?? 0} · ${t('unread')} ${history?.unreadCount ?? 0}`),
|
|
137
|
+
h('ul', null, (history?.items || []).map(item => h('li', { key: item.id },
|
|
138
|
+
h('strong', null, item.title), text('message', item.displayMessage ?? item.message),
|
|
139
|
+
h('p', { className: 'meta' }, `${t('version')} ${item.methodVersion} · ${item.createdAt ?? ''}`),
|
|
140
|
+
item.status === 'unread' ? button(t('acknowledge'), () => acknowledge(item.id), busy || detailLoading) : h('span', null, t('read'))))),
|
|
141
|
+
!history?.total && h('p', null, t('noNotifications')),
|
|
142
|
+
(history?.offset > 0 || history?.nextOffset != null) && h('nav', { 'aria-label': t('history') },
|
|
143
|
+
button(t('previous'), () => historyPage(Math.max(0, history.offset - 20)), busy || detailLoading || history.offset === 0),
|
|
144
|
+
button(t('next'), () => historyPage(history.nextOffset), busy || detailLoading || history.nextOffset == null))))))));
|
|
145
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { deployedRoutes, validLearningChoice } from './model-settings-source.mjs';
|
|
3
|
+
const h = React.createElement;
|
|
4
|
+
const PHASE_LABELS = { ready: 'modelsPhaseReady', starting: 'modelsPhaseStarting',
|
|
5
|
+
'requires-restart': 'modelsPhaseRestart', failed: 'modelsPhaseFailed', closed: 'modelsPhaseClosed' };
|
|
6
|
+
const REASON_LABELS = {
|
|
7
|
+
'main-not-configured': 'modelsReasonNoModel', 'learn-not-configured': 'modelsReasonNoModel',
|
|
8
|
+
'native-credential-not-configured': 'modelsReasonCredential',
|
|
9
|
+
'transport-not-configured': 'modelsReasonUnsupported', 'selected-route-not-supported': 'modelsReasonUnsupported',
|
|
10
|
+
'selected-model-controls-unavailable': 'modelsReasonUnsupported', 'main-output-limit-unavailable': 'modelsReasonUnsupported',
|
|
11
|
+
'selection-outside-model-policy': 'modelsReasonUnsupported', 'model-catalog-controls-invalid': 'modelsReasonUnsupported',
|
|
12
|
+
'model-catalog-has-no-common-controls': 'modelsReasonUnsupported',
|
|
13
|
+
'native-selection-changed-during-status': 'modelsReasonChanged', 'learning-selection-changed-during-status': 'modelsReasonChanged',
|
|
14
|
+
'runtime-not-ready': 'modelsReasonNotReady', 'runtime-readiness-unavailable': 'modelsReasonNotReady',
|
|
15
|
+
'guard-unavailable': 'modelsReasonNotReady', 'startup-failed': 'modelsReasonNotReady', 'runtime-closed': 'modelsReasonNotReady',
|
|
16
|
+
'runtime-restart-required': 'modelsReasonRestart', 'critical-component-changed': 'modelsReasonRestart',
|
|
17
|
+
'critical-configuration-changed': 'modelsReasonRestart', 'model-topology-changed': 'modelsReasonRestart',
|
|
18
|
+
};
|
|
19
|
+
const EFFORT_LABELS = { off: 'modelsEffortOff', low: 'modelsEffortLow', medium: 'modelsEffortMedium',
|
|
20
|
+
high: 'modelsEffortHigh', xhigh: 'modelsEffortXHigh', max: 'modelsEffortMax' };
|
|
21
|
+
const reasonText = (reason, t) => t(REASON_LABELS[reason] || 'modelsReasonUnknown');
|
|
22
|
+
const effortText = (effort, t) => t(EFFORT_LABELS[effort] || 'modelsEffortOther');
|
|
23
|
+
const selectionText = (value, t) => value ? [value.provider, value.model,
|
|
24
|
+
value.reasoningEffort ? effortText(value.reasoningEffort, t) : null].filter(Boolean).join(' · ') : '';
|
|
25
|
+
function outcomeText(result, t, credential = false) {
|
|
26
|
+
if (result?.status === 'saved') return t(result.refreshed ? 'modelsSaved' : 'modelsSavedUnread');
|
|
27
|
+
if (credential) return t(result?.status === 'unknown' ? 'modelsCredentialUnknown' : 'modelsCredentialFailed');
|
|
28
|
+
return t(result?.status === 'unknown' ? 'modelsUnknown' : result?.code === 'conflict' ? 'modelsConflict' : 'modelsRefused');
|
|
29
|
+
}
|
|
30
|
+
function CredentialCard({ route, info, disabled, readFailed, onStore, onRemove, t }) {
|
|
31
|
+
const [draft, setDraft] = useState(''), [busy, setBusy] = useState(false), [notice, setNotice] = useState('');
|
|
32
|
+
const pending = useRef(false), mounted = useRef(true);
|
|
33
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
34
|
+
async function write(remove) {
|
|
35
|
+
if (pending.current || disabled || !info?.writable) return;
|
|
36
|
+
pending.current = true; setBusy(true); setNotice('');
|
|
37
|
+
const value = draft;
|
|
38
|
+
setDraft('');
|
|
39
|
+
try {
|
|
40
|
+
const result = remove ? await onRemove(route.provider) : await onStore(route.provider, value);
|
|
41
|
+
if (mounted.current) setNotice(outcomeText(result, t, true));
|
|
42
|
+
} catch {
|
|
43
|
+
if (mounted.current) setNotice(t('modelsCredentialUnknown'));
|
|
44
|
+
} finally {
|
|
45
|
+
pending.current = false;
|
|
46
|
+
if (mounted.current) { setDraft(''); setBusy(false); }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return h('article', { className: 'model-card' }, h('h3', null, route.label),
|
|
50
|
+
h('p', null, readFailed ? t('modelsCredentialReadFailed') : t(info?.configured ? 'modelsConfigured' : 'modelsNotConfigured')),
|
|
51
|
+
info?.source && h('p', { className: 'meta' }, t('modelsSource') + ': ' + info.source),
|
|
52
|
+
!readFailed && !info?.writable && h('p', null, t('modelsReadOnly')),
|
|
53
|
+
h('label', null, route.label + ' · ' + t('modelsCredential'),
|
|
54
|
+
h('input', { type: 'password', value: draft, autoComplete: 'new-password', spellCheck: false,
|
|
55
|
+
disabled: disabled || busy || !info?.writable,
|
|
56
|
+
onChange: event => setDraft(event.target.value) })),
|
|
57
|
+
h('div', { className: 'actions' },
|
|
58
|
+
h('button', { type: 'button', disabled: disabled || busy || !info?.writable || !draft.trim(), onClick: () => { void write(false); } }, t('modelsCredentialSave')),
|
|
59
|
+
h('button', { type: 'button', disabled: disabled || busy || !info?.writable || !info?.configured, onClick: () => { void write(true); } }, t('modelsCredentialRemove')),
|
|
60
|
+
h('button', { type: 'button', onClick: () => { setDraft(''); setNotice(''); } }, t('modelsCredentialCancel'))),
|
|
61
|
+
notice && h('p', { role: 'status' }, notice));
|
|
62
|
+
}
|
|
63
|
+
function Usage({ route, t }) {
|
|
64
|
+
const usage = route.usage;
|
|
65
|
+
const scope = { limited: 'modelsScopeLimited', 'web-unlimited': 'modelsScopeWeb', 'web-openai': 'modelsScopeOpenai' };
|
|
66
|
+
const row = (label, value) => h('div', { key: label }, h('dt', null, t(label)), h('dd', null, value));
|
|
67
|
+
return h('article', { className: 'model-card' }, h('h3', null, route.label),
|
|
68
|
+
!usage ? h('p', null, t('modelsUsageMissing')) : h(React.Fragment, null,
|
|
69
|
+
h('p', { className: 'meta' }, scope[usage.scope] ? t(scope[usage.scope]) : usage.scope),
|
|
70
|
+
h('dl', null, row('modelsRequests', usage.requests), row('modelsUnknownCount', usage.unknownCount),
|
|
71
|
+
usage.accounting === 'cny' ? [
|
|
72
|
+
row('modelsSpent', '¥ ' + usage.spentCny), row('modelsReserved', '¥ ' + usage.reservedCny),
|
|
73
|
+
row('modelsCeiling', usage.ceilingCny === null ? t('modelsNoCeiling') : '¥ ' + usage.ceilingCny),
|
|
74
|
+
] : [
|
|
75
|
+
row('modelsPrompt', usage.promptTokens), row('modelsCompletion', usage.completionTokens),
|
|
76
|
+
row('modelsCached', usage.cachedTokens), row('modelsPending', usage.pendingCount),
|
|
77
|
+
]),
|
|
78
|
+
usage.accounting === 'tokens_only' && h('p', null, t('modelsTokensOnly'))));
|
|
79
|
+
}
|
|
80
|
+
function LearningForm({ data, disabled, onSave, t }) {
|
|
81
|
+
const defaults = data.learning?.defaults;
|
|
82
|
+
const [draft, setDraft] = useState(() => ({ ...defaults }));
|
|
83
|
+
const [busy, setBusy] = useState(false), [notice, setNotice] = useState('');
|
|
84
|
+
const pending = useRef(false), mounted = useRef(true);
|
|
85
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
86
|
+
useEffect(() => { setDraft({ ...defaults }); }, [defaults?.revision, defaults?.provider, defaults?.model, defaults?.reasoningEffort]);
|
|
87
|
+
const routes = deployedRoutes(data), route = routes.find(row => row.provider === draft.provider);
|
|
88
|
+
const model = route?.models?.find(row => row.id === draft.model);
|
|
89
|
+
const blocked = disabled || busy || data.gate?.phase !== 'ready' || !data.learning?.enabled ||
|
|
90
|
+
!defaults;
|
|
91
|
+
const select = (label, value, options, update) => h('label', { key: label }, t(label),
|
|
92
|
+
h('select', { value: options.some(item => item.id === value) ? value : '', disabled: blocked,
|
|
93
|
+
onChange: event => update(event.target.value) },
|
|
94
|
+
h('option', { value: '', disabled: true }, t('modelsChoose')),
|
|
95
|
+
options.map(item => h('option', { key: item.id, value: item.id }, item.label || item.id))));
|
|
96
|
+
async function save() {
|
|
97
|
+
if (pending.current || blocked || !validLearningChoice(data, draft)) return;
|
|
98
|
+
pending.current = true; setBusy(true); setNotice('');
|
|
99
|
+
try {
|
|
100
|
+
const result = await onSave({ expectedRevision: defaults.revision, provider: draft.provider,
|
|
101
|
+
model: draft.model, reasoningEffort: draft.reasoningEffort });
|
|
102
|
+
if (mounted.current) setNotice(outcomeText(result, t));
|
|
103
|
+
} catch { if (mounted.current) setNotice(t('modelsUnknown')); }
|
|
104
|
+
finally { pending.current = false; if (mounted.current) setBusy(false); }
|
|
105
|
+
}
|
|
106
|
+
return h('div', null,
|
|
107
|
+
!data.learning?.enabled && h('p', null, t('modelsLearnDisabled')),
|
|
108
|
+
!defaults && h('p', null, t('modelsNoDefault')),
|
|
109
|
+
!data.learning?.availability?.available && h('p', null, reasonText(data.learning?.availability?.reason, t)),
|
|
110
|
+
defaults && h('p', { className: 'meta' }, t('modelsRevision') + ': ' + defaults.revision + ' · ' + selectionText(defaults, t)),
|
|
111
|
+
h('div', { className: 'model-fields' },
|
|
112
|
+
select('modelsProvider', draft.provider, routes.map(row => ({ id: row.provider, label: row.label })),
|
|
113
|
+
provider => { setNotice(''); setDraft({ provider, model: '', reasoningEffort: '' }); }),
|
|
114
|
+
select('modelsModel', draft.model, route?.models || [],
|
|
115
|
+
model => { setNotice(''); setDraft({ ...draft, model, reasoningEffort: '' }); }),
|
|
116
|
+
select('modelsReasoning', draft.reasoningEffort, (model?.reasoningEfforts || []).map(id => ({ id, label: effortText(id, t) })),
|
|
117
|
+
reasoningEffort => { setNotice(''); setDraft({ ...draft, reasoningEffort }); })),
|
|
118
|
+
h('button', { type: 'button', disabled: blocked || !validLearningChoice(data, draft), onClick: () => { void save(); } },
|
|
119
|
+
t(busy ? 'modelsBusy' : 'modelsSave')),
|
|
120
|
+
notice && h('p', { role: 'status' }, notice));
|
|
121
|
+
}
|
|
122
|
+
export function ModelSettings({ useModels, onRefresh, onSaveLearning, onStoreCredential, onRemoveCredential, t }) {
|
|
123
|
+
const state = useModels(value => value), data = state.data;
|
|
124
|
+
return h('section', { className: 'arcfold-methods arcfold-model-settings', 'aria-label': t('modelsTitle') },
|
|
125
|
+
h('header', null, h('h2', null, t('modelsTitle')),
|
|
126
|
+
h('button', { type: 'button', disabled: state.loading, onClick: () => { void onRefresh(); } }, t('modelsRefresh'))),
|
|
127
|
+
state.loading && h('p', { role: 'status' }, t('modelsLoading')),
|
|
128
|
+
state.error && h('p', { role: 'alert' }, t('modelsReadFailed')),
|
|
129
|
+
data && h(React.Fragment, null,
|
|
130
|
+
h('p', { className: 'meta' }, t('modelsGate') + ': ' + t(PHASE_LABELS[data.gate.phase] || 'modelsReasonUnknown') + (data.gate.reason ? ' · ' + reasonText(data.gate.reason, t) : '')),
|
|
131
|
+
h('h3', null, t('modelsMain')), h('p', null, t('modelsMainScope')),
|
|
132
|
+
h('p', null, t('modelsDefault') + ': ' + (selectionText(data.main?.defaultSelection, t) || t('modelsUnavailable'))),
|
|
133
|
+
!data.main?.availability?.available && h('p', null, reasonText(data.main?.availability?.reason, t)),
|
|
134
|
+
h('h3', null, t('modelsLearn')), h('p', null, t('modelsLearnScope')),
|
|
135
|
+
h(LearningForm, { data,
|
|
136
|
+
disabled: Boolean(state.error || state.loading), onSave: onSaveLearning, t }),
|
|
137
|
+
h('h3', null, t('modelsCredentials')), h('p', null, t('modelsCredentialScope')),
|
|
138
|
+
!deployedRoutes(data).length && h('p', null, t('modelsNoRoutes')),
|
|
139
|
+
deployedRoutes(data).map(route => h(CredentialCard, { key: route.provider + ':' + route.apiKeyRef, route,
|
|
140
|
+
info: state.credentials[route.apiKeyRef], readFailed: state.credentialError,
|
|
141
|
+
disabled: Boolean(state.error || state.loading), onStore: onStoreCredential, onRemove: onRemoveCredential, t })),
|
|
142
|
+
h('h3', null, t('modelsUsage')), h('p', null, t('modelsUsageBoundary')),
|
|
143
|
+
deployedRoutes(data).map(route => h(Usage, { key: route.provider, route, t }))));
|
|
144
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
const h = React.createElement;
|
|
3
|
+
|
|
4
|
+
function status(item, t) {
|
|
5
|
+
const key = item.control?.dismissed ? 'opportunityDismissed' : item.stopped ? 'opportunityStopped'
|
|
6
|
+
: !item.run ? (item.rejection ? 'opportunityRejected' : 'opportunityPending')
|
|
7
|
+
: ({ prepared: 'opportunityPrepared', submitted: 'opportunitySubmitted', unknown: 'opportunityUncertain',
|
|
8
|
+
stopped: 'opportunityStopped', settled: 'opportunitySettled' }[item.run.state] ?? 'presentationUnknown');
|
|
9
|
+
return t(key);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function OpportunityCard({ item, readDetails, sessionId, t }) {
|
|
13
|
+
const [detail, setDetail] = useState(null), [error, setError] = useState(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let active = true;
|
|
16
|
+
// Browsing never grants permission, marks a notice as read, starts
|
|
17
|
+
// preparation, or submits a new instruction.
|
|
18
|
+
Promise.resolve().then(() => readDetails(item.opportunity.id)).then(value => {
|
|
19
|
+
if (active) setDetail(value);
|
|
20
|
+
}, failure => { if (active) setError(failure.message); });
|
|
21
|
+
return () => { active = false; };
|
|
22
|
+
}, [item.opportunity.id, readDetails]);
|
|
23
|
+
const current = detail ?? item, row = current.opportunity;
|
|
24
|
+
return h('article', { className: 'opportunity-card', 'aria-label': row.description },
|
|
25
|
+
h('div', { className: 'opportunity-status' },
|
|
26
|
+
h('span', { className: 'status-badge' }, status(current, t)),
|
|
27
|
+
current.notice && h('span', { className: 'meta' }, t(current.notice.status === 'unread' ? 'opportunityUnread' : 'opportunityAlreadyRead'))),
|
|
28
|
+
h('h3', { className: 'text' }, row.description),
|
|
29
|
+
sessionId && row.conversationId !== sessionId && h('p', { className: 'meta' }, t('browseOtherSession')),
|
|
30
|
+
row.rationale && h('div', { className: 'opportunity-field' }, h('h4', null, t('opportunityRationale')), h('p', { className: 'text' }, row.rationale)),
|
|
31
|
+
row.suggestedPreparation && h('div', { className: 'opportunity-field' }, h('h4', null, t('opportunitySuggestion')), h('p', { className: 'text' }, row.suggestedPreparation)),
|
|
32
|
+
error && h('p', { role: 'alert' }, error),
|
|
33
|
+
current.reason && (current.run || current.control || current.rejection) && h('p', { className: 'meta' }, current.reason),
|
|
34
|
+
current.rejection && !current.run && h('p', { role: 'alert' }, current.rejection.error.message),
|
|
35
|
+
current.run?.error && h('p', { role: 'alert' }, current.run.error.message),
|
|
36
|
+
current.artifact && h('div', { className: 'opportunity-draft' }, h('h4', null, t('opportunityDraft')),
|
|
37
|
+
!current.available && h('p', { role: 'alert' }, t('opportunityStopped')),
|
|
38
|
+
h('p', { className: 'meta' }, current.artifact.evidence.path), h('pre', { className: 'text' }, current.artifact.evidence.text),
|
|
39
|
+
h('p', { className: 'meta' }, t('opportunityNotUsefulYet'))),
|
|
40
|
+
detail && h('details', { className: 'opportunity-evidence' }, h('summary', null, t('opportunityEvidence')),
|
|
41
|
+
h('pre', { className: 'text' }, JSON.stringify(detail.run?.input ?? detail.originals, null, 2))));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function OpportunityDock({ useOpportunities, onOpportunityDetails, onOpportunityRefresh, onOpportunityPage, opportunitySessionId, viewScope, t }) {
|
|
45
|
+
const state = useOpportunities(value => value);
|
|
46
|
+
const [open, setOpen] = useState(false), [refreshVersion, setRefreshVersion] = useState(0);
|
|
47
|
+
const button = (label, action, disabled = false, extra = {}) => h('button', { type: 'button', disabled, onClick: action, ...extra }, label);
|
|
48
|
+
return h('section', { className: 'arcfold-methods arcfold-opportunities', 'aria-label': t('opportunityTitle') },
|
|
49
|
+
h('header', null, h('strong', null, `${t('opportunityTitle')} · ${state.total}`),
|
|
50
|
+
button(t(open ? 'opportunityHide' : 'opportunityOpen'), () => setOpen(!open), false, { 'aria-expanded': open }),
|
|
51
|
+
button(t('refresh'), () => { setRefreshVersion(value => value + 1); void onOpportunityRefresh(); })),
|
|
52
|
+
state.error && h('p', { role: 'alert' }, state.error),
|
|
53
|
+
open && h('div', { className: 'detail opportunity-content' },
|
|
54
|
+
h('div', { className: 'opportunity-overview' },
|
|
55
|
+
h('p', null, t('opportunityReadOnly')),
|
|
56
|
+
viewScope !== 'all' && h('div', { className: 'opportunity-status' }, h('span', { className: 'status-badge' }, t(state.permission?.enabled ? 'opportunityOn' : 'opportunityOff'))),
|
|
57
|
+
state.workspace && h('p', { className: 'meta' }, `${t('opportunityWorkspace')} · ${state.workspace}`)),
|
|
58
|
+
h('ul', { className: 'opportunity-list' }, state.items.map(item => h('li', {
|
|
59
|
+
// Versioned snapshots discard late reads across reconnects and refresh.
|
|
60
|
+
key: JSON.stringify([item, state.permission?.revision, state.resetVersion, refreshVersion]),
|
|
61
|
+
}, h(OpportunityCard, { item, readDetails: onOpportunityDetails, sessionId: opportunitySessionId, t })))),
|
|
62
|
+
!state.loading && !state.items.length && h('p', { className: 'meta' }, t('opportunityEmpty'))),
|
|
63
|
+
open && (state.offset > 0 || state.nextOffset != null) && h('nav', { className: 'opportunity-pagination', 'aria-label': t('opportunityTitle') },
|
|
64
|
+
button(t('previous'), () => onOpportunityPage(Math.max(0, state.offset - 20)), !state.offset),
|
|
65
|
+
button(t('next'), () => onOpportunityPage(state.nextOffset), state.nextOffset == null)));
|
|
66
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { presentationLabel } from './locales.mjs';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
const h = React.createElement;
|
|
4
|
+
const literal = value => typeof value === 'string' ? value : JSON.stringify(value ?? '');
|
|
5
|
+
export function UnderstandingDock({ useUnderstandings, onUnderstandingDetails, onUnderstandingDecide,
|
|
6
|
+
onUnderstandingCorrect, onUnderstandingRefresh, onUnderstandingPage, t }) {
|
|
7
|
+
const state = useUnderstandings(value => value);
|
|
8
|
+
const [expanded, expand] = useState(false), [detail, setDetail] = useState(null), [edit, setEdit] = useState(null);
|
|
9
|
+
const [busy, setBusy] = useState(false), [loading, setLoading] = useState(false), [notice, setNotice] = useState(''), [error, setError] = useState('');
|
|
10
|
+
const generation = useRef(0), pending = useRef(false), identities = useRef(new Map()), mounted = useRef(true);
|
|
11
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; generation.current++; }; }, []);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
generation.current++; setDetail(null); setEdit(null); setLoading(false); setError(''); setNotice('');
|
|
14
|
+
}, [state.readEpoch]);
|
|
15
|
+
async function show(id, retainNotice = false, retainedEdit = null) {
|
|
16
|
+
const current = ++generation.current; setLoading(true); setEdit(null); setDetail(null);
|
|
17
|
+
if (!retainNotice) { setError(''); setNotice(''); }
|
|
18
|
+
try {
|
|
19
|
+
const value = await onUnderstandingDetails(id);
|
|
20
|
+
if (mounted.current && current === generation.current) {
|
|
21
|
+
setDetail(value);
|
|
22
|
+
if (retainedEdit?.id === value.understanding.id && retainedEdit.revision === value.understanding.revision) setEdit(retainedEdit);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (failure) { if (mounted.current && current === generation.current) setError(failure.message || t('failed')); }
|
|
26
|
+
finally { if (mounted.current && current === generation.current) setLoading(false); }
|
|
27
|
+
}
|
|
28
|
+
async function refresh() {
|
|
29
|
+
const current = generation.current, id = detail?.understanding.id;
|
|
30
|
+
try {
|
|
31
|
+
await onUnderstandingRefresh();
|
|
32
|
+
if (mounted.current && current === generation.current && id) await show(id);
|
|
33
|
+
} catch (failure) { if (mounted.current && current === generation.current) setError(failure.message || t('failed')); }
|
|
34
|
+
}
|
|
35
|
+
async function act(action, source) {
|
|
36
|
+
if (pending.current || loading || !detail || detail.browse?.related === false) return;
|
|
37
|
+
const current = detail.understanding, selectedGeneration = generation.current;
|
|
38
|
+
if (action === 'correct' && (!edit || edit.id !== current.id || edit.revision !== current.revision || !edit.text.trim())) return;
|
|
39
|
+
const request = { understandingId: current.id, expectedRevision: current.revision,
|
|
40
|
+
...(action === 'correct' ? { text: edit.text } : { action }),
|
|
41
|
+
...(source ? { sourceRef: { id: source.id, revision: source.revision, expectedControlRevision: source.expectedControlRevision } } : {}) };
|
|
42
|
+
const key = JSON.stringify(request);
|
|
43
|
+
if (!identities.current.has(key)) identities.current.set(key, globalThis.crypto.randomUUID());
|
|
44
|
+
request.requestId = identities.current.get(key);
|
|
45
|
+
pending.current = true; setBusy(true); setNotice(''); setError('');
|
|
46
|
+
try {
|
|
47
|
+
const value = await (action === 'correct' ? onUnderstandingCorrect(request) : onUnderstandingDecide(request));
|
|
48
|
+
if (!mounted.current || selectedGeneration !== generation.current) return;
|
|
49
|
+
const command = value.command;
|
|
50
|
+
setNotice(t(command?.result?.state === 'applied' ? 'understandingApplied' : command?.result?.state === 'failed' ? 'understandingFailed'
|
|
51
|
+
: action !== 'correct' ? 'understandingSaved' : command?.admission?.state === 'accepted' ? 'understandingPending' : 'understandingUnknown'));
|
|
52
|
+
await onUnderstandingRefresh();
|
|
53
|
+
if (mounted.current && selectedGeneration === generation.current)
|
|
54
|
+
await show(current.id, true, action === 'correct' && !command?.result && command?.admission?.state !== 'accepted' ? edit : null);
|
|
55
|
+
} catch (failure) {
|
|
56
|
+
if (mounted.current && selectedGeneration === generation.current) {
|
|
57
|
+
if (failure.code === 'revision_conflict') {
|
|
58
|
+
setError(failure.message || t('failed'));
|
|
59
|
+
await show(current.id, true);
|
|
60
|
+
} else setError((failure.message || t('failed')) + ' ' + t('understandingUnknown'));
|
|
61
|
+
}
|
|
62
|
+
} finally { pending.current = false; if (mounted.current) setBusy(false); }
|
|
63
|
+
}
|
|
64
|
+
const button = (label, action, disabled = false, props = {}) => h('button', { type: 'button', disabled,
|
|
65
|
+
onClick: () => { void action(); }, ...props }, t(label));
|
|
66
|
+
const body = value => h('div', { className: 'text' }, literal(value));
|
|
67
|
+
const item = detail?.understanding;
|
|
68
|
+
const claim = value => presentationLabel('claim', value, t);
|
|
69
|
+
return h('section', { className: 'arcfold-methods arcfold-understandings', 'aria-label': t('understandingTitle') },
|
|
70
|
+
h('header', null, h('strong', null, `${t('understandingTitle')} · ${state.total}`),
|
|
71
|
+
button(expanded ? 'understandingHide' : 'understandingOpen', () => expand(!expanded), false, { 'aria-expanded': expanded }),
|
|
72
|
+
button('refresh', refresh, busy || loading)),
|
|
73
|
+
expanded && h('div', { className: 'detail' },
|
|
74
|
+
h('p', { className: 'meta' }, t('understandingBoundary')),
|
|
75
|
+
(state.loading || loading) && h('p', { role: 'status' }, t('understandingLoading')),
|
|
76
|
+
(error || state.error) && h('p', { role: 'alert' }, error || state.error), notice && h('p', { role: 'status' }, notice),
|
|
77
|
+
h('ul', null, state.items.map(row => h('li', { key: row.understanding.id },
|
|
78
|
+
body(row.understanding.content), h('p', { className: 'meta' }, claim(row.understanding.claimType)),
|
|
79
|
+
button('understandingOpen', () => show(row.understanding.id), busy || loading)))),
|
|
80
|
+
!state.loading && !state.error && !state.items.length && h('p', null, t('understandingEmpty')),
|
|
81
|
+
h('nav', null, button('previous', () => onUnderstandingPage(Math.max(0, state.offset - 20)), busy || loading || !state.offset),
|
|
82
|
+
button('next', () => onUnderstandingPage(state.nextOffset), busy || loading || state.nextOffset == null)),
|
|
83
|
+
item && h('article', { 'aria-label': t('understandingTitle') + ' ' + item.id },
|
|
84
|
+
h('h3', null, claim(item.claimType)), body(item.content),
|
|
85
|
+
detail.browse?.related === false && h('p', { className: 'meta' }, t('browseReturnToOrigin')),
|
|
86
|
+
h('p', { className: 'meta' }, `${t('revision')} ${item.revision} · ${t('understandingVersion')} ${item.version ?? '—'} · ${presentationLabel('understanding', item.status, t)} · ${t(detail.availability.allowed ? 'understandingAvailable' : 'understandingUnavailable')}`),
|
|
87
|
+
!detail.availability.allowed && h('p', null, detail.availability.message),
|
|
88
|
+
h('p', { className: 'meta' }, t('understandingWithdrawalHelp')),
|
|
89
|
+
h('div', { className: 'actions' },
|
|
90
|
+
button('understandingStop', () => act('stop-understanding'), busy || loading || !detail.controls?.stopAllowed),
|
|
91
|
+
button('understandingCorrect', () => setEdit({ id: item.id, revision: item.revision, text: item.content }), busy || loading || !detail.controls?.correctionAllowed)),
|
|
92
|
+
edit && h('div', { className: 'understanding-correction' },
|
|
93
|
+
h('p', null, t('understandingCorrectionHelp')),
|
|
94
|
+
h('label', null, t('understandingReplacement'), h('textarea', { value: edit.text, maxLength: 16000, disabled: busy || loading,
|
|
95
|
+
onChange: event => setEdit({ ...edit, text: event.target.value }) })),
|
|
96
|
+
button('understandingSubmit', () => act('correct'), busy || loading || !edit.text.trim()),
|
|
97
|
+
button('understandingCancel', () => setEdit(null), busy)),
|
|
98
|
+
h('h4', null, t('understandingSource')),
|
|
99
|
+
(detail.sources || []).map(source => h('div', { className: 'source', key: source.id },
|
|
100
|
+
h('p', { className: 'meta' }, `${source.id} · ${source.author && source.author !== source.role ? source.author : presentationLabel('role', source.role, t)} · ${source.occurredAt || ''} · ${t('revision')} ${source.revision ?? '—'} · ${t(source.available ? 'understandingAvailable' : 'understandingUnavailable')}`),
|
|
101
|
+
body(source.missing ? t('understandingMissing') : source.content),
|
|
102
|
+
h('details', null, h('summary', null, t('understandingProvenance')), body(source.provenance), source.control && body(source.control)),
|
|
103
|
+
button('understandingWithdraw', () => act('withdraw-source', source), busy || loading || detail.controls?.sourceControlAllowed === false || !source.available || !Number.isSafeInteger(source.revision)))),
|
|
104
|
+
h('h4', null, t('understandingSuccessors')),
|
|
105
|
+
(detail.successors || []).map(next => h('div', { key: next.id }, h('span', null, `${next.id} · ${t('revision')} ${next.revision} `),
|
|
106
|
+
button('understandingOpen', () => show(next.id), busy || loading))),
|
|
107
|
+
h('details', null, h('summary', null, `${t('understandingHistory')} · ${detail.history?.length || 0}/${detail.historyTotal || 0}`),
|
|
108
|
+
(detail.history || []).map(row => h('div', { className: 'source', key: row.understanding.id },
|
|
109
|
+
h('p', { className: 'meta' }, `${row.understanding.id} · ${t('revision')} ${row.understanding.revision} · ${t('understandingVersion')} ${row.understanding.version ?? '—'}`), body(row.understanding.content)))),
|
|
110
|
+
h('h4', null, `${t('understandingCommands')} · ${detail.commands?.length || 0}/${detail.commandsTotal || 0}`),
|
|
111
|
+
(detail.commands || []).map(command => h('div', { className: 'source', key: command.id },
|
|
112
|
+
h('p', null, `${presentationLabel('understandingAction', command.action, t)} · ${command.requestId} · ${t(command.result?.state === 'applied' ? 'understandingApplied' : command.result?.state === 'failed' ? 'understandingFailed' : 'understandingAwaiting')}`),
|
|
113
|
+
command.admission && h('p', null, t('understandingAdmission') + ': ' + presentationLabel('admission', command.admission.state, t)),
|
|
114
|
+
command.request?.text !== undefined && body(command.request.text), command.result?.error && body(command.result.error.message),
|
|
115
|
+
command.result?.changes?.length > 0 && h('details', null, h('summary', null, t('understandingEffects')), body(command.result.changes)),
|
|
116
|
+
command.result?.goalImpacts?.length > 0 && h('details', null, h('summary', null, t('understandingGoalEffects')), body(command.result.goalImpacts)))))));
|
|
117
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The UI host is packaged separately from core; keep its wire validation local.
|
|
2
|
+
export function browseScope(value, fallback = 'current') {
|
|
3
|
+
if (value === undefined) return fallback;
|
|
4
|
+
if (value === 'all' || value === 'current') return value;
|
|
5
|
+
throw Object.assign(new Error('请选择全部积累或与当前聊天相关的内容。'), { code: 'invalid_browse_scope' });
|
|
6
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Browsing changes a read projection, never a record's scope or an action's authority.
|
|
2
|
+
export function viewScopeFields(viewScope) {
|
|
3
|
+
if (viewScope !== undefined && !['all', 'current'].includes(viewScope))
|
|
4
|
+
throw new TypeError('Unknown Arcfold view scope');
|
|
5
|
+
return viewScope === undefined ? {} : { viewScope };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function requireViewScope(viewScope) {
|
|
9
|
+
if (viewScope === undefined) throw new TypeError('A view scope is required');
|
|
10
|
+
viewScopeFields(viewScope);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createBrowseScopeSource(sources) {
|
|
14
|
+
let snapshot = { viewScope: 'all', epoch: 0 }, disposed = false;
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
return {
|
|
17
|
+
getSnapshot: () => snapshot,
|
|
18
|
+
subscribe(listener) {
|
|
19
|
+
if (disposed) return () => {};
|
|
20
|
+
listeners.add(listener); return () => listeners.delete(listener);
|
|
21
|
+
},
|
|
22
|
+
setViewScope(viewScope) {
|
|
23
|
+
requireViewScope(viewScope);
|
|
24
|
+
if (disposed || snapshot.viewScope === viewScope) return;
|
|
25
|
+
for (const source of sources) source.setViewScope?.(viewScope);
|
|
26
|
+
snapshot = { viewScope, epoch: snapshot.epoch + 1 };
|
|
27
|
+
for (const listener of listeners) listener();
|
|
28
|
+
},
|
|
29
|
+
dispose() { disposed = true; listeners.clear(); },
|
|
30
|
+
};
|
|
31
|
+
}
|