@lobehub/chat 0.157.0 → 0.157.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +30 -30
  3. package/src/app/api/chat/[provider]/route.test.ts +2 -2
  4. package/src/app/api/chat/[provider]/route.ts +1 -1
  5. package/src/app/api/chat/models/[provider]/route.ts +1 -1
  6. package/src/app/api/config.test.ts +1 -51
  7. package/src/app/api/openai/createBizOpenAI/auth.test.ts +52 -0
  8. package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
  9. package/src/app/api/plugin/gateway/route.ts +1 -1
  10. package/src/app/api/text-to-image/[provider]/route.ts +61 -0
  11. package/src/components/GalleyGrid/index.tsx +2 -2
  12. package/src/database/client/schemas/message.ts +2 -0
  13. package/src/features/Conversation/Actions/Assistant.tsx +3 -2
  14. package/src/features/Conversation/Actions/Tool.tsx +23 -11
  15. package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +9 -14
  16. package/src/features/Conversation/Messages/Assistant/index.tsx +7 -3
  17. package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +1 -1
  18. package/src/features/Conversation/Plugins/Render/index.tsx +11 -2
  19. package/src/hooks/useTokenCount.test.ts +38 -0
  20. package/src/hooks/useTokenCount.ts +1 -2
  21. package/src/libs/agent-runtime/AgentRuntime.ts +9 -1
  22. package/src/libs/agent-runtime/BaseAI.ts +3 -0
  23. package/src/libs/agent-runtime/types/index.ts +1 -0
  24. package/src/libs/agent-runtime/types/textToImage.ts +34 -0
  25. package/src/libs/agent-runtime/utils/createError.ts +1 -0
  26. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +51 -0
  27. package/src/locales/default/tool.ts +1 -0
  28. package/src/services/_url.ts +1 -1
  29. package/src/services/{imageGeneration.ts → textToImage.ts} +11 -2
  30. package/src/store/chat/initialState.ts +1 -1
  31. package/src/store/chat/selectors.ts +1 -1
  32. package/src/store/chat/slices/{tool → builtinTool}/action.test.ts +1 -1
  33. package/src/store/chat/slices/{tool → builtinTool}/action.ts +16 -4
  34. package/src/store/chat/slices/enchance/action.ts +10 -11
  35. package/src/store/chat/slices/message/action.ts +30 -92
  36. package/src/store/chat/slices/message/initialState.ts +5 -0
  37. package/src/store/chat/slices/message/selectors.ts +8 -0
  38. package/src/store/chat/slices/plugin/action.test.ts +1 -1
  39. package/src/store/chat/slices/plugin/action.ts +95 -80
  40. package/src/store/chat/store.ts +2 -2
  41. package/src/store/tool/slices/store/action.test.ts +6 -2
  42. package/src/store/tool/slices/store/action.ts +3 -1
  43. package/src/tools/dalle/Render/Item/Error.tsx +50 -0
  44. package/src/tools/dalle/Render/Item/Image.tsx +44 -0
  45. package/src/tools/dalle/Render/{Item.tsx → Item/index.tsx} +20 -29
  46. package/src/utils/fetch.test.ts +208 -3
  47. package/src/utils/fetch.ts +242 -19
  48. package/src/app/api/openai/images/createImageGeneration.ts +0 -26
  49. package/src/app/api/openai/images/route.ts +0 -16
  50. package/src/features/Conversation/Actions/Function.tsx +0 -17
  51. /package/src/app/api/{chat → middleware}/auth/index.test.ts +0 -0
  52. /package/src/app/api/{chat → middleware}/auth/index.ts +0 -0
  53. /package/src/app/api/{chat → middleware}/auth/utils.ts +0 -0
  54. /package/src/app/api/{auth.ts → openai/createBizOpenAI/auth.ts} +0 -0
  55. /package/src/store/chat/slices/{tool → builtinTool}/initialState.ts +0 -0
  56. /package/src/store/chat/slices/{tool → builtinTool}/selectors.ts +0 -0
  57. /package/src/tools/dalle/Render/{EditMode.tsx → Item/EditMode.tsx} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { fetchEventSource } from '@microsoft/fetch-event-source';
