@lobehub/lobehub 2.0.0-next.290 → 2.0.0-next.291
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 +35 -0
- package/changelog/v1.json +12 -0
- package/locales/en-US/chat.json +2 -0
- package/locales/zh-CN/chat.json +2 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +18 -9
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +68 -23
- package/src/features/ChatInput/ChatInputProvider.tsx +1 -1
- package/src/features/Conversation/Messages/Assistant/components/MessageContent.tsx +9 -16
- package/src/features/Conversation/Messages/Task/components/MessageContent.tsx +1 -0
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +64 -0
- package/src/features/Conversation/Messages/components/DisplayContent.tsx +4 -2
- package/src/features/OllamaModelDownloader/index.tsx +3 -3
- package/src/libs/swr/index.ts +11 -13
- package/src/locales/default/chat.ts +2 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +72 -4
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.291](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.290...v2.0.0-next.291)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-15**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **settings**: Add instant UI feedback for provider config switches.
|
|
12
|
+
- **misc**: Click lobe ai topic trigger create new agent.
|
|
13
|
+
|
|
14
|
+
#### 💄 Styles
|
|
15
|
+
|
|
16
|
+
- **misc**: Improve agent loading state.
|
|
17
|
+
|
|
18
|
+
<br/>
|
|
19
|
+
|
|
20
|
+
<details>
|
|
21
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
22
|
+
|
|
23
|
+
#### What's fixed
|
|
24
|
+
|
|
25
|
+
- **settings**: Add instant UI feedback for provider config switches, closes [#11362](https://github.com/lobehub/lobe-chat/issues/11362) ([a758d01](https://github.com/lobehub/lobe-chat/commit/a758d01))
|
|
26
|
+
- **misc**: Click lobe ai topic trigger create new agent, closes [#11508](https://github.com/lobehub/lobe-chat/issues/11508) ([2443189](https://github.com/lobehub/lobe-chat/commit/2443189))
|
|
27
|
+
|
|
28
|
+
#### Styles
|
|
29
|
+
|
|
30
|
+
- **misc**: Improve agent loading state, closes [#11511](https://github.com/lobehub/lobe-chat/issues/11511) ([3bb7f33](https://github.com/lobehub/lobe-chat/commit/3bb7f33))
|
|
31
|
+
|
|
32
|
+
</details>
|
|
33
|
+
|
|
34
|
+
<div align="right">
|
|
35
|
+
|
|
36
|
+
[](#readme-top)
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
|
|
5
40
|
## [Version 2.0.0-next.290](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.289...v2.0.0-next.290)
|
|
6
41
|
|
|
7
42
|
<sup>Released on **2026-01-15**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Click lobe ai topic trigger create new agent."
|
|
6
|
+
],
|
|
7
|
+
"improvements": [
|
|
8
|
+
"Improve agent loading state."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"date": "2026-01-15",
|
|
12
|
+
"version": "2.0.0-next.291"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"children": {
|
|
4
16
|
"fixes": [
|
package/locales/en-US/chat.json
CHANGED
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"noMembersYet": "This group doesn't have any members yet. Click the + button to invite agents.",
|
|
204
204
|
"noSelectedAgents": "No members selected yet",
|
|
205
205
|
"openInNewWindow": "Open in New Window",
|
|
206
|
+
"operation.execAgentRuntime": "Preparing response",
|
|
207
|
+
"operation.sendMessage": "Sending message",
|
|
206
208
|
"owner": "Group owner",
|
|
207
209
|
"pageCopilot.title": "Page Agent",
|
|
208
210
|
"pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and I’ll refine the rest.",
|
package/locales/zh-CN/chat.json
CHANGED
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"noMembersYet": "这个群组还没有成员。点击「+」邀请助理加入",
|
|
204
204
|
"noSelectedAgents": "还未选择成员",
|
|
205
205
|
"openInNewWindow": "在新窗口打开",
|
|
206
|
+
"operation.execAgentRuntime": "准备响应中",
|
|
207
|
+
"operation.sendMessage": "消息发送中",
|
|
206
208
|
"owner": "群主",
|
|
207
209
|
"pageCopilot.title": "文稿助理",
|
|
208
210
|
"pageCopilot.welcome": "**让文字更清晰、更到位**\n\n起草、改写、润色都可以。你把意图说清楚,其余交给我打磨",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.291",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -6,7 +6,7 @@ import { ModelIcon } from '@lobehub/icons';
|
|
|
6
6
|
import { Alert, Button, Flexbox, Highlighter, Icon, Select } from '@lobehub/ui';
|
|
7
7
|
import { cssVar } from 'antd-style';
|
|
8
8
|
import { Loader2Icon } from 'lucide-react';
|
|
9
|
-
import { type ReactNode, memo, useState } from 'react';
|
|
9
|
+
import { type ReactNode, memo, useEffect, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
|
|
12
12
|
import { useProviderName } from '@/hooks/useProviderName';
|
|
@@ -58,9 +58,10 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
|
58
58
|
({ model, provider, checkErrorRender: CheckErrorRender, onBeforeCheck, onAfterCheck }) => {
|
|
59
59
|
const { t } = useTranslation('setting');
|
|
60
60
|
|
|
61
|
-
const isProviderConfigUpdating = useAiInfraStore(
|
|
62
|
-
aiProviderSelectors.isProviderConfigUpdating(provider),
|
|
63
|
-
|
|
61
|
+
const [isProviderConfigUpdating, updateAiProviderConfig] = useAiInfraStore((s) => [
|
|
62
|
+
aiProviderSelectors.isProviderConfigUpdating(provider)(s),
|
|
63
|
+
s.updateAiProviderConfig,
|
|
64
|
+
]);
|
|
64
65
|
const totalModels = useAiInfraStore(aiModelSelectors.aiProviderChatModelListIds);
|
|
65
66
|
|
|
66
67
|
const [loading, setLoading] = useState(false);
|
|
@@ -69,6 +70,11 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
|
69
70
|
|
|
70
71
|
const [error, setError] = useState<ChatMessageError | undefined>();
|
|
71
72
|
|
|
73
|
+
// Sync checkModel state when model prop changes
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setCheckModel(model);
|
|
76
|
+
}, [model]);
|
|
77
|
+
|
|
72
78
|
const checkConnection = async () => {
|
|
73
79
|
// Clear previous check results immediately
|
|
74
80
|
setPass(false);
|
|
@@ -131,11 +137,14 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
|
131
137
|
<Select
|
|
132
138
|
listItemHeight={36}
|
|
133
139
|
onSelect={async (value) => {
|
|
134
|
-
//
|
|
135
|
-
// Persisting it to provider config would trigger global refresh/revalidation.
|
|
140
|
+
// Update local state
|
|
136
141
|
setCheckModel(value);
|
|
137
142
|
setPass(false);
|
|
138
143
|
setError(undefined);
|
|
144
|
+
|
|
145
|
+
// Persist the selected model to provider config
|
|
146
|
+
// This allows the model to be retained after page refresh
|
|
147
|
+
await updateAiProviderConfig(provider, { checkModel: value });
|
|
139
148
|
}}
|
|
140
149
|
optionRender={({ value }) => {
|
|
141
150
|
return (
|
|
@@ -177,9 +186,9 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
|
177
186
|
style={
|
|
178
187
|
pass
|
|
179
188
|
? {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
borderColor: cssVar.colorSuccess,
|
|
190
|
+
color: cssVar.colorSuccess,
|
|
191
|
+
}
|
|
183
192
|
: undefined
|
|
184
193
|
}
|
|
185
194
|
>
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@lobehub/ui';
|
|
14
14
|
import { Center, Flexbox, Skeleton } from '@lobehub/ui';
|
|
15
15
|
import { useDebounceFn } from 'ahooks';
|
|
16
|
-
import { Switch } from 'antd';
|
|
16
|
+
import { Form as AntdForm, Switch } from 'antd';
|
|
17
17
|
import { createStaticStyles, cssVar, cx, responsive } from 'antd-style';
|
|
18
18
|
import { Loader2Icon, LockIcon } from 'lucide-react';
|
|
19
19
|
import Link from 'next/link';
|
|
@@ -150,26 +150,72 @@ const ProviderConfig = memo<ProviderConfigProps>(
|
|
|
150
150
|
enabled,
|
|
151
151
|
isLoading,
|
|
152
152
|
configUpdating,
|
|
153
|
-
|
|
154
|
-
isProviderEndpointNotEmpty,
|
|
155
|
-
isProviderApiKeyNotEmpty,
|
|
153
|
+
providerRuntimeConfig,
|
|
156
154
|
] = useAiInfraStore((s) => [
|
|
157
155
|
aiProviderSelectors.providerDetailById(id)(s),
|
|
158
156
|
s.updateAiProviderConfig,
|
|
159
157
|
aiProviderSelectors.isProviderEnabled(id)(s),
|
|
160
158
|
aiProviderSelectors.isAiProviderConfigLoading(id)(s),
|
|
161
159
|
aiProviderSelectors.isProviderConfigUpdating(id)(s),
|
|
162
|
-
aiProviderSelectors.
|
|
163
|
-
aiProviderSelectors.isActiveProviderEndpointNotEmpty(s),
|
|
164
|
-
aiProviderSelectors.isActiveProviderApiKeyNotEmpty(s),
|
|
160
|
+
aiProviderSelectors.providerConfigById(id)(s),
|
|
165
161
|
]);
|
|
166
162
|
|
|
163
|
+
// Watch form values in real-time to show/hide switches immediately
|
|
164
|
+
// Watch nested form values for endpoints
|
|
165
|
+
const formBaseURL = AntdForm.useWatch(['keyVaults', 'baseURL'], form);
|
|
166
|
+
const formEndpoint = AntdForm.useWatch(['keyVaults', 'endpoint'], form);
|
|
167
|
+
// Watch all possible credential fields for different providers
|
|
168
|
+
const formApiKey = AntdForm.useWatch(['keyVaults', 'apiKey'], form);
|
|
169
|
+
const formAccessKeyId = AntdForm.useWatch(['keyVaults', 'accessKeyId'], form);
|
|
170
|
+
const formSecretAccessKey = AntdForm.useWatch(['keyVaults', 'secretAccessKey'], form);
|
|
171
|
+
const formUsername = AntdForm.useWatch(['keyVaults', 'username'], form);
|
|
172
|
+
const formPassword = AntdForm.useWatch(['keyVaults', 'password'], form);
|
|
173
|
+
|
|
174
|
+
// Check if provider has endpoint and apiKey based on runtime config
|
|
175
|
+
// Fallback to data.keyVaults if runtime config is not yet loaded
|
|
176
|
+
const keyVaults = providerRuntimeConfig?.keyVaults || data?.keyVaults;
|
|
177
|
+
// Use form values first (for immediate update), fallback to stored values
|
|
178
|
+
const isProviderEndpointNotEmpty =
|
|
179
|
+
!!formBaseURL || !!formEndpoint || !!keyVaults?.baseURL || !!keyVaults?.endpoint;
|
|
180
|
+
// Check if any credential is present for different authentication types:
|
|
181
|
+
// - Standard apiKey (OpenAI, Azure, Cloudflare, VertexAI, etc.)
|
|
182
|
+
// - AWS Bedrock credentials (accessKeyId, secretAccessKey)
|
|
183
|
+
// - ComfyUI basic auth (username and password)
|
|
184
|
+
const isProviderApiKeyNotEmpty = !!(
|
|
185
|
+
formApiKey ||
|
|
186
|
+
keyVaults?.apiKey ||
|
|
187
|
+
formAccessKeyId ||
|
|
188
|
+
keyVaults?.accessKeyId ||
|
|
189
|
+
formSecretAccessKey ||
|
|
190
|
+
keyVaults?.secretAccessKey ||
|
|
191
|
+
(formUsername && formPassword) ||
|
|
192
|
+
(keyVaults?.username && keyVaults?.password)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Track the last initialized provider ID to avoid resetting form during edits
|
|
196
|
+
const lastInitializedIdRef = useRef<string | null>(null);
|
|
197
|
+
|
|
167
198
|
useLayoutEffect(() => {
|
|
168
199
|
if (isLoading) return;
|
|
169
200
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
// Only initialize form when:
|
|
202
|
+
// 1. First load (lastInitializedIdRef.current === null)
|
|
203
|
+
// 2. Provider ID changed (switching between providers)
|
|
204
|
+
const shouldInitialize = lastInitializedIdRef.current !== id;
|
|
205
|
+
if (!shouldInitialize) return;
|
|
206
|
+
|
|
207
|
+
// Merge data from both sources to ensure all fields are initialized correctly
|
|
208
|
+
// data: contains basic info like apiKey, baseURL, fetchOnClient
|
|
209
|
+
// providerRuntimeConfig: contains nested config like enableResponseApi
|
|
210
|
+
const mergedData = {
|
|
211
|
+
...data,
|
|
212
|
+
...(providerRuntimeConfig?.config && { config: providerRuntimeConfig.config }),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Set form values and mark as initialized
|
|
216
|
+
form.setFieldsValue(mergedData);
|
|
217
|
+
lastInitializedIdRef.current = id;
|
|
218
|
+
}, [isLoading, id, data, providerRuntimeConfig, form]);
|
|
173
219
|
|
|
174
220
|
// 标记是否正在进行连接测试
|
|
175
221
|
const isCheckingConnection = useRef(false);
|
|
@@ -298,24 +344,23 @@ const ProviderConfig = memo<ProviderConfigProps>(
|
|
|
298
344
|
(defaultShowBrowserRequest ||
|
|
299
345
|
(showEndpoint && isProviderEndpointNotEmpty) ||
|
|
300
346
|
(showApiKey && isProviderApiKeyNotEmpty));
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
347
|
+
|
|
348
|
+
const clientFetchItem = showClientFetch
|
|
349
|
+
? {
|
|
350
|
+
children: isLoading ? <SkeletonSwitch /> : <Switch loading={configUpdating} />,
|
|
351
|
+
desc: t('providerModels.config.fetchOnClient.desc'),
|
|
352
|
+
label: t('providerModels.config.fetchOnClient.title'),
|
|
353
|
+
minWidth: undefined,
|
|
354
|
+
name: 'fetchOnClient',
|
|
355
|
+
}
|
|
356
|
+
: undefined;
|
|
308
357
|
|
|
309
358
|
const configItems = [
|
|
310
359
|
...apiKeyItem,
|
|
311
360
|
endpointItem,
|
|
312
361
|
supportResponsesApi
|
|
313
362
|
? {
|
|
314
|
-
children: isLoading ?
|
|
315
|
-
<Skeleton.Button active />
|
|
316
|
-
) : (
|
|
317
|
-
<Switch loading={configUpdating} value={enableResponseApi} />
|
|
318
|
-
),
|
|
363
|
+
children: isLoading ? <Skeleton.Button active /> : <Switch loading={configUpdating} />,
|
|
319
364
|
desc: t('providerModels.config.responsesApi.desc'),
|
|
320
365
|
label: t('providerModels.config.responsesApi.title'),
|
|
321
366
|
minWidth: undefined,
|
|
@@ -364,7 +409,7 @@ const ProviderConfig = memo<ProviderConfigProps>(
|
|
|
364
409
|
|
|
365
410
|
{isCustom && <UpdateProviderInfo />}
|
|
366
411
|
{canDeactivate && !(ENABLE_BUSINESS_FEATURES && id === 'lobehub') && (
|
|
367
|
-
<EnableSwitch id={id} />
|
|
412
|
+
<EnableSwitch id={id} key={id} />
|
|
368
413
|
)}
|
|
369
414
|
</Flexbox>
|
|
370
415
|
),
|
|
@@ -21,7 +21,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
|
|
|
21
21
|
chatInputEditorRef,
|
|
22
22
|
onMarkdownContentChange,
|
|
23
23
|
mentionItems,
|
|
24
|
-
allowExpand,
|
|
24
|
+
allowExpand = true,
|
|
25
25
|
}) => {
|
|
26
26
|
const editor = useEditor();
|
|
27
27
|
const slashMenuRef = useRef<HTMLDivElement>(null);
|
|
@@ -8,7 +8,6 @@ import { CollapsedMessage } from '../../AssistantGroup/components/CollapsedMessa
|
|
|
8
8
|
import DisplayContent from '../../components/DisplayContent';
|
|
9
9
|
import FileChunks from '../../components/FileChunks';
|
|
10
10
|
import ImageFileListViewer from '../../components/ImageFileListViewer';
|
|
11
|
-
import IntentUnderstanding from '../../components/IntentUnderstanding';
|
|
12
11
|
import Reasoning from '../../components/Reasoning';
|
|
13
12
|
import SearchGrounding from '../../components/SearchGrounding';
|
|
14
13
|
import { useMarkdown } from '../useMarkdown';
|
|
@@ -23,9 +22,6 @@ const MessageContent = memo<UIChatMessage>(
|
|
|
23
22
|
|
|
24
23
|
const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
|
|
25
24
|
|
|
26
|
-
// TODO: Need to implement isIntentUnderstanding selector in ConversationStore if needed
|
|
27
|
-
const isIntentUnderstanding = false;
|
|
28
|
-
|
|
29
25
|
const showSearch = !!search && !!search.citations?.length;
|
|
30
26
|
const showImageItems = !!imageList && imageList.length > 0;
|
|
31
27
|
|
|
@@ -46,18 +42,15 @@ const MessageContent = memo<UIChatMessage>(
|
|
|
46
42
|
)}
|
|
47
43
|
{showFileChunks && <FileChunks data={chunksList} />}
|
|
48
44
|
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
tempDisplayContent={metadata?.tempDisplayContent}
|
|
59
|
-
/>
|
|
60
|
-
)}
|
|
45
|
+
<DisplayContent
|
|
46
|
+
content={content}
|
|
47
|
+
hasImages={showImageItems}
|
|
48
|
+
id={id}
|
|
49
|
+
isMultimodal={metadata?.isMultimodal}
|
|
50
|
+
isToolCallGenerating={isToolCallGenerating}
|
|
51
|
+
markdownProps={markdownProps}
|
|
52
|
+
tempDisplayContent={metadata?.tempDisplayContent}
|
|
53
|
+
/>
|
|
61
54
|
{showImageItems && <ImageFileListViewer items={imageList} />}
|
|
62
55
|
</Flexbox>
|
|
63
56
|
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Flexbox, Text } from '@lobehub/ui';
|
|
2
|
+
import { memo, useEffect, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
import BubblesLoading from '@/components/BubblesLoading';
|
|
6
|
+
import { useChatStore } from '@/store/chat';
|
|
7
|
+
import { operationSelectors } from '@/store/chat/selectors';
|
|
8
|
+
import type { OperationType } from '@/store/chat/slices/operation/types';
|
|
9
|
+
|
|
10
|
+
const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
|
|
11
|
+
|
|
12
|
+
interface ContentLoadingProps {
|
|
13
|
+
id: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
|
|
17
|
+
const { t } = useTranslation('chat');
|
|
18
|
+
const operations = useChatStore(operationSelectors.getOperationsByMessage(id));
|
|
19
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
20
|
+
|
|
21
|
+
// Get the running operation
|
|
22
|
+
const runningOp = operations.find((op) => op.status === 'running');
|
|
23
|
+
const operationType = runningOp?.type as OperationType | undefined;
|
|
24
|
+
const startTime = runningOp?.metadata?.startTime;
|
|
25
|
+
|
|
26
|
+
// Track elapsed time, reset when operation type changes
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!startTime) {
|
|
29
|
+
setElapsedSeconds(0);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const updateElapsed = () => {
|
|
34
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
35
|
+
setElapsedSeconds(elapsed);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
updateElapsed();
|
|
39
|
+
const interval = setInterval(updateElapsed, 1000);
|
|
40
|
+
|
|
41
|
+
return () => clearInterval(interval);
|
|
42
|
+
}, [startTime, operationType]);
|
|
43
|
+
|
|
44
|
+
// Get localized label based on operation type
|
|
45
|
+
const operationLabel = operationType
|
|
46
|
+
? (t(`operation.${operationType}` as any) as string)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Flexbox align={'center'} horizontal>
|
|
53
|
+
<BubblesLoading />
|
|
54
|
+
{operationLabel && (
|
|
55
|
+
<Text type={'secondary'}>
|
|
56
|
+
{operationLabel}...
|
|
57
|
+
{showElapsedTime && ` (${elapsedSeconds}s)`}
|
|
58
|
+
</Text>
|
|
59
|
+
)}
|
|
60
|
+
</Flexbox>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export default ContentLoading;
|
|
@@ -2,17 +2,18 @@ import { deserializeParts } from '@lobechat/utils';
|
|
|
2
2
|
import { type MarkdownProps } from '@lobehub/ui';
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
|
|
5
|
-
import BubblesLoading from '@/components/BubblesLoading';
|
|
6
5
|
import { LOADING_FLAT } from '@/const/message';
|
|
7
6
|
import MarkdownMessage from '@/features/Conversation/Markdown';
|
|
8
7
|
|
|
9
8
|
import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
|
|
9
|
+
import ContentLoading from './ContentLoading';
|
|
10
10
|
import { RichContentRenderer } from './RichContentRenderer';
|
|
11
11
|
|
|
12
12
|
const DisplayContent = memo<{
|
|
13
13
|
addIdOnDOM?: boolean;
|
|
14
14
|
content: string;
|
|
15
15
|
hasImages?: boolean;
|
|
16
|
+
id: string;
|
|
16
17
|
isMultimodal?: boolean;
|
|
17
18
|
isToolCallGenerating?: boolean;
|
|
18
19
|
markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
|
|
@@ -25,11 +26,12 @@ const DisplayContent = memo<{
|
|
|
25
26
|
hasImages,
|
|
26
27
|
isMultimodal,
|
|
27
28
|
tempDisplayContent,
|
|
29
|
+
id,
|
|
28
30
|
}) => {
|
|
29
31
|
const message = normalizeThinkTags(processWithArtifact(content));
|
|
30
32
|
if (isToolCallGenerating) return;
|
|
31
33
|
|
|
32
|
-
if ((!content && !hasImages) || content === LOADING_FLAT) return <
|
|
34
|
+
if ((!content && !hasImages) || content === LOADING_FLAT) return <ContentLoading id={id} />;
|
|
33
35
|
|
|
34
36
|
const contentParts = isMultimodal ? deserializeParts(tempDisplayContent || content) : null;
|
|
35
37
|
|
|
@@ -41,10 +41,10 @@ const OllamaModelDownloader = memo<OllamaModelDownloaderProps>(
|
|
|
41
41
|
isValidating: isDownloading,
|
|
42
42
|
error,
|
|
43
43
|
} = useActionSWR(
|
|
44
|
-
[modelToPull],
|
|
45
|
-
async (
|
|
44
|
+
['ollama.downloadModel', modelToPull],
|
|
45
|
+
async () => {
|
|
46
46
|
await modelsService.downloadModel(
|
|
47
|
-
{ model, provider: 'ollama' },
|
|
47
|
+
{ model: modelToPull, provider: 'ollama' },
|
|
48
48
|
{ onProgress: handleProgress },
|
|
49
49
|
);
|
|
50
50
|
|
package/src/libs/swr/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useSWR, { type SWRHook } from 'swr';
|
|
2
|
-
|
|
2
|
+
import useSWRMutation from 'swr/mutation';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* This type of request method is relatively flexible data, which will be triggered on the first time
|
|
@@ -63,20 +63,18 @@ export const useOnlyFetchOnceSWR: SWRHook = (key, fetch, config) =>
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* 这一类请求方法用于做操作触发,必须使用
|
|
67
|
-
* 可以很简单地完成 loading / error 态的交互处理,同时,相同 swr key 的请求会自动共享 loading态(例如新建助理按钮和右上角的 + 号)
|
|
66
|
+
* 这一类请求方法用于做操作触发,必须使用 mutate 来触发请求操作,好处是自带了 loading / error 状态。
|
|
67
|
+
* 可以很简单地完成 loading / error 态的交互处理,同时,相同 swr key 的请求会自动共享 loading 态(例如新建助理按钮和右上角的 + 号)
|
|
68
68
|
* 非常适用于新建等操作。
|
|
69
|
+
*
|
|
70
|
+
* 使用 useSWRMutation 而非 useSWR,因为 useSWR 即使设置了 revalidateOnMount: false,
|
|
71
|
+
* 在缓存为空时仍会自动调用 fetcher。而 useSWRMutation 只会在手动调用 trigger 时执行。
|
|
69
72
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
revalidateOnFocus: false,
|
|
76
|
-
revalidateOnMount: false,
|
|
77
|
-
revalidateOnReconnect: false,
|
|
78
|
-
...config,
|
|
79
|
-
});
|
|
73
|
+
export const useActionSWR = <T>(key: string | any[], fetcher: () => Promise<T>, config?: any) => {
|
|
74
|
+
const { trigger, isMutating, ...rest } = useSWRMutation(key, fetcher, config);
|
|
75
|
+
// Return with legacy property names for backward compatibility
|
|
76
|
+
return { ...rest, isValidating: isMutating, mutate: trigger };
|
|
77
|
+
};
|
|
80
78
|
|
|
81
79
|
export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
|
|
82
80
|
action: A;
|
|
@@ -229,6 +229,8 @@ export default {
|
|
|
229
229
|
'noMembersYet': "This group doesn't have any members yet. Click the + button to invite agents.",
|
|
230
230
|
'noSelectedAgents': 'No members selected yet',
|
|
231
231
|
'openInNewWindow': 'Open in New Window',
|
|
232
|
+
'operation.execAgentRuntime': 'Preparing response',
|
|
233
|
+
'operation.sendMessage': 'Sending message',
|
|
232
234
|
'owner': 'Group owner',
|
|
233
235
|
'pageCopilot.title': 'Page Agent',
|
|
234
236
|
'pageCopilot.welcome':
|
|
@@ -77,10 +77,10 @@ export const normalizeImageModel = async (
|
|
|
77
77
|
const fallbackParametersPromise = model.parameters
|
|
78
78
|
? Promise.resolve<ModelParamsSchema | undefined>(model.parameters)
|
|
79
79
|
: getModelPropertyWithFallback<ModelParamsSchema | undefined>(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
model.id,
|
|
81
|
+
'parameters',
|
|
82
|
+
model.providerId,
|
|
83
|
+
);
|
|
84
84
|
|
|
85
85
|
const modelWithPricing = model as AIImageModelCard;
|
|
86
86
|
const fallbackPricingPromise = modelWithPricing.pricing
|
|
@@ -260,6 +260,19 @@ export const createAiProviderSlice: StateCreator<
|
|
|
260
260
|
toggleProviderEnabled: async (id: string, enabled: boolean) => {
|
|
261
261
|
get().internal_toggleAiProviderLoading(id, true);
|
|
262
262
|
await aiProviderService.toggleProviderEnabled(id, enabled);
|
|
263
|
+
|
|
264
|
+
// Immediately update local aiProviderList to reflect the change
|
|
265
|
+
// This ensures the switch displays correctly without waiting for SWR refresh
|
|
266
|
+
set(
|
|
267
|
+
(state) => ({
|
|
268
|
+
aiProviderList: state.aiProviderList.map((item) =>
|
|
269
|
+
item.id === id ? { ...item, enabled } : item,
|
|
270
|
+
),
|
|
271
|
+
}),
|
|
272
|
+
false,
|
|
273
|
+
'toggleProviderEnabled/syncEnabled',
|
|
274
|
+
);
|
|
275
|
+
|
|
263
276
|
await get().refreshAiProviderList();
|
|
264
277
|
|
|
265
278
|
get().internal_toggleAiProviderLoading(id, false);
|
|
@@ -277,6 +290,61 @@ export const createAiProviderSlice: StateCreator<
|
|
|
277
290
|
updateAiProviderConfig: async (id, value) => {
|
|
278
291
|
get().internal_toggleAiProviderConfigUpdating(id, true);
|
|
279
292
|
await aiProviderService.updateAiProviderConfig(id, value);
|
|
293
|
+
|
|
294
|
+
// Immediately update local state for instant UI feedback
|
|
295
|
+
set(
|
|
296
|
+
(state) => {
|
|
297
|
+
const currentRuntimeConfig = state.aiProviderRuntimeConfig[id];
|
|
298
|
+
const currentDetailConfig = state.aiProviderDetailMap[id];
|
|
299
|
+
|
|
300
|
+
const updates: Partial<typeof currentRuntimeConfig> = {};
|
|
301
|
+
const detailUpdates: Partial<typeof currentDetailConfig> = {};
|
|
302
|
+
|
|
303
|
+
// Update fetchOnClient if changed
|
|
304
|
+
if (typeof value.fetchOnClient !== 'undefined') {
|
|
305
|
+
// Convert null to undefined to match the interface definition
|
|
306
|
+
const fetchOnClientValue = value.fetchOnClient === null ? undefined : value.fetchOnClient;
|
|
307
|
+
updates.fetchOnClient = fetchOnClientValue;
|
|
308
|
+
detailUpdates.fetchOnClient = fetchOnClientValue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update config.enableResponseApi if changed
|
|
312
|
+
if (value.config?.enableResponseApi !== undefined && currentRuntimeConfig?.config) {
|
|
313
|
+
updates.config = {
|
|
314
|
+
...currentRuntimeConfig.config,
|
|
315
|
+
enableResponseApi: value.config.enableResponseApi,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
// Update detail map for form display
|
|
321
|
+
aiProviderDetailMap:
|
|
322
|
+
currentDetailConfig && Object.keys(detailUpdates).length > 0
|
|
323
|
+
? {
|
|
324
|
+
...state.aiProviderDetailMap,
|
|
325
|
+
[id]: {
|
|
326
|
+
...currentDetailConfig,
|
|
327
|
+
...detailUpdates,
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: state.aiProviderDetailMap,
|
|
331
|
+
// Update runtime config for selectors
|
|
332
|
+
aiProviderRuntimeConfig:
|
|
333
|
+
currentRuntimeConfig && Object.keys(updates).length > 0
|
|
334
|
+
? {
|
|
335
|
+
...state.aiProviderRuntimeConfig,
|
|
336
|
+
[id]: {
|
|
337
|
+
...currentRuntimeConfig,
|
|
338
|
+
...updates,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
: state.aiProviderRuntimeConfig,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
false,
|
|
345
|
+
'updateAiProviderConfig/syncChanges',
|
|
346
|
+
);
|
|
347
|
+
|
|
280
348
|
await get().refreshAiProviderDetail();
|
|
281
349
|
|
|
282
350
|
get().internal_toggleAiProviderConfigUpdating(id, false);
|
|
@@ -204,8 +204,9 @@ export const conversationLifecycle: StateCreator<
|
|
|
204
204
|
);
|
|
205
205
|
get().internal_toggleMessageLoading(true, tempId);
|
|
206
206
|
|
|
207
|
-
// Associate temp
|
|
207
|
+
// Associate temp messages with operation
|
|
208
208
|
get().associateMessageWithOperation(tempId, operationId);
|
|
209
|
+
get().associateMessageWithOperation(tempAssistantId, operationId);
|
|
209
210
|
|
|
210
211
|
// Store editor state in operation metadata for cancel restoration
|
|
211
212
|
const jsonState = mainInputEditor?.getJSONState();
|