@lobehub/lobehub 2.0.0-next.223 → 2.0.0-next.225
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/.github/workflows/test.yml +45 -77
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +10 -0
- package/locales/en-US/common.json +3 -2
- package/package.json +1 -1
- package/packages/database/src/models/user.ts +33 -0
- package/packages/database/src/repositories/knowledge/index.ts +1 -1
- package/src/app/[variants]/router/index.tsx +8 -12
- package/src/business/client/hooks/useRenderBusinessChatErrorMessageExtra.tsx +10 -0
- package/src/features/CommandMenu/ContextCommands.tsx +97 -37
- package/src/features/CommandMenu/SearchResults.tsx +100 -276
- package/src/features/CommandMenu/components/CommandItem.tsx +1 -1
- package/src/features/CommandMenu/utils/contextCommands.ts +56 -1
- package/src/features/Conversation/Error/index.tsx +7 -1
- package/src/libs/redis/manager.ts +51 -15
- package/src/locales/default/common.ts +2 -2
|
@@ -6,9 +6,14 @@ permissions:
|
|
|
6
6
|
actions: write
|
|
7
7
|
contents: read
|
|
8
8
|
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
9
13
|
jobs:
|
|
10
14
|
# Check for duplicate runs
|
|
11
|
-
|
|
15
|
+
check-duplicate-run:
|
|
16
|
+
name: Check Duplicate Run
|
|
12
17
|
runs-on: ubuntu-latest
|
|
13
18
|
outputs:
|
|
14
19
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
|
@@ -16,69 +21,18 @@ jobs:
|
|
|
16
21
|
- id: skip_check
|
|
17
22
|
uses: fkirc/skip-duplicate-actions@v5
|
|
18
23
|
with:
|
|
19
|
-
concurrent_skipping:
|
|
20
|
-
skip_after_successful_duplicate:
|
|
24
|
+
concurrent_skipping: "same_content_newer"
|
|
25
|
+
skip_after_successful_duplicate: "true"
|
|
21
26
|
do_not_skip: '["workflow_dispatch", "schedule"]'
|
|
22
27
|
|
|
23
|
-
# Package tests -
|
|
24
|
-
test-intenral-packages:
|
|
25
|
-
needs: pre_job
|
|
26
|
-
if: needs.pre_job.outputs.should_skip != 'true'
|
|
27
|
-
runs-on: ubuntu-latest
|
|
28
|
-
strategy:
|
|
29
|
-
matrix:
|
|
30
|
-
package:
|
|
31
|
-
- file-loaders
|
|
32
|
-
- prompts
|
|
33
|
-
- model-runtime
|
|
34
|
-
- web-crawler
|
|
35
|
-
- electron-server-ipc
|
|
36
|
-
- utils
|
|
37
|
-
- python-interpreter
|
|
38
|
-
- context-engine
|
|
39
|
-
- agent-runtime
|
|
40
|
-
- conversation-flow
|
|
41
|
-
- ssrf-safe-fetch
|
|
42
|
-
- memory-user-memory
|
|
43
|
-
|
|
44
|
-
name: Test package ${{ matrix.package }}
|
|
45
|
-
|
|
46
|
-
steps:
|
|
47
|
-
- uses: actions/checkout@v6
|
|
48
|
-
|
|
49
|
-
- name: Setup Node.js
|
|
50
|
-
uses: actions/setup-node@v6
|
|
51
|
-
with:
|
|
52
|
-
node-version: 24.11.1
|
|
53
|
-
package-manager-cache: false
|
|
54
|
-
|
|
55
|
-
- name: Install bun
|
|
56
|
-
uses: oven-sh/setup-bun@v2
|
|
57
|
-
with:
|
|
58
|
-
bun-version: ${{ secrets.BUN_VERSION }}
|
|
59
|
-
|
|
60
|
-
- name: Install deps
|
|
61
|
-
run: bun i
|
|
62
|
-
|
|
63
|
-
- name: Test ${{ matrix.package }} package with coverage
|
|
64
|
-
run: bun run --filter @lobechat/${{ matrix.package }} test:coverage
|
|
65
|
-
|
|
66
|
-
- name: Upload ${{ matrix.package }} coverage to Codecov
|
|
67
|
-
uses: codecov/codecov-action@v5
|
|
68
|
-
with:
|
|
69
|
-
token: ${{ secrets.CODECOV_TOKEN }}
|
|
70
|
-
files: ./packages/${{ matrix.package }}/coverage/lcov.info
|
|
71
|
-
flags: packages/${{ matrix.package }}
|
|
72
|
-
|
|
28
|
+
# Package tests - all packages in single job to save runner resources
|
|
73
29
|
test-packages:
|
|
74
|
-
needs:
|
|
75
|
-
if: needs.
|
|
30
|
+
needs: check-duplicate-run
|
|
31
|
+
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
|
76
32
|
runs-on: ubuntu-latest
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
name: Test package ${{ matrix.package }}
|
|
33
|
+
name: Test Packages
|
|
34
|
+
env:
|
|
35
|
+
PACKAGES: "@lobechat/file-loaders @lobechat/prompts @lobechat/model-runtime @lobechat/web-crawler @lobechat/electron-server-ipc @lobechat/utils @lobechat/python-interpreter @lobechat/context-engine @lobechat/agent-runtime @lobechat/conversation-flow @lobechat/ssrf-safe-fetch @lobechat/memory-user-memory model-bank"
|
|
82
36
|
|
|
83
37
|
steps:
|
|
84
38
|
- uses: actions/checkout@v6
|
|
@@ -92,25 +46,40 @@ jobs:
|
|
|
92
46
|
- name: Install bun
|
|
93
47
|
uses: oven-sh/setup-bun@v2
|
|
94
48
|
with:
|
|
95
|
-
bun-version:
|
|
49
|
+
bun-version: ${{ secrets.BUN_VERSION }}
|
|
96
50
|
|
|
97
51
|
- name: Install deps
|
|
98
52
|
run: bun i
|
|
99
53
|
|
|
100
|
-
- name: Test
|
|
101
|
-
run:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
54
|
+
- name: Test packages with coverage
|
|
55
|
+
run: |
|
|
56
|
+
for package in $PACKAGES; do
|
|
57
|
+
echo "::group::Testing $package"
|
|
58
|
+
bun run --filter $package test:coverage
|
|
59
|
+
echo "::endgroup::"
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
- name: Upload coverage to Codecov
|
|
63
|
+
if: always()
|
|
64
|
+
run: |
|
|
65
|
+
curl -Os https://cli.codecov.io/latest/linux/codecov
|
|
66
|
+
chmod +x codecov
|
|
67
|
+
for package in $PACKAGES; do
|
|
68
|
+
dir="${package#@lobechat/}"
|
|
69
|
+
if [ -f "./packages/$dir/coverage/lcov.info" ]; then
|
|
70
|
+
echo "Uploading coverage for $dir..."
|
|
71
|
+
./codecov upload-process \
|
|
72
|
+
-t ${{ secrets.CODECOV_TOKEN }} \
|
|
73
|
+
-f ./packages/$dir/coverage/lcov.info \
|
|
74
|
+
-F packages/$dir \
|
|
75
|
+
--disable-search
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
109
78
|
|
|
110
79
|
# App tests
|
|
111
80
|
test-website:
|
|
112
|
-
needs:
|
|
113
|
-
if: needs.
|
|
81
|
+
needs: check-duplicate-run
|
|
82
|
+
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
|
114
83
|
name: Test Website
|
|
115
84
|
|
|
116
85
|
runs-on: ubuntu-latest
|
|
@@ -143,8 +112,8 @@ jobs:
|
|
|
143
112
|
flags: app
|
|
144
113
|
|
|
145
114
|
test-desktop:
|
|
146
|
-
needs:
|
|
147
|
-
if: needs.
|
|
115
|
+
needs: check-duplicate-run
|
|
116
|
+
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
|
148
117
|
name: Test Desktop App
|
|
149
118
|
|
|
150
119
|
runs-on: ubuntu-latest
|
|
@@ -185,8 +154,8 @@ jobs:
|
|
|
185
154
|
flags: desktop
|
|
186
155
|
|
|
187
156
|
test-databsae:
|
|
188
|
-
needs:
|
|
189
|
-
if: needs.
|
|
157
|
+
needs: check-duplicate-run
|
|
158
|
+
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
|
190
159
|
name: Test Database
|
|
191
160
|
|
|
192
161
|
runs-on: ubuntu-latest
|
|
@@ -199,7 +168,6 @@ jobs:
|
|
|
199
168
|
options: >-
|
|
200
169
|
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
|
201
170
|
|
|
202
|
-
|
|
203
171
|
ports:
|
|
204
172
|
- 5432:5432
|
|
205
173
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.225](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.224...v2.0.0-next.225)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-06**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **ModelSwitchPanel**: Add provider preference storage in By Model view.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **ModelSwitchPanel**: Add provider preference storage in By Model view, closes [#11246](https://github.com/lobehub/lobe-chat/issues/11246) ([d778093](https://github.com/lobehub/lobe-chat/commit/d778093))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.224](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.223...v2.0.0-next.224)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-06**</sup>
|
|
33
|
+
|
|
34
|
+
#### ♻ Code Refactoring
|
|
35
|
+
|
|
36
|
+
- **router**: Replace client-side rendering with dynamic import for DesktopClientRouter.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### Code refactoring
|
|
44
|
+
|
|
45
|
+
- **router**: Replace client-side rendering with dynamic import for DesktopClientRouter, closes [#11276](https://github.com/lobehub/lobe-chat/issues/11276) ([f50305b](https://github.com/lobehub/lobe-chat/commit/f50305b))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.223](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.222...v2.0.0-next.223)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-06**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
"cmdk.navigate": "Navigate",
|
|
120
120
|
"cmdk.newAgent": "Create New Agent",
|
|
121
121
|
"cmdk.newAgentTeam": "Create New Group",
|
|
122
|
-
"cmdk.newLibrary": "New Library",
|
|
123
|
-
"cmdk.newPage": "New Page",
|
|
122
|
+
"cmdk.newLibrary": "Create New Library",
|
|
123
|
+
"cmdk.newPage": "Create New Page",
|
|
124
124
|
"cmdk.newTopic": "New topic in current Agent",
|
|
125
125
|
"cmdk.noResults": "No results found",
|
|
126
126
|
"cmdk.openSettings": "Open Settings",
|
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
"cmdk.themeLight": "Light",
|
|
159
159
|
"cmdk.toOpen": "Open",
|
|
160
160
|
"cmdk.toSelect": "Select",
|
|
161
|
+
"cmdk.upgradePlan": "Upgrade Plan",
|
|
161
162
|
"confirm": "Confirm",
|
|
162
163
|
"contact": "Contact Us",
|
|
163
164
|
"copy": "Copy",
|
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.225",
|
|
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",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SSOProvider,
|
|
3
|
+
UserGeneralConfig,
|
|
3
4
|
UserGuide,
|
|
4
5
|
UserKeyVaults,
|
|
5
6
|
UserPreference,
|
|
@@ -45,6 +46,11 @@ export type ListUsersForMemoryExtractorOptions = {
|
|
|
45
46
|
whitelist?: string[];
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
export interface UserInfoForAIGeneration {
|
|
50
|
+
responseLanguage: string;
|
|
51
|
+
userName: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
export class UserModel {
|
|
49
55
|
private userId: string;
|
|
50
56
|
private db: LobeChatDatabase;
|
|
@@ -339,4 +345,31 @@ export class UserModel {
|
|
|
339
345
|
where,
|
|
340
346
|
});
|
|
341
347
|
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get user info for AI generation (name and language preference)
|
|
351
|
+
*/
|
|
352
|
+
static getInfoForAIGeneration = async (
|
|
353
|
+
db: LobeChatDatabase,
|
|
354
|
+
userId: string,
|
|
355
|
+
): Promise<UserInfoForAIGeneration> => {
|
|
356
|
+
const result = await db
|
|
357
|
+
.select({
|
|
358
|
+
firstName: users.firstName,
|
|
359
|
+
fullName: users.fullName,
|
|
360
|
+
general: userSettings.general,
|
|
361
|
+
})
|
|
362
|
+
.from(users)
|
|
363
|
+
.leftJoin(userSettings, eq(users.id, userSettings.id))
|
|
364
|
+
.where(eq(users.id, userId))
|
|
365
|
+
.limit(1);
|
|
366
|
+
|
|
367
|
+
const user = result[0];
|
|
368
|
+
const general = user?.general as UserGeneralConfig | undefined;
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
responseLanguage: general?.responseLanguage || 'en-US',
|
|
372
|
+
userName: user?.fullName || user?.firstName || 'User',
|
|
373
|
+
};
|
|
374
|
+
};
|
|
342
375
|
}
|
|
@@ -29,7 +29,7 @@ export interface KnowledgeItem {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Resources Repository - combines files and documents into a unified interface
|
|
33
33
|
*/
|
|
34
34
|
export class KnowledgeRepo {
|
|
35
35
|
private userId: string;
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import Loading from '@/components/Loading/BrandTextLoading';
|
|
6
|
+
|
|
7
|
+
const DesktopRouterClient = dynamic(() => import('./DesktopClientRouter'), {
|
|
8
|
+
loading: () => <Loading debugId="DesktopRouter" />,
|
|
9
|
+
ssr: false,
|
|
10
|
+
});
|
|
6
11
|
|
|
7
|
-
const useIsClient = () => {
|
|
8
|
-
const [isClient, setIsClient] = useState(false);
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
setIsClient(true);
|
|
11
|
-
}, []);
|
|
12
|
-
return isClient;
|
|
13
|
-
};
|
|
14
12
|
const DesktopRouter = () => {
|
|
15
|
-
|
|
16
|
-
if (!isClient) return null;
|
|
17
|
-
return <DesktopClientRouter />;
|
|
13
|
+
return <DesktopRouterClient />;
|
|
18
14
|
};
|
|
19
15
|
|
|
20
16
|
export default DesktopRouter;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ChatMessageError } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
export default function useRenderBusinessChatErrorMessageExtra(
|
|
4
|
+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
|
5
|
+
error: ChatMessageError | null | undefined,
|
|
6
|
+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
|
7
|
+
messageId: string,
|
|
8
|
+
) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
@@ -6,11 +6,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
6
6
|
import { useCommandMenuContext } from './CommandMenuContext';
|
|
7
7
|
import { CommandItem } from './components';
|
|
8
8
|
import { useCommandMenu } from './useCommandMenu';
|
|
9
|
-
import { getContextCommands } from './utils/contextCommands';
|
|
9
|
+
import { CONTEXT_COMMANDS, getContextCommands } from './utils/contextCommands';
|
|
10
10
|
|
|
11
11
|
const ContextCommands = memo(() => {
|
|
12
12
|
const { t } = useTranslation('setting');
|
|
13
13
|
const { t: tAuth } = useTranslation('auth');
|
|
14
|
+
const { t: tSubscription } = useTranslation('subscription');
|
|
14
15
|
const { t: tCommon } = useTranslation('common');
|
|
15
16
|
const { handleNavigate } = useCommandMenu();
|
|
16
17
|
const { menuContext, pathname } = useCommandMenuContext();
|
|
@@ -23,48 +24,107 @@ const ContextCommands = memo(() => {
|
|
|
23
24
|
|
|
24
25
|
const commands = getContextCommands(menuContext, subPath);
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
// Get settings commands to show globally (when not in settings context)
|
|
28
|
+
const globalSettingsCommands = useMemo(() => {
|
|
29
|
+
if (menuContext === 'settings') return [];
|
|
30
|
+
return CONTEXT_COMMANDS.settings;
|
|
31
|
+
}, [menuContext]);
|
|
32
|
+
|
|
33
|
+
const hasCommands = commands.length > 0 || globalSettingsCommands.length > 0;
|
|
34
|
+
|
|
35
|
+
if (!hasCommands) return null;
|
|
27
36
|
|
|
28
37
|
// Get localized context name
|
|
29
38
|
const contextName = tCommon(`cmdk.context.${menuContext}`, { defaultValue: menuContext });
|
|
39
|
+
const settingsContextName = tCommon('cmdk.context.settings', { defaultValue: 'settings' });
|
|
30
40
|
|
|
31
41
|
return (
|
|
32
|
-
|
|
33
|
-
{commands
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
label =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
<>
|
|
43
|
+
{/* Current context commands */}
|
|
44
|
+
{commands.length > 0 && (
|
|
45
|
+
<Command.Group>
|
|
46
|
+
{commands.map((cmd) => {
|
|
47
|
+
const Icon = cmd.icon;
|
|
48
|
+
// Get localized label using the correct namespace
|
|
49
|
+
let label = cmd.label;
|
|
50
|
+
if (cmd.labelKey) {
|
|
51
|
+
if (cmd.labelNamespace === 'auth') {
|
|
52
|
+
label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
|
|
53
|
+
} else if (cmd.labelNamespace === 'subscription') {
|
|
54
|
+
label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
|
|
55
|
+
} else {
|
|
56
|
+
label = t(cmd.labelKey, { defaultValue: cmd.label });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const searchValue = `${contextName} ${label} ${cmd.keywords.join(' ')}`;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<CommandItem
|
|
63
|
+
icon={<Icon />}
|
|
64
|
+
key={cmd.path}
|
|
65
|
+
onSelect={() => handleNavigate(cmd.path)}
|
|
66
|
+
value={searchValue}
|
|
67
|
+
>
|
|
68
|
+
<span style={{ opacity: 0.5 }}>{contextName}</span>
|
|
69
|
+
<ChevronRight
|
|
70
|
+
size={14}
|
|
71
|
+
style={{
|
|
72
|
+
display: 'inline',
|
|
73
|
+
marginInline: '6px',
|
|
74
|
+
opacity: 0.5,
|
|
75
|
+
verticalAlign: 'middle',
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
{label}
|
|
79
|
+
</CommandItem>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</Command.Group>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Global settings commands (searchable from any page) */}
|
|
86
|
+
{globalSettingsCommands.length > 0 && (
|
|
87
|
+
<Command.Group>
|
|
88
|
+
{globalSettingsCommands.map((cmd) => {
|
|
89
|
+
const Icon = cmd.icon;
|
|
90
|
+
// Get localized label using the correct namespace
|
|
91
|
+
let label = cmd.label;
|
|
92
|
+
if (cmd.labelKey) {
|
|
93
|
+
if (cmd.labelNamespace === 'auth') {
|
|
94
|
+
label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
|
|
95
|
+
} else if (cmd.labelNamespace === 'subscription') {
|
|
96
|
+
label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
|
|
97
|
+
} else {
|
|
98
|
+
label = t(cmd.labelKey, { defaultValue: cmd.label });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const searchValue = `${settingsContextName} ${label} ${cmd.keywords.join(' ')}`;
|
|
45
102
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
return (
|
|
104
|
+
<CommandItem
|
|
105
|
+
icon={<Icon />}
|
|
106
|
+
key={cmd.path}
|
|
107
|
+
onSelect={() => handleNavigate(cmd.path)}
|
|
108
|
+
unpinned={true}
|
|
109
|
+
value={searchValue}
|
|
110
|
+
>
|
|
111
|
+
<span style={{ opacity: 0.5 }}>{settingsContextName}</span>
|
|
112
|
+
<ChevronRight
|
|
113
|
+
size={14}
|
|
114
|
+
style={{
|
|
115
|
+
display: 'inline',
|
|
116
|
+
marginInline: '6px',
|
|
117
|
+
opacity: 0.5,
|
|
118
|
+
verticalAlign: 'middle',
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
{label}
|
|
122
|
+
</CommandItem>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</Command.Group>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
68
128
|
);
|
|
69
129
|
});
|
|
70
130
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Command } from 'cmdk';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Bot,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
FileText,
|
|
7
|
+
MessageCircle,
|
|
8
|
+
MessageSquare,
|
|
9
|
+
Plug,
|
|
10
|
+
Puzzle,
|
|
11
|
+
Sparkles,
|
|
12
|
+
} from 'lucide-react';
|
|
4
13
|
import { markdownToTxt } from 'markdown-to-txt';
|
|
5
14
|
import { memo } from 'react';
|
|
6
15
|
import { useTranslation } from 'react-i18next';
|
|
@@ -8,7 +17,6 @@ import { useNavigate } from 'react-router-dom';
|
|
|
8
17
|
|
|
9
18
|
import type { SearchResult } from '@/database/repositories/search';
|
|
10
19
|
|
|
11
|
-
import { useCommandMenuContext } from './CommandMenuContext';
|
|
12
20
|
import { CommandItem } from './components';
|
|
13
21
|
import { styles } from './styles';
|
|
14
22
|
import type { ValidSearchType } from './utils/queryParser';
|
|
@@ -29,7 +37,6 @@ const SearchResults = memo<SearchResultsProps>(
|
|
|
29
37
|
({ isLoading, onClose, onSetTypeFilter, results, searchQuery, typeFilter }) => {
|
|
30
38
|
const { t } = useTranslation('common');
|
|
31
39
|
const navigate = useNavigate();
|
|
32
|
-
const { menuContext } = useCommandMenuContext();
|
|
33
40
|
|
|
34
41
|
const handleNavigate = (result: SearchResult) => {
|
|
35
42
|
switch (result.type) {
|
|
@@ -146,15 +153,6 @@ const SearchResults = memo<SearchResultsProps>(
|
|
|
146
153
|
}
|
|
147
154
|
};
|
|
148
155
|
|
|
149
|
-
// Get trailing label for search results (shows "Market" for marketplace items)
|
|
150
|
-
const getTrailingLabel = (type: SearchResult['type']) => {
|
|
151
|
-
// Marketplace items: MCP, plugins, assistants
|
|
152
|
-
if (type === 'mcp' || type === 'plugin' || type === 'communityAgent') {
|
|
153
|
-
return t('cmdk.search.market');
|
|
154
|
-
}
|
|
155
|
-
return getTypeLabel(type);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
156
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
159
157
|
const getItemValue = (result: SearchResult) => {
|
|
160
158
|
const meta = [result.title, result.description].filter(Boolean).join(' ');
|
|
@@ -193,26 +191,6 @@ const SearchResults = memo<SearchResultsProps>(
|
|
|
193
191
|
onSetTypeFilter(type);
|
|
194
192
|
};
|
|
195
193
|
|
|
196
|
-
// Helper to render "Search More" button
|
|
197
|
-
const renderSearchMore = (type: ValidSearchType, count: number) => {
|
|
198
|
-
// Don't show if already filtering by this type
|
|
199
|
-
if (typeFilter) return null;
|
|
200
|
-
|
|
201
|
-
// Show if there are results (might have more)
|
|
202
|
-
if (count === 0) return null;
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<CommandItem
|
|
206
|
-
forceMount
|
|
207
|
-
icon={getIcon(type)}
|
|
208
|
-
onSelect={() => handleSearchMore(type)}
|
|
209
|
-
title={t('cmdk.search.searchMore', { type: getTypeLabel(type) })}
|
|
210
|
-
value={`action-show-more-results-for-type-${type}`}
|
|
211
|
-
variant="detailed"
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
194
|
const hasResults = results.length > 0;
|
|
217
195
|
|
|
218
196
|
// Group results by type
|
|
@@ -225,289 +203,135 @@ const SearchResults = memo<SearchResultsProps>(
|
|
|
225
203
|
const pluginResults = results.filter((r) => r.type === 'plugin');
|
|
226
204
|
const assistantResults = results.filter((r) => r.type === 'communityAgent');
|
|
227
205
|
|
|
228
|
-
// Detect context types
|
|
229
|
-
const isResourceContext = menuContext === 'resource';
|
|
230
|
-
const isPageContext = menuContext === 'page';
|
|
231
|
-
|
|
232
206
|
// Don't render anything if no results and not loading
|
|
233
207
|
if (!hasResults && !isLoading) {
|
|
234
208
|
return null;
|
|
235
209
|
}
|
|
236
210
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
description={result.type === 'file' ? result.fileType : undefined}
|
|
264
|
-
icon={getIcon(result.type)}
|
|
265
|
-
key={`file-page-context-${result.id}`}
|
|
266
|
-
onSelect={() => handleNavigate(result)}
|
|
267
|
-
title={result.title}
|
|
268
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
269
|
-
value={getItemValue(result)}
|
|
270
|
-
variant="detailed"
|
|
271
|
-
/>
|
|
272
|
-
))}
|
|
273
|
-
{renderSearchMore('file', fileResults.length)}
|
|
274
|
-
</Command.Group>
|
|
275
|
-
)}
|
|
211
|
+
// Render a single result item with type prefix (like "Message > content")
|
|
212
|
+
const renderResultItem = (result: SearchResult) => {
|
|
213
|
+
const typeLabel = getTypeLabel(result.type);
|
|
214
|
+
const subtitle = getSubtitle(result);
|
|
215
|
+
|
|
216
|
+
// Hide type prefix when filtering by specific type
|
|
217
|
+
const showTypePrefix = !typeFilter;
|
|
218
|
+
|
|
219
|
+
// Create title with or without type prefix
|
|
220
|
+
const titleWithPrefix = showTypePrefix ? (
|
|
221
|
+
<>
|
|
222
|
+
<span style={{ opacity: 0.5 }}>{typeLabel}</span>
|
|
223
|
+
<ChevronRight
|
|
224
|
+
size={14}
|
|
225
|
+
style={{
|
|
226
|
+
display: 'inline',
|
|
227
|
+
marginInline: '6px',
|
|
228
|
+
opacity: 0.5,
|
|
229
|
+
verticalAlign: 'middle',
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
{result.title}
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
result.title
|
|
236
|
+
);
|
|
276
237
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/>
|
|
290
|
-
))}
|
|
291
|
-
{renderSearchMore('agent', agentResults.length)}
|
|
292
|
-
</Command.Group>
|
|
293
|
-
)}
|
|
238
|
+
return (
|
|
239
|
+
<CommandItem
|
|
240
|
+
description={subtitle}
|
|
241
|
+
icon={getIcon(result.type)}
|
|
242
|
+
key={result.id}
|
|
243
|
+
onSelect={() => handleNavigate(result)}
|
|
244
|
+
title={titleWithPrefix}
|
|
245
|
+
value={getItemValue(result)}
|
|
246
|
+
variant="detailed"
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
294
250
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
description={getSubtitle(result)}
|
|
300
|
-
icon={getIcon(result.type)}
|
|
301
|
-
key={`topic-page-context-${result.id}`}
|
|
302
|
-
onSelect={() => handleNavigate(result)}
|
|
303
|
-
title={result.title}
|
|
304
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
305
|
-
value={getItemValue(result)}
|
|
306
|
-
variant="detailed"
|
|
307
|
-
/>
|
|
308
|
-
))}
|
|
309
|
-
{renderSearchMore('topic', topicResults.length)}
|
|
310
|
-
</Command.Group>
|
|
311
|
-
)}
|
|
251
|
+
// Helper to render "Search More" button
|
|
252
|
+
const renderSearchMore = (type: ValidSearchType, count: number) => {
|
|
253
|
+
// Don't show if already filtering by this type
|
|
254
|
+
if (typeFilter) return null;
|
|
312
255
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
{messageResults.map((result) => (
|
|
316
|
-
<CommandItem
|
|
317
|
-
description={getSubtitle(result)}
|
|
318
|
-
icon={getIcon(result.type)}
|
|
319
|
-
key={`message-page-context-${result.id}`}
|
|
320
|
-
onSelect={() => handleNavigate(result)}
|
|
321
|
-
title={result.title}
|
|
322
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
323
|
-
value={getItemValue(result)}
|
|
324
|
-
variant="detailed"
|
|
325
|
-
/>
|
|
326
|
-
))}
|
|
327
|
-
{renderSearchMore('message', messageResults.length)}
|
|
328
|
-
</Command.Group>
|
|
329
|
-
)}
|
|
256
|
+
// Show if there are results (might have more)
|
|
257
|
+
if (count === 0) return null;
|
|
330
258
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<Command.Group heading={t('cmdk.search.pages')} key="pages-resource">
|
|
334
|
-
{pageResults.map((result) => (
|
|
335
|
-
<CommandItem
|
|
336
|
-
description={result.description}
|
|
337
|
-
icon={getIcon(result.type)}
|
|
338
|
-
key={`page-resource-${result.id}`}
|
|
339
|
-
onSelect={() => handleNavigate(result)}
|
|
340
|
-
title={result.title}
|
|
341
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
342
|
-
value={getItemValue(result)}
|
|
343
|
-
variant="detailed"
|
|
344
|
-
/>
|
|
345
|
-
))}
|
|
346
|
-
{renderSearchMore('page', pageResults.length)}
|
|
347
|
-
</Command.Group>
|
|
348
|
-
)}
|
|
259
|
+
const typeLabel = getTypeLabel(type);
|
|
260
|
+
const titleText = `${t('cmdk.search.searchMore', { type: typeLabel })} with "${searchQuery}"`;
|
|
349
261
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)}
|
|
262
|
+
return (
|
|
263
|
+
<Command.Item
|
|
264
|
+
forceMount
|
|
265
|
+
key={`search-more-${type}`}
|
|
266
|
+
keywords={[`zzz-action-${type}`]}
|
|
267
|
+
onSelect={() => handleSearchMore(type)}
|
|
268
|
+
value={`zzz-action-${type}-search-more`}
|
|
269
|
+
>
|
|
270
|
+
<div className={styles.itemContent}>
|
|
271
|
+
<div className={styles.itemIcon}>{getIcon(type)}</div>
|
|
272
|
+
<div className={styles.itemDetails}>
|
|
273
|
+
<div className={styles.itemTitle}>{titleText}</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</Command.Item>
|
|
277
|
+
);
|
|
278
|
+
};
|
|
368
279
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
key={`message-${result.id}`}
|
|
376
|
-
onSelect={() => handleNavigate(result)}
|
|
377
|
-
title={result.title}
|
|
378
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
379
|
-
value={getItemValue(result)}
|
|
380
|
-
variant="detailed"
|
|
381
|
-
/>
|
|
382
|
-
))}
|
|
280
|
+
return (
|
|
281
|
+
<>
|
|
282
|
+
{/* Render search results grouped by type without headers */}
|
|
283
|
+
{messageResults.length > 0 && (
|
|
284
|
+
<Command.Group>
|
|
285
|
+
{messageResults.map((result) => renderResultItem(result))}
|
|
383
286
|
{renderSearchMore('message', messageResults.length)}
|
|
384
287
|
</Command.Group>
|
|
385
288
|
)}
|
|
386
289
|
|
|
387
|
-
{
|
|
388
|
-
<Command.Group
|
|
389
|
-
{agentResults.map((result) => (
|
|
390
|
-
<CommandItem
|
|
391
|
-
description={getDescription(result)}
|
|
392
|
-
icon={getIcon(result.type)}
|
|
393
|
-
key={`agent-${result.id}`}
|
|
394
|
-
onSelect={() => handleNavigate(result)}
|
|
395
|
-
title={result.title}
|
|
396
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
397
|
-
value={getItemValue(result)}
|
|
398
|
-
variant="detailed"
|
|
399
|
-
/>
|
|
400
|
-
))}
|
|
290
|
+
{agentResults.length > 0 && (
|
|
291
|
+
<Command.Group>
|
|
292
|
+
{agentResults.map((result) => renderResultItem(result))}
|
|
401
293
|
{renderSearchMore('agent', agentResults.length)}
|
|
402
294
|
</Command.Group>
|
|
403
295
|
)}
|
|
404
296
|
|
|
405
|
-
{
|
|
406
|
-
<Command.Group
|
|
407
|
-
{topicResults.map((result) => (
|
|
408
|
-
<CommandItem
|
|
409
|
-
description={getSubtitle(result)}
|
|
410
|
-
icon={getIcon(result.type)}
|
|
411
|
-
key={`topic-${result.id}`}
|
|
412
|
-
onSelect={() => handleNavigate(result)}
|
|
413
|
-
title={result.title}
|
|
414
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
415
|
-
value={getItemValue(result)}
|
|
416
|
-
variant="detailed"
|
|
417
|
-
/>
|
|
418
|
-
))}
|
|
297
|
+
{topicResults.length > 0 && (
|
|
298
|
+
<Command.Group>
|
|
299
|
+
{topicResults.map((result) => renderResultItem(result))}
|
|
419
300
|
{renderSearchMore('topic', topicResults.length)}
|
|
420
301
|
</Command.Group>
|
|
421
302
|
)}
|
|
422
303
|
|
|
423
|
-
{
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
{pageResults.map((result) => (
|
|
427
|
-
<CommandItem
|
|
428
|
-
description={result.description}
|
|
429
|
-
icon={getIcon(result.type)}
|
|
430
|
-
key={`page-normal-${result.id}`}
|
|
431
|
-
onSelect={() => handleNavigate(result)}
|
|
432
|
-
title={result.title}
|
|
433
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
434
|
-
value={getItemValue(result)}
|
|
435
|
-
variant="detailed"
|
|
436
|
-
/>
|
|
437
|
-
))}
|
|
304
|
+
{pageResults.length > 0 && (
|
|
305
|
+
<Command.Group>
|
|
306
|
+
{pageResults.map((result) => renderResultItem(result))}
|
|
438
307
|
{renderSearchMore('page', pageResults.length)}
|
|
439
308
|
</Command.Group>
|
|
440
309
|
)}
|
|
441
310
|
|
|
442
|
-
{
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
{fileResults.map((result) => (
|
|
446
|
-
<CommandItem
|
|
447
|
-
description={result.type === 'file' ? result.fileType : undefined}
|
|
448
|
-
icon={getIcon(result.type)}
|
|
449
|
-
key={`file-${result.id}`}
|
|
450
|
-
onSelect={() => handleNavigate(result)}
|
|
451
|
-
title={result.title}
|
|
452
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
453
|
-
value={getItemValue(result)}
|
|
454
|
-
variant="detailed"
|
|
455
|
-
/>
|
|
456
|
-
))}
|
|
311
|
+
{fileResults.length > 0 && (
|
|
312
|
+
<Command.Group>
|
|
313
|
+
{fileResults.map((result) => renderResultItem(result))}
|
|
457
314
|
{renderSearchMore('file', fileResults.length)}
|
|
458
315
|
</Command.Group>
|
|
459
316
|
)}
|
|
460
317
|
|
|
461
|
-
{
|
|
462
|
-
<Command.Group
|
|
463
|
-
{mcpResults.map((result) => (
|
|
464
|
-
<CommandItem
|
|
465
|
-
description={getDescription(result)}
|
|
466
|
-
icon={getIcon(result.type)}
|
|
467
|
-
key={`mcp-${result.id}`}
|
|
468
|
-
onSelect={() => handleNavigate(result)}
|
|
469
|
-
title={result.title}
|
|
470
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
471
|
-
value={getItemValue(result)}
|
|
472
|
-
variant="detailed"
|
|
473
|
-
/>
|
|
474
|
-
))}
|
|
318
|
+
{mcpResults.length > 0 && (
|
|
319
|
+
<Command.Group>
|
|
320
|
+
{mcpResults.map((result) => renderResultItem(result))}
|
|
475
321
|
{renderSearchMore('mcp', mcpResults.length)}
|
|
476
322
|
</Command.Group>
|
|
477
323
|
)}
|
|
478
324
|
|
|
479
|
-
{
|
|
480
|
-
<Command.Group
|
|
481
|
-
{pluginResults.map((result) => (
|
|
482
|
-
<CommandItem
|
|
483
|
-
description={getDescription(result)}
|
|
484
|
-
icon={getIcon(result.type)}
|
|
485
|
-
key={`plugin-${result.id}`}
|
|
486
|
-
onSelect={() => handleNavigate(result)}
|
|
487
|
-
title={result.title}
|
|
488
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
489
|
-
value={getItemValue(result)}
|
|
490
|
-
variant="detailed"
|
|
491
|
-
/>
|
|
492
|
-
))}
|
|
325
|
+
{pluginResults.length > 0 && (
|
|
326
|
+
<Command.Group>
|
|
327
|
+
{pluginResults.map((result) => renderResultItem(result))}
|
|
493
328
|
{renderSearchMore('plugin', pluginResults.length)}
|
|
494
329
|
</Command.Group>
|
|
495
330
|
)}
|
|
496
331
|
|
|
497
|
-
{
|
|
498
|
-
<Command.Group
|
|
499
|
-
{assistantResults.map((result) => (
|
|
500
|
-
<CommandItem
|
|
501
|
-
description={getDescription(result)}
|
|
502
|
-
icon={getIcon(result.type)}
|
|
503
|
-
key={`assistant-${result.id}`}
|
|
504
|
-
onSelect={() => handleNavigate(result)}
|
|
505
|
-
title={result.title}
|
|
506
|
-
trailingLabel={getTrailingLabel(result.type)}
|
|
507
|
-
value={getItemValue(result)}
|
|
508
|
-
variant="detailed"
|
|
509
|
-
/>
|
|
510
|
-
))}
|
|
332
|
+
{assistantResults.length > 0 && (
|
|
333
|
+
<Command.Group>
|
|
334
|
+
{assistantResults.map((result) => renderResultItem(result))}
|
|
511
335
|
{renderSearchMore('communityAgent', assistantResults.length)}
|
|
512
336
|
</Command.Group>
|
|
513
337
|
)}
|
|
@@ -5,7 +5,7 @@ import { cloneElement, isValidElement, memo } from 'react';
|
|
|
5
5
|
import { useCommandMenuContext } from '../CommandMenuContext';
|
|
6
6
|
import { styles } from '../styles';
|
|
7
7
|
|
|
8
|
-
type BaseCommandItemProps = Omit<ComponentProps<typeof Command.Item>, 'children'> & {
|
|
8
|
+
type BaseCommandItemProps = Omit<ComponentProps<typeof Command.Item>, 'children' | 'title'> & {
|
|
9
9
|
/**
|
|
10
10
|
* Hide the item from default view but keep it searchable
|
|
11
11
|
* When true, the item won't show in the default list but will appear in search results
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
|
1
2
|
import type { LucideIcon } from 'lucide-react';
|
|
2
3
|
import {
|
|
3
4
|
Brain,
|
|
4
5
|
ChartColumnBigIcon,
|
|
6
|
+
Coins,
|
|
7
|
+
CreditCard,
|
|
5
8
|
EthernetPort,
|
|
9
|
+
Gift,
|
|
6
10
|
Image as ImageIcon,
|
|
7
11
|
Info,
|
|
8
12
|
KeyIcon,
|
|
9
13
|
KeyboardIcon,
|
|
14
|
+
Map,
|
|
10
15
|
Palette as PaletteIcon,
|
|
16
|
+
PieChart,
|
|
11
17
|
UserCircle,
|
|
12
18
|
} from 'lucide-react';
|
|
13
19
|
|
|
@@ -18,7 +24,7 @@ export interface ContextCommand {
|
|
|
18
24
|
keywords: string[];
|
|
19
25
|
label: string;
|
|
20
26
|
labelKey?: string; // i18n key for the label
|
|
21
|
-
labelNamespace?: 'setting' | 'auth'; // i18n namespace for the label
|
|
27
|
+
labelNamespace?: 'setting' | 'auth' | 'subscription'; // i18n namespace for the label
|
|
22
28
|
path: string;
|
|
23
29
|
subPath: string;
|
|
24
30
|
}
|
|
@@ -114,6 +120,55 @@ export const CONTEXT_COMMANDS: Record<ContextType, ContextCommand[]> = {
|
|
|
114
120
|
path: '/settings/about',
|
|
115
121
|
subPath: 'about',
|
|
116
122
|
},
|
|
123
|
+
...(ENABLE_BUSINESS_FEATURES
|
|
124
|
+
? [
|
|
125
|
+
{
|
|
126
|
+
icon: Map,
|
|
127
|
+
keywords: ['subscription', 'plan', 'upgrade', 'pricing'],
|
|
128
|
+
label: 'Subscription Plans',
|
|
129
|
+
labelKey: 'tab.plans',
|
|
130
|
+
labelNamespace: 'subscription' as const,
|
|
131
|
+
path: '/settings/plans',
|
|
132
|
+
subPath: 'plans',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
icon: Coins,
|
|
136
|
+
keywords: ['funds', 'balance', 'credit', 'money'],
|
|
137
|
+
label: 'Funds',
|
|
138
|
+
labelKey: 'tab.funds',
|
|
139
|
+
labelNamespace: 'subscription' as const,
|
|
140
|
+
path: '/settings/funds',
|
|
141
|
+
subPath: 'funds',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
icon: PieChart,
|
|
145
|
+
keywords: ['usage', 'statistics', 'consumption', 'quota'],
|
|
146
|
+
label: 'Usage',
|
|
147
|
+
labelKey: 'tab.usage',
|
|
148
|
+
labelNamespace: 'subscription' as const,
|
|
149
|
+
path: '/settings/usage',
|
|
150
|
+
subPath: 'usage',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
icon: CreditCard,
|
|
154
|
+
keywords: ['billing', 'payment', 'invoice', 'transaction'],
|
|
155
|
+
label: 'Billing',
|
|
156
|
+
labelKey: 'tab.billing',
|
|
157
|
+
labelNamespace: 'subscription' as const,
|
|
158
|
+
path: '/settings/billing',
|
|
159
|
+
subPath: 'billing',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
icon: Gift,
|
|
163
|
+
keywords: ['referral', 'rewards', 'invite', 'bonus'],
|
|
164
|
+
label: 'Referral Rewards',
|
|
165
|
+
labelKey: 'tab.referral',
|
|
166
|
+
labelNamespace: 'subscription' as const,
|
|
167
|
+
path: '/settings/referral',
|
|
168
|
+
subPath: 'referral',
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
: []),
|
|
117
172
|
],
|
|
118
173
|
};
|
|
119
174
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
|
1
2
|
import { AgentRuntimeErrorType, type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
|
2
3
|
import { ChatErrorType, type ChatMessageError, type ErrorType } from '@lobechat/types';
|
|
3
4
|
import { type IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
@@ -6,6 +7,7 @@ import dynamic from 'next/dynamic';
|
|
|
6
7
|
import { memo, useMemo } from 'react';
|
|
7
8
|
import { useTranslation } from 'react-i18next';
|
|
8
9
|
|
|
10
|
+
import useRenderBusinessChatErrorMessageExtra from '@/business/client/hooks/useRenderBusinessChatErrorMessageExtra';
|
|
9
11
|
import ErrorContent from '@/features/Conversation/ChatItem/components/ErrorContent';
|
|
10
12
|
import { useProviderName } from '@/hooks/useProviderName';
|
|
11
13
|
|
|
@@ -103,7 +105,11 @@ interface ErrorExtraProps {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
const ErrorMessageExtra = memo<ErrorExtraProps>(({ error: alertError, data }) => {
|
|
106
|
-
const error = data.error
|
|
108
|
+
const error = data.error;
|
|
109
|
+
const businessChatErrorMessageExtra = useRenderBusinessChatErrorMessageExtra(error, data.id);
|
|
110
|
+
if (ENABLE_BUSINESS_FEATURES && businessChatErrorMessageExtra)
|
|
111
|
+
return businessChatErrorMessageExtra;
|
|
112
|
+
|
|
107
113
|
if (!error?.type) return;
|
|
108
114
|
|
|
109
115
|
switch (error.type) {
|
|
@@ -2,6 +2,33 @@ import { IoRedisRedisProvider } from './redis';
|
|
|
2
2
|
import { type BaseRedisProvider, type RedisConfig } from './types';
|
|
3
3
|
import { UpstashRedisProvider } from './upstash';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Create a Redis provider instance based on config
|
|
7
|
+
*
|
|
8
|
+
* @param config - Redis config
|
|
9
|
+
* @param prefix - Optional custom prefix to override config.prefix
|
|
10
|
+
* @returns Provider instance or null if disabled/unsupported
|
|
11
|
+
*/
|
|
12
|
+
const createProvider = (config: RedisConfig, prefix?: string): BaseRedisProvider | null => {
|
|
13
|
+
if (!config.enabled) return null;
|
|
14
|
+
|
|
15
|
+
const actualPrefix = prefix ?? config.prefix;
|
|
16
|
+
|
|
17
|
+
if (config.provider === 'redis') {
|
|
18
|
+
return new IoRedisRedisProvider({ ...config, prefix: actualPrefix });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (config.provider === 'upstash') {
|
|
22
|
+
return new UpstashRedisProvider({
|
|
23
|
+
prefix: actualPrefix,
|
|
24
|
+
token: config.token,
|
|
25
|
+
url: config.url,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
5
32
|
class RedisManager {
|
|
6
33
|
private static instance: BaseRedisProvider | null = null;
|
|
7
34
|
// NOTICE: initPromise keeps concurrent initialize() calls sharing the same in-flight setup,
|
|
@@ -13,25 +40,13 @@ class RedisManager {
|
|
|
13
40
|
if (RedisManager.initPromise) return RedisManager.initPromise;
|
|
14
41
|
|
|
15
42
|
RedisManager.initPromise = (async () => {
|
|
16
|
-
|
|
43
|
+
const provider = createProvider(config);
|
|
44
|
+
|
|
45
|
+
if (!provider) {
|
|
17
46
|
RedisManager.instance = null;
|
|
18
47
|
return null;
|
|
19
48
|
}
|
|
20
49
|
|
|
21
|
-
let provider: BaseRedisProvider;
|
|
22
|
-
|
|
23
|
-
if (config.provider === 'redis') {
|
|
24
|
-
provider = new IoRedisRedisProvider(config);
|
|
25
|
-
} else if (config.provider === 'upstash') {
|
|
26
|
-
provider = new UpstashRedisProvider({
|
|
27
|
-
prefix: config.prefix,
|
|
28
|
-
token: config.token,
|
|
29
|
-
url: config.url,
|
|
30
|
-
});
|
|
31
|
-
} else {
|
|
32
|
-
throw new Error(`Unsupported redis provider: ${String((config as any).provider)}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
50
|
await provider.initialize();
|
|
36
51
|
RedisManager.instance = provider;
|
|
37
52
|
|
|
@@ -58,3 +73,24 @@ export const initializeRedis = (config: RedisConfig) => RedisManager.initialize(
|
|
|
58
73
|
export const resetRedisClient = () => RedisManager.reset();
|
|
59
74
|
export const isRedisEnabled = (config: RedisConfig) => config.enabled;
|
|
60
75
|
export { RedisManager };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a Redis client with custom prefix
|
|
79
|
+
*
|
|
80
|
+
* Unlike initializeRedis, this creates an independent client
|
|
81
|
+
* that doesn't share the singleton instance.
|
|
82
|
+
*
|
|
83
|
+
* @param config - Redis config
|
|
84
|
+
* @param prefix - Custom prefix for all keys (e.g., 'aiGeneration')
|
|
85
|
+
* @returns Redis client or null if Redis is disabled
|
|
86
|
+
*/
|
|
87
|
+
export const createRedisWithPrefix = async (
|
|
88
|
+
config: RedisConfig,
|
|
89
|
+
prefix: string,
|
|
90
|
+
): Promise<BaseRedisProvider | null> => {
|
|
91
|
+
const provider = createProvider(config, prefix);
|
|
92
|
+
if (!provider) return null;
|
|
93
|
+
|
|
94
|
+
await provider.initialize();
|
|
95
|
+
return provider;
|
|
96
|
+
};
|
|
@@ -133,8 +133,8 @@ export default {
|
|
|
133
133
|
'cmdk.navigate': 'Navigate',
|
|
134
134
|
'cmdk.newAgent': 'Create New Agent',
|
|
135
135
|
'cmdk.newAgentTeam': 'Create New Group',
|
|
136
|
-
'cmdk.newLibrary': 'New Library',
|
|
137
|
-
'cmdk.newPage': 'New Page',
|
|
136
|
+
'cmdk.newLibrary': 'Create New Library',
|
|
137
|
+
'cmdk.newPage': 'Create New Page',
|
|
138
138
|
'cmdk.newTopic': 'New topic in current Agent',
|
|
139
139
|
'cmdk.noResults': 'No results found',
|
|
140
140
|
'cmdk.openSettings': 'Open Settings',
|