flare-chat-core 0.2.1 → 0.2.3

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.
Files changed (120) hide show
  1. package/README.md +28 -0
  2. package/docs/CAPABILITY-INVENTORY.md +42 -0
  3. package/docs/CHAT-CORE-BOUNDARY.md +47 -0
  4. package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +86 -0
  5. package/docs/SSOT-CHAT-CORE-BOUNDARY.md +73 -0
  6. package/docs/SSOT-CHAT-CORE-DATAFLOW.md +97 -0
  7. package/index.html +12 -0
  8. package/package.json +24 -2
  9. package/src/adapters/index.js +6 -0
  10. package/src/adapters/message-api.adapter.js +59 -0
  11. package/src/adapters/session-api.adapter.js +133 -0
  12. package/src/adapters/session-message-api.http.js +161 -0
  13. package/src/adapters/session-message-api.js +34 -0
  14. package/src/adapters/session-message-api.normalize-source-record.test.mjs +180 -0
  15. package/src/adapters/session-message-api.normalizers.js +153 -0
  16. package/src/adapters/source-api.adapter.js +135 -0
  17. package/src/adapters/sse-client.js +244 -0
  18. package/src/adapters/sse-event-dispatcher.js +121 -0
  19. package/src/app/App.jsx +11 -0
  20. package/src/app/AppProviders.jsx +12 -0
  21. package/src/app/ChatWorkspaceScreen.jsx +33 -0
  22. package/src/app/WorkspaceLayout.jsx +190 -0
  23. package/src/app/components/AppCanvasPanel.jsx +64 -0
  24. package/src/app/components/TriggerThresholdPopoverContent.jsx +122 -0
  25. package/src/app/components/WorkspaceBodySection.jsx +156 -0
  26. package/src/app/components/WorkspaceMainPane.jsx +121 -0
  27. package/src/app/components/WorkspaceSessionPane.jsx +70 -0
  28. package/src/app/components/WorkspaceTopBarSection.jsx +71 -0
  29. package/src/app/core-chat-entry/ComposerSectionNode.jsx +241 -0
  30. package/src/app/core-chat-entry/attachmentSendRefs.js +154 -0
  31. package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +101 -0
  32. package/src/app/core-chat-entry/composerActionRouter.js +26 -0
  33. package/src/app/core-chat-entry/constants.js +108 -0
  34. package/src/app/core-chat-entry/selectors.js +28 -0
  35. package/src/app/core-chat-entry/useAppActionErrorGuards.js +68 -0
  36. package/src/app/core-chat-entry/useChatCorePipelines.js +110 -0
  37. package/src/app/core-chat-entry/useComposerModeSuggestion.js +89 -0
  38. package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +22 -0
  39. package/src/app/core-chat-entry/useProjectNameEditing.js +41 -0
  40. package/src/app/core-chat-entry/useProjectSourceUpload.js +341 -0
  41. package/src/app/core-chat-entry/useRealApiReadinessGate.js +103 -0
  42. package/src/app/core-chat-entry/useUnavailableActionError.js +29 -0
  43. package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +177 -0
  44. package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +171 -0
  45. package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +199 -0
  46. package/src/app/core-chat-entry/useWorkspaceController.jsx +226 -0
  47. package/src/app/core-chat-entry/useWorkspacePanels.js +55 -0
  48. package/src/app/hooks/useComposerAttachmentSync.js +223 -0
  49. package/src/app/hooks/useComposerChooserHandlers.js +52 -0
  50. package/src/app/hooks/useSendWithContextRefs.js +140 -0
  51. package/src/app/hooks/useSendWithContextRefs.test.mjs +29 -0
  52. package/src/app/hooks/useUserThresholdProfile.js +121 -0
  53. package/src/app/index.js +1 -0
  54. package/src/app/selectors/assistantTextSelector.js +73 -0
  55. package/src/app/selectors/canvasEvidenceSummarySelector.js +28 -0
  56. package/src/app/selectors/canvasReportTemplateSelector.js +28 -0
  57. package/src/app/selectors/canvasTabsSelector.js +58 -0
  58. package/src/app/selectors/evidenceProjectionSelector.js +175 -0
  59. package/src/app/selectors/evidenceProjectionSelector.test.mjs +107 -0
  60. package/src/app/selectors/modeSuggestionSelector.js +50 -0
  61. package/src/chat-core/app/mockRuntime.js +291 -0
  62. package/src/chat-core/app/useAppStream.js +187 -0
  63. package/src/chat-core/app/useAppStream.refs.test.mjs +44 -0
  64. package/src/chat-core/app/useAppStream.request-body.test.mjs +116 -0
  65. package/src/chat-core/app/useCoreChatApp.js +115 -0
  66. package/src/chat-core/facade/useBasicConversationFacade.js +280 -0
  67. package/src/chat-core/index.js +9 -1
  68. package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +36 -0
  69. package/src/chat-core/messages/buildTimelineItems.js +139 -13
  70. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +182 -0
  71. package/src/chat-core/messages/contextUsageDefaults.js +3 -0
  72. package/src/chat-core/messages/contextUsageViewModel.js +147 -0
  73. package/src/chat-core/messages/contextUsageViewModel.test.mjs +74 -0
  74. package/src/chat-core/messages/useContextUsageViewModel.js +41 -0
  75. package/src/chat-core/orchestration/useBasicSendHandler.js +55 -0
  76. package/src/chat-core/pipelines/build-action-request.js +46 -0
  77. package/src/chat-core/pipelines/build-stream-request.js +74 -0
  78. package/src/chat-core/pipelines/entity-extraction.js +159 -0
  79. package/src/chat-core/pipelines/preprocess-message.js +16 -0
  80. package/src/chat-core/pipelines/stream-persist-utils.js +32 -0
  81. package/src/chat-core/pipelines/transport/send-mock-stream.js +86 -0
  82. package/src/chat-core/pipelines/transport/send-real-stream.js +330 -0
  83. package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +27 -0
  84. package/src/chat-core/pipelines/transport/send-sourcing-search.js +86 -0
  85. package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +14 -0
  86. package/src/chat-core/pipelines/transport/sourcing-response-templates.js +55 -0
  87. package/src/chat-core/pipelines/transport/sourcing-search-api.js +155 -0
  88. package/src/chat-core/runtime/runtimeMode.js +69 -0
  89. package/src/chat-core/session/chatSessionActionTypes.js +24 -0
  90. package/src/chat-core/session/chatSessionReducer.js +352 -0
  91. package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +39 -0
  92. package/src/chat-core/session/index.js +2 -0
  93. package/src/chat-core/session/sessionActionsMessages.js +44 -0
  94. package/src/chat-core/session/sessionActionsSessionCrud.js +131 -0
  95. package/src/chat-core/session/sessionActionsStreaming.js +80 -0
  96. package/src/chat-core/session/sessionActionsUiState.js +51 -0
  97. package/src/chat-core/session/useChatSessionReducer.js +62 -455
  98. package/src/chat-core/session/useSessionListController.js +67 -0
  99. package/src/chat-core/stream/sse-client.js +1 -244
  100. package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
  101. package/src/chat-core/stream/sse-events.js +1 -867
  102. package/src/chat-core/stream/useSSEStream.js +1 -356
  103. package/src/chat-core/stream/useStreamSendController.js +46 -0
  104. package/src/contracts/context-ssot.js +47 -0
  105. package/src/contracts/index.js +1 -0
  106. package/src/contracts/sse-events/base-parsers.js +79 -0
  107. package/src/contracts/sse-events/domain-parsers.js +3 -0
  108. package/src/contracts/sse-events/internal-normalizers.js +143 -0
  109. package/src/contracts/sse-events/parsers-intake.js +235 -0
  110. package/src/contracts/sse-events/parsers-runtime.js +37 -0
  111. package/src/contracts/sse-events/parsers-sourcing.js +179 -0
  112. package/src/contracts/sse-events/patch-event-parser.js +121 -0
  113. package/src/contracts/sse-events/runtime-parsers.js +79 -0
  114. package/src/contracts/sse-events.js +4 -0
  115. package/src/index.js +5 -0
  116. package/src/main.jsx +28 -0
  117. package/src/orchestration/index.js +6 -0
  118. package/src/orchestration/useSSEStream.js +221 -0
  119. package/src/state/index.js +4 -0
  120. package/vite.config.js +36 -0
