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,352 @@
1
+ import {
2
+ SESSION_LOADED,
3
+ SESSION_RESET,
4
+ SESSION_ERROR,
5
+ TITLE_UPDATED,
6
+ MESSAGES_REFRESHED,
7
+ MESSAGE_APPENDED,
8
+ STREAMING_RESET,
9
+ STREAMING_CHUNK,
10
+ STREAMING_REPLACE,
11
+ AGENT_STATUS_SET,
12
+ THINKING_TRACE_SET,
13
+ EXECUTION_TRACE_SET,
14
+ STREAM_KNOWLEDGE_SEARCH_SET,
15
+ STREAM_SOURCING_CANDIDATES_SET,
16
+ STREAM_KNOWLEDGE_CITATION_SET,
17
+ STREAMING_DONE,
18
+ DOC_GENERATED,
19
+ LEGEND_HINT_SHOWN,
20
+ EXECUTION_CARD_UPSERTED,
21
+ UI_CARDS_SET,
22
+ LAST_USER_MESSAGE_SET,
23
+ UI_CARD_UPDATED,
24
+ INSTANCE_PROFILE_SET,
25
+ SESSION_PROMOTED,
26
+ } from './chatSessionActionTypes.js';
27
+
28
+ export const initialStreaming = {
29
+ content: '',
30
+ agentStatus: null,
31
+ thinkingTrace: '',
32
+ executionTrace: null,
33
+ knowledgeSearch: null,
34
+ sourcingCandidates: null,
35
+ knowledgeCitation: null,
36
+ roundKey: '',
37
+ };
38
+
39
+ export const initialState = {
40
+ sessionId: null,
41
+ sessionTitle: '',
42
+ sessionStatus: 'active',
43
+ sessionDetail: null,
44
+ instanceProfile: null,
45
+ messages: [],
46
+ executionCards: [],
47
+ uiCards: [],
48
+ lastUserMessage: '',
49
+ generatedDoc: null,
50
+ streaming: initialStreaming,
51
+ sessionError: null,
52
+ legendHintsShown: {
53
+ knowledge_base: false,
54
+ ai: false,
55
+ },
56
+ };
57
+
58
+ export function reducer(state, { type, payload }) {
59
+ switch (type) {
60
+ case SESSION_LOADED: {
61
+ const {
62
+ sessionId,
63
+ title,
64
+ status,
65
+ messages,
66
+ detail,
67
+ } = payload;
68
+ return {
69
+ ...state,
70
+ sessionId,
71
+ sessionTitle: title,
72
+ sessionStatus: status || 'active',
73
+ sessionDetail: (detail && typeof detail === 'object' && !Array.isArray(detail)) ? detail : null,
74
+ instanceProfile: state.instanceProfile,
75
+ messages,
76
+ executionCards: [],
77
+ uiCards: [],
78
+ lastUserMessage: '',
79
+ generatedDoc: null,
80
+ streaming: initialStreaming,
81
+ legendHintsShown: initialState.legendHintsShown,
82
+ };
83
+ }
84
+
85
+ case SESSION_RESET:
86
+ return { ...initialState, instanceProfile: state.instanceProfile };
87
+
88
+ case SESSION_ERROR:
89
+ return { ...state, sessionError: payload };
90
+
91
+ case TITLE_UPDATED:
92
+ return { ...state, sessionTitle: payload };
93
+
94
+ case MESSAGES_REFRESHED:
95
+ return { ...state, messages: payload };
96
+
97
+ case MESSAGE_APPENDED:
98
+ return {
99
+ ...state,
100
+ messages: [...state.messages, payload],
101
+ lastUserMessage: payload.role === 'user' ? payload.content : state.lastUserMessage,
102
+ };
103
+
104
+ case LAST_USER_MESSAGE_SET:
105
+ return { ...state, lastUserMessage: payload };
106
+
107
+ case UI_CARDS_SET:
108
+ return { ...state, uiCards: payload };
109
+
110
+ case UI_CARD_UPDATED: {
111
+ const nextCard = payload;
112
+ return {
113
+ ...state,
114
+ uiCards: state.uiCards.map((card) =>
115
+ card.id === nextCard.id ? { ...card, ...nextCard } : card
116
+ ),
117
+ };
118
+ }
119
+
120
+ case INSTANCE_PROFILE_SET:
121
+ return { ...state, instanceProfile: payload };
122
+
123
+ case SESSION_PROMOTED: {
124
+ const sessionId = String(payload?.sessionId || '').trim();
125
+ if (!sessionId) {
126
+ return state;
127
+ }
128
+ const title = String(payload?.title || '').trim();
129
+ const detailPayload = (
130
+ payload?.detail
131
+ && typeof payload.detail === 'object'
132
+ && !Array.isArray(payload.detail)
133
+ ) ? payload.detail : null;
134
+ const nextDetail = detailPayload
135
+ ? {
136
+ ...(state.sessionDetail && typeof state.sessionDetail === 'object' ? state.sessionDetail : {}),
137
+ ...detailPayload,
138
+ sessionId,
139
+ }
140
+ : (
141
+ state.sessionDetail && typeof state.sessionDetail === 'object'
142
+ ? {
143
+ ...state.sessionDetail,
144
+ sessionId,
145
+ }
146
+ : null
147
+ );
148
+ return {
149
+ ...state,
150
+ sessionId,
151
+ sessionTitle: title || state.sessionTitle,
152
+ sessionStatus: String(payload?.status || state.sessionStatus || 'active'),
153
+ sessionDetail: nextDetail,
154
+ };
155
+ }
156
+
157
+ case EXECUTION_CARD_UPSERTED: {
158
+ const nextCard = payload;
159
+ const cardKey = nextCard.step_id || `${nextCard.agent}-${nextCard.label}-${nextCard.stage}`;
160
+ const existingIndex = state.executionCards.findIndex((card) => {
161
+ const existingKey = card.step_id || `${card.agent}-${card.label}-${card.stage}`;
162
+ return existingKey === cardKey;
163
+ });
164
+
165
+ const updatedCards = existingIndex >= 0
166
+ ? state.executionCards.map((card, index) =>
167
+ index === existingIndex ? { ...card, ...nextCard } : card
168
+ )
169
+ : [...state.executionCards, nextCard];
170
+
171
+ return {
172
+ ...state,
173
+ executionCards: updatedCards.slice(-6),
174
+ };
175
+ }
176
+
177
+ case STREAMING_RESET:
178
+ return { ...state, streaming: initialStreaming };
179
+
180
+ case STREAMING_CHUNK:
181
+ return {
182
+ ...state,
183
+ streaming: {
184
+ ...state.streaming,
185
+ content: state.streaming.content + payload,
186
+ },
187
+ };
188
+
189
+ case STREAMING_REPLACE:
190
+ return {
191
+ ...state,
192
+ streaming: {
193
+ ...state.streaming,
194
+ content: String(payload || ''),
195
+ },
196
+ };
197
+
198
+ case AGENT_STATUS_SET:
199
+ return {
200
+ ...state,
201
+ streaming: { ...state.streaming, agentStatus: payload },
202
+ };
203
+
204
+ case THINKING_TRACE_SET:
205
+ return {
206
+ ...state,
207
+ streaming: { ...state.streaming, thinkingTrace: payload },
208
+ };
209
+
210
+ case EXECUTION_TRACE_SET:
211
+ return {
212
+ ...state,
213
+ streaming: {
214
+ ...state.streaming,
215
+ executionTrace: {
216
+ ...state.streaming.executionTrace,
217
+ ...payload,
218
+ },
219
+ },
220
+ };
221
+
222
+ case STREAM_KNOWLEDGE_SEARCH_SET:
223
+ return {
224
+ ...state,
225
+ streaming: {
226
+ ...state.streaming,
227
+ knowledgeSearch: (
228
+ payload
229
+ && typeof payload === 'object'
230
+ && !Array.isArray(payload)
231
+ ) ? payload : null,
232
+ },
233
+ };
234
+
235
+ case STREAM_SOURCING_CANDIDATES_SET:
236
+ return {
237
+ ...state,
238
+ streaming: {
239
+ ...state.streaming,
240
+ sourcingCandidates: (
241
+ payload
242
+ && typeof payload === 'object'
243
+ && !Array.isArray(payload)
244
+ ) ? payload : null,
245
+ },
246
+ };
247
+
248
+ case STREAM_KNOWLEDGE_CITATION_SET:
249
+ return {
250
+ ...state,
251
+ streaming: {
252
+ ...state.streaming,
253
+ knowledgeCitation: (
254
+ payload
255
+ && typeof payload === 'object'
256
+ && !Array.isArray(payload)
257
+ ) ? payload : null,
258
+ },
259
+ };
260
+
261
+ case STREAMING_DONE: {
262
+ const donePayload = (
263
+ payload
264
+ && typeof payload === 'object'
265
+ && !Array.isArray(payload)
266
+ ) ? payload : { content: payload };
267
+ const doneContent = String(donePayload.content || '');
268
+ const doneMeta = (
269
+ donePayload.meta
270
+ && typeof donePayload.meta === 'object'
271
+ && !Array.isArray(donePayload.meta)
272
+ ) ? donePayload.meta : {};
273
+ const newMessages = doneContent
274
+ ? [
275
+ ...state.messages,
276
+ {
277
+ message_id: `temp-assistant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
278
+ role: 'assistant',
279
+ content: doneContent,
280
+ created_at: new Date().toISOString(),
281
+ round_key: String(doneMeta.round_key || doneMeta.roundKey || '').trim(),
282
+ round_state: String(doneMeta.round_state || doneMeta.roundState || '').trim(),
283
+ chooser_state: (
284
+ doneMeta.chooser_state
285
+ && typeof doneMeta.chooser_state === 'object'
286
+ && !Array.isArray(doneMeta.chooser_state)
287
+ ) ? doneMeta.chooser_state : null,
288
+ knowledge_search: (
289
+ state.streaming.knowledgeSearch
290
+ && typeof state.streaming.knowledgeSearch === 'object'
291
+ && !Array.isArray(state.streaming.knowledgeSearch)
292
+ ) ? state.streaming.knowledgeSearch : null,
293
+ sourcing_candidates: (
294
+ state.streaming.sourcingCandidates
295
+ && typeof state.streaming.sourcingCandidates === 'object'
296
+ && !Array.isArray(state.streaming.sourcingCandidates)
297
+ ) ? state.streaming.sourcingCandidates : null,
298
+ knowledge_citation: (
299
+ state.streaming.knowledgeCitation
300
+ && typeof state.streaming.knowledgeCitation === 'object'
301
+ && !Array.isArray(state.streaming.knowledgeCitation)
302
+ ) ? state.streaming.knowledgeCitation : null,
303
+ agent_status: (
304
+ state.streaming.agentStatus
305
+ && typeof state.streaming.agentStatus === 'object'
306
+ && !Array.isArray(state.streaming.agentStatus)
307
+ ) ? state.streaming.agentStatus : null,
308
+ thinking_trace: String(state.streaming.thinkingTrace || ''),
309
+ execution_trace: (
310
+ state.streaming.executionTrace
311
+ && typeof state.streaming.executionTrace === 'object'
312
+ && !Array.isArray(state.streaming.executionTrace)
313
+ ) ? state.streaming.executionTrace : null,
314
+ context_usage: (
315
+ doneMeta.context_usage
316
+ && typeof doneMeta.context_usage === 'object'
317
+ && !Array.isArray(doneMeta.context_usage)
318
+ ) ? doneMeta.context_usage : null,
319
+ },
320
+ ]
321
+ : state.messages;
322
+
323
+ return {
324
+ ...state,
325
+ messages: newMessages,
326
+ streaming: initialStreaming,
327
+ };
328
+ }
329
+
330
+ case DOC_GENERATED:
331
+ return { ...state, generatedDoc: payload };
332
+
333
+ case LEGEND_HINT_SHOWN:
334
+ return {
335
+ ...state,
336
+ legendHintsShown: {
337
+ ...state.legendHintsShown,
338
+ [payload]: true,
339
+ },
340
+ };
341
+
342
+ default:
343
+ return state;
344
+ }
345
+ }
346
+
347
+ export function requireAPI(api, name) {
348
+ if (!api) {
349
+ throw new Error(`${name} is required`);
350
+ }
351
+ return api;
352
+ }
@@ -0,0 +1,39 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { reducer, initialState } from './chatSessionReducer.js';
4
+ import {
5
+ AGENT_STATUS_SET,
6
+ THINKING_TRACE_SET,
7
+ EXECUTION_TRACE_SET,
8
+ STREAM_KNOWLEDGE_SEARCH_SET,
9
+ STREAM_KNOWLEDGE_CITATION_SET,
10
+ STREAMING_DONE,
11
+ } from './chatSessionActionTypes.js';
12
+
13
+ test('STREAMING_DONE snapshots thinking/search/citation metadata into assistant message', () => {
14
+ let state = { ...initialState };
15
+ state = reducer(state, { type: AGENT_STATUS_SET, payload: { status: 'completed', stage: 'analysis' } });
16
+ state = reducer(state, { type: THINKING_TRACE_SET, payload: '阶段:上下文处理' });
17
+ state = reducer(state, { type: EXECUTION_TRACE_SET, payload: { step_id: 'knowledge_search', status: 'completed' } });
18
+ state = reducer(state, { type: STREAM_KNOWLEDGE_SEARCH_SET, payload: { run_id: 'retrieval-1', results: [{ id: 'r1' }] } });
19
+ state = reducer(state, { type: STREAM_KNOWLEDGE_CITATION_SET, payload: { run_id: 'retrieval-1', citations: [{ citation_id: 'cite_1' }] } });
20
+
21
+ state = reducer(state, {
22
+ type: STREAMING_DONE,
23
+ payload: {
24
+ content: '总结完成',
25
+ meta: {
26
+ context_usage: { token_budget: 1200 },
27
+ },
28
+ },
29
+ });
30
+
31
+ const lastMessage = state.messages[state.messages.length - 1];
32
+ assert.equal(lastMessage.role, 'assistant');
33
+ assert.equal(lastMessage.content, '总结完成');
34
+ assert.equal(lastMessage.thinking_trace, '阶段:上下文处理');
35
+ assert.equal(lastMessage.execution_trace?.step_id, 'knowledge_search');
36
+ assert.equal(lastMessage.knowledge_search?.run_id, 'retrieval-1');
37
+ assert.equal(lastMessage.knowledge_citation?.run_id, 'retrieval-1');
38
+ assert.equal(lastMessage.context_usage?.token_budget, 1200);
39
+ });
@@ -0,0 +1,2 @@
1
+ export { default as useChatSessionReducer, initialState } from './useChatSessionReducer.js';
2
+ export { default as useSessionListController } from './useSessionListController.js';
@@ -0,0 +1,44 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ MESSAGES_REFRESHED,
4
+ MESSAGE_APPENDED,
5
+ } from './chatSessionActionTypes.js';
6
+ import { requireAPI } from './chatSessionReducer.js';
7
+
8
+ export function useMessageActions({ dispatch, messageAPI }) {
9
+ const refreshMessages = useCallback(async (sessionId) => {
10
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
11
+ const response = await messageApi.list(sessionId);
12
+ const messagesWithIds = response.messages.map((msg, index) => ({
13
+ ...msg,
14
+ message_id: msg.message_id || `fallback-${Date.now()}-${index}`,
15
+ }));
16
+ dispatch({ type: MESSAGES_REFRESHED, payload: messagesWithIds });
17
+ }, [dispatch, messageAPI]);
18
+
19
+ const appendUserMessage = useCallback((content, metadata = {}) => {
20
+ const payloadMeta = (
21
+ metadata
22
+ && typeof metadata === 'object'
23
+ && !Array.isArray(metadata)
24
+ )
25
+ ? metadata
26
+ : {};
27
+ dispatch({
28
+ type: MESSAGE_APPENDED,
29
+ payload: {
30
+ message_id: `temp-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
31
+ role: 'user',
32
+ content,
33
+ created_at: new Date().toISOString(),
34
+ ...payloadMeta,
35
+ },
36
+ });
37
+ }, [dispatch]);
38
+
39
+ return {
40
+ refreshMessages,
41
+ appendUserMessage,
42
+ };
43
+ }
44
+
@@ -0,0 +1,131 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ SESSION_LOADED,
4
+ SESSION_RESET,
5
+ TITLE_UPDATED,
6
+ SESSION_PROMOTED,
7
+ } from './chatSessionActionTypes.js';
8
+ import { requireAPI } from './chatSessionReducer.js';
9
+
10
+ export function useSessionCrudActions({ dispatch, sessionAPI, messageAPI }) {
11
+ const loadSession = useCallback(async (sessionId) => {
12
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
13
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
14
+
15
+ const [detail, msgs] = await Promise.all([
16
+ sessionApi.get(sessionId),
17
+ messageApi.list(sessionId),
18
+ ]);
19
+
20
+ dispatch({
21
+ type: SESSION_LOADED,
22
+ payload: {
23
+ sessionId,
24
+ title: detail.title,
25
+ status: detail.status || 'active',
26
+ messages: msgs.messages,
27
+ detail,
28
+ },
29
+ });
30
+ return detail;
31
+ }, [dispatch, messageAPI, sessionAPI]);
32
+
33
+ const createSession = useCallback(async ({
34
+ function_type,
35
+ title,
36
+ ...extraPayload
37
+ } = {}) => {
38
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
39
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
40
+
41
+ dispatch({
42
+ type: SESSION_LOADED,
43
+ payload: {
44
+ sessionId: response.sessionId,
45
+ title: String(response.title || title || '').trim(),
46
+ status: response.status || 'active',
47
+ messages: [],
48
+ detail: response,
49
+ },
50
+ });
51
+
52
+ return response.sessionId;
53
+ }, [dispatch, sessionAPI]);
54
+
55
+ const createOrLoadSession = useCallback(async ({
56
+ function_type,
57
+ defaultTitle,
58
+ forceNew = false,
59
+ ...extraPayload
60
+ } = {}) => {
61
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
62
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
63
+
64
+ let sessionId;
65
+ let title;
66
+
67
+ if (forceNew) {
68
+ title = defaultTitle;
69
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
70
+ sessionId = response.sessionId;
71
+ } else {
72
+ const sessionList = await sessionApi.list({
73
+ function_type,
74
+ status: 'active',
75
+ page: 1,
76
+ page_size: 1,
77
+ ...extraPayload,
78
+ });
79
+
80
+ if (sessionList.sessions.length > 0) {
81
+ sessionId = sessionList.sessions[0].sessionId;
82
+ title = sessionList.sessions[0].title;
83
+ } else {
84
+ title = defaultTitle;
85
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
86
+ sessionId = response.sessionId;
87
+ }
88
+ }
89
+
90
+ const [msgs, detail] = await Promise.all([
91
+ messageApi.list(sessionId),
92
+ sessionApi.get(sessionId),
93
+ ]);
94
+
95
+ dispatch({
96
+ type: SESSION_LOADED,
97
+ payload: {
98
+ sessionId,
99
+ title,
100
+ status: detail.status || 'active',
101
+ messages: msgs.messages,
102
+ detail,
103
+ },
104
+ });
105
+
106
+ return sessionId;
107
+ }, [dispatch, messageAPI, sessionAPI]);
108
+
109
+ const resetSession = useCallback(() => {
110
+ dispatch({ type: SESSION_RESET });
111
+ }, [dispatch]);
112
+
113
+ const updateTitle = useCallback(async (sessionId, title) => {
114
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
115
+ await sessionApi.update(sessionId, { title });
116
+ dispatch({ type: TITLE_UPDATED, payload: title });
117
+ }, [dispatch, sessionAPI]);
118
+
119
+ const promoteSession = useCallback((payload = {}) => {
120
+ dispatch({ type: SESSION_PROMOTED, payload });
121
+ }, [dispatch]);
122
+
123
+ return {
124
+ loadSession,
125
+ createSession,
126
+ createOrLoadSession,
127
+ resetSession,
128
+ updateTitle,
129
+ promoteSession,
130
+ };
131
+ }
@@ -0,0 +1,80 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ STREAMING_RESET,
4
+ STREAMING_CHUNK,
5
+ STREAMING_REPLACE,
6
+ AGENT_STATUS_SET,
7
+ THINKING_TRACE_SET,
8
+ EXECUTION_TRACE_SET,
9
+ STREAM_KNOWLEDGE_SEARCH_SET,
10
+ STREAM_SOURCING_CANDIDATES_SET,
11
+ STREAM_KNOWLEDGE_CITATION_SET,
12
+ EXECUTION_CARD_UPSERTED,
13
+ STREAMING_DONE,
14
+ } from './chatSessionActionTypes.js';
15
+
16
+ export function useStreamingActions({ dispatch }) {
17
+ const resetStreaming = useCallback(() => {
18
+ dispatch({ type: STREAMING_RESET });
19
+ }, [dispatch]);
20
+
21
+ const appendStreamChunk = useCallback((chunk) => {
22
+ dispatch({ type: STREAMING_CHUNK, payload: chunk });
23
+ }, [dispatch]);
24
+
25
+ const replaceStreamContent = useCallback((content) => {
26
+ dispatch({ type: STREAMING_REPLACE, payload: content });
27
+ }, [dispatch]);
28
+
29
+ const setAgentStatus = useCallback((agentStatus) => {
30
+ dispatch({ type: AGENT_STATUS_SET, payload: agentStatus });
31
+ }, [dispatch]);
32
+
33
+ const setThinkingTrace = useCallback((trace) => {
34
+ dispatch({ type: THINKING_TRACE_SET, payload: trace });
35
+ }, [dispatch]);
36
+
37
+ const setExecutionTrace = useCallback((executionTrace) => {
38
+ dispatch({ type: EXECUTION_TRACE_SET, payload: executionTrace });
39
+ dispatch({ type: EXECUTION_CARD_UPSERTED, payload: executionTrace });
40
+ }, [dispatch]);
41
+
42
+ const setKnowledgeSearch = useCallback((knowledgeSearch) => {
43
+ dispatch({ type: STREAM_KNOWLEDGE_SEARCH_SET, payload: knowledgeSearch });
44
+ }, [dispatch]);
45
+
46
+ const setSourcingCandidates = useCallback((sourcingCandidates) => {
47
+ dispatch({ type: STREAM_SOURCING_CANDIDATES_SET, payload: sourcingCandidates });
48
+ }, [dispatch]);
49
+
50
+ const setKnowledgeCitation = useCallback((knowledgeCitation) => {
51
+ dispatch({ type: STREAM_KNOWLEDGE_CITATION_SET, payload: knowledgeCitation });
52
+ }, [dispatch]);
53
+
54
+ const completeStreaming = useCallback((finalContent, finalMeta = null) => {
55
+ dispatch({
56
+ type: STREAMING_DONE,
57
+ payload: {
58
+ content: finalContent,
59
+ meta: (
60
+ finalMeta
61
+ && typeof finalMeta === 'object'
62
+ && !Array.isArray(finalMeta)
63
+ ) ? finalMeta : null,
64
+ },
65
+ });
66
+ }, [dispatch]);
67
+
68
+ return {
69
+ resetStreaming,
70
+ appendStreamChunk,
71
+ replaceStreamContent,
72
+ setAgentStatus,
73
+ setThinkingTrace,
74
+ setExecutionTrace,
75
+ setKnowledgeSearch,
76
+ setSourcingCandidates,
77
+ setKnowledgeCitation,
78
+ completeStreaming,
79
+ };
80
+ }
@@ -0,0 +1,51 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ UI_CARDS_SET,
4
+ UI_CARD_UPDATED,
5
+ LAST_USER_MESSAGE_SET,
6
+ DOC_GENERATED,
7
+ LEGEND_HINT_SHOWN,
8
+ SESSION_ERROR,
9
+ INSTANCE_PROFILE_SET,
10
+ } from './chatSessionActionTypes.js';
11
+
12
+ export function useUiStateActions({ dispatch }) {
13
+ const setUICards = useCallback((cards) => {
14
+ dispatch({ type: UI_CARDS_SET, payload: Array.isArray(cards) ? cards : [] });
15
+ }, [dispatch]);
16
+
17
+ const updateUICard = useCallback((card) => {
18
+ dispatch({ type: UI_CARD_UPDATED, payload: card });
19
+ }, [dispatch]);
20
+
21
+ const setLastUserMessage = useCallback((content) => {
22
+ dispatch({ type: LAST_USER_MESSAGE_SET, payload: content || '' });
23
+ }, [dispatch]);
24
+
25
+ const setGeneratedDoc = useCallback((doc) => {
26
+ dispatch({ type: DOC_GENERATED, payload: doc });
27
+ }, [dispatch]);
28
+
29
+ const markLegendHintShown = useCallback((sourceType) => {
30
+ dispatch({ type: LEGEND_HINT_SHOWN, payload: sourceType });
31
+ }, [dispatch]);
32
+
33
+ const setSessionError = useCallback((error) => {
34
+ dispatch({ type: SESSION_ERROR, payload: error });
35
+ }, [dispatch]);
36
+
37
+ const setInstanceProfile = useCallback((profile) => {
38
+ dispatch({ type: INSTANCE_PROFILE_SET, payload: profile || null });
39
+ }, [dispatch]);
40
+
41
+ return {
42
+ setUICards,
43
+ updateUICard,
44
+ setLastUserMessage,
45
+ setGeneratedDoc,
46
+ markLegendHintShown,
47
+ setSessionError,
48
+ setInstanceProfile,
49
+ };
50
+ }
51
+