flare-chat-core 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6343 -0
- package/package.json +19 -22
- package/docs/CAPABILITY-INVENTORY.md +0 -42
- package/docs/CHAT-CORE-BOUNDARY.md +0 -47
- package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +0 -86
- package/docs/SSOT-CHAT-CORE-BOUNDARY.md +0 -73
- package/docs/SSOT-CHAT-CORE-DATAFLOW.md +0 -97
- package/index.html +0 -12
- package/scripts/check.sh +0 -15
- package/src/adapters/index.js +0 -6
- package/src/adapters/message-api.adapter.js +0 -59
- package/src/adapters/session-api.adapter.js +0 -133
- package/src/adapters/session-message-api.http.js +0 -161
- package/src/adapters/session-message-api.js +0 -34
- package/src/adapters/session-message-api.normalize-source-record.test.mjs +0 -180
- package/src/adapters/session-message-api.normalizers.js +0 -153
- package/src/adapters/source-api.adapter.js +0 -135
- package/src/adapters/sse-client.js +0 -244
- package/src/adapters/sse-event-dispatcher.js +0 -121
- package/src/app/App.jsx +0 -11
- package/src/app/AppProviders.jsx +0 -12
- package/src/app/ChatWorkspaceScreen.jsx +0 -33
- package/src/app/WorkspaceLayout.jsx +0 -190
- package/src/app/components/AppCanvasPanel.jsx +0 -64
- package/src/app/components/TriggerThresholdPopoverContent.jsx +0 -122
- package/src/app/components/WorkspaceBodySection.jsx +0 -156
- package/src/app/components/WorkspaceMainPane.jsx +0 -121
- package/src/app/components/WorkspaceSessionPane.jsx +0 -70
- package/src/app/components/WorkspaceTopBarSection.jsx +0 -71
- package/src/app/core-chat-entry/ComposerSectionNode.jsx +0 -241
- package/src/app/core-chat-entry/attachmentSendRefs.js +0 -154
- package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +0 -101
- package/src/app/core-chat-entry/composerActionRouter.js +0 -26
- package/src/app/core-chat-entry/constants.js +0 -108
- package/src/app/core-chat-entry/selectors.js +0 -28
- package/src/app/core-chat-entry/useAppActionErrorGuards.js +0 -68
- package/src/app/core-chat-entry/useChatCorePipelines.js +0 -110
- package/src/app/core-chat-entry/useComposerModeSuggestion.js +0 -89
- package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +0 -22
- package/src/app/core-chat-entry/useProjectNameEditing.js +0 -41
- package/src/app/core-chat-entry/useProjectSourceUpload.js +0 -341
- package/src/app/core-chat-entry/useRealApiReadinessGate.js +0 -103
- package/src/app/core-chat-entry/useUnavailableActionError.js +0 -29
- package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +0 -177
- package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +0 -171
- package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +0 -199
- package/src/app/core-chat-entry/useWorkspaceController.jsx +0 -226
- package/src/app/core-chat-entry/useWorkspacePanels.js +0 -55
- package/src/app/hooks/useComposerAttachmentSync.js +0 -223
- package/src/app/hooks/useComposerChooserHandlers.js +0 -52
- package/src/app/hooks/useSendWithContextRefs.js +0 -140
- package/src/app/hooks/useSendWithContextRefs.test.mjs +0 -29
- package/src/app/hooks/useUserThresholdProfile.js +0 -121
- package/src/app/index.js +0 -1
- package/src/app/selectors/assistantTextSelector.js +0 -73
- package/src/app/selectors/canvasEvidenceSummarySelector.js +0 -28
- package/src/app/selectors/canvasReportTemplateSelector.js +0 -28
- package/src/app/selectors/canvasTabsSelector.js +0 -58
- package/src/app/selectors/evidenceProjectionSelector.js +0 -175
- package/src/app/selectors/evidenceProjectionSelector.test.mjs +0 -107
- package/src/app/selectors/modeSuggestionSelector.js +0 -50
- package/src/chat-core/app/mockRuntime.js +0 -291
- package/src/chat-core/app/useAppStream.js +0 -187
- package/src/chat-core/app/useAppStream.refs.test.mjs +0 -44
- package/src/chat-core/app/useAppStream.request-body.test.mjs +0 -116
- package/src/chat-core/app/useCoreChatApp.js +0 -115
- package/src/chat-core/facade/useBasicConversationFacade.js +0 -280
- package/src/chat-core/index.js +0 -14
- package/src/chat-core/input/useChatInput.js +0 -103
- package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +0 -36
- package/src/chat-core/messages/buildTimelineItems.js +0 -201
- package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +0 -182
- package/src/chat-core/messages/contextUsageDefaults.js +0 -3
- package/src/chat-core/messages/contextUsageViewModel.js +0 -147
- package/src/chat-core/messages/contextUsageViewModel.test.mjs +0 -74
- package/src/chat-core/messages/useContextUsageViewModel.js +0 -41
- package/src/chat-core/orchestration/useBasicSendHandler.js +0 -55
- package/src/chat-core/pipelines/build-action-request.js +0 -46
- package/src/chat-core/pipelines/build-stream-request.js +0 -74
- package/src/chat-core/pipelines/entity-extraction.js +0 -159
- package/src/chat-core/pipelines/preprocess-message.js +0 -16
- package/src/chat-core/pipelines/stream-persist-utils.js +0 -32
- package/src/chat-core/pipelines/transport/send-mock-stream.js +0 -86
- package/src/chat-core/pipelines/transport/send-real-stream.js +0 -330
- package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +0 -27
- package/src/chat-core/pipelines/transport/send-sourcing-search.js +0 -86
- package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +0 -14
- package/src/chat-core/pipelines/transport/sourcing-response-templates.js +0 -55
- package/src/chat-core/pipelines/transport/sourcing-search-api.js +0 -155
- package/src/chat-core/runtime/runtimeMode.js +0 -69
- package/src/chat-core/session/chatSessionActionTypes.js +0 -24
- package/src/chat-core/session/chatSessionReducer.js +0 -352
- package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +0 -39
- package/src/chat-core/session/index.js +0 -2
- package/src/chat-core/session/sessionActionsMessages.js +0 -44
- package/src/chat-core/session/sessionActionsSessionCrud.js +0 -131
- package/src/chat-core/session/sessionActionsStreaming.js +0 -80
- package/src/chat-core/session/sessionActionsUiState.js +0 -51
- package/src/chat-core/session/useChatSessionReducer.js +0 -131
- package/src/chat-core/session/useSessionListController.js +0 -67
- package/src/chat-core/stream/sse-client.js +0 -1
- package/src/chat-core/stream/sse-event-dispatcher.js +0 -1
- package/src/chat-core/stream/sse-events.js +0 -1
- package/src/chat-core/stream/useSSEStream.js +0 -1
- package/src/chat-core/stream/useStreamSendController.js +0 -46
- package/src/contracts/context-ssot.js +0 -47
- package/src/contracts/index.js +0 -1
- package/src/contracts/sse-events/base-parsers.js +0 -79
- package/src/contracts/sse-events/domain-parsers.js +0 -3
- package/src/contracts/sse-events/internal-normalizers.js +0 -143
- package/src/contracts/sse-events/parsers-intake.js +0 -235
- package/src/contracts/sse-events/parsers-runtime.js +0 -37
- package/src/contracts/sse-events/parsers-sourcing.js +0 -179
- package/src/contracts/sse-events/patch-event-parser.js +0 -121
- package/src/contracts/sse-events/runtime-parsers.js +0 -79
- package/src/contracts/sse-events.js +0 -4
- package/src/index.js +0 -6
- package/src/main.jsx +0 -28
- package/src/orchestration/index.js +0 -6
- package/src/orchestration/useSSEStream.js +0 -221
- package/src/state/index.js +0 -4
- package/vite.config.js +0 -36
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
const CITATION_HIT_SCORE_THRESHOLD = 0.05;
|
|
2
|
-
|
|
3
|
-
function hasRenderableCitation(citation = {}) {
|
|
4
|
-
const score = Number(citation?.score);
|
|
5
|
-
if (Number.isFinite(score)) {
|
|
6
|
-
return score >= CITATION_HIT_SCORE_THRESHOLD;
|
|
7
|
-
}
|
|
8
|
-
const matchedText = String(citation?.matched_text || citation?.hit_reason || '').trim();
|
|
9
|
-
if (matchedText.length >= 2) {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
const snippet = String(citation?.snippet || '').trim();
|
|
13
|
-
return snippet.length >= 12;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeCitationPayload(payload) {
|
|
17
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const citations = Array.isArray(payload.citations) ? payload.citations : [];
|
|
21
|
-
const filtered = citations.filter((citation) => hasRenderableCitation(citation));
|
|
22
|
-
if (filtered.length === 0) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
...payload,
|
|
27
|
-
citations: filtered,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function buildTimelineItems(state = {}, options = {}) {
|
|
32
|
-
const messages = Array.isArray(state.messages) ? state.messages : [];
|
|
33
|
-
const uiCards = Array.isArray(state.uiCards) ? state.uiCards : [];
|
|
34
|
-
const executionCards = Array.isArray(state.executionCards) ? state.executionCards : [];
|
|
35
|
-
const streaming = state.streaming || {};
|
|
36
|
-
const loading = Boolean(options?.loading);
|
|
37
|
-
|
|
38
|
-
const items = [];
|
|
39
|
-
if (messages.length === 0) {
|
|
40
|
-
return {
|
|
41
|
-
isEmpty: true,
|
|
42
|
-
items: [],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
messages.forEach((msg, index) => {
|
|
47
|
-
const messageId = msg.message_id || `msg-fallback-${index}`;
|
|
48
|
-
const primaryFlow = (
|
|
49
|
-
msg.primary_flow
|
|
50
|
-
&& typeof msg.primary_flow === 'object'
|
|
51
|
-
&& !Array.isArray(msg.primary_flow)
|
|
52
|
-
) ? msg.primary_flow : null;
|
|
53
|
-
const chooserState = (
|
|
54
|
-
primaryFlow?.chooser_state
|
|
55
|
-
&& typeof primaryFlow.chooser_state === 'object'
|
|
56
|
-
&& !Array.isArray(primaryFlow.chooser_state)
|
|
57
|
-
) ? primaryFlow.chooser_state : null;
|
|
58
|
-
const authoritativeRoundId = String(primaryFlow?.round_id || '').trim();
|
|
59
|
-
items.push({
|
|
60
|
-
id: messageId,
|
|
61
|
-
type: 'message',
|
|
62
|
-
role: msg.role,
|
|
63
|
-
content: msg.content,
|
|
64
|
-
createdAt: msg.created_at,
|
|
65
|
-
client_request_id: msg.client_request_id || '',
|
|
66
|
-
intake_session_id: msg.intake_session_id || '',
|
|
67
|
-
roundKey: msg.round_key || authoritativeRoundId || '',
|
|
68
|
-
roundId: authoritativeRoundId,
|
|
69
|
-
roundState: String(primaryFlow?.round_state || '').trim(),
|
|
70
|
-
chooserKind: String(chooserState?.kind || '').trim(),
|
|
71
|
-
chooserVisible: chooserState?.visible === true,
|
|
72
|
-
highlights: msg.highlights ?? [],
|
|
73
|
-
sourceTypes: msg.sourceTypes ?? [],
|
|
74
|
-
attachments: Array.isArray(msg.attachments) ? msg.attachments : [],
|
|
75
|
-
contextUsage: (
|
|
76
|
-
msg.context_usage
|
|
77
|
-
&& typeof msg.context_usage === 'object'
|
|
78
|
-
&& !Array.isArray(msg.context_usage)
|
|
79
|
-
) ? msg.context_usage : null,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (msg.role === 'assistant') {
|
|
83
|
-
if (msg.knowledge_search && typeof msg.knowledge_search === 'object') {
|
|
84
|
-
items.push({
|
|
85
|
-
id: `knowledge-search-${messageId}`,
|
|
86
|
-
type: 'knowledge_search',
|
|
87
|
-
payload: msg.knowledge_search,
|
|
88
|
-
roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (msg.sourcing_candidates && typeof msg.sourcing_candidates === 'object') {
|
|
92
|
-
items.push({
|
|
93
|
-
id: `sourcing-candidates-history-${messageId}`,
|
|
94
|
-
type: 'sourcing_candidates',
|
|
95
|
-
payload: msg.sourcing_candidates,
|
|
96
|
-
roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const historicalCitationPayload = normalizeCitationPayload(msg.knowledge_citation);
|
|
100
|
-
if (historicalCitationPayload) {
|
|
101
|
-
items.push({
|
|
102
|
-
id: `knowledge-citation-${messageId}`,
|
|
103
|
-
type: 'knowledge_citation',
|
|
104
|
-
payload: historicalCitationPayload,
|
|
105
|
-
roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (uiCards.length > 0) {
|
|
112
|
-
items.push({
|
|
113
|
-
id: 'conversation-ui-cards',
|
|
114
|
-
type: 'ui_cards',
|
|
115
|
-
cards: uiCards,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
streaming.knowledgeSearch
|
|
121
|
-
&& typeof streaming.knowledgeSearch === 'object'
|
|
122
|
-
&& !Array.isArray(streaming.knowledgeSearch)
|
|
123
|
-
) {
|
|
124
|
-
items.push({
|
|
125
|
-
id: `knowledge-search-${String(streaming.knowledgeSearch.run_id || 'latest')}`,
|
|
126
|
-
type: 'knowledge_search',
|
|
127
|
-
payload: streaming.knowledgeSearch,
|
|
128
|
-
roundKey: String(streaming.roundKey || '').trim(),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
streaming.sourcingCandidates
|
|
134
|
-
&& typeof streaming.sourcingCandidates === 'object'
|
|
135
|
-
&& !Array.isArray(streaming.sourcingCandidates)
|
|
136
|
-
) {
|
|
137
|
-
const analysisRoute = (
|
|
138
|
-
streaming.sourcingCandidates.analysis_route
|
|
139
|
-
&& typeof streaming.sourcingCandidates.analysis_route === 'object'
|
|
140
|
-
&& !Array.isArray(streaming.sourcingCandidates.analysis_route)
|
|
141
|
-
)
|
|
142
|
-
? streaming.sourcingCandidates.analysis_route
|
|
143
|
-
: (
|
|
144
|
-
streaming.analysisRoute
|
|
145
|
-
&& typeof streaming.analysisRoute === 'object'
|
|
146
|
-
&& !Array.isArray(streaming.analysisRoute)
|
|
147
|
-
)
|
|
148
|
-
? streaming.analysisRoute
|
|
149
|
-
: null;
|
|
150
|
-
items.push({
|
|
151
|
-
id: `sourcing-candidates-${String(streaming.sourcingCandidates.run_id || 'latest')}`,
|
|
152
|
-
type: 'sourcing_candidates',
|
|
153
|
-
payload: streaming.sourcingCandidates,
|
|
154
|
-
analysisRoute,
|
|
155
|
-
roundKey: String(streaming.roundKey || '').trim(),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const streamingCitationPayload = normalizeCitationPayload(streaming.knowledgeCitation);
|
|
160
|
-
if (streamingCitationPayload) {
|
|
161
|
-
items.push({
|
|
162
|
-
id: `knowledge-citation-${String(streamingCitationPayload.run_id || 'latest')}`,
|
|
163
|
-
type: 'knowledge_citation',
|
|
164
|
-
payload: streamingCitationPayload,
|
|
165
|
-
roundKey: String(streaming.roundKey || '').trim(),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (streaming.content) {
|
|
170
|
-
items.push({
|
|
171
|
-
id: 'streaming-message',
|
|
172
|
-
type: 'streaming',
|
|
173
|
-
content: streaming.content,
|
|
174
|
-
roundKey: String(streaming.roundKey || '').trim(),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const hasThinkingSignal = Boolean(streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace);
|
|
179
|
-
if (hasThinkingSignal || loading) {
|
|
180
|
-
items.push({
|
|
181
|
-
id: hasThinkingSignal ? 'thinking-bubble' : 'thinking-bubble-loading',
|
|
182
|
-
type: 'thinking',
|
|
183
|
-
agentStatus: streaming.agentStatus || {
|
|
184
|
-
status: 'running',
|
|
185
|
-
agent: '助手',
|
|
186
|
-
},
|
|
187
|
-
thinkingTrace: streaming.thinkingTrace,
|
|
188
|
-
executionTrace: streaming.executionTrace,
|
|
189
|
-
executionCards,
|
|
190
|
-
roundKey: String(streaming.roundKey || '').trim(),
|
|
191
|
-
loading,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
isEmpty: false,
|
|
197
|
-
items,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export default buildTimelineItems;
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { buildTimelineItems } from './buildTimelineItems.js';
|
|
4
|
-
|
|
5
|
-
test('buildTimelineItems appends knowledge_citation artifact from streaming state', () => {
|
|
6
|
-
const timeline = buildTimelineItems({
|
|
7
|
-
messages: [
|
|
8
|
-
{
|
|
9
|
-
message_id: 'm1',
|
|
10
|
-
role: 'user',
|
|
11
|
-
content: 'hello',
|
|
12
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
13
|
-
},
|
|
14
|
-
],
|
|
15
|
-
streaming: {
|
|
16
|
-
content: '',
|
|
17
|
-
knowledgeCitation: {
|
|
18
|
-
run_id: 'retrieval-1',
|
|
19
|
-
citations: [{
|
|
20
|
-
citation_id: 'cite_1',
|
|
21
|
-
source_id: 'src-1',
|
|
22
|
-
matched_text: '公司20周年庆典礼品采购',
|
|
23
|
-
score: 0.23,
|
|
24
|
-
}],
|
|
25
|
-
},
|
|
26
|
-
roundKey: 'rk-1',
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const citationItem = timeline.items.find((item) => item.type === 'knowledge_citation');
|
|
31
|
-
assert.ok(citationItem);
|
|
32
|
-
assert.equal(citationItem.id, 'knowledge-citation-retrieval-1');
|
|
33
|
-
assert.equal(citationItem.payload.run_id, 'retrieval-1');
|
|
34
|
-
assert.equal(citationItem.payload.citations[0].source_id, 'src-1');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('buildTimelineItems skips knowledge_citation artifact when citations are non-hit noise', () => {
|
|
38
|
-
const timeline = buildTimelineItems({
|
|
39
|
-
messages: [
|
|
40
|
-
{
|
|
41
|
-
message_id: 'm1',
|
|
42
|
-
role: 'user',
|
|
43
|
-
content: 'hello',
|
|
44
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
streaming: {
|
|
48
|
-
content: '',
|
|
49
|
-
knowledgeCitation: {
|
|
50
|
-
run_id: 'retrieval-noise',
|
|
51
|
-
citations: [
|
|
52
|
-
{
|
|
53
|
-
citation_id: 'cite_noise',
|
|
54
|
-
source_id: 'src-noise',
|
|
55
|
-
score: 0.0001,
|
|
56
|
-
snippet: '周年庆',
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
roundKey: 'rk-1',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const citationItem = timeline.items.find((item) => item.type === 'knowledge_citation');
|
|
65
|
-
assert.equal(citationItem, undefined);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('buildTimelineItems appends knowledge_search artifact from streaming state', () => {
|
|
69
|
-
const timeline = buildTimelineItems({
|
|
70
|
-
messages: [
|
|
71
|
-
{
|
|
72
|
-
message_id: 'm1',
|
|
73
|
-
role: 'user',
|
|
74
|
-
content: 'hello',
|
|
75
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
streaming: {
|
|
79
|
-
content: '',
|
|
80
|
-
knowledgeSearch: {
|
|
81
|
-
run_id: 'search-1',
|
|
82
|
-
query: '上海大型erp供应商',
|
|
83
|
-
results: [{ id: 'vendor-1', name: 'Vendor A' }],
|
|
84
|
-
},
|
|
85
|
-
roundKey: 'rk-1',
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const searchItem = timeline.items.find((item) => item.type === 'knowledge_search');
|
|
90
|
-
assert.ok(searchItem);
|
|
91
|
-
assert.equal(searchItem.id, 'knowledge-search-search-1');
|
|
92
|
-
assert.equal(searchItem.payload.query, '上海大型erp供应商');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('buildTimelineItems appends sourcing_candidates artifact from streaming state', () => {
|
|
96
|
-
const timeline = buildTimelineItems({
|
|
97
|
-
messages: [
|
|
98
|
-
{
|
|
99
|
-
message_id: 'm1',
|
|
100
|
-
role: 'user',
|
|
101
|
-
content: 'hello',
|
|
102
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
streaming: {
|
|
106
|
-
content: '',
|
|
107
|
-
sourcingCandidates: {
|
|
108
|
-
run_id: 'source-1',
|
|
109
|
-
candidates: [{ supplier_id: 'sup-1', supplier_name: 'Vendor A' }],
|
|
110
|
-
},
|
|
111
|
-
roundKey: 'rk-1',
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const sourcingItem = timeline.items.find((item) => item.type === 'sourcing_candidates');
|
|
116
|
-
assert.ok(sourcingItem);
|
|
117
|
-
assert.equal(sourcingItem.id, 'sourcing-candidates-source-1');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('buildTimelineItems keeps executionCards on thinking item for step-by-step display', () => {
|
|
121
|
-
const timeline = buildTimelineItems({
|
|
122
|
-
messages: [
|
|
123
|
-
{
|
|
124
|
-
message_id: 'm1',
|
|
125
|
-
role: 'user',
|
|
126
|
-
content: 'hello',
|
|
127
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
executionCards: [
|
|
131
|
-
{ step_id: 'context_binding', label: '上下文绑定', detail: '绑定 2 个来源', status: 'completed' },
|
|
132
|
-
],
|
|
133
|
-
streaming: {
|
|
134
|
-
content: '',
|
|
135
|
-
executionTrace: { step_id: 'llm', label: '生成回复', status: 'running' },
|
|
136
|
-
roundKey: 'rk-2',
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const thinkingItem = timeline.items.find((item) => item.type === 'thinking');
|
|
141
|
-
assert.ok(thinkingItem);
|
|
142
|
-
assert.equal(Array.isArray(thinkingItem.executionCards), true);
|
|
143
|
-
assert.equal(thinkingItem.executionCards.length, 1);
|
|
144
|
-
assert.equal(thinkingItem.executionCards[0].step_id, 'context_binding');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('buildTimelineItems does not append historical assistant thinking item', () => {
|
|
148
|
-
const timeline = buildTimelineItems({
|
|
149
|
-
messages: [
|
|
150
|
-
{
|
|
151
|
-
message_id: 'u1',
|
|
152
|
-
role: 'user',
|
|
153
|
-
content: 'hello',
|
|
154
|
-
created_at: '2026-04-19T00:00:00Z',
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
message_id: 'a1',
|
|
158
|
-
role: 'assistant',
|
|
159
|
-
content: 'first answer',
|
|
160
|
-
created_at: '2026-04-19T00:00:01Z',
|
|
161
|
-
execution_trace: { step_id: 'knowledge_search', status: 'completed' },
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
message_id: 'u2',
|
|
165
|
-
role: 'user',
|
|
166
|
-
content: 'next',
|
|
167
|
-
created_at: '2026-04-19T00:00:02Z',
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
message_id: 'a2',
|
|
171
|
-
role: 'assistant',
|
|
172
|
-
content: 'second answer',
|
|
173
|
-
created_at: '2026-04-19T00:00:03Z',
|
|
174
|
-
execution_trace: { step_id: 'knowledge_search', status: 'completed' },
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
streaming: {},
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const thinkingItems = timeline.items.filter((item) => item.type === 'thinking');
|
|
181
|
-
assert.equal(thinkingItems.length, 0);
|
|
182
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
function coercePositiveInteger(value) {
|
|
2
|
-
const parsed = Number(value);
|
|
3
|
-
if (!Number.isFinite(parsed)) {
|
|
4
|
-
return 0;
|
|
5
|
-
}
|
|
6
|
-
return Math.max(0, Math.round(parsed));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function extractTimelineMessageText(item) {
|
|
10
|
-
if (!item || typeof item !== 'object') {
|
|
11
|
-
return '';
|
|
12
|
-
}
|
|
13
|
-
const candidates = [item.content, item.text, item.message, item.value];
|
|
14
|
-
for (const candidate of candidates) {
|
|
15
|
-
const normalized = String(candidate || '').replace(/\s+/g, ' ').trim();
|
|
16
|
-
if (normalized) {
|
|
17
|
-
return normalized;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return '';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function extractContextUsageFromTrace(trace) {
|
|
24
|
-
if (!trace || typeof trace !== 'object' || Array.isArray(trace)) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
if (String(trace.step_id || '').trim() !== 'context_usage') {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const detail = String(trace.detail || '').trim();
|
|
31
|
-
if (!detail) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const modeMatch = detail.match(/mode=([^;/]+)\/([^;]+)/);
|
|
35
|
-
const profileMatch = detail.match(/profile=([^;]+)/);
|
|
36
|
-
const tokenMatch = detail.match(/tokens≈(\d+)/);
|
|
37
|
-
return {
|
|
38
|
-
mode_key: modeMatch ? String(modeMatch[1] || '').trim() : '',
|
|
39
|
-
mode_state: modeMatch ? String(modeMatch[2] || '').trim() : '',
|
|
40
|
-
profile: profileMatch ? String(profileMatch[1] || '').trim() : '',
|
|
41
|
-
token_estimate: {
|
|
42
|
-
total_prompt_tokens_estimate: tokenMatch ? coercePositiveInteger(tokenMatch[1]) : 0,
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function resolveLatestTimelineContextUsage(items) {
|
|
48
|
-
const timelineItems = Array.isArray(items) ? items : [];
|
|
49
|
-
for (let index = timelineItems.length - 1; index >= 0; index -= 1) {
|
|
50
|
-
const item = timelineItems[index];
|
|
51
|
-
if (String(item?.type || '').trim() === 'message' && String(item?.role || '').trim() === 'assistant') {
|
|
52
|
-
const usage = item?.contextUsage;
|
|
53
|
-
if (usage && typeof usage === 'object' && !Array.isArray(usage)) {
|
|
54
|
-
return { source: 'backend', usage };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (String(item?.type || '').trim() === 'thinking') {
|
|
58
|
-
const usage = extractContextUsageFromTrace(item?.executionTrace);
|
|
59
|
-
if (usage) {
|
|
60
|
-
return { source: 'stream_trace', usage };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { source: 'estimate', usage: null };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function buildLayerSummaryLabel(layers) {
|
|
68
|
-
if (!Array.isArray(layers) || layers.length === 0) {
|
|
69
|
-
return '';
|
|
70
|
-
}
|
|
71
|
-
const normalizedLayers = layers.filter((layer) => layer && typeof layer === 'object');
|
|
72
|
-
if (normalizedLayers.length === 0) {
|
|
73
|
-
return '';
|
|
74
|
-
}
|
|
75
|
-
const includedLayers = normalizedLayers.filter((layer) => layer.included !== false).length;
|
|
76
|
-
return `layers ${includedLayers}/${normalizedLayers.length}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function buildBudgetStatusLabel({ tokens, limit }) {
|
|
80
|
-
if (!Number.isFinite(tokens) || !Number.isFinite(limit) || limit <= 0) {
|
|
81
|
-
return '';
|
|
82
|
-
}
|
|
83
|
-
const ratio = tokens / limit;
|
|
84
|
-
if (ratio >= 1) {
|
|
85
|
-
return '超限';
|
|
86
|
-
}
|
|
87
|
-
if (ratio >= 0.85) {
|
|
88
|
-
return '临界';
|
|
89
|
-
}
|
|
90
|
-
if (ratio >= 0.6) {
|
|
91
|
-
return '偏高';
|
|
92
|
-
}
|
|
93
|
-
return '正常';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function buildContextUsageViewModel({
|
|
97
|
-
timelineItems,
|
|
98
|
-
fallbackTokenLimit,
|
|
99
|
-
tokenEstimateDivisor = 2.4,
|
|
100
|
-
baseTokensPerMessage = 32,
|
|
101
|
-
}) {
|
|
102
|
-
const normalizedTimelineItems = Array.isArray(timelineItems) ? timelineItems : [];
|
|
103
|
-
const messageItems = normalizedTimelineItems.filter((item) => String(item?.type || '').trim() === 'message');
|
|
104
|
-
const messageTexts = messageItems.map((item) => extractTimelineMessageText(item)).filter(Boolean);
|
|
105
|
-
const messageCount = messageTexts.length;
|
|
106
|
-
const textCharCount = messageTexts.reduce((total, text) => total + text.length, 0);
|
|
107
|
-
const { source, usage } = resolveLatestTimelineContextUsage(timelineItems);
|
|
108
|
-
const sourceLabel = source === 'backend'
|
|
109
|
-
? '后端'
|
|
110
|
-
: (source === 'stream_trace' ? '流式' : '估算');
|
|
111
|
-
const totalTokens = coercePositiveInteger(usage?.token_estimate?.total_prompt_tokens_estimate);
|
|
112
|
-
const tokenLimit = coercePositiveInteger(usage?.budget_tokens?.total_tokens) || coercePositiveInteger(fallbackTokenLimit);
|
|
113
|
-
const byChars = tokenEstimateDivisor > 0 ? Math.round(textCharCount / tokenEstimateDivisor) : 0;
|
|
114
|
-
const byMessages = messageCount * coercePositiveInteger(baseTokensPerMessage);
|
|
115
|
-
const estimatedTokens = totalTokens > 0 ? totalTokens : Math.max(byChars, byMessages, 0);
|
|
116
|
-
const windowPercent = tokenLimit > 0
|
|
117
|
-
? Math.max(0, Math.min(100, Math.round((estimatedTokens / tokenLimit) * 100)))
|
|
118
|
-
: 0;
|
|
119
|
-
const modeKey = String(usage?.mode_key || '').trim();
|
|
120
|
-
const modeState = String(usage?.mode_state || '').trim();
|
|
121
|
-
const profile = String(usage?.profile || '').trim();
|
|
122
|
-
const status = buildBudgetStatusLabel({ tokens: estimatedTokens, limit: tokenLimit });
|
|
123
|
-
const layerSummary = buildLayerSummaryLabel(usage?.layers);
|
|
124
|
-
const profileLabel = profile ? ` · ${profile}` : '';
|
|
125
|
-
const modeLabel = modeKey ? ` · ${modeKey}${modeState ? `/${modeState}` : ''}` : '';
|
|
126
|
-
const statusLabel = status ? ` · ${status}` : '';
|
|
127
|
-
const layerLabel = layerSummary ? ` · ${layerSummary}` : '';
|
|
128
|
-
const budgetLabel = tokenLimit > 0
|
|
129
|
-
? `${estimatedTokens.toLocaleString('zh-CN')} / ${tokenLimit.toLocaleString('zh-CN')}`
|
|
130
|
-
: `${estimatedTokens.toLocaleString('zh-CN')}`;
|
|
131
|
-
return {
|
|
132
|
-
source,
|
|
133
|
-
usage,
|
|
134
|
-
modeKey,
|
|
135
|
-
modeState,
|
|
136
|
-
profile,
|
|
137
|
-
status,
|
|
138
|
-
layerSummary,
|
|
139
|
-
messageCount,
|
|
140
|
-
textCharCount,
|
|
141
|
-
totalTokens,
|
|
142
|
-
estimatedTokens,
|
|
143
|
-
tokenLimit,
|
|
144
|
-
windowPercent,
|
|
145
|
-
text: `${sourceLabel}${profileLabel}${modeLabel}${statusLabel}${layerLabel} · ${budgetLabel}`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { buildContextUsageViewModel } from './contextUsageViewModel.js';
|
|
4
|
-
|
|
5
|
-
test('buildContextUsageViewModel prefers backend usage over stream trace', () => {
|
|
6
|
-
const result = buildContextUsageViewModel({
|
|
7
|
-
timelineItems: [
|
|
8
|
-
{
|
|
9
|
-
type: 'thinking',
|
|
10
|
-
executionTrace: {
|
|
11
|
-
step_id: 'context_usage',
|
|
12
|
-
detail: 'mode=auto/running; profile=medium; tokens≈444',
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
type: 'message',
|
|
17
|
-
role: 'assistant',
|
|
18
|
-
content: 'done',
|
|
19
|
-
contextUsage: {
|
|
20
|
-
profile: 'heavy',
|
|
21
|
-
token_estimate: {
|
|
22
|
-
total_prompt_tokens_estimate: 123,
|
|
23
|
-
},
|
|
24
|
-
layers: [
|
|
25
|
-
{ included: true },
|
|
26
|
-
{ included: false },
|
|
27
|
-
],
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
fallbackTokenLimit: 1000,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
assert.equal(result.source, 'backend');
|
|
35
|
-
assert.equal(result.estimatedTokens, 123);
|
|
36
|
-
assert.equal(result.profile, 'heavy');
|
|
37
|
-
assert.equal(result.layerSummary, 'layers 1/2');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('buildContextUsageViewModel falls back to stream trace then estimate', () => {
|
|
41
|
-
const traceResult = buildContextUsageViewModel({
|
|
42
|
-
timelineItems: [
|
|
43
|
-
{
|
|
44
|
-
type: 'thinking',
|
|
45
|
-
executionTrace: {
|
|
46
|
-
step_id: 'context_usage',
|
|
47
|
-
detail: 'mode=requirement/collecting; profile=light; tokens≈333',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
fallbackTokenLimit: 1000,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
assert.equal(traceResult.source, 'stream_trace');
|
|
55
|
-
assert.equal(traceResult.estimatedTokens, 333);
|
|
56
|
-
assert.equal(traceResult.modeKey, 'requirement');
|
|
57
|
-
|
|
58
|
-
const estimateResult = buildContextUsageViewModel({
|
|
59
|
-
timelineItems: [
|
|
60
|
-
{
|
|
61
|
-
type: 'message',
|
|
62
|
-
role: 'user',
|
|
63
|
-
content: '需要一份采购计划。',
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
fallbackTokenLimit: 1000,
|
|
67
|
-
tokenEstimateDivisor: 2,
|
|
68
|
-
baseTokensPerMessage: 10,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
assert.equal(estimateResult.source, 'estimate');
|
|
72
|
-
assert.ok(estimateResult.estimatedTokens > 0);
|
|
73
|
-
assert.equal(estimateResult.tokenLimit, 1000);
|
|
74
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { buildTimelineItems } from './buildTimelineItems.js';
|
|
3
|
-
import { buildContextUsageViewModel } from './contextUsageViewModel.js';
|
|
4
|
-
|
|
5
|
-
export function useContextUsageViewModel({
|
|
6
|
-
timelineItems,
|
|
7
|
-
chatSessionState,
|
|
8
|
-
fallbackTokenLimit = 12000,
|
|
9
|
-
tokenEstimateDivisor = 2.4,
|
|
10
|
-
baseTokensPerMessage = 32,
|
|
11
|
-
loading = false,
|
|
12
|
-
}) {
|
|
13
|
-
const resolvedTimelineItems = useMemo(() => {
|
|
14
|
-
if (Array.isArray(timelineItems)) {
|
|
15
|
-
return timelineItems;
|
|
16
|
-
}
|
|
17
|
-
const timeline = buildTimelineItems({
|
|
18
|
-
messages: chatSessionState?.messages,
|
|
19
|
-
uiCards: chatSessionState?.uiCards,
|
|
20
|
-
executionCards: chatSessionState?.executionCards,
|
|
21
|
-
streaming: chatSessionState?.streaming,
|
|
22
|
-
}, { loading });
|
|
23
|
-
return timeline.items;
|
|
24
|
-
}, [
|
|
25
|
-
chatSessionState?.executionCards,
|
|
26
|
-
chatSessionState?.messages,
|
|
27
|
-
chatSessionState?.streaming,
|
|
28
|
-
chatSessionState?.uiCards,
|
|
29
|
-
loading,
|
|
30
|
-
timelineItems,
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
return useMemo(() => buildContextUsageViewModel({
|
|
34
|
-
timelineItems: resolvedTimelineItems,
|
|
35
|
-
fallbackTokenLimit,
|
|
36
|
-
tokenEstimateDivisor,
|
|
37
|
-
baseTokensPerMessage,
|
|
38
|
-
}), [resolvedTimelineItems, fallbackTokenLimit, tokenEstimateDivisor, baseTokensPerMessage]);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export default useContextUsageViewModel;
|