@@ -0,0 +1,115 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { useBasicConversationFacade } from '../facade/useBasicConversationFacade.js';
3
+
4
+ /**
5
+ * Core app-level chat orchestration hook.
6
+ * Owns state/actions mapping for UI component libraries.
7
+ */
8
+ export default function useCoreChatApp({
9
+ sessionAPI,
10
+ messageAPI,
11
+ stream,
12
+ functionType,
13
+ defaultSessionTitle,
14
+ listParams,
15
+ createSessionPayload = {},
16
+ } = {}) {
17
+ const didInitialRefreshRef = useRef(false);
18
+ const chat = useBasicConversationFacade({
19
+ sessionAPI,
20
+ messageAPI,
21
+ stream,
22
+ functionType,
23
+ defaultSessionTitle,
24
+ listParams,
25
+ defaultSessionPayload: createSessionPayload,
26
+ });
27
+
28
+ useEffect(() => {
29
+ if (didInitialRefreshRef.current) {
30
+ return;
31
+ }
32
+ didInitialRefreshRef.current = true;
33
+ chat.actions.refreshSessionList();
34
+ }, [chat.actions.refreshSessionList]);
35
+
36
+ const viewModel = useMemo(() => ({
37
+ sessionList: {
38
+ sessions: chat.state.sessions.items,
39
+ error: chat.state.sessions.error,
40
+ activeSessionId: chat.state.session.sessionId,
41
+ onSelectSession: chat.actions.selectSession,
42
+ onCreateSession: () => chat.actions.createSession(createSessionPayload),
43
+ onCreateProject: () => chat.actions.createSession(createSessionPayload),
44
+ onRenameSession: async (session, nextTitle) => {
45
+ const sessionId = String(session?.sessionId || session?.session_id || session?.id || '').trim();
46
+ const title = String(nextTitle || '').trim();
47
+ if (!sessionId || !title) {
48
+ return;
49
+ }
50
+ await chat.actions.updateTitle(sessionId, title);
51
+ await chat.actions.refreshSessionList();
52
+ },
53
+ onArchiveSession: async (session) => {
54
+ const sessionId = String(session?.sessionId || session?.session_id || session?.id || '').trim();
55
+ if (!sessionId || !sessionAPI?.update) {
56
+ return;
57
+ }
58
+ await sessionAPI.update(sessionId, { status: 'archived' });
59
+ const latestSessions = await chat.actions.refreshSessionList();
60
+ if (sessionId !== chat.state.session.sessionId) {
61
+ return;
62
+ }
63
+ const nextActive = Array.isArray(latestSessions)
64
+ ? latestSessions.find((item) => {
65
+ const id = String(item?.sessionId || item?.session_id || item?.id || '').trim();
66
+ const status = String(item?.status || 'active').trim();
67
+ return Boolean(id) && status !== 'archived';
68
+ })
69
+ : null;
70
+ const nextSessionId = String(
71
+ nextActive?.sessionId
72
+ || nextActive?.session_id
73
+ || nextActive?.id
74
+ || ''
75
+ ).trim();
76
+ if (nextSessionId) {
77
+ await chat.actions.selectSession(nextSessionId);
78
+ return;
79
+ }
80
+ chat.actions.resetSession();
81
+ },
82
+ projectItems: Array.isArray(chat.state.sessions.items) && chat.state.sessions.items.length > 0
83
+ ? null
84
+ : [],
85
+ },
86
+ timeline: {
87
+ items: chat.state.timeline.items,
88
+ loading: chat.state.composer.loading,
89
+ },
90
+ composer: {
91
+ value: chat.state.composer.inputValue,
92
+ loading: chat.state.composer.loading,
93
+ error: chat.state.composer.error,
94
+ fileList: chat.state.composer.fileList,
95
+ hintText: chat.state.session.error
96
+ ? String(chat.state.session.error?.message || chat.state.session.error)
97
+ : '',
98
+ onChange: (event) => chat.actions.setInputValue(event?.target?.value || ''),
99
+ onSend: (options = {}) => chat.actions.send(options),
100
+ onAttachFiles: (files) => chat.actions.attachFiles(files),
101
+ onRemoveFile: (file) => chat.actions.removeFile(file),
102
+ },
103
+ toolbar: {
104
+ onRefreshSessions: chat.actions.refreshSessionList,
105
+ onCreateSession: () => chat.actions.createSession(createSessionPayload),
106
+ },
107
+ }), [chat.actions, chat.state.composer.error, chat.state.composer.fileList, chat.state.composer.inputValue, chat.state.composer.loading, chat.state.session.error, chat.state.session.sessionId, chat.state.sessions.error, chat.state.sessions.items, chat.state.timeline.items, createSessionPayload, sessionAPI]);
108
+
109
+ return {
110
+ chat,
111
+ viewModel,
112
+ };
113
+ }
114
+
115
+ export { useCoreChatApp };
@@ -0,0 +1,280 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import useChatSessionReducer from '../session/useChatSessionReducer.js';
3
+ import { useChatInput } from '../input/useChatInput.js';
4
+ import { useSSEStream } from '../stream/useSSEStream.js';
5
+ import { buildTimelineItems } from '../messages/buildTimelineItems.js';
6
+ import { useSessionListController } from '../session/useSessionListController.js';
7
+ import { useStreamSendController } from '../stream/useStreamSendController.js';
8
+ import { useBasicSendHandler } from '../orchestration/useBasicSendHandler.js';
9
+
10
+ function normalizeRefs(value) {
11
+ if (!Array.isArray(value)) {
12
+ return null;
13
+ }
14
+ return value.filter((item) => item !== undefined && item !== null);
15
+ }
16
+
17
+ function resolvePromoteSessionPayload(raw = {}) {
18
+ const source = (
19
+ raw
20
+ && typeof raw === 'object'
21
+ && !Array.isArray(raw)
22
+ ) ? raw : {};
23
+ const sessionEnvelope = (
24
+ source.session
25
+ && typeof source.session === 'object'
26
+ && !Array.isArray(source.session)
27
+ ) ? source.session : {};
28
+ const payloadEnvelope = (
29
+ source.payload
30
+ && typeof source.payload === 'object'
31
+ && !Array.isArray(source.payload)
32
+ ) ? source.payload : {};
33
+ const payloadSessionEnvelope = (
34
+ payloadEnvelope.session
35
+ && typeof payloadEnvelope.session === 'object'
36
+ && !Array.isArray(payloadEnvelope.session)
37
+ ) ? payloadEnvelope.session : {};
38
+ const sessionId = String(
39
+ source.sessionId
40
+ || sessionEnvelope.session_id
41
+ || ''
42
+ ).trim();
43
+ const title = String(
44
+ payloadSessionEnvelope.title
45
+ || ''
46
+ ).trim();
47
+ return { sessionId, title };
48
+ }
49
+
50
+ /**
51
+ * Basic conversation facade.
52
+ *
53
+ * Data flow:
54
+ * 1) UI intent -> facade action
55
+ * 2) facade preflight -> round execution
56
+ * 3) stream event -> session state
57
+ * 4) selector(buildTimelineItems) -> UI model
58
+ */
59
+ export default function useBasicConversationFacade({
60
+ sessionAPI,
61
+ messageAPI,
62
+ stream,
63
+ listParams = {},
64
+ functionType = 'chat_component_debug',
65
+ defaultSessionTitle = '新会话',
66
+ defaultSessionPayload = {},
67
+ } = {}) {
68
+ const chatSession = useChatSessionReducer({ sessionAPI, messageAPI });
69
+ const chatInput = useChatInput();
70
+ const sseStream = useSSEStream(chatSession.sessionId);
71
+ const runtimeStream = stream || sseStream;
72
+
73
+ const sessionList = useSessionListController({
74
+ sessionAPI,
75
+ listParams,
76
+ onFailed: chatSession.setSessionError,
77
+ });
78
+
79
+ const streamSend = useStreamSendController({
80
+ stream: runtimeStream,
81
+ onStarted: () => {
82
+ chatSession.setSessionError(null);
83
+ },
84
+ onChunk: (chunk) => {
85
+ chatSession.appendStreamChunk(chunk);
86
+ },
87
+ onFailed: (error) => {
88
+ chatSession.setSessionError(error);
89
+ },
90
+ });
91
+
92
+ const resolveSendPreflight = useCallback(async (options = {}) => {
93
+ const content = String(options?.content ?? chatInput.inputValue ?? '').trim();
94
+ if (!content) {
95
+ return { blocked: true };
96
+ }
97
+
98
+ let sessionId = String(chatSession.sessionId || '').trim();
99
+ let createdNewSession = false;
100
+ if (!sessionId) {
101
+ sessionId = await chatSession.createSession({
102
+ function_type: functionType,
103
+ title: defaultSessionTitle,
104
+ ...defaultSessionPayload,
105
+ });
106
+ createdNewSession = true;
107
+ }
108
+
109
+ const streamOptions = (
110
+ options.streamOptions
111
+ && typeof options.streamOptions === 'object'
112
+ && !Array.isArray(options.streamOptions)
113
+ ) ? { ...options.streamOptions } : {};
114
+ const contextRefs = normalizeRefs(options.context_refs);
115
+ const knowledgeRefs = normalizeRefs(options.knowledge_refs);
116
+ if (contextRefs && !Object.prototype.hasOwnProperty.call(streamOptions, 'context_refs')) {
117
+ streamOptions.context_refs = contextRefs;
118
+ }
119
+ if (knowledgeRefs && !Object.prototype.hasOwnProperty.call(streamOptions, 'knowledge_refs')) {
120
+ streamOptions.knowledge_refs = knowledgeRefs;
121
+ }
122
+
123
+ return {
124
+ blocked: false,
125
+ content,
126
+ sessionId,
127
+ createdNewSession,
128
+ preserveInputOnError: options.preserveInputOnError === true,
129
+ attachedSessionFilesSnapshot: Array.isArray(chatInput.fileList) ? chatInput.fileList : [],
130
+ streamOptions,
131
+ };
132
+ }, [chatInput.fileList, chatInput.inputValue, chatSession, defaultSessionPayload, defaultSessionTitle, functionType]);
133
+
134
+ const executeSendRound = useCallback(async ({ sendPreflight }) => {
135
+ chatSession.appendUserMessage(sendPreflight.content);
136
+ chatSession.setLastUserMessage(sendPreflight.content);
137
+ chatSession.resetStreaming();
138
+ chatInput.clearInput();
139
+
140
+ await streamSend.executeStreamSend(
141
+ sendPreflight.content,
142
+ {
143
+ onTextReplace: (content) => {
144
+ chatSession.replaceStreamContent(content);
145
+ },
146
+ onAgentStatus: (agentStatus) => {
147
+ chatSession.setAgentStatus(agentStatus);
148
+ },
149
+ onThinkingTrace: (trace) => {
150
+ chatSession.setThinkingTrace(trace);
151
+ },
152
+ onExecutionTrace: (trace) => {
153
+ chatSession.setExecutionTrace(trace);
154
+ },
155
+ onKnowledgeSearch: (searchPayload) => {
156
+ chatSession.setKnowledgeSearch(searchPayload);
157
+ },
158
+ onSourcingCandidates: (sourcingPayload) => {
159
+ chatSession.setSourcingCandidates(sourcingPayload);
160
+ },
161
+ onKnowledgeCitation: (citationPayload) => {
162
+ chatSession.setKnowledgeCitation(citationPayload);
163
+ },
164
+ onPatchEvent: (patchEvent) => {
165
+ const patchSession = resolvePromoteSessionPayload(patchEvent);
166
+ if (!patchSession.sessionId) {
167
+ return;
168
+ }
169
+ if (patchSession.sessionId !== chatSession.sessionId || patchSession.title) {
170
+ chatSession.promoteSession({
171
+ sessionId: patchSession.sessionId,
172
+ ...(patchSession.title ? { title: patchSession.title } : {}),
173
+ });
174
+ }
175
+ },
176
+ onComplete: async (finalContent, doneMeta = {}) => {
177
+ const doneSession = resolvePromoteSessionPayload(doneMeta);
178
+ if (doneSession.sessionId && doneSession.sessionId !== chatSession.sessionId) {
179
+ chatSession.promoteSession({ sessionId: doneSession.sessionId });
180
+ }
181
+
182
+ chatSession.completeStreaming(finalContent, {
183
+ session_id: doneSession.sessionId || sendPreflight.sessionId,
184
+ });
185
+
186
+ if (sendPreflight.createdNewSession) {
187
+ void sessionList.loadSessionList({ background: true });
188
+ }
189
+ },
190
+ },
191
+ {
192
+ sessionIdOverride: sendPreflight.sessionId,
193
+ ...sendPreflight.streamOptions,
194
+ }
195
+ );
196
+ }, [chatInput, chatSession, sessionList, streamSend]);
197
+
198
+ const handleSendFailureRecovery = useCallback(({ preserveInputOnError, content }) => {
199
+ if (preserveInputOnError) {
200
+ chatInput.handleInputChange({ target: { value: content } });
201
+ }
202
+ }, [chatInput]);
203
+
204
+ const sendHandler = useBasicSendHandler({
205
+ resolveSendPreflight,
206
+ executeSendRound,
207
+ handleSendFailureRecovery,
208
+ emptyArray: [],
209
+ });
210
+
211
+ const createSession = useCallback(async (options = {}) => {
212
+ const mergedOptions = {
213
+ ...defaultSessionPayload,
214
+ ...options,
215
+ };
216
+ const sessionId = await chatSession.createSession({
217
+ function_type: functionType,
218
+ title: String(mergedOptions?.title || defaultSessionTitle),
219
+ project_id: mergedOptions?.project_id ?? null,
220
+ user_id: mergedOptions?.user_id ?? null,
221
+ });
222
+ await sessionList.loadSessionList({ background: true });
223
+ return sessionId;
224
+ }, [chatSession, defaultSessionPayload, defaultSessionTitle, functionType, sessionList]);
225
+
226
+ const selectSession = useCallback(async (sessionId) => {
227
+ await chatSession.loadSession(sessionId);
228
+ }, [chatSession]);
229
+
230
+ const refreshSessionList = useCallback(async () => {
231
+ return sessionList.loadSessionList({ background: true });
232
+ }, [sessionList]);
233
+
234
+ const timeline = useMemo(() => buildTimelineItems(chatSession, { loading: runtimeStream.loading }), [chatSession, runtimeStream.loading]);
235
+
236
+ const state = useMemo(() => ({
237
+ session: {
238
+ sessionId: chatSession.sessionId,
239
+ title: chatSession.sessionTitle,
240
+ detail: chatSession.sessionDetail,
241
+ error: chatSession.sessionError,
242
+ },
243
+ sessions: {
244
+ items: sessionList.sessions,
245
+ loading: sessionList.loading,
246
+ error: sessionList.error,
247
+ },
248
+ composer: {
249
+ inputValue: chatInput.inputValue,
250
+ fileList: chatInput.fileList,
251
+ isDragging: chatInput.isDragging,
252
+ loading: runtimeStream.loading,
253
+ error: runtimeStream.error,
254
+ },
255
+ timeline,
256
+ }), [chatInput.fileList, chatInput.inputValue, chatInput.isDragging, chatSession.sessionDetail, chatSession.sessionError, chatSession.sessionId, chatSession.sessionTitle, runtimeStream.error, runtimeStream.loading, sessionList.error, sessionList.loading, sessionList.sessions, timeline]);
257
+
258
+ const actions = useMemo(() => ({
259
+ createSession,
260
+ selectSession,
261
+ refreshSessionList,
262
+ resetSession: chatSession.resetSession,
263
+ loadSession: chatSession.loadSession,
264
+ updateTitle: chatSession.updateTitle,
265
+ setInputValue: (value) => chatInput.handleInputChange({ target: { value } }),
266
+ clearInput: chatInput.clearInput,
267
+ attachFiles: chatInput.appendFiles,
268
+ removeFile: chatInput.handleRemoveFile,
269
+ send: sendHandler.handleSend,
270
+ retry: runtimeStream.retry,
271
+ abort: runtimeStream.abort,
272
+ }), [chatInput, chatSession.loadSession, chatSession.resetSession, chatSession.updateTitle, createSession, refreshSessionList, runtimeStream.abort, runtimeStream.retry, selectSession, sendHandler.handleSend]);
273
+
274
+ return {
275
+ state,
276
+ actions,
277
+ };
278
+ }
279
+
280
+ export { useBasicConversationFacade };
@@ -1,6 +1,14 @@
1
- export { default as useChatSessionReducer, initialState as chatSessionInitialState } from './session/useChatSessionReducer.js';
1
+ export { useChatSessionReducer, initialState as chatSessionInitialState } from './session/index.js';
2
2
  export { useChatInput } from './input/useChatInput.js';