2
2
  import { FetchEventSourceInit } from '@microsoft/fetch-event-source';
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { ZodError } from 'zod';
4
5
 
5
6
  import { ErrorResponse } from '@/types/fetch';
6
7
 
@@ -82,6 +83,16 @@ describe('getMessageError', () => {
82
83
  });
83
84
  expect(mockResponse.json).toHaveBeenCalled();
84
85
  });
86
+
87
+ it('should handle timeout error correctly', async () => {
88
+ const mockResponse = createMockResponse(undefined, false, 504);
89
+ const error = await getMessageError(mockResponse as any);
90
+
91
+ expect(error).toEqual({
92
+ message: 'translated_response.504',
93
+ type: 504,
94
+ });
95
+ });
85
96
  });
86
97
 
87
98
  describe('parseToolCalls', () => {
@@ -173,6 +184,34 @@ describe('parseToolCalls', () => {
173
184
  },
174
185
  ]);
175
186
  });
187
+
188
+ it('should throw error if incomplete tool calls data', () => {
189
+ const origin = [
190
+ {
191
+ id: '1',
192
+ type: 'function',
193
+ function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
194
+ },
195
+ ];
196
+
197
+ const chunk = [{ index: 1, id: '2', type: 'function' }];
198
+
199
+ try {
200
+ parseToolCalls(origin, chunk as any);
201
+ } catch (e) {
202
+ expect(e).toEqual(
203
+ new ZodError([
204
+ {
205
+ code: 'invalid_type',
206
+ expected: 'object',
207
+ received: 'undefined',
208
+ path: ['function'],
209
+ message: 'Required',
210
+ },
211
+ ]),
212
+ );
213
+ }
214
+ });
176
215
  });
177
216
 
178
217
  describe('fetchSSE', () => {
@@ -188,7 +227,11 @@ describe('fetchSSE', () => {
188
227
  },
189
228
  );
190
229
 
191
- await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
230
+ await fetchSSE('/', {
231
+ onMessageHandle: mockOnMessageHandle,
232
+ onFinish: mockOnFinish,
233
+ smoothing: false,
234
+ });
192
235
 
193
236
  expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
194
237
  expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
@@ -222,7 +265,11 @@ describe('fetchSSE', () => {
222
265
  },
223
266
  );
224
267
 
225
- await fetchSSE('/', { onMessageHandle: mockOnMessageHandle, onFinish: mockOnFinish });
268
+ await fetchSSE('/', {
269
+ onMessageHandle: mockOnMessageHandle,
270
+ onFinish: mockOnFinish,
271
+ smoothing: false,
272
+ });
226
273
 
227
274
  expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
228
275
  tool_calls: [{ id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } }],
@@ -256,7 +303,7 @@ describe('fetchSSE', () => {
256
303
  },
257
304
  );
258
305
 
259
- await fetchSSE('/', { onAbort: mockOnAbort });
306
+ await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
260
307
 
261
308
  expect(mockOnAbort).toHaveBeenCalledWith('Hello');
262
309
  });
@@ -320,4 +367,162 @@ describe('fetchSSE', () => {
320
367
  type: 'done',
321
368
  });
322
369
  });
