@lobehub/chat 1.85.8 → 1.85.10
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/CHANGELOG.md +58 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/apps/desktop/build/nsis-header.bmp +0 -0
- package/apps/desktop/build/nsis-sidebar.bmp +0 -0
- package/apps/desktop/electron-builder.js +3 -0
- package/apps/desktop/src/main/core/App.ts +3 -0
- package/apps/desktop/src/main/core/Browser.ts +65 -11
- package/changelog/v1.json +21 -0
- package/docker-compose/local/init_data.json +2 -2
- package/package.json +2 -2
- package/src/components/Thinking/index.tsx +38 -22
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -1
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +3 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +2 -1
- package/src/features/ElectronTitlebar/WinControl/index.tsx +9 -5
- package/src/features/ElectronTitlebar/const.ts +1 -1
- package/src/features/Portal/Artifacts/Body/index.tsx +4 -1
- package/src/features/Portal/Artifacts/Header.tsx +2 -6
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/features/Portal/Plugins/Header.tsx +2 -2
- package/src/features/Portal/components/Header.tsx +1 -0
- package/src/features/User/UserPanel/LangButton.tsx +3 -8
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/features/User/UserPanel/ThemeButton.tsx +1 -7
- package/src/middleware.ts +20 -9
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +14 -2
- package/src/store/chat/slices/message/selectors.ts +5 -0
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +5 -18
- package/src/utils/fetch/fetchSSE.ts +62 -3
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,64 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.85.10](https://github.com/lobehub/lobe-chat/compare/v1.85.9...v1.85.10)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-14**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Update electron style on windows.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Update electron style on windows, closes [#7839](https://github.com/lobehub/lobe-chat/issues/7839) ([474de56](https://github.com/lobehub/lobe-chat/commit/474de56))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.85.9](https://github.com/lobehub/lobe-chat/compare/v1.85.8...v1.85.9)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-14**</sup>
|
33
|
+
|
34
|
+
#### 🐛 Bug Fixes
|
35
|
+
|
36
|
+
- **misc**: Redirect unauthorized next-auth user to signin page.
|
37
|
+
|
38
|
+
#### 💄 Styles
|
39
|
+
|
40
|
+
- **misc**: Improve smoothing on completion.
|
41
|
+
|
42
|
+
<br/>
|
43
|
+
|
44
|
+
<details>
|
45
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
46
|
+
|
47
|
+
#### What's fixed
|
48
|
+
|
49
|
+
- **misc**: Redirect unauthorized next-auth user to signin page, closes [#7813](https://github.com/lobehub/lobe-chat/issues/7813) ([6160784](https://github.com/lobehub/lobe-chat/commit/6160784))
|
50
|
+
|
51
|
+
#### Styles
|
52
|
+
|
53
|
+
- **misc**: Improve smoothing on completion, closes [#7833](https://github.com/lobehub/lobe-chat/issues/7833) ([6434686](https://github.com/lobehub/lobe-chat/commit/6434686))
|
54
|
+
|
55
|
+
</details>
|
56
|
+
|
57
|
+
<div align="right">
|
58
|
+
|
59
|
+
[](#readme-top)
|
60
|
+
|
61
|
+
</div>
|
62
|
+
|
5
63
|
### [Version 1.85.8](https://github.com/lobehub/lobe-chat/compare/v1.85.7...v1.85.8)
|
6
64
|
|
7
65
|
<sup>Released on **2025-05-11**</sup>
|
package/README.md
CHANGED
@@ -333,7 +333,7 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
|
333
333
|
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
|
334
334
|
| [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
|
335
335
|
|
336
|
-
> 📊 Total plugins: [<kbd>**
|
336
|
+
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
|
337
337
|
|
338
338
|
<!-- PLUGIN LIST -->
|
339
339
|
|
package/README.zh-CN.md
CHANGED
@@ -326,7 +326,7 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
326
326
|
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
|
327
327
|
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
|
328
328
|
|
329
|
-
> 📊 Total plugins: [<kbd>**
|
329
|
+
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
|
330
330
|
|
331
331
|
<!-- PLUGIN LIST -->
|
332
332
|
|
Binary file
|
Binary file
|
@@ -79,9 +79,12 @@ const config = {
|
|
79
79
|
allowToChangeInstallationDirectory: true,
|
80
80
|
artifactName: '${productName}-${version}-setup.${ext}',
|
81
81
|
createDesktopShortcut: 'always',
|
82
|
+
installerHeader: './build/nsis-header.bmp',
|
83
|
+
installerSidebar: './build/nsis-sidebar.bmp',
|
82
84
|
oneClick: false,
|
83
85
|
shortcutName: '${productName}',
|
84
86
|
uninstallDisplayName: '${productName}',
|
87
|
+
uninstallerSidebar: './build/nsis-sidebar.bmp',
|
85
88
|
},
|
86
89
|
publish: [
|
87
90
|
{
|
@@ -41,6 +41,7 @@ export class App {
|
|
41
41
|
updaterManager: UpdaterManager;
|
42
42
|
shortcutManager: ShortcutManager;
|
43
43
|
trayManager: TrayManager;
|
44
|
+
chromeFlags: string[] = ['OverlayScrollbar', 'FluentOverlayScrollbar', 'FluentScrollbar'];
|
44
45
|
|
45
46
|
/**
|
46
47
|
* whether app is in quiting
|
@@ -185,6 +186,8 @@ export class App {
|
|
185
186
|
}
|
186
187
|
});
|
187
188
|
|
189
|
+
app.commandLine.appendSwitch('enable-features', this.chromeFlags.join(','));
|
190
|
+
|
188
191
|
logger.debug('Waiting for app to be ready');
|
189
192
|
await app.whenReady();
|
190
193
|
logger.debug('Application ready');
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
2
|
-
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron';
|
2
|
+
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, nativeTheme } from 'electron';
|
3
|
+
import os from 'node:os';
|
3
4
|
import { join } from 'node:path';
|
4
5
|
|
5
6
|
import { createLogger } from '@/utils/logger';
|
@@ -188,17 +189,32 @@ export default class Browser {
|
|
188
189
|
`[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
|
189
190
|
);
|
190
191
|
|
192
|
+
const { isWindows11, isWindows } = this.getWindowsVersion();
|
193
|
+
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
194
|
+
|
191
195
|
const browserWindow = new BrowserWindow({
|
192
196
|
...res,
|
193
|
-
|
197
|
+
...(isWindows
|
198
|
+
? {
|
199
|
+
titleBarStyle: 'hidden',
|
200
|
+
}
|
201
|
+
: {}),
|
202
|
+
...(isWindows11
|
203
|
+
? {
|
204
|
+
backgroundMaterial: isDarkMode ? 'mica' : 'acrylic',
|
205
|
+
vibrancy: 'under-window',
|
206
|
+
visualEffectState: 'active',
|
207
|
+
}
|
208
|
+
: {}),
|
209
|
+
autoHideMenuBar: true,
|
210
|
+
backgroundColor: '#00000000',
|
211
|
+
frame: false,
|
194
212
|
|
213
|
+
height: savedState?.height || height,
|
195
214
|
// Always create hidden first
|
196
215
|
show: false,
|
197
|
-
|
198
216
|
title,
|
199
217
|
|
200
|
-
transparent: true,
|
201
|
-
|
202
218
|
webPreferences: {
|
203
219
|
// Context isolation environment
|
204
220
|
// https://www.electronjs.org/docs/tutorial/context-isolation
|
@@ -211,17 +227,13 @@ export default class Browser {
|
|
211
227
|
this._browserWindow = browserWindow;
|
212
228
|
logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
|
213
229
|
|
230
|
+
if (isWindows11) this.applyVisualEffects();
|
231
|
+
|
214
232
|
logger.debug(`[${this.identifier}] Setting up nextInterceptor.`);
|
215
233
|
this.stopInterceptHandler = this.app.nextInterceptor({
|
216
234
|
session: browserWindow.webContents.session,
|
217
235
|
});
|
218
236
|
|
219
|
-
// Windows 11 can use this new API
|
220
|
-
if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
|
221
|
-
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
|
222
|
-
browserWindow.setBackgroundMaterial('acrylic');
|
223
|
-
}
|
224
|
-
|
225
237
|
logger.debug(`[${this.identifier}] Initiating placeholder and URL loading sequence.`);
|
226
238
|
this.loadPlaceholder().then(() => {
|
227
239
|
this.loadUrl(path).catch((e) => {
|
@@ -334,6 +346,16 @@ export default class Browser {
|
|
334
346
|
this._browserWindow.webContents.send(channel, data);
|
335
347
|
};
|
336
348
|
|
349
|
+
applyVisualEffects() {
|
350
|
+
// Windows 11 can use this new API
|
351
|
+
if (this._browserWindow) {
|
352
|
+
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
|
353
|
+
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
354
|
+
this._browserWindow?.setBackgroundMaterial(isDarkMode ? 'mica' : 'acrylic');
|
355
|
+
this._browserWindow?.setVibrancy('under-window');
|
356
|
+
}
|
357
|
+
}
|
358
|
+
|
337
359
|
toggleVisible() {
|
338
360
|
logger.debug(`Toggling visibility for window: ${this.identifier}`);
|
339
361
|
if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
|
@@ -343,4 +365,36 @@ export default class Browser {
|
|
343
365
|
this._browserWindow.focus();
|
344
366
|
}
|
345
367
|
}
|
368
|
+
|
369
|
+
getWindowsVersion() {
|
370
|
+
if (process.platform !== 'win32') {
|
371
|
+
return {
|
372
|
+
isWindows: false,
|
373
|
+
isWindows10: false,
|
374
|
+
isWindows11: false,
|
375
|
+
version: null,
|
376
|
+
};
|
377
|
+
}
|
378
|
+
|
379
|
+
// 获取操作系统版本(如 "10.0.22621")
|
380
|
+
const release = os.release();
|
381
|
+
const parts = release.split('.');
|
382
|
+
|
383
|
+
// 主版本和次版本
|
384
|
+
const majorVersion = parseInt(parts[0], 10);
|
385
|
+
const minorVersion = parseInt(parts[1], 10);
|
386
|
+
|
387
|
+
// 构建号是第三部分
|
388
|
+
const buildNumber = parseInt(parts[2], 10);
|
389
|
+
|
390
|
+
// Windows 11 的构建号从 22000 开始
|
391
|
+
const isWindows11 = majorVersion === 10 && minorVersion === 0 && buildNumber >= 22_000;
|
392
|
+
|
393
|
+
return {
|
394
|
+
buildNumber,
|
395
|
+
isWindows: true,
|
396
|
+
isWindows11,
|
397
|
+
version: release,
|
398
|
+
};
|
399
|
+
}
|
346
400
|
}
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,25 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Update electron style on windows."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-14",
|
9
|
+
"version": "1.85.10"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"fixes": [
|
14
|
+
"Redirect unauthorized next-auth user to signin page."
|
15
|
+
],
|
16
|
+
"improvements": [
|
17
|
+
"Improve smoothing on completion."
|
18
|
+
]
|
19
|
+
},
|
20
|
+
"date": "2025-05-14",
|
21
|
+
"version": "1.85.9"
|
22
|
+
},
|
2
23
|
{
|
3
24
|
"children": {
|
4
25
|
"fixes": [
|
@@ -42,7 +42,7 @@
|
|
42
42
|
"cert": "cert-built-in",
|
43
43
|
"headerHtml": "",
|
44
44
|
"enablePassword": true,
|
45
|
-
"enableSignUp":
|
45
|
+
"enableSignUp": false,
|
46
46
|
"enableSigninSession": false,
|
47
47
|
"enableAutoSignin": false,
|
48
48
|
"enableCodeSignin": false,
|
@@ -1235,4 +1235,4 @@
|
|
1235
1235
|
}
|
1236
1236
|
],
|
1237
1237
|
"webhooks": []
|
1238
|
-
}
|
1238
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.85.
|
3
|
+
"version": "1.85.10",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -150,7 +150,7 @@
|
|
150
150
|
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
151
151
|
"@lobehub/icons": "^2.0.0",
|
152
152
|
"@lobehub/tts": "^2.0.0",
|
153
|
-
"@lobehub/ui": "^2.0.
|
153
|
+
"@lobehub/ui": "^2.0.13",
|
154
154
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
155
155
|
"@neondatabase/serverless": "^1.0.0",
|
156
156
|
"@next/third-parties": "^15.3.0",
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { CopyButton, Icon, Markdown } from '@lobehub/ui';
|
1
|
+
import { ActionIcon, CopyButton, Icon, Markdown } from '@lobehub/ui';
|
2
2
|
import { createStyles } from 'antd-style';
|
3
3
|
import { AnimatePresence, motion } from 'framer-motion';
|
4
4
|
import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
@@ -9,21 +9,32 @@ import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
10
10
|
import { CitationItem } from '@/types/message';
|
11
11
|
|
12
|
-
const useStyles = createStyles(({ css, token
|
12
|
+
const useStyles = createStyles(({ css, token }) => ({
|
13
13
|
container: css`
|
14
|
-
|
15
|
-
|
16
|
-
padding-inline: 8px;
|
17
|
-
border-radius: 6px;
|
18
|
-
|
14
|
+
overflow: hidden;
|
15
|
+
border-radius: ${token.borderRadius}px;
|
19
16
|
color: ${token.colorTextTertiary};
|
17
|
+
transition: all 0.2s ${token.motionEaseOut};
|
18
|
+
`,
|
19
|
+
expand: css`
|
20
|
+
color: ${token.colorTextSecondary};
|
21
|
+
background: ${token.colorFillTertiary};
|
22
|
+
`,
|
23
|
+
|
24
|
+
header: css`
|
25
|
+
padding-block: 4px;
|
26
|
+
padding-inline: 8px 4px;
|
27
|
+
transition: background 0.2s ${token.motionEaseOut};
|
28
|
+
transition: all 0.2s ${token.motionEaseOut};
|
20
29
|
|
21
30
|
&:hover {
|
22
|
-
background: ${
|
31
|
+
background: ${token.colorFillQuaternary};
|
23
32
|
}
|
24
33
|
`,
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
headerExpand: css`
|
36
|
+
color: ${token.colorTextSecondary};
|
37
|
+
background: ${token.colorFillQuaternary};
|
27
38
|
`,
|
28
39
|
shinyText: css`
|
29
40
|
color: ${rgba(token.colorText, 0.45)};
|
@@ -70,7 +81,7 @@ interface ThinkingProps {
|
|
70
81
|
|
71
82
|
const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, citations }) => {
|
72
83
|
const { t } = useTranslation(['components', 'common']);
|
73
|
-
const { styles, cx } = useStyles();
|
84
|
+
const { styles, cx, theme } = useStyles();
|
74
85
|
|
75
86
|
const [showDetail, setShowDetail] = useState(false);
|
76
87
|
|
@@ -79,8 +90,13 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
79
90
|
}, [thinking]);
|
80
91
|
|
81
92
|
return (
|
82
|
-
<Flexbox
|
93
|
+
<Flexbox
|
94
|
+
className={cx(styles.container, showDetail && styles.expand)}
|
95
|
+
style={style}
|
96
|
+
width={'100%'}
|
97
|
+
>
|
83
98
|
<Flexbox
|
99
|
+
className={cx(styles.header, showDetail && styles.headerExpand)}
|
84
100
|
distribution={'space-between'}
|
85
101
|
flex={1}
|
86
102
|
gap={8}
|
@@ -89,17 +105,18 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
89
105
|
setShowDetail(!showDetail);
|
90
106
|
}}
|
91
107
|
style={{ cursor: 'pointer' }}
|
108
|
+
width={'100%'}
|
92
109
|
>
|
93
110
|
{thinking ? (
|
94
|
-
<Flexbox align={'center'} gap={8} horizontal>
|
95
|
-
<Icon icon={AtomIcon} />
|
111
|
+
<Flexbox align={'center'} gap={8} horizontal width={'100%'}>
|
112
|
+
<Icon color={theme.purple} icon={AtomIcon} />
|
96
113
|
<Flexbox className={styles.shinyText} horizontal>
|
97
114
|
{t('Thinking.thinking')}
|
98
115
|
</Flexbox>
|
99
116
|
</Flexbox>
|
100
117
|
) : (
|
101
|
-
<Flexbox align={'center'} gap={8} horizontal>
|
102
|
-
<Icon icon={AtomIcon} />
|
118
|
+
<Flexbox align={'center'} gap={8} horizontal width={'100%'}>
|
119
|
+
<Icon color={showDetail ? theme.purple : undefined} icon={AtomIcon} />
|
103
120
|
<Flexbox>
|
104
121
|
{!duration
|
105
122
|
? t('Thinking.thoughtWithDuration')
|
@@ -117,28 +134,27 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
117
134
|
<CopyButton content={content} size={'small'} title={t('copy', { ns: 'common' })} />
|
118
135
|
</div>
|
119
136
|
)}
|
120
|
-
<
|
137
|
+
<ActionIcon icon={showDetail ? ChevronDown : ChevronRight} size={'small'} />
|
121
138
|
</Flexbox>
|
122
139
|
</Flexbox>
|
123
|
-
|
124
140
|
<AnimatePresence initial={false}>
|
125
141
|
{showDetail && (
|
126
142
|
<motion.div
|
127
143
|
animate="open"
|
128
144
|
exit="collapsed"
|
129
145
|
initial="collapsed"
|
130
|
-
style={{ overflow: 'hidden' }}
|
146
|
+
style={{ overflow: 'hidden', padding: 12 }}
|
131
147
|
transition={{
|
132
148
|
duration: 0.2,
|
133
149
|
ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
|
134
150
|
}}
|
135
151
|
variants={{
|
136
|
-
collapsed: {
|
137
|
-
open: {
|
152
|
+
collapsed: { opacity: 0, width: 'auto' },
|
153
|
+
open: { opacity: 1, width: 'auto' },
|
138
154
|
}}
|
139
155
|
>
|
140
156
|
{typeof content === 'string' ? (
|
141
|
-
<Markdown citations={citations} variant={'chat'}>
|
157
|
+
<Markdown animated={thinking} citations={citations} variant={'chat'}>
|
142
158
|
{content}
|
143
159
|
</Markdown>
|
144
160
|
) : (
|
@@ -31,9 +31,10 @@ const Tools = memo(() => {
|
|
31
31
|
<Suspense fallback={<Action disabled icon={Blocks} title={t('tools.title')} />}>
|
32
32
|
<Action
|
33
33
|
dropdown={{
|
34
|
-
|
34
|
+
maxHeight: 500,
|
35
|
+
maxWidth: 480,
|
35
36
|
menu: { items },
|
36
|
-
minWidth:
|
37
|
+
minWidth: 320,
|
37
38
|
}}
|
38
39
|
icon={Blocks}
|
39
40
|
loading={updating}
|
@@ -171,6 +171,7 @@ const Item = memo<ChatListItemProps>(
|
|
171
171
|
|
172
172
|
const markdownProps = useMemo(
|
173
173
|
() => ({
|
174
|
+
animated: generating,
|
174
175
|
citations: item?.role === 'user' ? undefined : item?.search?.citations,
|
175
176
|
components,
|
176
177
|
customRender: markdownCustomRender,
|
@@ -186,7 +187,7 @@ const Item = memo<ChatListItemProps>(
|
|
186
187
|
// if the citations's url and title are all the same, we should not show the citations
|
187
188
|
item?.search?.citations.every((item) => item.title !== item.url),
|
188
189
|
}),
|
189
|
-
[components, markdownCustomRender, item?.role, item?.search],
|
190
|
+
[generating, components, markdownCustomRender, item?.role, item?.search],
|
190
191
|
);
|
191
192
|
|
192
193
|
const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
2
1
|
import { createStyles } from 'antd-style';
|
3
2
|
import { Minus, Square, XIcon } from 'lucide-react';
|
4
3
|
|
@@ -12,7 +11,7 @@ const useStyles = createStyles(({ css, cx, token }) => {
|
|
12
11
|
align-items: center;
|
13
12
|
justify-content: center;
|
14
13
|
|
15
|
-
width:
|
14
|
+
width: ${TITLE_BAR_HEIGHT * 1.2}px;
|
16
15
|
min-height: ${TITLE_BAR_HEIGHT}px;
|
17
16
|
|
18
17
|
color: ${token.colorTextSecondary};
|
@@ -22,10 +21,12 @@ const useStyles = createStyles(({ css, cx, token }) => {
|
|
22
21
|
-webkit-app-region: no-drag;
|
23
22
|
|
24
23
|
&:hover {
|
24
|
+
color: ${token.colorText};
|
25
25
|
background: ${token.colorFillTertiary};
|
26
26
|
}
|
27
27
|
|
28
28
|
&:active {
|
29
|
+
color: ${token.colorText};
|
29
30
|
background: ${token.colorFillSecondary};
|
30
31
|
}
|
31
32
|
`;
|
@@ -33,6 +34,8 @@ const useStyles = createStyles(({ css, cx, token }) => {
|
|
33
34
|
close: cx(
|
34
35
|
icon,
|
35
36
|
css`
|
37
|
+
padding-inline-end: 2px;
|
38
|
+
|
36
39
|
&:hover {
|
37
40
|
color: ${token.colorTextLightSolid};
|
38
41
|
|
@@ -58,6 +61,7 @@ const useStyles = createStyles(({ css, cx, token }) => {
|
|
58
61
|
|
59
62
|
const WinControl = () => {
|
60
63
|
const { styles } = useStyles();
|
64
|
+
|
61
65
|
return (
|
62
66
|
<div className={styles.container}>
|
63
67
|
<div
|
@@ -66,7 +70,7 @@ const WinControl = () => {
|
|
66
70
|
electronSystemService.minimizeWindow();
|
67
71
|
}}
|
68
72
|
>
|
69
|
-
<
|
73
|
+
<Minus absoluteStrokeWidth size={14} strokeWidth={1.2} />
|
70
74
|
</div>
|
71
75
|
<div
|
72
76
|
className={styles.icon}
|
@@ -74,7 +78,7 @@ const WinControl = () => {
|
|
74
78
|
electronSystemService.maximizeWindow();
|
75
79
|
}}
|
76
80
|
>
|
77
|
-
<
|
81
|
+
<Square absoluteStrokeWidth size={10} strokeWidth={1.2} />
|
78
82
|
</div>
|
79
83
|
<div
|
80
84
|
className={styles.close}
|
@@ -82,7 +86,7 @@ const WinControl = () => {
|
|
82
86
|
electronSystemService.closeWindow();
|
83
87
|
}}
|
84
88
|
>
|
85
|
-
<
|
89
|
+
<XIcon absoluteStrokeWidth size={14} strokeWidth={1.2} />
|
86
90
|
</div>
|
87
91
|
</div>
|
88
92
|
);
|
@@ -1 +1 @@
|
|
1
|
-
export const TITLE_BAR_HEIGHT =
|
1
|
+
export const TITLE_BAR_HEIGHT = 30;
|
@@ -79,7 +79,10 @@ const ArtifactsUI = memo(() => {
|
|
79
79
|
style={{ overflow: 'hidden' }}
|
80
80
|
>
|
81
81
|
{showCode ? (
|
82
|
-
<Highlighter
|
82
|
+
<Highlighter
|
83
|
+
language={language || 'txt'}
|
84
|
+
style={{ fontSize: 12, height: '100%', overflow: 'auto' }}
|
85
|
+
>
|
83
86
|
{artifactContent}
|
84
87
|
</Highlighter>
|
85
88
|
) : (
|
@@ -33,12 +33,8 @@ const Header = () => {
|
|
33
33
|
return (
|
34
34
|
<Flexbox align={'center'} flex={1} gap={12} horizontal justify={'space-between'} width={'100%'}>
|
35
35
|
<Flexbox align={'center'} gap={4} horizontal>
|
36
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeArtifact()} />
|
37
|
-
<Typography.Text
|
38
|
-
className={cx(oneLineEllipsis)}
|
39
|
-
style={{ fontSize: 16 }}
|
40
|
-
type={'secondary'}
|
41
|
-
>
|
36
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeArtifact()} size={'small'} />
|
37
|
+
<Typography.Text className={cx(oneLineEllipsis)} type={'secondary'}>
|
42
38
|
{artifactTitle}
|
43
39
|
</Typography.Text>
|
44
40
|
</Flexbox>
|
@@ -20,7 +20,7 @@ const Header = () => {
|
|
20
20
|
|
21
21
|
return (
|
22
22
|
<Flexbox align={'center'} gap={4} horizontal>
|
23
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeFilePreview()} />
|
23
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeFilePreview()} size={'small'} />
|
24
24
|
|
25
25
|
{isLoading ? (
|
26
26
|
<Skeleton.Button active style={{ height: 28 }} />
|
@@ -25,7 +25,7 @@ const Header = () => {
|
|
25
25
|
if (toolUIIdentifier === WebBrowsingManifest.identifier) {
|
26
26
|
return (
|
27
27
|
<Flexbox align={'center'} gap={8} horizontal>
|
28
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} />
|
28
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} size={'small'} />
|
29
29
|
<Icon icon={Globe} size={16} />
|
30
30
|
<Typography.Text style={{ fontSize: 16 }} type={'secondary'}>
|
31
31
|
{t('search.title')}
|
@@ -35,7 +35,7 @@ const Header = () => {
|
|
35
35
|
}
|
36
36
|
return (
|
37
37
|
<Flexbox align={'center'} gap={4} horizontal>
|
38
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} />
|
38
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} size={'small'} />
|
39
39
|
<PluginAvatar identifier={toolUIIdentifier} size={28} />
|
40
40
|
<Typography.Text style={{ fontSize: 16 }} type={'secondary'}>
|
41
41
|
{pluginTitle}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { ActionIcon } from '@lobehub/ui';
|
2
2
|
import { Popover, type PopoverProps } from 'antd';
|
3
|
-
import { useTheme } from 'antd-style';
|
4
3
|
import { Languages } from 'lucide-react';
|
5
4
|
import { memo, useMemo } from 'react';
|
6
5
|
import { useTranslation } from 'react-i18next';
|
@@ -12,8 +11,6 @@ import { globalGeneralSelectors } from '@/store/global/selectors';
|
|
12
11
|
import { LocaleMode } from '@/types/locale';
|
13
12
|
|
14
13
|
const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
|
15
|
-
const theme = useTheme();
|
16
|
-
|
17
14
|
const [language, switchLocale] = useGlobalStore((s) => [
|
18
15
|
globalGeneralSelectors.language(s),
|
19
16
|
s.switchLocale,
|
@@ -48,16 +45,14 @@ const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement
|
|
48
45
|
placement={placement}
|
49
46
|
styles={{
|
50
47
|
body: {
|
48
|
+
maxHeight: 360,
|
49
|
+
overflow: 'auto',
|
51
50
|
padding: 0,
|
52
51
|
},
|
53
52
|
}}
|
54
53
|
trigger={['click', 'hover']}
|
55
54
|
>
|
56
|
-
<ActionIcon
|
57
|
-
icon={Languages}
|
58
|
-
size={{ blockSize: 32, size: 16 }}
|
59
|
-
style={{ border: `1px solid ${theme.colorFillSecondary}` }}
|
60
|
-
/>
|
55
|
+
<ActionIcon icon={Languages} size={{ blockSize: 32, size: 16 }} />
|
61
56
|
</Popover>
|
62
57
|
);
|
63
58
|
});
|
@@ -63,7 +63,7 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
|
63
63
|
) : (
|
64
64
|
<BrandWatermark />
|
65
65
|
)}
|
66
|
-
<Flexbox align={'center'} flex={'none'} gap={
|
66
|
+
<Flexbox align={'center'} flex={'none'} gap={2} horizontal>
|
67
67
|
<LangButton />
|
68
68
|
<ThemeButton />
|
69
69
|
</Flexbox>
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
2
|
import { Popover, type PopoverProps } from 'antd';
|
3
|
-
import { useTheme } from 'antd-style';
|
4
3
|
import { Monitor, Moon, Sun } from 'lucide-react';
|
5
4
|
import { memo, useMemo } from 'react';
|
6
5
|
import { useTranslation } from 'react-i18next';
|
@@ -16,7 +15,6 @@ const themeIcons = {
|
|
16
15
|
};
|
17
16
|
|
18
17
|
const ThemeButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
|
19
|
-
const theme = useTheme();
|
20
18
|
const [themeMode, switchThemeMode] = useGlobalStore((s) => [
|
21
19
|
systemStatusSelectors.themeMode(s),
|
22
20
|
s.switchThemeMode,
|
@@ -60,11 +58,7 @@ const ThemeButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement
|
|
60
58
|
}}
|
61
59
|
trigger={['click', 'hover']}
|
62
60
|
>
|
63
|
-
<ActionIcon
|
64
|
-
icon={themeIcons[themeMode]}
|
65
|
-
size={{ blockSize: 32, size: 16 }}
|
66
|
-
style={{ border: `1px solid ${theme.colorFillSecondary}` }}
|
67
|
-
/>
|
61
|
+
<ActionIcon icon={themeIcons[themeMode]} size={{ blockSize: 32, size: 16 }} />
|
68
62
|
</Popover>
|
69
63
|
);
|
70
64
|
});
|
package/src/middleware.ts
CHANGED
@@ -134,12 +134,23 @@ const defaultMiddleware = (request: NextRequest) => {
|
|
134
134
|
return NextResponse.rewrite(url, { status: 200 });
|
135
135
|
};
|
136
136
|
|
137
|
+
const isProtectedRoute = createRouteMatcher([
|
138
|
+
'/settings(.*)',
|
139
|
+
'/files(.*)',
|
140
|
+
'/onboard(.*)',
|
141
|
+
'/oauth(.*)',
|
142
|
+
// ↓ cloud ↓
|
143
|
+
]);
|
144
|
+
|
137
145
|
// Initialize an Edge compatible NextAuth middleware
|
138
146
|
const nextAuthMiddleware = NextAuthEdge.auth((req) => {
|
139
147
|
logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
|
140
148
|
|
141
149
|
const response = defaultMiddleware(req);
|
142
150
|
|
151
|
+
const isProtected = isProtectedRoute(req);
|
152
|
+
logNextAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public');
|
153
|
+
|
143
154
|
// Just check if session exists
|
144
155
|
const session = req.auth;
|
145
156
|
|
@@ -165,20 +176,20 @@ const nextAuthMiddleware = NextAuthEdge.auth((req) => {
|
|
165
176
|
response.headers.set(OIDC_SESSION_HEADER, session.user.id);
|
166
177
|
}
|
167
178
|
} else {
|
168
|
-
|
179
|
+
// If request a protected route, redirect to sign-in page
|
180
|
+
// ref: https://authjs.dev/getting-started/session-management/protecting
|
181
|
+
if (isProtected) {
|
182
|
+
logNextAuth('Request a protected route, redirecting to sign-in page');
|
183
|
+
const nextLoginUrl = new URL('/next-auth/signin', req.nextUrl.origin);
|
184
|
+
nextLoginUrl.searchParams.set('callbackUrl', req.nextUrl.pathname);
|
185
|
+
return Response.redirect(nextLoginUrl);
|
186
|
+
}
|
187
|
+
logNextAuth('Request a free route but not login, allow visit without auth header');
|
169
188
|
}
|
170
189
|
|
171
190
|
return response;
|
172
191
|
});
|
173
192
|
|
174
|
-
const isProtectedRoute = createRouteMatcher([
|
175
|
-
'/settings(.*)',
|
176
|
-
'/files(.*)',
|
177
|
-
'/onboard(.*)',
|
178
|
-
'/oauth(.*)',
|
179
|
-
// ↓ cloud ↓
|
180
|
-
]);
|
181
|
-
|
182
193
|
const clerkAuthMiddleware = clerkMiddleware(
|
183
194
|
async (auth, req) => {
|
184
195
|
logClerk('Clerk middleware processing request: %s %s', req.method, req.url);
|
@@ -677,7 +677,15 @@ export const generateAIChat: StateCreator<
|
|
677
677
|
// if there is no duration, it means the end of reasoning
|
678
678
|
if (!duration) {
|
679
679
|
duration = Date.now() - thinkingStartAt;
|
680
|
-
|
680
|
+
|
681
|
+
const isInChatReasoning = chatSelectors.isMessageInChatReasoning(messageId)(get());
|
682
|
+
if (isInChatReasoning) {
|
683
|
+
internal_toggleChatReasoning(
|
684
|
+
false,
|
685
|
+
messageId,
|
686
|
+
n('toggleChatReasoning/false') as string,
|
687
|
+
);
|
688
|
+
}
|
681
689
|
}
|
682
690
|
|
683
691
|
internal_dispatchMessage({
|
@@ -695,7 +703,11 @@ export const generateAIChat: StateCreator<
|
|
695
703
|
// if there is no thinkingStartAt, it means the start of reasoning
|
696
704
|
if (!thinkingStartAt) {
|
697
705
|
thinkingStartAt = Date.now();
|
698
|
-
internal_toggleChatReasoning(
|
706
|
+
internal_toggleChatReasoning(
|
707
|
+
true,
|
708
|
+
messageId,
|
709
|
+
n('toggleChatReasoning/true') as string,
|
710
|
+
);
|
699
711
|
}
|
700
712
|
|
701
713
|
thinking += chunk.text;
|
@@ -157,6 +157,9 @@ const isMessageLoading = (id: string) => (s: ChatStoreState) => s.messageLoading
|
|
157
157
|
const isMessageGenerating = (id: string) => (s: ChatStoreState) => s.chatLoadingIds.includes(id);
|
158
158
|
const isMessageInRAGFlow = (id: string) => (s: ChatStoreState) =>
|
159
159
|
s.messageRAGLoadingIds.includes(id);
|
160
|
+
const isMessageInChatReasoning = (id: string) => (s: ChatStoreState) =>
|
161
|
+
s.reasoningLoadingIds.includes(id);
|
162
|
+
|
160
163
|
const isPluginApiInvoking = (id: string) => (s: ChatStoreState) =>
|
161
164
|
s.pluginApiLoadingIds.includes(id);
|
162
165
|
|
@@ -170,6 +173,7 @@ const isToolCallStreaming = (id: string, index: number) => (s: ChatStoreState) =
|
|
170
173
|
|
171
174
|
const isAIGenerating = (s: ChatStoreState) =>
|
172
175
|
s.chatLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
|
176
|
+
|
173
177
|
const isInRAGFlow = (s: ChatStoreState) =>
|
174
178
|
s.messageRAGLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
|
175
179
|
|
@@ -208,6 +212,7 @@ export const chatSelectors = {
|
|
208
212
|
isHasMessageLoading,
|
209
213
|
isMessageEditing,
|
210
214
|
isMessageGenerating,
|
215
|
+
isMessageInChatReasoning,
|
211
216
|
isMessageInRAGFlow,
|
212
217
|
isMessageLoading,
|
213
218
|
isPluginApiInvoking,
|
@@ -41,8 +41,7 @@ describe('fetchSSE', () => {
|
|
41
41
|
smoothing: false,
|
42
42
|
});
|
43
43
|
|
44
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
|
45
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
|
44
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello World', type: 'text' });
|
46
45
|
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
47
46
|
observationId: null,
|
48
47
|
toolCalls: undefined,
|
@@ -123,7 +122,7 @@ describe('fetchSSE', () => {
|
|
123
122
|
});
|
124
123
|
});
|
125
124
|
|
126
|
-
it('should handle text event with smoothing correctly', async () => {
|
125
|
+
it.skip('should handle text event with smoothing correctly', async () => {
|
127
126
|
const mockOnMessageHandle = vi.fn();
|
128
127
|
const mockOnFinish = vi.fn();
|
129
128
|
|
@@ -178,9 +177,9 @@ describe('fetchSSE', () => {
|
|
178
177
|
async (url: string, options: FetchEventSourceInit) => {
|
179
178
|
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
180
179
|
options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
|
181
|
-
await sleep(
|
180
|
+
await sleep(400);
|
182
181
|
options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
|
183
|
-
await sleep(
|
182
|
+
await sleep(400);
|
184
183
|
options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
|
185
184
|
},
|
186
185
|
);
|
@@ -321,19 +320,7 @@ describe('fetchSSE', () => {
|
|
321
320
|
smoothing: true,
|
322
321
|
});
|
323
322
|
|
324
|
-
const expectedMessages = [
|
325
|
-
{ text: 'H', type: 'text' },
|
326
|
-
{ text: 'e', type: 'text' },
|
327
|
-
{ text: 'l', type: 'text' },
|
328
|
-
{ text: 'l', type: 'text' },
|
329
|
-
{ text: 'o', type: 'text' },
|
330
|
-
{ text: ' ', type: 'text' },
|
331
|
-
{ text: 'W', type: 'text' },
|
332
|
-
{ text: 'o', type: 'text' },
|
333
|
-
{ text: 'r', type: 'text' },
|
334
|
-
{ text: 'l', type: 'text' },
|
335
|
-
{ text: 'd', type: 'text' },
|
336
|
-
];
|
323
|
+
const expectedMessages = [{ text: 'Hello World', type: 'text' }];
|
337
324
|
|
338
325
|
expectedMessages.forEach((message, index) => {
|
339
326
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(index + 1, message);
|
@@ -315,11 +315,27 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
315
315
|
|
316
316
|
const { smoothing } = options;
|
317
317
|
|
318
|
-
const textSmoothing =
|
318
|
+
const textSmoothing = false;
|
319
|
+
// TODO: 看下后面就是完全移除 smoothing 还是怎么说
|
320
|
+
// const textSmoothing = typeof smoothing === 'boolean' ? smoothing : (smoothing?.text ?? true);
|
319
321
|
const toolsCallingSmoothing =
|
320
322
|
typeof smoothing === 'boolean' ? smoothing : (smoothing?.toolsCalling ?? true);
|
323
|
+
|
321
324
|
const smoothingSpeed = isObject(smoothing) ? smoothing.speed : undefined;
|
322
325
|
|
326
|
+
// 添加文本buffer和计时器相关变量
|
327
|
+
let textBuffer = '';
|
328
|
+
// eslint-disable-next-line no-undef
|
329
|
+
let bufferTimer: NodeJS.Timeout | null = null;
|
330
|
+
const BUFFER_INTERVAL = 300; // 300ms
|
331
|
+
|
332
|
+
const flushTextBuffer = () => {
|
333
|
+
if (textBuffer) {
|
334
|
+
options.onMessageHandle?.({ text: textBuffer, type: 'text' });
|
335
|
+
textBuffer = '';
|
336
|
+
}
|
337
|
+
};
|
338
|
+
|
323
339
|
let output = '';
|
324
340
|
const textController = createSmoothMessage({
|
325
341
|
onTextUpdate: (delta, text) => {
|
@@ -340,6 +356,18 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
340
356
|
startSpeed: smoothingSpeed,
|
341
357
|
});
|
342
358
|
|
359
|
+
let thinkingBuffer = '';
|
360
|
+
// eslint-disable-next-line no-undef
|
361
|
+
let thinkingBufferTimer: NodeJS.Timeout | null = null;
|
362
|
+
|
363
|
+
// 创建一个函数来处理buffer的刷新
|
364
|
+
const flushThinkingBuffer = () => {
|
365
|
+
if (thinkingBuffer) {
|
366
|
+
options.onMessageHandle?.({ text: thinkingBuffer, type: 'reasoning' });
|
367
|
+
thinkingBuffer = '';
|
368
|
+
}
|
369
|
+
};
|
370
|
+
|
343
371
|
const toolCallsController = createSmoothToolCalls({
|
344
372
|
onToolCallsUpdate: (toolCalls, isAnimationActives) => {
|
345
373
|
options.onMessageHandle?.({ isAnimationActives, tool_calls: toolCalls, type: 'tool_calls' });
|
@@ -430,7 +458,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
430
458
|
if (!textController.isAnimationActive) textController.startAnimation();
|
431
459
|
} else {
|
432
460
|
output += data;
|
433
|
-
|
461
|
+
|
462
|
+
// 使用buffer机制
|
463
|
+
textBuffer += data;
|
464
|
+
|
465
|
+
// 如果还没有设置计时器,创建一个
|
466
|
+
if (!bufferTimer) {
|
467
|
+
bufferTimer = setTimeout(() => {
|
468
|
+
flushTextBuffer();
|
469
|
+
bufferTimer = null;
|
470
|
+
}, BUFFER_INTERVAL);
|
471
|
+
}
|
434
472
|
}
|
435
473
|
|
436
474
|
break;
|
@@ -466,7 +504,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
466
504
|
if (!thinkingController.isAnimationActive) thinkingController.startAnimation();
|
467
505
|
} else {
|
468
506
|
thinking += data;
|
469
|
-
|
507
|
+
|
508
|
+
// 使用buffer机制
|
509
|
+
thinkingBuffer += data;
|
510
|
+
|
511
|
+
// 如果还没有设置计时器,创建一个
|
512
|
+
if (!thinkingBufferTimer) {
|
513
|
+
thinkingBufferTimer = setTimeout(() => {
|
514
|
+
flushThinkingBuffer();
|
515
|
+
thinkingBufferTimer = null;
|
516
|
+
}, BUFFER_INTERVAL);
|
517
|
+
}
|
470
518
|
}
|
471
519
|
|
472
520
|
break;
|
@@ -509,6 +557,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
509
557
|
textController.stopAnimation();
|
510
558
|
toolCallsController.stopAnimations();
|
511
559
|
|
560
|
+
// 确保所有缓冲区数据都被处理
|
561
|
+
if (bufferTimer) {
|
562
|
+
clearTimeout(bufferTimer);
|
563
|
+
flushTextBuffer();
|
564
|
+
}
|
565
|
+
|
566
|
+
if (thinkingBufferTimer) {
|
567
|
+
clearTimeout(thinkingBufferTimer);
|
568
|
+
flushThinkingBuffer();
|
569
|
+
}
|
570
|
+
|
512
571
|
if (response.ok) {
|
513
572
|
// if there is no onMessageHandler, we should call onHandleMessage first
|
514
573
|
if (!triggerOnMessageHandler) {
|