@lobehub/chat 1.85.8 → 1.85.9
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 +33 -0
- package/changelog/v1.json +12 -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/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,39 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.85.9](https://github.com/lobehub/lobe-chat/compare/v1.85.8...v1.85.9)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-14**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Redirect unauthorized next-auth user to signin page.
|
12
|
+
|
13
|
+
#### 💄 Styles
|
14
|
+
|
15
|
+
- **misc**: Improve smoothing on completion.
|
16
|
+
|
17
|
+
<br/>
|
18
|
+
|
19
|
+
<details>
|
20
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
21
|
+
|
22
|
+
#### What's fixed
|
23
|
+
|
24
|
+
- **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))
|
25
|
+
|
26
|
+
#### Styles
|
27
|
+
|
28
|
+
- **misc**: Improve smoothing on completion, closes [#7833](https://github.com/lobehub/lobe-chat/issues/7833) ([6434686](https://github.com/lobehub/lobe-chat/commit/6434686))
|
29
|
+
|
30
|
+
</details>
|
31
|
+
|
32
|
+
<div align="right">
|
33
|
+
|
34
|
+
[](#readme-top)
|
35
|
+
|
36
|
+
</div>
|
37
|
+
|
5
38
|
### [Version 1.85.8](https://github.com/lobehub/lobe-chat/compare/v1.85.7...v1.85.8)
|
6
39
|
|
7
40
|
<sup>Released on **2025-05-11**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,16 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"fixes": [
|
5
|
+
"Redirect unauthorized next-auth user to signin page."
|
6
|
+
],
|
7
|
+
"improvements": [
|
8
|
+
"Improve smoothing on completion."
|
9
|
+
]
|
10
|
+
},
|
11
|
+
"date": "2025-05-14",
|
12
|
+
"version": "1.85.9"
|
13
|
+
},
|
2
14
|
{
|
3
15
|
"children": {
|
4
16
|
"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.9",
|
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]);
|
@@ -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) {
|