370
+
371
+ it('should handle text event with smoothing correctly', async () => {
372
+ const mockOnMessageHandle = vi.fn();
373
+ const mockOnFinish = vi.fn();
374
+
375
+ (fetchEventSource as any).mockImplementationOnce(
376
+ (url: string, options: FetchEventSourceInit) => {
377
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
378
+ options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
379
+ options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
380
+ },
381
+ );
382
+
383
+ await fetchSSE('/', {
384
+ onMessageHandle: mockOnMessageHandle,
385
+ onFinish: mockOnFinish,
386
+ });
387
+
388
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
389
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
390
+ // more assertions for each character...
391
+ expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
392
+ observationId: null,
393
+ toolCalls: undefined,
394
+ traceId: null,
395
+ type: 'done',
396
+ });
397
+ });
398
+
399
+ it('should handle tool_calls event with smoothing correctly', async () => {
400
+ const mockOnMessageHandle = vi.fn();
401
+ const mockOnFinish = vi.fn();
402
+
403
+ (fetchEventSource as any).mockImplementationOnce(
404
+ (url: string, options: FetchEventSourceInit) => {
405
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
406
+ options.onmessage!({
407
+ event: 'tool_calls',
408
+ data: JSON.stringify([
409
+ { index: 0, id: '1', type: 'function', function: { name: 'func1', arguments: 'a' } },
410
+ ]),
411
+ } as any);
412
+ options.onmessage!({
413
+ event: 'tool_calls',
414
+ data: JSON.stringify([
415
+ { index: 0, function: { arguments: 'rg1' } },
416
+ { index: 1, id: '2', type: 'function', function: { name: 'func2', arguments: 'a' } },
417
+ ]),
418
+ } as any);
419
+ options.onmessage!({
420
+ event: 'tool_calls',
421
+ data: JSON.stringify([{ index: 1, function: { arguments: 'rg2' } }]),
422
+ } as any);
423
+ },
424
+ );
425
+
426
+ await fetchSSE('/', {
427
+ onMessageHandle: mockOnMessageHandle,
428
+ onFinish: mockOnFinish,
429
+ });
430
+
431
+ // TODO: need to check whether the `aarg1` is correct
432
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
433
+ isAnimationActives: [true],
434
+ tool_calls: [
435
+ { id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
436
+ { function: { arguments: 'aarg2', name: 'func2' }, id: '2', type: 'function' },
437
+ ],
438
+ type: 'tool_calls',
439
+ });
440
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
441
+ isAnimationActives: [true, true],
442
+ tool_calls: [
443
+ { id: '1', type: 'function', function: { name: 'func1', arguments: 'aarg1' } },
444
+ { id: '2', type: 'function', function: { name: 'func2', arguments: 'aarg2' } },
445
+ ],
446
+ type: 'tool_calls',
447
+ });
448
+
449
+ // more assertions for each character...
450
+ expect(mockOnFinish).toHaveBeenCalledWith('', {
451
+ observationId: null,
452
+ toolCalls: [
453
+ { id: '1', type: 'function', function: { name: 'func1', arguments: 'arg1' } },
454
+ { id: '2', type: 'function', function: { name: 'func2', arguments: 'arg2' } },
455
+ ],
456
+ traceId: null,
457
+ type: 'done',
458
+ });
459
+ });
460
+
461
+ it('should handle request interruption and resumption correctly', async () => {
462
+ const mockOnMessageHandle = vi.fn();
463
+ const mockOnFinish = vi.fn();
464
+ const abortController = new AbortController();
465
+
466
+ (fetchEventSource as any).mockImplementationOnce(
467
+ (url: string, options: FetchEventSourceInit) => {
468
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
469
+ options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
470
+ abortController.abort();
471
+ options.onmessage!({ event: 'text', data: JSON.stringify(' World') } as any);
472
+ },
473
+ );
474
+
475
+ await fetchSSE('/', {
476
+ onMessageHandle: mockOnMessageHandle,
477
+ onFinish: mockOnFinish,
478
+ signal: abortController.signal,
479
+ });
480
+
481
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'He', type: 'text' });
482
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'llo World', type: 'text' });
483
+
484
+ expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
485
+ type: 'done',
486
+ observationId: null,
487
+ traceId: null,
488
+ });
489
+ });
490
+
491
+ it('should call onFinish with correct parameters for different finish types', async () => {
492
+ const mockOnFinish = vi.fn();
493
+
494
+ (fetchEventSource as any).mockImplementationOnce(
495
+ (url: string, options: FetchEventSourceInit) => {
496
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
497
+ options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
498
+ options.onerror!({ name: 'AbortError' });
499
+ },
500
+ );
501
+
502
+ await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
503
+
504
+ expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
505
+ observationId: null,
506
+ toolCalls: undefined,
507
+ traceId: null,
508
+ type: 'abort',
509
+ });
510
+
511
+ (fetchEventSource as any).mockImplementationOnce(
512
+ (url: string, options: FetchEventSourceInit) => {
513
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
514
+ options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
515
+ options.onerror!(new Error('Unknown error'));
516
+ },
517
+ );
518
+
519
+ await fetchSSE('/', { onFinish: mockOnFinish, smoothing: false });
520
+
521
+ expect(mockOnFinish).toHaveBeenCalledWith('Hello', {
522
+ observationId: null,
523
+ toolCalls: undefined,
524
+ traceId: null,
525
+ type: 'error',
526
+ });
527
+ });
323
528
  });
