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 +15 -0
- package/package.json +22 -0
- package/src/chat-core/index.js +6 -0
- package/src/chat-core/input/useChatInput.js +70 -0
- package/src/chat-core/messages/buildTimelineItems.js +74 -0
- package/src/chat-core/session/useChatSessionReducer.js +446 -0
- package/src/chat-core/stream/sse-client.js +123 -0
- package/src/chat-core/stream/sse-events.js +425 -0
- package/src/chat-core/stream/useSSEStream.js +209 -0
- package/src/index.js +1 -0
package/README.md
ADDED
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';
|