flare-chat-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # flare-chat-core
2
+
3
+ Chat 核心能力包。
4
+
5
+ 职责:
6
+
7
+ 1. 会话状态与生命周期。
8
+ 2. 输入状态与文件/拖拽处理。
9
+ 3. SSE 流式通信与事件解析。
10
+ 4. 消息时间线组装。
11
+
12
+ 不包含:
13
+
14
+ 1. Chat GUI 宿主层。
15
+ 2. 生成式 UI 渲染层。
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "flare-chat-core",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./index.js": "./src/index.js",
11
+ "./chat-core/index.js": "./src/chat-core/index.js",
12
+ "./chat-core/input/useChatInput.js": "./src/chat-core/input/useChatInput.js",
13
+ "./chat-core/messages/buildTimelineItems.js": "./src/chat-core/messages/buildTimelineItems.js",
14
+ "./chat-core/session/useChatSessionReducer.js": "./src/chat-core/session/useChatSessionReducer.js",
15
+ "./chat-core/stream/sse-client.js": "./src/chat-core/stream/sse-client.js",
16
+ "./chat-core/stream/sse-events.js": "./src/chat-core/stream/sse-events.js",
17
+ "./chat-core/stream/useSSEStream.js": "./src/chat-core/stream/useSSEStream.js"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "^18.0.0"
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ export { default as useChatSessionReducer, initialState as chatSessionInitialState } from './session/useChatSessionReducer.js';
2
+ export { useChatInput } from './input/useChatInput.js';
3
+ export { useSSEStream } from './stream/useSSEStream.js';
4
+ export { SSEClient } from './stream/sse-client.js';
5
+ export * from './stream/sse-events.js';
6
+ export { buildTimelineItems } from './messages/buildTimelineItems.js';
@@ -0,0 +1,70 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export function useChatInput() {
4
+ const [inputValue, setInputValue] = useState('');
5
+ const [fileList, setFileList] = useState([]);
6
+ const [isDragging, setIsDragging] = useState(false);
7
+
8
+ const handleInputChange = useCallback((e) => {
9
+ setInputValue(e.target.value);
10
+ }, []);
11
+
12
+ const handleFileChange = useCallback(({ fileList: newFileList }) => {
13
+ setFileList(newFileList);
14
+ }, []);
15
+
16
+ const handleRemoveFile = useCallback((file) => {
17
+ setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
18
+ }, []);
19
+
20
+ const handleDrop = useCallback((e) => {
21
+ e.preventDefault();
22
+ e.stopPropagation();
23
+ setIsDragging(false);
24
+
25
+ const files = Array.from(e.dataTransfer.files);
26
+ const newFiles = files.map((file, index) => ({
27
+ uid: `${Date.now()}-${index}`,
28
+ name: file.name,
29
+ size: file.size,
30
+ type: file.type,
31
+ originFileObj: file,
32
+ }));
33
+
34
+ setFileList((prev) => [...prev, ...newFiles]);
35
+ return files.length;
36
+ }, []);
37
+
38
+ const handleDragOver = useCallback((e) => {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ setIsDragging(true);
42
+ }, []);
43
+
44
+ const handleDragLeave = useCallback((e) => {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ setIsDragging(false);
48
+ }, []);
49
+
50
+ const clearInput = useCallback(() => {
51
+ setInputValue('');
52
+ setFileList([]);
53
+ }, []);
54
+
55
+ return {
56
+ inputValue,
57
+ fileList,
58
+ isDragging,
59
+ handleInputChange,
60
+ handleFileChange,
61
+ handleRemoveFile,
62
+ handleDrop,
63
+ handleDragOver,
64
+ handleDragLeave,
65
+ clearInput,
66
+ };
67
+ }
68
+
69
+ export default useChatInput;
70
+
@@ -0,0 +1,74 @@
1
+ export function buildTimelineItems(state = {}) {
2
+ const messages = Array.isArray(state.messages) ? state.messages : [];
3
+ const executionCards = Array.isArray(state.executionCards) ? state.executionCards : [];
4
+ const uiCards = Array.isArray(state.uiCards) ? state.uiCards : [];
5
+ const streaming = state.streaming || {};
6
+
7
+ const items = [];
8
+
9
+ if (messages.length === 0) {
10
+ return {
11
+ isEmpty: true,
12
+ items: [],
13
+ };
14
+ }
15
+
16
+ messages.forEach((msg, index) => {
17
+ const messageId = msg.message_id || `msg-fallback-${index}`;
18
+ items.push({
19
+ id: messageId,
20
+ type: 'message',
21
+ role: msg.role,
22
+ content: msg.content,
23
+ createdAt: msg.created_at,
24
+ highlights: msg.highlights ?? [],
25
+ sourceTypes: msg.sourceTypes ?? [],
26
+ });
27
+ });
28
+
29
+ executionCards
30
+ .filter(Boolean)
31
+ .filter((card) => card.step_id !== streaming.executionTrace?.step_id)
32
+ .forEach((card, index) => {
33
+ const executionId = card.step_id || `execution-fallback-${index}`;
34
+ items.push({
35
+ id: executionId,
36
+ type: 'execution',
37
+ executionTrace: card,
38
+ });
39
+ });
40
+
41
+ if (uiCards.length > 0) {
42
+ items.push({
43
+ id: 'conversation-ui-cards',
44
+ type: 'ui_cards',
45
+ cards: uiCards,
46
+ });
47
+ }
48
+
49
+ if (streaming.content) {
50
+ items.push({
51
+ id: 'streaming-message',
52
+ type: 'streaming',
53
+ content: streaming.content,
54
+ });
55
+ }
56
+
57
+ if (streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace) {
58
+ items.push({
59
+ id: 'thinking-bubble',
60
+ type: 'thinking',
61
+ agentStatus: streaming.agentStatus,
62
+ thinkingTrace: streaming.thinkingTrace,
63
+ executionTrace: streaming.executionTrace,
64
+ });
65
+ }
66
+
67
+ return {
68
+ isEmpty: false,
69
+ items,
70
+ };
71
+ }
72
+
73
+ export default buildTimelineItems;
74
+
@@ -0,0 +1,446 @@
1
+ import { useReducer, useCallback, useMemo } from 'react';
2
+
3
+ const SESSION_LOADED = 'SESSION_LOADED';
4
+ const SESSION_RESET = 'SESSION_RESET';
5
+ const SESSION_ERROR = 'SESSION_ERROR';
6
+ const TITLE_UPDATED = 'TITLE_UPDATED';
7
+ const MESSAGES_REFRESHED = 'MESSAGES_REFRESHED';
8
+ const MESSAGE_APPENDED = 'MESSAGE_APPENDED';
9
+ const STREAMING_RESET = 'STREAMING_RESET';
10
+ const STREAMING_CHUNK = 'STREAMING_CHUNK';
11
+ const AGENT_STATUS_SET = 'AGENT_STATUS_SET';
12
+ const THINKING_TRACE_SET = 'THINKING_TRACE_SET';
13
+ const EXECUTION_TRACE_SET = 'EXECUTION_TRACE_SET';
14
+ const STREAMING_DONE = 'STREAMING_DONE';
15
+ const DOC_GENERATED = 'DOC_GENERATED';
16
+ const LEGEND_HINT_SHOWN = 'LEGEND_HINT_SHOWN';
17
+ const EXECUTION_CARD_UPSERTED = 'EXECUTION_CARD_UPSERTED';
18
+ const UI_CARDS_SET = 'UI_CARDS_SET';
19
+ const LAST_USER_MESSAGE_SET = 'LAST_USER_MESSAGE_SET';
20
+ const UI_CARD_UPDATED = 'UI_CARD_UPDATED';
21
+ const INSTANCE_PROFILE_SET = 'INSTANCE_PROFILE_SET';
22
+
23
+ const initialStreaming = {
24
+ content: '',
25
+ agentStatus: null,
26
+ thinkingTrace: '',
27
+ executionTrace: null,
28
+ };
29
+
30
+ export const initialState = {
31
+ sessionId: null,
32
+ sessionTitle: '',
33
+ sessionStatus: 'active',
34
+ instanceProfile: null,
35
+ messages: [],
36
+ executionCards: [],
37
+ uiCards: [],
38
+ lastUserMessage: '',
39
+ generatedDoc: null,
40
+ streaming: initialStreaming,
41
+ sessionError: null,
42
+ legendHintsShown: {
43
+ knowledge_base: false,
44
+ ai: false,
45
+ },
46
+ };
47
+
48
+ function reducer(state, { type, payload }) {
49
+ switch (type) {
50
+ case SESSION_LOADED: {
51
+ const { sessionId, title, status, messages } = payload;
52
+ return {
53
+ ...state,
54
+ sessionId,
55
+ sessionTitle: title,
56
+ sessionStatus: status || 'active',
57
+ instanceProfile: state.instanceProfile,
58
+ messages,
59
+ executionCards: [],
60
+ uiCards: [],
61
+ lastUserMessage: '',
62
+ generatedDoc: null,
63
+ streaming: initialStreaming,
64
+ legendHintsShown: initialState.legendHintsShown,
65
+ };
66
+ }
67
+
68
+ case SESSION_RESET:
69
+ return { ...initialState, instanceProfile: state.instanceProfile };
70
+
71
+ case SESSION_ERROR:
72
+ return { ...state, sessionError: payload };
73
+
74
+ case TITLE_UPDATED:
75
+ return { ...state, sessionTitle: payload };
76
+
77
+ case MESSAGES_REFRESHED:
78
+ return { ...state, messages: payload };
79
+
80
+ case MESSAGE_APPENDED:
81
+ return {
82
+ ...state,
83
+ messages: [...state.messages, payload],
84
+ lastUserMessage: payload.role === 'user' ? payload.content : state.lastUserMessage,
85
+ };
86
+
87
+ case LAST_USER_MESSAGE_SET:
88
+ return { ...state, lastUserMessage: payload };
89
+
90
+ case UI_CARDS_SET:
91
+ return { ...state, uiCards: payload };
92
+
93
+ case UI_CARD_UPDATED: {
94
+ const nextCard = payload;
95
+ return {
96
+ ...state,
97
+ uiCards: state.uiCards.map((card) =>
98
+ card.id === nextCard.id ? { ...card, ...nextCard } : card
99
+ ),
100
+ };
101
+ }
102
+
103
+ case INSTANCE_PROFILE_SET:
104
+ return { ...state, instanceProfile: payload };
105
+
106
+ case EXECUTION_CARD_UPSERTED: {
107
+ const nextCard = payload;
108
+ const cardKey = nextCard.step_id || `${nextCard.agent}-${nextCard.label}-${nextCard.stage}`;
109
+ const existingIndex = state.executionCards.findIndex((card) => {
110
+ const existingKey = card.step_id || `${card.agent}-${card.label}-${card.stage}`;
111
+ return existingKey === cardKey;
112
+ });
113
+
114
+ const updatedCards = existingIndex >= 0
115
+ ? state.executionCards.map((card, index) =>
116
+ index === existingIndex ? { ...card, ...nextCard } : card
117
+ )
118
+ : [...state.executionCards, nextCard];
119
+
120
+ return {
121
+ ...state,
122
+ executionCards: updatedCards.slice(-6),
123
+ };
124
+ }
125
+
126
+ case STREAMING_RESET:
127
+ return { ...state, streaming: initialStreaming };
128
+
129
+ case STREAMING_CHUNK:
130
+ return {
131
+ ...state,
132
+ streaming: {
133
+ ...state.streaming,
134
+ content: state.streaming.content + payload,
135
+ },
136
+ };
137
+
138
+ case AGENT_STATUS_SET:
139
+ return {
140
+ ...state,
141
+ streaming: { ...state.streaming, agentStatus: payload },
142
+ };
143
+
144
+ case THINKING_TRACE_SET:
145
+ return {
146
+ ...state,
147
+ streaming: { ...state.streaming, thinkingTrace: payload },
148
+ };
149
+
150
+ case EXECUTION_TRACE_SET:
151
+ return {
152
+ ...state,
153
+ streaming: {
154
+ ...state.streaming,
155
+ executionTrace: {
156
+ ...state.streaming.executionTrace,
157
+ ...payload,
158
+ },
159
+ },
160
+ };
161
+
162
+ case STREAMING_DONE: {
163
+ const newMessages = payload
164
+ ? [
165
+ ...state.messages,
166
+ {
167
+ message_id: `temp-assistant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
168
+ role: 'assistant',
169
+ content: payload,
170
+ created_at: new Date().toISOString(),
171
+ },
172
+ ]
173
+ : state.messages;
174
+
175
+ return {
176
+ ...state,
177
+ messages: newMessages,
178
+ streaming: initialStreaming,
179
+ };
180
+ }
181
+
182
+ case DOC_GENERATED:
183
+ return { ...state, generatedDoc: payload };
184
+
185
+ case LEGEND_HINT_SHOWN:
186
+ return {
187
+ ...state,
188
+ legendHintsShown: {
189
+ ...state.legendHintsShown,
190
+ [payload]: true,
191
+ },
192
+ };
193
+
194
+ default:
195
+ return state;
196
+ }
197
+ }
198
+
199
+ function requireAPI(api, name) {
200
+ if (!api) {
201
+ throw new Error(`${name} is required`);
202
+ }
203
+ return api;
204
+ }
205
+
206
+ export default function useChatSessionReducer(deps = {}) {
207
+ const { sessionAPI, messageAPI } = deps;
208
+ const [state, dispatch] = useReducer(reducer, initialState);
209
+
210
+ const loadSession = useCallback(async (sessionId) => {
211
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
212
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
213
+
214
+ const [detail, msgs] = await Promise.all([
215
+ sessionApi.get(sessionId),
216
+ messageApi.list(sessionId),
217
+ ]);
218
+
219
+ dispatch({
220
+ type: SESSION_LOADED,
221
+ payload: {
222
+ sessionId,
223
+ title: detail.title,
224
+ status: detail.status || 'active',
225
+ messages: msgs.messages,
226
+ },
227
+ });
228
+ }, [sessionAPI, messageAPI]);
229
+
230
+ const createSession = useCallback(async ({
231
+ function_type,
232
+ title,
233
+ ...extraPayload
234
+ } = {}) => {
235
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
236
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
237
+
238
+ dispatch({
239
+ type: SESSION_LOADED,
240
+ payload: {
241
+ sessionId: response.sessionId,
242
+ title,
243
+ status: response.status || 'active',
244
+ messages: [],
245
+ },
246
+ });
247
+
248
+ return response.sessionId;
249
+ }, [sessionAPI]);
250
+
251
+ const createOrLoadSession = useCallback(async ({
252
+ function_type,
253
+ defaultTitle,
254
+ forceNew = false,
255
+ ...extraPayload
256
+ } = {}) => {
257
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
258
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
259
+
260
+ let sessionId;
261
+ let title;
262
+
263
+ if (forceNew) {
264
+ title = defaultTitle;
265
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
266
+ sessionId = response.sessionId;
267
+ } else {
268
+ const sessionList = await sessionApi.list({
269
+ function_type,
270
+ status: 'active',
271
+ page: 1,
272
+ page_size: 1,
273
+ ...extraPayload,
274
+ });
275
+
276
+ if (sessionList.sessions.length > 0) {
277
+ sessionId = sessionList.sessions[0].sessionId;
278
+ title = sessionList.sessions[0].title;
279
+ } else {
280
+ title = defaultTitle;
281
+ const response = await sessionApi.create({ function_type, title, ...extraPayload });
282
+ sessionId = response.sessionId;
283
+ }
284
+ }
285
+
286
+ const [msgs, detail] = await Promise.all([
287
+ messageApi.list(sessionId),
288
+ sessionApi.get(sessionId),
289
+ ]);
290
+
291
+ dispatch({
292
+ type: SESSION_LOADED,
293
+ payload: {
294
+ sessionId,
295
+ title,
296
+ status: detail.status || 'active',
297
+ messages: msgs.messages,
298
+ },
299
+ });
300
+
301
+ return sessionId;
302
+ }, [sessionAPI, messageAPI]);
303
+
304
+ const resetSession = useCallback(() => {
305
+ dispatch({ type: SESSION_RESET });
306
+ }, []);
307
+
308
+ const updateTitle = useCallback(async (sessionId, title) => {
309
+ const sessionApi = requireAPI(sessionAPI, 'sessionAPI');
310
+ await sessionApi.update(sessionId, { title });
311
+ dispatch({ type: TITLE_UPDATED, payload: title });
312
+ }, [sessionAPI]);
313
+
314
+ const refreshMessages = useCallback(async (sessionId) => {
315
+ const messageApi = requireAPI(messageAPI, 'messageAPI');
316
+ const response = await messageApi.list(sessionId);
317
+ const messagesWithIds = response.messages.map((msg, index) => ({
318
+ ...msg,
319
+ message_id: msg.message_id || `fallback-${Date.now()}-${index}`,
320
+ }));
321
+ dispatch({ type: MESSAGES_REFRESHED, payload: messagesWithIds });
322
+ }, [messageAPI]);
323
+
324
+ const appendUserMessage = useCallback((content) => {
325
+ dispatch({
326
+ type: MESSAGE_APPENDED,
327
+ payload: {
328
+ message_id: `temp-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
329
+ role: 'user',
330
+ content,
331
+ created_at: new Date().toISOString(),
332
+ },
333
+ });
334
+ }, []);
335
+
336
+ const setUICards = useCallback((cards) => {
337
+ dispatch({ type: UI_CARDS_SET, payload: Array.isArray(cards) ? cards : [] });
338
+ }, []);
339
+
340
+ const updateUICard = useCallback((card) => {
341
+ dispatch({ type: UI_CARD_UPDATED, payload: card });
342
+ }, []);
343
+
344
+ const setLastUserMessage = useCallback((content) => {
345
+ dispatch({ type: LAST_USER_MESSAGE_SET, payload: content || '' });
346
+ }, []);
347
+
348
+ const resetStreaming = useCallback(() => {
349
+ dispatch({ type: STREAMING_RESET });
350
+ }, []);
351
+
352
+ const appendStreamChunk = useCallback((chunk) => {
353
+ dispatch({ type: STREAMING_CHUNK, payload: chunk });
354
+ }, []);
355
+
356
+ const setAgentStatus = useCallback((agentStatus) => {
357
+ dispatch({ type: AGENT_STATUS_SET, payload: agentStatus });
358
+ }, []);
359
+
360
+ const setThinkingTrace = useCallback((trace) => {
361
+ dispatch({ type: THINKING_TRACE_SET, payload: trace });
362
+ }, []);
363
+
364
+ const setExecutionTrace = useCallback((executionTrace) => {
365
+ dispatch({ type: EXECUTION_TRACE_SET, payload: executionTrace });
366
+ dispatch({ type: EXECUTION_CARD_UPSERTED, payload: executionTrace });
367
+ }, []);
368
+
369
+ const completeStreaming = useCallback((finalContent) => {
370
+ dispatch({ type: STREAMING_DONE, payload: finalContent });
371
+ }, []);
372
+
373
+ const setGeneratedDoc = useCallback((doc) => {
374
+ dispatch({ type: DOC_GENERATED, payload: doc });
375
+ }, []);
376
+
377
+ const markLegendHintShown = useCallback((sourceType) => {
378
+ dispatch({ type: LEGEND_HINT_SHOWN, payload: sourceType });
379
+ }, []);
380
+
381
+ const setSessionError = useCallback((error) => {
382
+ dispatch({ type: SESSION_ERROR, payload: error });
383
+ }, []);
384
+
385
+ const setInstanceProfile = useCallback((profile) => {
386
+ dispatch({ type: INSTANCE_PROFILE_SET, payload: profile || null });
387
+ }, []);
388
+
389
+ return useMemo(() => ({
390
+ sessionId: state.sessionId,
391
+ sessionTitle: state.sessionTitle,
392
+ instanceProfile: state.instanceProfile,
393
+ messages: state.messages,
394
+ executionCards: state.executionCards,
395
+ uiCards: state.uiCards,
396
+ lastUserMessage: state.lastUserMessage,
397
+ generatedDoc: state.generatedDoc,
398
+ streaming: state.streaming,
399
+ sessionError: state.sessionError,
400
+ legendHintsShown: state.legendHintsShown,
401
+ loadSession,
402
+ createSession,
403
+ createOrLoadSession,
404
+ resetSession,
405
+ updateTitle,
406
+ refreshMessages,
407
+ setSessionError,
408
+ appendUserMessage,
409
+ setUICards,
410
+ updateUICard,
411
+ setLastUserMessage,
412
+ resetStreaming,
413
+ appendStreamChunk,
414
+ setAgentStatus,
415
+ setThinkingTrace,
416
+ setExecutionTrace,
417
+ completeStreaming,
418
+ setGeneratedDoc,
419
+ setInstanceProfile,
420
+ markLegendHintShown,
421
+ }), [
422
+ state,
423
+ loadSession,
424
+ createSession,
425
+ createOrLoadSession,
426
+ resetSession,
427
+ updateTitle,
428
+ refreshMessages,
429
+ setSessionError,
430
+ appendUserMessage,
431
+ setUICards,
432
+ updateUICard,
433
+ setLastUserMessage,
434
+ resetStreaming,
435
+ appendStreamChunk,
436
+ setAgentStatus,
437
+ setThinkingTrace,
438
+ setExecutionTrace,
439
+ completeStreaming,
440
+ setGeneratedDoc,
441
+ setInstanceProfile,
442
+ markLegendHintShown,
443
+ ]);
444
+ }
445
+
446
+ export { useChatSessionReducer };
@@ -0,0 +1,123 @@
1
+ const API_BASE_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL || '';
2
+ const DEFAULT_ENDPOINT = '/api/v1/chat/stream';
3
+
4
+ function joinUrl(baseUrl, endpoint) {
5
+ if (!baseUrl) {
6
+ return endpoint;
7
+ }
8
+
9
+ if (!endpoint) {
10
+ return baseUrl;
11
+ }
12
+
13
+ return `${baseUrl.replace(/\/+$/, '')}/${endpoint.replace(/^\/+/, '')}`;
14
+ }
15
+
16
+ export class SSEClient {
17
+ constructor({ endpoint = DEFAULT_ENDPOINT, baseUrl = API_BASE_URL } = {}) {
18
+ this.abortController = null;
19
+ this.endpoint = endpoint;
20
+ this.baseUrl = baseUrl;
21
+ }
22
+
23
+ async sendMessage(sessionOrParams, contentArg, onEventArg, onCompleteArg, onErrorArg, optionsArg = {}) {
24
+ const isObjectCall = typeof sessionOrParams === 'object' && sessionOrParams !== null;
25
+ const params = isObjectCall
26
+ ? sessionOrParams
27
+ : {
28
+ sessionId: sessionOrParams,
29
+ content: contentArg,
30
+ enabledCapabilities: optionsArg.enabledCapabilities || [],
31
+ };
32
+
33
+ const sessionId = params.sessionId;
34
+ const content = params.content;
35
+ const enabledCapabilities = Array.isArray(params.enabledCapabilities) ? params.enabledCapabilities : [];
36
+ const onEvent = isObjectCall ? contentArg : onEventArg;
37
+ const onComplete = isObjectCall ? onEventArg : onCompleteArg;
38
+ const onError = isObjectCall ? onCompleteArg : onErrorArg;
39
+ const url = params.url || joinUrl(params.baseUrl ?? this.baseUrl, params.endpoint || this.endpoint);
40
+
41
+ this.abortController = new AbortController();
42
+
43
+ try {
44
+ const response = await fetch(url, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify({
50
+ message: content,
51
+ session_id: sessionId,
52
+ enabled_capabilities: enabledCapabilities,
53
+ }),
54
+ signal: this.abortController.signal,
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
59
+ }
60
+
61
+ const reader = response.body.getReader();
62
+ const decoder = new TextDecoder();
63
+ let buffer = '';
64
+ let currentEvent = '';
65
+
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+
70
+ buffer += decoder.decode(value, { stream: true });
71
+ const lines = buffer.split('\n');
72
+ buffer = lines.pop() || '';
73
+
74
+ for (const line of lines) {
75
+ if (line.startsWith('event:')) {
76
+ currentEvent = line.substring(6).trim();
77
+ continue;
78
+ }
79
+
80
+ if (!line.startsWith('data:')) {
81
+ continue;
82
+ }
83
+
84
+ const data = line.substring(5).trim();
85
+ if (!data || !currentEvent) {
86
+ continue;
87
+ }
88
+
89
+ try {
90
+ const eventData = JSON.parse(data);
91
+ onEvent?.({
92
+ type: currentEvent,
93
+ data: eventData,
94
+ });
95
+
96
+ if (currentEvent === 'complete') {
97
+ onComplete?.();
98
+ return;
99
+ }
100
+ } catch (error) {
101
+ console.error('SSE 数据解析失败:', error, 'data:', data);
102
+ }
103
+ }
104
+ }
105
+
106
+ onComplete?.();
107
+ } catch (error) {
108
+ if (error?.name === 'AbortError') {
109
+ return;
110
+ }
111
+
112
+ onError?.(error instanceof Error ? error.message : '发送消息失败');
113
+ }
114
+ }
115
+
116
+ abort() {
117
+ if (this.abortController) {
118
+ this.abortController.abort();
119
+ this.abortController = null;
120
+ }
121
+ }
122
+ }
123
+
@@ -0,0 +1,425 @@
1
+ /**
2
+ * SSE 事件规范化层
3
+ */
4
+
5
+ /**
6
+ * @typedef {'thinking'|'searching'|'generating'|'completed'|string} AgentStatusType
7
+ *
8
+ * @typedef {Object} AgentStatusPayload
9
+ * @property {string} agent
10
+ * @property {AgentStatusType} status
11
+ * @property {number|null} timestamp
12
+ *
13
+ * @typedef {Object} ThinkingTracePayload
14
+ * @property {string} trace
15
+ *
16
+ * @typedef {Object} ExecutionSourcePayload
17
+ * @property {string} source_type
18
+ * @property {string} source_label
19
+ * @property {string} title
20
+ *
21
+ * @typedef {Object} ExecutionTracePayload
22
+ * @property {string} session_id
23
+ * @property {string} step_id
24
+ * @property {string} agent
25
+ * @property {string} stage
26
+ * @property {string} label
27
+ * @property {string} detail
28
+ * @property {string} reason
29
+ * @property {string} status
30
+ * @property {Array<ExecutionSourcePayload>} sources
31
+ * @property {number|null} timestamp
32
+ *
33
+ * @typedef {Object} ContentPayload
34
+ * @property {string} chunk
35
+ *
36
+ * @typedef {Object} TracePayload
37
+ * @property {string} trace
38
+ * @property {string} summary
39
+ * @property {string} agent
40
+ *
41
+ * @typedef {Object} FieldSource
42
+ * @property {number|null} confidence
43
+ *
44
+ * @typedef {Object} FieldProgressPayload
45
+ * @property {Record<string, string>} fields
46
+ * @property {Record<string, FieldSource>} sources
47
+ * @property {number|null} progress
48
+ * @property {Array<string>} collected
49
+ * @property {Array<string>} missing
50
+ * @property {Object|null} last_field
51
+ * @property {string|null} refresh_reason
52
+ *
53
+ * @typedef {Object} DocPayload
54
+ * @property {Object|null} doc
55
+ *
56
+ * @typedef {Object} CapabilitySuggestionPayload
57
+ * @property {string} type
58
+ * @property {string} reason
59
+ * @property {string} benefit
60
+ * @property {number} confidence
61
+ * @property {number} estimated_cost
62
+ *
63
+ * @typedef {Object} ModeArtifactPayload
64
+ * @property {string} mode_key
65
+ * @property {string|null} project_id
66
+ * @property {string} message_excerpt
67
+ * @property {string} render_hint
68
+ *
69
+ * @typedef {Object} FieldDefinition
70
+ * @property {string} id
71
+ * @property {string} label
72
+ * @property {string} priority
73
+ *
74
+ * @typedef {Object} FieldsUpdatedPayload
75
+ * @property {number} total_fields
76
+ * @property {number} base_fields
77
+ * @property {number} category_fields
78
+ * @property {Array<FieldDefinition>} new_fields
79
+ * @property {string|null} refresh_reason
80
+ *
81
+ * @typedef {Object} WorkspaceActivationPayload
82
+ * @property {string} function_type
83
+ * @property {string} mode
84
+ * @property {string} title
85
+ * @property {boolean} trace_in_chat
86
+ * @property {string} right_panel
87
+ * @property {boolean} knowledge_base_available
88
+ *
89
+ * @typedef {Object} UICardActionPayload
90
+ * @property {string} type
91
+ * @property {Record<string, unknown>} payload
92
+ *
93
+ * @typedef {Object} UICardPayload
94
+ * @property {string} id
95
+ * @property {string} kind
96
+ * @property {string} title
97
+ * @property {string} description
98
+ * @property {boolean} available
99
+ * @property {string} status
100
+ * @property {string} reason
101
+ * @property {UICardActionPayload|null} action
102
+ *
103
+ * @typedef {Object} UICardsPayload
104
+ * @property {string} scope
105
+ * @property {Array<UICardPayload>} cards
106
+ *
107
+ * @typedef {Object} KnowledgeBaseStatePayload
108
+ * @property {boolean} enabled
109
+ * @property {string} status
110
+ * @property {string} label
111
+ * @property {string} message
112
+ *
113
+ * @typedef {Object} CategoryIdentifiedPayload
114
+ * @property {string} category
115
+ * @property {number|null} confidence
116
+ *
117
+ * @typedef {Object} SSEErrorPayload
118
+ * @property {string} message
119
+ */
120
+
121
+ export const parseAgentStatus = (raw) => ({
122
+ agent: raw?.agent ?? '',
123
+ status: raw?.status ?? '',
124
+ timestamp: raw?.timestamp ?? null,
125
+ });
126
+
127
+ export const parseThinkingTrace = (raw) => ({
128
+ trace: raw?.trace ?? '',
129
+ });
130
+
131
+ export const parseExecutionTrace = (raw) => ({
132
+ session_id: raw?.session_id ?? '',
133
+ step_id: raw?.step_id ?? '',
134
+ agent: raw?.agent ?? '',
135
+ stage: raw?.stage ?? '',
136
+ label: raw?.label ?? '',
137
+ detail: raw?.detail ?? raw?.trace ?? '',
138
+ reason: raw?.reason ?? '',
139
+ status: raw?.status ?? raw?.step_status ?? '',
140
+ sources: Array.isArray(raw?.sources) ? raw.sources : [],
141
+ timestamp: raw?.timestamp ?? null,
142
+ });
143
+
144
+ export const parseContent = (raw) => ({
145
+ chunk: raw?.content ?? '',
146
+ });
147
+
148
+ export const parseTrace = (raw) => ({
149
+ trace: raw?.trace ?? raw?.detail ?? '',
150
+ summary: raw?.summary ?? raw?.label ?? '',
151
+ agent: raw?.agent ?? '',
152
+ });
153
+
154
+ function resolveModeEventPayload(raw) {
155
+ if (raw?.payload && typeof raw.payload === 'object' && !Array.isArray(raw.payload)) {
156
+ return raw.payload;
157
+ }
158
+
159
+ return raw && typeof raw === 'object' ? raw : {};
160
+ }
161
+
162
+ function pickFirstArray(payload, keys) {
163
+ for (const key of keys) {
164
+ const value = payload?.[key];
165
+ if (Array.isArray(value)) {
166
+ return value;
167
+ }
168
+ }
169
+ return [];
170
+ }
171
+
172
+ export const parseFieldProgress = (raw) => {
173
+ const payload = resolveModeEventPayload(raw);
174
+ return {
175
+ fields: payload?.fields ?? {},
176
+ sources: payload?.sources ?? {},
177
+ field_entries: Array.isArray(payload?.field_entries) ? payload.field_entries : [],
178
+ field_groups: payload?.field_groups && typeof payload.field_groups === 'object' && !Array.isArray(payload.field_groups)
179
+ ? payload.field_groups
180
+ : {},
181
+ extra_fields: Array.isArray(payload?.extra_fields) ? payload.extra_fields : [],
182
+ progress: payload?.progress ?? null,
183
+ required_progress: payload?.required_progress ?? null,
184
+ recommended_progress: payload?.recommended_progress ?? null,
185
+ optional_progress: payload?.optional_progress ?? null,
186
+ collected: Array.isArray(payload?.collected) ? payload.collected : [],
187
+ missing: Array.isArray(payload?.missing) ? payload.missing : [],
188
+ required_collected: Array.isArray(payload?.required_collected) ? payload.required_collected : [],
189
+ required_missing: Array.isArray(payload?.required_missing) ? payload.required_missing : [],
190
+ recommended_collected: Array.isArray(payload?.recommended_collected) ? payload.recommended_collected : [],
191
+ recommended_missing: Array.isArray(payload?.recommended_missing) ? payload.recommended_missing : [],
192
+ optional_collected: Array.isArray(payload?.optional_collected) ? payload.optional_collected : [],
193
+ optional_missing: Array.isArray(payload?.optional_missing) ? payload.optional_missing : [],
194
+ last_field: payload?.last_field ?? null,
195
+ refresh_reason: payload?.refresh_reason ?? null,
196
+ field_definitions: Array.isArray(payload?.field_definitions) ? payload.field_definitions : [],
197
+ required_fields: Array.isArray(payload?.required_fields) ? payload.required_fields : [],
198
+ recommended_fields: Array.isArray(payload?.recommended_fields) ? payload.recommended_fields : [],
199
+ optional_fields: Array.isArray(payload?.optional_fields) ? payload.optional_fields : [],
200
+ field_priorities: payload?.field_priorities && typeof payload.field_priorities === 'object' && !Array.isArray(payload.field_priorities)
201
+ ? payload.field_priorities
202
+ : {},
203
+ };
204
+ };
205
+
206
+ export const parseDoc = (raw) => ({
207
+ doc: raw?.doc ?? null,
208
+ });
209
+
210
+ export const parseCapabilitySuggestion = (raw) => ({
211
+ type: raw?.type ?? '',
212
+ reason: raw?.reason ?? '',
213
+ benefit: raw?.benefit ?? '',
214
+ confidence: raw?.confidence ?? 0,
215
+ estimated_cost: raw?.estimated_cost ?? 0,
216
+ });
217
+
218
+ export const parseRequirementDraft = (raw) => {
219
+ const payload = resolveModeEventPayload(raw);
220
+ const draft = payload?.draft && typeof payload.draft === 'object' && !Array.isArray(payload.draft)
221
+ ? payload.draft
222
+ : {};
223
+
224
+ return {
225
+ mode_key: payload?.mode_key ?? '',
226
+ project_id: payload?.project_id ?? null,
227
+ message_excerpt: payload?.message_excerpt ?? '',
228
+ draft: {
229
+ ...draft,
230
+ fields: Array.isArray(draft.fields) ? draft.fields : [],
231
+ },
232
+ render_hint: payload?.render_hint ?? 'requirement_draft',
233
+ };
234
+ };
235
+
236
+ export const parseNextActions = (raw) => {
237
+ const payload = resolveModeEventPayload(raw);
238
+ const actions = Array.isArray(payload?.actions)
239
+ ? payload.actions
240
+ : (Array.isArray(payload?.next_actions) ? payload.next_actions : []);
241
+ return {
242
+ mode_key: payload?.mode_key ?? '',
243
+ project_id: payload?.project_id ?? null,
244
+ message_excerpt: payload?.message_excerpt ?? '',
245
+ actions,
246
+ completion_state: payload?.completion_state ?? '',
247
+ required_missing: Array.isArray(payload?.required_missing) ? payload.required_missing : [],
248
+ recommended_missing: Array.isArray(payload?.recommended_missing) ? payload.recommended_missing : [],
249
+ optional_missing: Array.isArray(payload?.optional_missing) ? payload.optional_missing : [],
250
+ ready_for_sourcing: payload?.ready_for_sourcing ?? null,
251
+ status: payload?.status ?? '',
252
+ reason: payload?.reason ?? '',
253
+ render_hint: payload?.render_hint ?? 'next_actions',
254
+ };
255
+ };
256
+
257
+ export const parseSourcingCandidates = (raw) => {
258
+ const payload = resolveModeEventPayload(raw);
259
+ const missing = pickFirstArray(payload, ['missing', 'base_missing', 'missing_fields', 'required_missing']);
260
+ return {
261
+ mode_key: payload?.mode_key ?? '',
262
+ project_id: payload?.project_id ?? null,
263
+ message_excerpt: payload?.message_excerpt ?? '',
264
+ missing,
265
+ base_fields: Array.isArray(payload?.base_fields) ? payload.base_fields : [],
266
+ base_collected: Array.isArray(payload?.base_collected) ? payload.base_collected : [],
267
+ base_missing: Array.isArray(payload?.base_missing) ? payload.base_missing : [],
268
+ base_total: Number.isFinite(payload?.base_total) ? payload.base_total : 0,
269
+ base_progress: Number.isFinite(payload?.base_progress) ? payload.base_progress : null,
270
+ base_ready_for_matching: payload?.base_ready_for_matching === true,
271
+ mode_state: payload?.mode_state ?? '',
272
+ candidate_count: Number.isFinite(payload?.candidate_count) ? payload.candidate_count : null,
273
+ is_placeholder: payload?.is_placeholder ?? null,
274
+ required_missing: Array.isArray(payload?.required_missing) ? payload.required_missing : [],
275
+ recommended_missing: Array.isArray(payload?.recommended_missing) ? payload.recommended_missing : [],
276
+ optional_missing: Array.isArray(payload?.optional_missing) ? payload.optional_missing : [],
277
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
278
+ candidates: pickFirstArray(payload, ['candidates', 'sourcing_candidates', 'candidate_list', 'results']),
279
+ reasoning: Array.isArray(payload?.reasoning) ? payload.reasoning : [],
280
+ summary: payload?.sourcing_summary ?? payload?.summary ?? null,
281
+ render_hint: payload?.render_hint ?? 'sourcing_candidates',
282
+ };
283
+ };
284
+
285
+ export const parseRiskSummary = (raw) => {
286
+ const payload = resolveModeEventPayload(raw);
287
+ const risks = pickFirstArray(payload, ['risks', 'risk_items', 'items', 'risk_list']);
288
+ const sources = pickFirstArray(payload, ['sources', 'references', 'evidence']);
289
+ return {
290
+ mode_key: payload?.mode_key ?? '',
291
+ project_id: payload?.project_id ?? null,
292
+ message_excerpt: payload?.message_excerpt ?? '',
293
+ mode_state: payload?.mode_state ?? '',
294
+ base_fields: Array.isArray(payload?.base_fields) ? payload.base_fields : [],
295
+ base_collected: Array.isArray(payload?.base_collected) ? payload.base_collected : [],
296
+ base_missing: Array.isArray(payload?.base_missing) ? payload.base_missing : [],
297
+ base_total: Number.isFinite(payload?.base_total) ? payload.base_total : 0,
298
+ base_progress: Number.isFinite(payload?.base_progress) ? payload.base_progress : null,
299
+ base_ready_for_matching: payload?.base_ready_for_matching === true,
300
+ is_placeholder: payload?.is_placeholder ?? null,
301
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
302
+ risks,
303
+ sources,
304
+ summary: payload?.risk_summary ?? payload?.summary ?? null,
305
+ render_hint: payload?.render_hint ?? 'risk_summary',
306
+ };
307
+ };
308
+
309
+ export const parseShortlistUpdated = (raw) => {
310
+ const payload = resolveModeEventPayload(raw);
311
+ const shortlist = pickFirstArray(payload, ['shortlist', 'shortlist_updated', 'selected_candidates', 'items']);
312
+ return {
313
+ mode_key: payload?.mode_key ?? '',
314
+ project_id: payload?.project_id ?? null,
315
+ message_excerpt: payload?.message_excerpt ?? '',
316
+ mode_state: payload?.mode_state ?? '',
317
+ base_fields: Array.isArray(payload?.base_fields) ? payload.base_fields : [],
318
+ base_collected: Array.isArray(payload?.base_collected) ? payload.base_collected : [],
319
+ base_missing: Array.isArray(payload?.base_missing) ? payload.base_missing : [],
320
+ base_total: Number.isFinite(payload?.base_total) ? payload.base_total : 0,
321
+ base_progress: Number.isFinite(payload?.base_progress) ? payload.base_progress : null,
322
+ base_ready_for_matching: payload?.base_ready_for_matching === true,
323
+ shortlist,
324
+ status: payload?.status ?? payload?.state ?? '',
325
+ reason: payload?.reason ?? payload?.message ?? '',
326
+ selected_count: Number.isFinite(payload?.selected_count) ? payload.selected_count : null,
327
+ is_placeholder: payload?.is_placeholder ?? null,
328
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
329
+ render_hint: payload?.render_hint ?? 'shortlist_updated',
330
+ };
331
+ };
332
+
333
+ export const parseEvaluationReportReady = (raw) => {
334
+ const payload = resolveModeEventPayload(raw);
335
+ const nestedReport = payload?.evaluation_report_ready
336
+ && typeof payload.evaluation_report_ready === 'object'
337
+ && !Array.isArray(payload.evaluation_report_ready)
338
+ ? payload.evaluation_report_ready
339
+ : {};
340
+ const reportSource = payload?.report
341
+ || nestedReport.report
342
+ || nestedReport;
343
+ const report = reportSource && typeof reportSource === 'object' && !Array.isArray(reportSource)
344
+ ? reportSource
345
+ : {};
346
+ const compareTable = pickFirstArray(payload, ['compare_table', 'comparison_table', 'compare_rows'])
347
+ || pickFirstArray(report, ['compare_table', 'comparison_table', 'compare_rows'])
348
+ || pickFirstArray(nestedReport, ['compare_table', 'comparison_table', 'compare_rows']);
349
+
350
+ return {
351
+ mode_key: payload?.mode_key ?? '',
352
+ project_id: payload?.project_id ?? null,
353
+ message_excerpt: payload?.message_excerpt ?? '',
354
+ mode_state: payload?.mode_state ?? '',
355
+ base_fields: Array.isArray(payload?.base_fields) ? payload.base_fields : [],
356
+ base_collected: Array.isArray(payload?.base_collected) ? payload.base_collected : [],
357
+ base_missing: Array.isArray(payload?.base_missing) ? payload.base_missing : [],
358
+ base_total: Number.isFinite(payload?.base_total) ? payload.base_total : 0,
359
+ base_progress: Number.isFinite(payload?.base_progress) ? payload.base_progress : null,
360
+ base_ready_for_matching: payload?.base_ready_for_matching === true,
361
+ is_placeholder: payload?.is_placeholder ?? null,
362
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
363
+ report: {
364
+ ...report,
365
+ sections: Array.isArray(report.sections) ? report.sections : [],
366
+ },
367
+ compare_table: payload?.compare_table
368
+ ?? payload?.comparison_table
369
+ ?? payload?.compare_rows
370
+ ?? report?.compare_table
371
+ ?? report?.comparison_table
372
+ ?? report?.compare_rows
373
+ ?? nestedReport?.compare_table
374
+ ?? nestedReport?.comparison_table
375
+ ?? nestedReport?.compare_rows
376
+ ?? compareTable,
377
+ render_hint: payload?.render_hint ?? 'evaluation_report_ready',
378
+ };
379
+ };
380
+
381
+ export const parseWorkspaceActivation = (raw) => ({
382
+ function_type: raw?.function_type ?? '',
383
+ mode: raw?.mode ?? '',
384
+ title: raw?.title ?? '',
385
+ trace_in_chat: raw?.trace_in_chat === true,
386
+ right_panel: raw?.right_panel ?? '',
387
+ knowledge_base_available: raw?.knowledge_base_available === true,
388
+ });
389
+
390
+ export const parseUICards = (raw) => ({
391
+ scope: raw?.scope ?? 'conversation',
392
+ cards: Array.isArray(raw?.cards) ? raw.cards : [],
393
+ });
394
+
395
+ export const parseKnowledgeBaseState = (raw) => ({
396
+ enabled: raw?.enabled === true,
397
+ status: raw?.status ?? '',
398
+ label: raw?.label ?? '',
399
+ message: raw?.message ?? '',
400
+ });
401
+
402
+ export const parseCategoryIdentified = (raw) => ({
403
+ category: raw?.category ?? raw?.level3 ?? '',
404
+ confidence: raw?.confidence ?? null,
405
+ });
406
+
407
+ export const parseFieldsUpdated = (raw) => ({
408
+ total_fields: raw?.total_fields ?? 0,
409
+ base_fields: raw?.base_fields ?? 0,
410
+ category_fields: raw?.category_fields ?? 0,
411
+ new_fields: Array.isArray(raw?.new_fields) ? raw.new_fields : [],
412
+ refresh_reason: raw?.refresh_reason ?? null,
413
+ });
414
+
415
+ export const parseInstanceProfile = (raw) => ({
416
+ product_name: raw?.instance_profile?.product_name ?? raw?.product_name ?? '',
417
+ brand_tag: raw?.instance_profile?.brand_tag ?? raw?.brand_tag ?? '',
418
+ logo_text: raw?.instance_profile?.logo_text ?? raw?.logo_text ?? '',
419
+ logo_url: raw?.instance_profile?.logo_url ?? raw?.logo_url ?? '',
420
+ ui_labels: raw?.instance_profile?.ui_labels ?? raw?.ui_labels ?? {},
421
+ });
422
+
423
+ export const parseSSEError = (raw) => ({
424
+ message: raw?.message ?? '发生错误',
425
+ });
@@ -0,0 +1,209 @@
1
+ import { useRef, useState, useCallback } from 'react';
2
+ import { SSEClient } from './sse-client.js';
3
+ import {
4
+ parseAgentStatus,
5
+ parseThinkingTrace,
6
+ parseExecutionTrace,
7
+ parseContent,
8
+ parseTrace,
9
+ parseFieldProgress,
10
+ parseDoc,
11
+ parseRequirementDraft,
12
+ parseNextActions,
13
+ parseSourcingCandidates,
14
+ parseRiskSummary,
15
+ parseShortlistUpdated,
16
+ parseEvaluationReportReady,
17
+ parseWorkspaceActivation,
18
+ parseUICards,
19
+ parseKnowledgeBaseState,
20
+ parseCategoryIdentified,
21
+ parseCapabilitySuggestion,
22
+ parseFieldsUpdated,
23
+ parseInstanceProfile,
24
+ parseSSEError,
25
+ } from './sse-events.js';
26
+
27
+ function classifyError(errorMessage) {
28
+ if (!errorMessage) return 'unknown';
29
+
30
+ const lowerMsg = errorMessage.toLowerCase();
31
+ if (lowerMsg.includes('network') || lowerMsg.includes('fetch')) return 'network';
32
+ if (lowerMsg.includes('timeout') || lowerMsg.includes('timed out')) return 'timeout';
33
+ if (/\b(500|502|503|504)\b/.test(lowerMsg)) return 'api';
34
+ return 'unknown';
35
+ }
36
+
37
+ export function useSSEStream(sessionId) {
38
+ const clientRef = useRef(new SSEClient());
39
+ const [loading, setLoading] = useState(false);
40
+ const [error, setError] = useState(null);
41
+ const accumulatedRef = useRef('');
42
+ const lastMessageRef = useRef({ content: '', handlers: {} });
43
+
44
+ const send = useCallback(async (content, handlers = {}, options = {}) => {
45
+ const resolvedSessionId = options.sessionIdOverride || sessionId;
46
+ if (!resolvedSessionId || !content.trim()) return;
47
+
48
+ lastMessageRef.current = { content, handlers, options };
49
+ setError(null);
50
+ accumulatedRef.current = '';
51
+ setLoading(true);
52
+
53
+ const {
54
+ onAgentStatus,
55
+ onThinkingTrace,
56
+ onExecutionTrace,
57
+ onContent,
58
+ onTrace,
59
+ onWorkspaceActivation,
60
+ onUICards,
61
+ onKnowledgeBaseState,
62
+ onFieldProgress,
63
+ onRequirementDraft,
64
+ onNextActions,
65
+ onSourcingCandidates,
66
+ onRiskSummary,
67
+ onShortlistUpdated,
68
+ onEvaluationReportReady,
69
+ onCategoryIdentified,
70
+ onFieldsUpdated,
71
+ onInstanceProfile,
72
+ onDoc,
73
+ onCapabilitySuggestion,
74
+ onError,
75
+ onComplete,
76
+ } = handlers;
77
+
78
+ const eventHandler = (event) => {
79
+ switch (event.type) {
80
+ case 'agent_status':
81
+ onAgentStatus?.(parseAgentStatus(event.data));
82
+ break;
83
+ case 'thinking_trace':
84
+ onThinkingTrace?.(parseThinkingTrace(event.data));
85
+ break;
86
+ case 'execution_trace':
87
+ case 'step_started':
88
+ case 'step_updated':
89
+ case 'step_completed':
90
+ case 'step_failed':
91
+ onExecutionTrace?.(parseExecutionTrace(event.data));
92
+ break;
93
+ case 'content': {
94
+ const { chunk } = parseContent(event.data);
95
+ accumulatedRef.current += chunk;
96
+ onContent?.(chunk);
97
+ break;
98
+ }
99
+ case 'trace':
100
+ onTrace?.(parseTrace(event.data));
101
+ break;
102
+ case 'workspace_activation':
103
+ onWorkspaceActivation?.(parseWorkspaceActivation(event.data));
104
+ break;
105
+ case 'ui_cards':
106
+ onUICards?.(parseUICards(event.data));
107
+ break;
108
+ case 'knowledge_base_state':
109
+ onKnowledgeBaseState?.(parseKnowledgeBaseState(event.data));
110
+ break;
111
+ case 'field_progress':
112
+ onFieldProgress?.(parseFieldProgress(event.data));
113
+ break;
114
+ case 'requirement_draft':
115
+ onRequirementDraft?.(parseRequirementDraft(event.data));
116
+ break;
117
+ case 'next_actions':
118
+ onNextActions?.(parseNextActions(event.data));
119
+ break;
120
+ case 'sourcing_candidates':
121
+ onSourcingCandidates?.(parseSourcingCandidates(event.data));
122
+ break;
123
+ case 'risk_summary':
124
+ onRiskSummary?.(parseRiskSummary(event.data));
125
+ break;
126
+ case 'shortlist_updated':
127
+ onShortlistUpdated?.(parseShortlistUpdated(event.data));
128
+ break;
129
+ case 'evaluation_report_ready':
130
+ onEvaluationReportReady?.(parseEvaluationReportReady(event.data));
131
+ break;
132
+ case 'category_identified':
133
+ onCategoryIdentified?.(parseCategoryIdentified(event.data));
134
+ break;
135
+ case 'fields_updated':
136
+ onFieldsUpdated?.(parseFieldsUpdated(event.data));
137
+ break;
138
+ case 'instance_profile':
139
+ onInstanceProfile?.(parseInstanceProfile(event.data));
140
+ break;
141
+ case 'doc':
142
+ onDoc?.(parseDoc(event.data));
143
+ break;
144
+ case 'capability_suggestion':
145
+ onCapabilitySuggestion?.(parseCapabilitySuggestion(event.data));
146
+ break;
147
+ case 'error':
148
+ onError?.(parseSSEError(event.data));
149
+ break;
150
+ default:
151
+ console.warn('[SSE] unknown event type:', event.type);
152
+ }
153
+ };
154
+
155
+ const completeHandler = () => {
156
+ setLoading(false);
157
+ onComplete?.(accumulatedRef.current);
158
+ accumulatedRef.current = '';
159
+ };
160
+
161
+ const transportErrorHandler = (errorMessage) => {
162
+ setLoading(false);
163
+ const errorType = classifyError(errorMessage);
164
+ setError({ message: errorMessage, type: errorType });
165
+ onError?.({ message: errorMessage });
166
+ accumulatedRef.current = '';
167
+ };
168
+
169
+ const requestOptions = {
170
+ sessionId: resolvedSessionId,
171
+ content,
172
+ enabledCapabilities: Array.isArray(options.enabledCapabilities) ? options.enabledCapabilities : [],
173
+ endpoint: options.endpoint,
174
+ baseUrl: options.baseUrl,
175
+ url: options.url,
176
+ };
177
+
178
+ if (!requestOptions.enabledCapabilities.length) {
179
+ delete requestOptions.enabledCapabilities;
180
+ }
181
+
182
+ if (!requestOptions.endpoint) delete requestOptions.endpoint;
183
+ if (!requestOptions.baseUrl) delete requestOptions.baseUrl;
184
+ if (!requestOptions.url) delete requestOptions.url;
185
+
186
+ await clientRef.current.sendMessage(
187
+ requestOptions,
188
+ eventHandler,
189
+ completeHandler,
190
+ transportErrorHandler
191
+ );
192
+ }, [sessionId]);
193
+
194
+ const retry = useCallback(async () => {
195
+ const { content, handlers, options } = lastMessageRef.current;
196
+ if (!content) return;
197
+ await send(content, handlers, options);
198
+ }, [send]);
199
+
200
+ const abort = useCallback(() => {
201
+ clientRef.current.abort();
202
+ setLoading(false);
203
+ accumulatedRef.current = '';
204
+ }, []);
205
+
206
+ return { send, loading, error, retry, abort };
207
+ }
208
+
209
+ export default useSSEStream;
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './chat-core/index.js';