@@ -51,6 +51,7 @@ export interface MessageTextChunk {
51
51
  }
52
52
 
53
53
  interface MessageToolCallsChunk {
54
+ isAnimationActives?: boolean[];
54
55
  tool_calls: MessageToolCall[];
55
56
  type: 'tool_calls';
56
57
  }
@@ -61,24 +62,201 @@ export interface FetchSSEOptions {
61
62
  onErrorHandle?: (error: ChatMessageError) => void;
62
63
  onFinish?: OnFinishHandler;
63
64
  onMessageHandle?: (chunk: MessageTextChunk | MessageToolCallsChunk) => void;
65
+ smoothing?: boolean;
64
66
  }
65
67
 
66
68
  export const parseToolCalls = (origin: MessageToolCall[], value: MessageToolCallChunk[]) =>
67
69
  produce(origin, (draft) => {
70
+ // if there is no origin, we should parse all the value and set it to draft
68
71
  if (draft.length === 0) {
69
72
  draft.push(...value.map((item) => MessageToolCallSchema.parse(item)));
70
- } else {
71
- value.forEach(({ index, ...item }) => {
72
- if (!draft?.[index]) {
73
- draft?.splice(index, 0, MessageToolCallSchema.parse(item));
73
+ return;
74
+ }
75
+
76
+ // if there is origin, we should merge the value to the origin
77
+ value.forEach(({ index, ...item }) => {
78
+ if (!draft?.[index]) {
79
+ // if not, we should insert it to the draft
80
+ draft?.splice(index, 0, MessageToolCallSchema.parse(item));
81
+ } else {
82
+ // if it is already in the draft, we should merge the arguments to the draft
83
+ if (item.function?.arguments) {
84
+ draft[index].function.arguments += item.function.arguments;
85
+ }
86
+ }
87
+ });
88
+ });
89
+
90
+ const createSmoothMessage = (params: { onTextUpdate: (delta: string, text: string) => void }) => {
91
+ let buffer = '';
92
+ // why use queue: https://shareg.pt/GLBrjpK
93
+ let outputQueue: string[] = [];
94
+
95
+ // eslint-disable-next-line no-undef
96
+ let animationTimeoutId: NodeJS.Timeout | null = null;
97
+ let isAnimationActive = false;
98
+
99
+ // when you need to stop the animation, call this function
100
+ const stopAnimation = () => {
101
+ isAnimationActive = false;
102
+ if (animationTimeoutId !== null) {
103
+ clearTimeout(animationTimeoutId);
104
+ animationTimeoutId = null;
105
+ }
106
+ };
107
+
108
+ // define startAnimation function to display the text in buffer smooth
109
+ // when you need to start the animation, call this function
110
+ const startAnimation = (speed = 2) =>
111
+ new Promise<void>((resolve) => {
112
+ if (isAnimationActive) {
113
+ resolve();
114
+ return;
115
+ }
116
+
117
+ isAnimationActive = true;
118
+
119
+ const updateText = () => {
120
+ // 如果动画已经不再激活,则停止更新文本
121
+ if (!isAnimationActive) {
122
+ clearTimeout(animationTimeoutId!);
123
+ animationTimeoutId = null;
124
+ resolve();
125
+ }
126
+
127
+ // 如果还有文本没有显示
128
+ // 检查队列中是否有字符待显示
129
+ if (outputQueue.length > 0) {
130
+ // 从队列中获取前两个字符(如果存在)
131
+ const charsToAdd = outputQueue.splice(0, speed).join('');
132
+ buffer += charsToAdd;
133
+
134
+ // 更新消息内容,这里可能需要结合实际情况调整
135
+ params.onTextUpdate(charsToAdd, buffer);
136
+
137
+ // 设置下一个字符的延迟
138
+ animationTimeoutId = setTimeout(updateText, 16); // 16 毫秒的延迟模拟打字机效果
74
139
  } else {
75
- if (item.function?.arguments) {
76
- draft[index].function.arguments += item.function.arguments;
77
- }
140
+ // 当所有字符都显示完毕时,清除动画状态
141
+ isAnimationActive = false;
142
+ animationTimeoutId = null;
143
+ resolve();
78
144
  }
79
- });
145
+ };
146
+
147
+ updateText();
148
+ });
149
+
150
+ const pushToQueue = (text: string) => {
151
+ outputQueue.push(...text.split(''));
152
+ };
153
+
154
+ return {
155
+ isAnimationActive,
156
+ isTokenRemain: () => outputQueue.length > 0,
157
+ pushToQueue,
158
+ startAnimation,
159
+ stopAnimation,
160
+ };
161
+ };
162
+
163
+ const createSmoothToolCalls = (params: {
164
+ onToolCallsUpdate: (toolCalls: MessageToolCall[], isAnimationActives: boolean[]) => void;
165
+ }) => {
166
+ let toolCallsBuffer: MessageToolCall[] = [];
167
+
168
+ // 为每个 tool_call 维护一个输出队列和动画控制器
169
+
170
+ // eslint-disable-next-line no-undef
171
+ const animationTimeoutIds: (NodeJS.Timeout | null)[] = [];
172
+ const outputQueues: string[][] = [];
173
+ const isAnimationActives: boolean[] = [];
174
+
175
+ const stopAnimation = (index: number) => {
176
+ isAnimationActives[index] = false;
177
+ if (animationTimeoutIds[index] !== null) {
178
+ clearTimeout(animationTimeoutIds[index]!);
179
+ animationTimeoutIds[index] = null;
80
180
  }
81
- });
181
+ };
182
+
183
+ const startAnimation = (index: number, speed = 2) =>
184
+ new Promise<void>((resolve) => {
185
+ if (isAnimationActives[index]) {
186
+ resolve();
187
+ return;
188
+ }
189
+
190
+ isAnimationActives[index] = true;
191
+
192
+ const updateToolCall = () => {
193
+ if (!isAnimationActives[index]) {
194
+ resolve();
195
+ }
196
+
197
+ if (outputQueues[index].length > 0) {
198
+ const charsToAdd = outputQueues[index].splice(0, speed).join('');
199
+
200
+ const toolCallToUpdate = toolCallsBuffer[index];
201
+
202
+ if (toolCallToUpdate) {
203
+ toolCallToUpdate.function.arguments += charsToAdd;
204
+
205
+ // 触发 ui 更新
206
+ params.onToolCallsUpdate(toolCallsBuffer, [...isAnimationActives]);
207
+ }
208
+
209
+ animationTimeoutIds[index] = setTimeout(updateToolCall, 16);
210
+ } else {
211
+ isAnimationActives[index] = false;
212
+ animationTimeoutIds[index] = null;
213
+ resolve();
214
+ }
215
+ };
216
+
217
+ updateToolCall();
218
+ });
219
+
220
+ const pushToQueue = (toolCallChunks: MessageToolCallChunk[]) => {
221
+ toolCallChunks.forEach((chunk) => {
222
+ // init the tool call buffer and output queue
223
+ if (!toolCallsBuffer[chunk.index]) {
224
+ toolCallsBuffer[chunk.index] = MessageToolCallSchema.parse(chunk);
225
+ }
226
+
227
+ if (!outputQueues[chunk.index]) {
228
+ outputQueues[chunk.index] = [];
229
+ isAnimationActives[chunk.index] = false;
230
+ animationTimeoutIds[chunk.index] = null;
231
+ }
232
+
233
+ outputQueues[chunk.index].push(...(chunk.function?.arguments || '').split(''));
234
+ });
235
+ };
236
+
237
+ const startAnimations = async (speed = 2) => {
238
+ const pools = toolCallsBuffer.map(async (_, index) => {
239
+ if (outputQueues[index].length > 0 && !isAnimationActives[index]) {
240
+ await startAnimation(index, speed);
241
+ }
242
+ });
243
+
244
+ await Promise.all(pools);
245
+ };
246
+ const stopAnimations = () => {
247
+ toolCallsBuffer.forEach((_, index) => {
248
+ stopAnimation(index);
249
+ });
250
+ };
251
+
252
+ return {
253
+ isAnimationActives,
254
+ isTokenRemain: () => outputQueues.some((token) => token.length > 0),
255
+ pushToQueue,
256
+ startAnimations,
257
+ stopAnimations,
258
+ };
259
+ };
82
260
 