3
3
  export { useSSEStream } from './stream/useSSEStream.js';
4
4
  export { SSEClient } from './stream/sse-client.js';
5
5
  export * from './stream/sse-events.js';
6
6
  export { buildTimelineItems } from './messages/buildTimelineItems.js';
7
+ export { useSessionListController } from './session/index.js';
8
+ export { useStreamSendController } from './stream/useStreamSendController.js';
9
+ export { useBasicSendHandler } from './orchestration/useBasicSendHandler.js';
10
+ export { useBasicConversationFacade } from './facade/useBasicConversationFacade.js';
11
+ export { useCoreChatApp } from './app/useCoreChatApp.js';
12
+ export { useAppStream } from './app/useAppStream.js';
13
+ export { createMockRuntime } from './app/mockRuntime.js';
14
+ export * from './runtime/runtimeMode.js';
@@ -0,0 +1,36 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { buildTimelineItems } from './buildTimelineItems.js';
5
+
6
+ test('buildTimelineItems attaches analysis route metadata on sourcing_candidates artifact', () => {
7
+ const timeline = buildTimelineItems({
8
+ messages: [
9
+ {
10
+ message_id: 'm1',
11
+ role: 'user',
12
+ content: '请分析这些供应商',
13
+ created_at: '2026-04-21T00:00:00Z',
14
+ },
15
+ ],
16
+ streaming: {
17
+ content: '',
18
+ sourcingCandidates: {
19
+ run_id: 'sourcing-run-1',
20
+ candidates: [{ supplier_id: 'sup-1' }],
21
+ analysis_route: {
22
+ analysis_type: 'sourcing',
23
+ template_id: 'analysis_sourcing_default',
24
+ reason_codes: ['ROUTE_SIGNAL_SOURCING_INTENT'],
25
+ confidence: 0.82,
26
+ },
27
+ },
28
+ roundKey: 'rk-1',
29
+ },
30
+ });
31
+
32
+ const sourcingItem = timeline.items.find((item) => item.type === 'sourcing_candidates');
33
+ assert.ok(sourcingItem);
34
+ assert.equal(sourcingItem.analysisRoute.analysis_type, 'sourcing');
35
+ assert.equal(sourcingItem.analysisRoute.template_id, 'analysis_sourcing_default');
36
+ });
@@ -1,11 +1,41 @@
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
+
1
31
  export function buildTimelineItems(state = {}, options = {}) {
2
32
  const messages = Array.isArray(state.messages) ? state.messages : [];
3
33
  const uiCards = Array.isArray(state.uiCards) ? state.uiCards : [];
34
+ const executionCards = Array.isArray(state.executionCards) ? state.executionCards : [];
4
35
  const streaming = state.streaming || {};
5
36
  const loading = Boolean(options?.loading);
6
37
 
7
38
  const items = [];
8
-
9
39
  if (messages.length === 0) {
10
40
  return {
11
41
  isEmpty: true,
@@ -15,6 +45,17 @@ export function buildTimelineItems(state = {}, options = {}) {
15
45
 
16
46
  messages.forEach((msg, index) => {
17
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();
18
59
  items.push({
19
60
  id: messageId,
20
61
  type: 'message',
@@ -23,11 +64,48 @@ export function buildTimelineItems(state = {}, options = {}) {
23
64
  createdAt: msg.created_at,
24
65
  client_request_id: msg.client_request_id || '',
25
66
  intake_session_id: msg.intake_session_id || '',
26
- roundKey: msg.round_key || '',
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,
27
72
  highlights: msg.highlights ?? [],
28
73
  sourceTypes: msg.sourceTypes ?? [],
29
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,
30
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
+ }
31
109
  });
32
110
 
33
111
  if (uiCards.length > 0) {
@@ -38,31 +116,79 @@ export function buildTimelineItems(state = {}, options = {}) {
38
116
  });
39
117
  }
40
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
+
41
169
  if (streaming.content) {
42
170
  items.push({
43
171
  id: 'streaming-message',
44
172
  type: 'streaming',
45
173
  content: streaming.content,
174
+ roundKey: String(streaming.roundKey || '').trim(),
46
175
  });
47
176
  }
48
177
 
49
178
  const hasThinkingSignal = Boolean(streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace);
50
- if (hasThinkingSignal) {
51
- items.push({
52
- id: 'thinking-bubble',
53
- type: 'thinking',
54
- agentStatus: streaming.agentStatus,
55
- thinkingTrace: streaming.thinkingTrace,
56
- executionTrace: streaming.executionTrace,
57
- });
58
- } else if (loading && !streaming.content) {
179
+ if (hasThinkingSignal || loading) {
59
180
  items.push({
60
- id: 'thinking-bubble-loading',
181
+ id: hasThinkingSignal ? 'thinking-bubble' : 'thinking-bubble-loading',
61
182
  type: 'thinking',
62
- agentStatus: {
183
+ agentStatus: streaming.agentStatus || {
63
184
  status: 'running',
64
185
  agent: '助手',
65
186
  },
187
+ thinkingTrace: streaming.thinkingTrace,
188
+ executionTrace: streaming.executionTrace,
189
+ executionCards,
190
+ roundKey: String(streaming.roundKey || '').trim(),
191
+ loading,
66
192
  });
67
193
  }
68
194