flare-chat-core 0.2.0 → 0.2.2

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 +125 -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 +109 -0
  26. package/src/app/components/WorkspaceMainPane.jsx +113 -0
  27. package/src/app/components/WorkspaceSessionPane.jsx +48 -0
  28. package/src/app/components/WorkspaceTopBarSection.jsx +65 -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 +172 -11
  70. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +183 -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 +67 -390
  98. package/src/chat-core/session/useSessionListController.js +67 -0
  99. package/src/chat-core/stream/sse-client.js +1 -142
  100. package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
  101. package/src/chat-core/stream/sse-events.js +1 -598
  102. package/src/chat-core/stream/useSSEStream.js +1 -273
  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,10 +1,49 @@
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 = [];
39
+ let latestAssistantMessageId = '';
40
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
41
+ const candidate = messages[index];
42
+ if (String(candidate?.role || '').trim() === 'assistant') {
43
+ latestAssistantMessageId = String(candidate?.message_id || `msg-fallback-${index}`).trim();
44
+ break;
45
+ }
46
+ }
8
47
 
9
48
  if (messages.length === 0) {
10
49
  return {
@@ -15,16 +54,90 @@ export function buildTimelineItems(state = {}, options = {}) {
15
54
 
16
55
  messages.forEach((msg, index) => {
17
56
  const messageId = msg.message_id || `msg-fallback-${index}`;
57
+ const primaryFlow = (
58
+ msg.primary_flow
59
+ && typeof msg.primary_flow === 'object'
60
+ && !Array.isArray(msg.primary_flow)
61
+ ) ? msg.primary_flow : null;
62
+ const chooserState = (
63
+ primaryFlow?.chooser_state
64
+ && typeof primaryFlow.chooser_state === 'object'
65
+ && !Array.isArray(primaryFlow.chooser_state)
66
+ ) ? primaryFlow.chooser_state : null;
67
+ const authoritativeRoundId = String(primaryFlow?.round_id || '').trim();
18
68
  items.push({
19
69
  id: messageId,
20
70
  type: 'message',
21
71
  role: msg.role,
22
72
  content: msg.content,
23
73
  createdAt: msg.created_at,
74
+ client_request_id: msg.client_request_id || '',
75
+ intake_session_id: msg.intake_session_id || '',
76
+ roundKey: msg.round_key || authoritativeRoundId || '',
77
+ roundId: authoritativeRoundId,
78
+ roundState: String(primaryFlow?.round_state || '').trim(),
79
+ chooserKind: String(chooserState?.kind || '').trim(),
80
+ chooserVisible: chooserState?.visible === true,
24
81
  highlights: msg.highlights ?? [],
25
82
  sourceTypes: msg.sourceTypes ?? [],
26
83
  attachments: Array.isArray(msg.attachments) ? msg.attachments : [],
84
+ contextUsage: (
85
+ msg.context_usage
86
+ && typeof msg.context_usage === 'object'
87
+ && !Array.isArray(msg.context_usage)
88
+ ) ? msg.context_usage : null,
27
89
  });
90
+
91
+ if (msg.role === 'assistant') {
92
+ const historicalThinking = Boolean(
93
+ msg.agent_status
94
+ || msg.execution_trace
95
+ || String(msg.thinking_trace || '').trim()
96
+ );
97
+ if (historicalThinking && messageId === latestAssistantMessageId) {
98
+ items.push({
99
+ id: `thinking-${messageId}`,
100
+ type: 'thinking',
101
+ agentStatus: msg.agent_status || {
102
+ status: 'completed',
103
+ agent: '助手',
104
+ },
105
+ thinkingTrace: String(msg.thinking_trace || '').trim(),
106
+ executionTrace: msg.execution_trace || {
107
+ status: 'completed',
108
+ step_id: 'historical_trace',
109
+ },
110
+ executionCards: [],
111
+ roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
112
+ loading: false,
113
+ });
114
+ }
115
+ if (msg.knowledge_search && typeof msg.knowledge_search === 'object') {
116
+ items.push({
117
+ id: `knowledge-search-${messageId}`,
118
+ type: 'knowledge_search',
119
+ payload: msg.knowledge_search,
120
+ roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
121
+ });
122
+ }
123
+ if (msg.sourcing_candidates && typeof msg.sourcing_candidates === 'object') {
124
+ items.push({
125
+ id: `sourcing-candidates-history-${messageId}`,
126
+ type: 'sourcing_candidates',
127
+ payload: msg.sourcing_candidates,
128
+ roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
129
+ });
130
+ }
131
+ const historicalCitationPayload = normalizeCitationPayload(msg.knowledge_citation);
132
+ if (historicalCitationPayload) {
133
+ items.push({
134
+ id: `knowledge-citation-${messageId}`,
135
+ type: 'knowledge_citation',
136
+ payload: historicalCitationPayload,
137
+ roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
138
+ });
139
+ }
140
+ }
28
141
  });
29
142
 
30
143
  if (uiCards.length > 0) {
@@ -35,31 +148,79 @@ export function buildTimelineItems(state = {}, options = {}) {
35
148
  });
36
149
  }
37
150
 
151
+ if (
152
+ streaming.knowledgeSearch
153
+ && typeof streaming.knowledgeSearch === 'object'
154
+ && !Array.isArray(streaming.knowledgeSearch)
155
+ ) {
156
+ items.push({
157
+ id: `knowledge-search-${String(streaming.knowledgeSearch.run_id || 'latest')}`,
158
+ type: 'knowledge_search',
159
+ payload: streaming.knowledgeSearch,
160
+ roundKey: String(streaming.roundKey || '').trim(),
161
+ });
162
+ }
163
+
164
+ if (
165
+ streaming.sourcingCandidates
166
+ && typeof streaming.sourcingCandidates === 'object'
167
+ && !Array.isArray(streaming.sourcingCandidates)
168
+ ) {
169
+ const analysisRoute = (
170
+ streaming.sourcingCandidates.analysis_route
171
+ && typeof streaming.sourcingCandidates.analysis_route === 'object'
172
+ && !Array.isArray(streaming.sourcingCandidates.analysis_route)
173
+ )
174
+ ? streaming.sourcingCandidates.analysis_route
175
+ : (
176
+ streaming.analysisRoute
177
+ && typeof streaming.analysisRoute === 'object'
178
+ && !Array.isArray(streaming.analysisRoute)
179
+ )
180
+ ? streaming.analysisRoute
181
+ : null;
182
+ items.push({
183
+ id: `sourcing-candidates-${String(streaming.sourcingCandidates.run_id || 'latest')}`,
184
+ type: 'sourcing_candidates',
185
+ payload: streaming.sourcingCandidates,
186
+ analysisRoute,
187
+ roundKey: String(streaming.roundKey || '').trim(),
188
+ });
189
+ }
190
+
191
+ const streamingCitationPayload = normalizeCitationPayload(streaming.knowledgeCitation);
192
+ if (streamingCitationPayload) {
193
+ items.push({
194
+ id: `knowledge-citation-${String(streamingCitationPayload.run_id || 'latest')}`,
195
+ type: 'knowledge_citation',
196
+ payload: streamingCitationPayload,
197
+ roundKey: String(streaming.roundKey || '').trim(),
198
+ });
199
+ }
200
+
38
201
  if (streaming.content) {
39
202
  items.push({
40
203
  id: 'streaming-message',
41
204
  type: 'streaming',
42
205
  content: streaming.content,
206
+ roundKey: String(streaming.roundKey || '').trim(),
43
207
  });
44
208
  }
45
209
 
46
210
  const hasThinkingSignal = Boolean(streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace);
47
- if (hasThinkingSignal) {
48
- items.push({
49
- id: 'thinking-bubble',
50
- type: 'thinking',
51
- agentStatus: streaming.agentStatus,
52
- thinkingTrace: streaming.thinkingTrace,
53
- executionTrace: streaming.executionTrace,
54
- });
55
- } else if (loading && !streaming.content) {
211
+ if (hasThinkingSignal || loading) {
56
212
  items.push({
57
- id: 'thinking-bubble-loading',
213
+ id: hasThinkingSignal ? 'thinking-bubble' : 'thinking-bubble-loading',
58
214
  type: 'thinking',
59
- agentStatus: {
215
+ agentStatus: streaming.agentStatus || {
60
216
  status: 'running',
61
217
  agent: '助手',
62
218
  },
219
+ thinkingTrace: streaming.thinkingTrace,
220
+ executionTrace: streaming.executionTrace,
221
+ executionCards,
222
+ roundKey: String(streaming.roundKey || '').trim(),
223
+ loading,
63
224
  });
64
225
  }
65
226