83
261
  /**
84
262
  * Fetch data using stream method
@@ -92,6 +270,21 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
92
270
  let finishedType: SSEFinishType = 'done';
93
271
  let response!: Response;
94
272
 
273
+ const { smoothing = true } = options;
274
+
275
+ const textController = createSmoothMessage({
276
+ onTextUpdate: (delta, text) => {
277
+ output = text;
278
+ options.onMessageHandle?.({ text: delta, type: 'text' });
279
+ },
280
+ });
281
+
282
+ const toolCallsController = createSmoothToolCalls({
283
+ onToolCallsUpdate: (toolCalls, isAnimationActives) => {
284
+ options.onMessageHandle?.({ isAnimationActives, tool_calls: toolCalls, type: 'tool_calls' });
285
+ },
286
+ });
287
+
95
288
  try {
96
289
  await fetchEventSource(url, {
97
290
  body: options.body,
@@ -102,6 +295,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
102
295
  if ((error as TypeError).name === 'AbortError') {
103
296
  finishedType = 'abort';
104
297
  options?.onAbort?.(output);
298
+ textController.stopAnimation();
105
299
  } else {
106
300
  finishedType = 'error';
107
301
  console.error(error);
@@ -122,22 +316,39 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
122
316
 
123
317
  switch (ev.event) {
124
318
  case 'text': {
125
- output += data;
126
- options.onMessageHandle?.({ text: data, type: 'text' });
319
+ if (smoothing) {
320
+ textController.pushToQueue(data);
321
+
322
+ if (!textController.isAnimationActive) textController.startAnimation();
323
+ } else {
324
+ output += data;
325
+ options.onMessageHandle?.({ text: data, type: 'text' });
326
+ }
327
+
127
328
  break;
128
329
  }
129
330
 
130
331
  case 'tool_calls': {
131
- if (!toolCalls) {
132
- toolCalls = [];
133
- }
134
-
332
+ // get finial
333
+ // if there is no tool calls, we should initialize the tool calls
334
+ if (!toolCalls) toolCalls = [];
135
335
  toolCalls = parseToolCalls(toolCalls, data);
136
336
 
137
- options.onMessageHandle?.({
138
- tool_calls: toolCalls,
139
- type: 'tool_calls',
140
- });
337
+ if (smoothing) {
338
+ // make the tool calls smooth
339
+
340
+ // push the tool calls to the smooth queue
341
+ toolCallsController.pushToQueue(data);
342
+ // if there is no animation active, we should start the animation
343
+ if (toolCallsController.isAnimationActives.some((value) => !value)) {
344
+ toolCallsController.startAnimations();
345
+ }
346
+ } else {
347
+ options.onMessageHandle?.({
348
+ tool_calls: toolCalls,
349
+ type: 'tool_calls',
350
+ });
351
+ }
141
352
  }
142
353
  }
143
354
  },
@@ -160,6 +371,9 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
160
371
  // only call onFinish when response is available
161
372
  // so like abort, we don't need to call onFinish
162
373
  if (response) {
374
+ textController.stopAnimation();
375
+ toolCallsController.stopAnimations();
376
+
163
377
  // if there is no onMessageHandler, we should call onHandleMessage first
164
378
  if (!triggerOnMessageHandler) {
165
379
  output = await response.clone().text();
@@ -168,6 +382,15 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
168
382
 
169
383
  const traceId = response.headers.get(LOBE_CHAT_TRACE_ID);
170
384
  const observationId = response.headers.get(LOBE_CHAT_OBSERVATION_ID);
385
+
386
+ if (textController.isTokenRemain()) {
387
+ await textController.startAnimation(15);
388
+ }
389
+
390
+ if (toolCallsController.isTokenRemain()) {
391
+ await toolCallsController.startAnimations(15);
392
+ }
393
+
171
394
  await options?.onFinish?.(output, { observationId, toolCalls, traceId, type: finishedType });
172
395
  }
173
396
 
@@ -1,26 +0,0 @@
1
- import OpenAI from 'openai';
2
-
3
- import { OpenAIImagePayload } from '@/types/openai/image';
4
-
5
- export const createImageGeneration = async ({
6
- openai,
7
- payload,
8
- }: {
9
- openai: OpenAI;
10
- payload: OpenAIImagePayload;
11
- }) => {
12
- const res = await openai.images.generate({ ...payload, response_format: 'url' });
13
-
14
- const urls = res.data.map((o) => o.url) as string[];
15
-
16
- return new Response(JSON.stringify(urls));
17
- };
18
-
19
- // const mockImages = [
20
- // 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292159272-032d5c8b-20be-48d9-8dbb-f2491f231bac.png',
21
- // 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292159798-cad89421-20c5-44b0-a337-fcbb857f1f70.png',
22
- // 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292160015-2263156f-d41f-48ae-9c2c-d96799b9d2b8.png',
23
- // 'https://github-production-user-asset-6210df.s3.amazonaws.com/28616219/292160229-592d112f-5dfd-47d7-98d3-44bc09dc91f7.png',
24
- // ];
25
- // export const createImageGeneration = async () =>
26
- // new Response(JSON.stringify([mockImages[Math.round(Math.random() * 3)]]));
@@ -1,16 +0,0 @@
1
- import { OpenAIImagePayload } from '@/types/openai/image';
2
-
3
- import { createBizOpenAI } from '../createBizOpenAI';
4
- import { createImageGeneration } from './createImageGeneration';
5
-
6
- export const runtime = 'edge';
7
-
8
- export const POST = async (req: Request) => {
9
- const payload = (await req.json()) as OpenAIImagePayload;
10
-
11
- const openaiOrErrResponse = createBizOpenAI(req);
12
- // if resOrOpenAI is a Response, it means there is an error,just return it
13
- if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;
14
-
15
- return createImageGeneration({ openai: openaiOrErrResponse, payload });
16
- };
@@ -1,17 +0,0 @@
1
- import { ActionIconGroup } from '@lobehub/ui';
2
- import { memo } from 'react';
3
-
4
- import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
5
- import { RenderAction } from '../types';
6
-
7
- export const FunctionActionsBar: RenderAction = memo(({ onActionClick }) => {
8
- const { regenerate, delAndRegenerate, del } = useChatListActionsBar();
9
- return (
10
- <ActionIconGroup
11
- dropdownMenu={[regenerate, delAndRegenerate, del]}
12
- items={[regenerate]}
13
- onActionClick={onActionClick}
14
- type="ghost"
15
- />
16
- );
17
- });
File without changes
File without changes