@lobehub/lobehub 2.0.0-next.24 → 2.0.0-next.26
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 +50 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +1 -0
- package/e2e/src/features/discover/detail-pages.feature +95 -0
- package/e2e/src/features/discover/interactions.feature +113 -0
- package/e2e/src/steps/discover/detail-pages.steps.ts +295 -0
- package/e2e/src/steps/discover/interactions.steps.ts +451 -0
- package/package.json +1 -1
- package/packages/database/migrations/0043_add_ai_model_settings.sql +1 -0
- package/packages/database/migrations/meta/0043_snapshot.json +8419 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +10 -2
- package/packages/database/src/repositories/aiInfra/index.test.ts +198 -0
- package/packages/database/src/repositories/aiInfra/index.ts +2 -1
- package/packages/database/src/schemas/aiInfra.ts +2 -0
- package/src/app/[variants]/(main)/labs/page.tsx +9 -8
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +152 -0
- package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +70 -0
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +21 -0
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +91 -0
- package/src/features/Conversation/Messages/Group/EditState.tsx +51 -0
- package/src/features/Conversation/Messages/Group/Error/index.tsx +53 -0
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +73 -0
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +39 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/BuiltinPluginTitle.tsx +49 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/Debug.tsx +70 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginResult.tsx +34 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginState.tsx +18 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/Settings.tsx +40 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/ToolTitle.tsx +92 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +176 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ObjectEntity.tsx +81 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ValueCell.tsx +43 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/index.tsx +134 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +88 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/ErrorResponse.tsx +35 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +29 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +66 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +105 -0
- package/src/features/Conversation/Messages/Group/Tool/index.tsx +75 -0
- package/src/features/Conversation/Messages/Group/Tools.tsx +46 -0
- package/src/features/Conversation/Messages/Group/index.tsx +140 -0
- package/src/features/Conversation/Messages/index.tsx +12 -0
- package/src/features/Conversation/components/ShareMessageModal/ShareImage/Preview.tsx +2 -2
- package/src/services/chat/contextEngineering.ts +6 -5
- package/src/services/message/server.ts +10 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +309 -2
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +2 -22
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +272 -14
|
@@ -301,6 +301,13 @@
|
|
|
301
301
|
"when": 1762232313711,
|
|
302
302
|
"tag": "0042_improve_agent_index",
|
|
303
303
|
"breakpoints": true
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"idx": 43,
|
|
307
|
+
"version": "7",
|
|
308
|
+
"when": 1762251112601,
|
|
309
|
+
"tag": "0043_add_ai_model_settings",
|
|
310
|
+
"breakpoints": true
|
|
304
311
|
}
|
|
305
312
|
],
|
|
306
313
|
"version": "6"
|
|
@@ -749,10 +749,18 @@
|
|
|
749
749
|
},
|
|
750
750
|
{
|
|
751
751
|
"sql": [
|
|
752
|
-
"CREATE INDEX \"agents_knowledge_bases_agent_id_idx\" ON \"agents_knowledge_bases\" USING btree (\"agent_id\")
|
|
752
|
+
"CREATE INDEX IF NOT EXISTS \"agents_knowledge_bases_agent_id_idx\" ON \"agents_knowledge_bases\" USING btree (\"agent_id\");\n"
|
|
753
753
|
],
|
|
754
754
|
"bps": true,
|
|
755
755
|
"folderMillis": 1762232313711,
|
|
756
|
-
"hash": "
|
|
756
|
+
"hash": "fd8e7ff36bf877ddc1a45d360edfe1fe9a579cd02d68d7664545d7df95b8eb09"
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"sql": [
|
|
760
|
+
"ALTER TABLE \"ai_models\" ADD COLUMN IF NOT EXISTS \"settings\" jsonb DEFAULT '{}'::jsonb;"
|
|
761
|
+
],
|
|
762
|
+
"bps": true,
|
|
763
|
+
"folderMillis": 1762251112601,
|
|
764
|
+
"hash": "923ccbdf46c32be9a981dabd348e6923b4a365444241e9b8cc174bf5b914cbc5"
|
|
757
765
|
}
|
|
758
766
|
]
|
|
@@ -731,6 +731,105 @@ describe('AiInfraRepos', () => {
|
|
|
731
731
|
// 无 settings
|
|
732
732
|
expect(merged?.settings).toBeUndefined();
|
|
733
733
|
});
|
|
734
|
+
|
|
735
|
+
it('should prefer user settings over builtin settings', async () => {
|
|
736
|
+
const mockProviders = [
|
|
737
|
+
{ enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
const userModel: EnabledAiModel = {
|
|
741
|
+
id: 'gpt-4',
|
|
742
|
+
providerId: 'openai',
|
|
743
|
+
enabled: true,
|
|
744
|
+
type: 'chat',
|
|
745
|
+
abilities: {},
|
|
746
|
+
settings: { searchImpl: 'params', searchProvider: 'user-provider' },
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const builtinModel = {
|
|
750
|
+
id: 'gpt-4',
|
|
751
|
+
enabled: true,
|
|
752
|
+
type: 'chat' as const,
|
|
753
|
+
settings: { searchImpl: 'tool', searchProvider: 'builtin-provider' },
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
|
|
757
|
+
vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
|
|
758
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
|
|
759
|
+
|
|
760
|
+
const result = await repo.getEnabledModels();
|
|
761
|
+
|
|
762
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
763
|
+
expect(merged).toBeDefined();
|
|
764
|
+
// 应该使用用户的 settings,不是内置的
|
|
765
|
+
expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'user-provider' });
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('should use builtin settings when user has no settings', async () => {
|
|
769
|
+
const mockProviders = [
|
|
770
|
+
{ enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
|
|
771
|
+
];
|
|
772
|
+
|
|
773
|
+
const userModel: EnabledAiModel = {
|
|
774
|
+
id: 'gpt-4',
|
|
775
|
+
providerId: 'openai',
|
|
776
|
+
enabled: true,
|
|
777
|
+
type: 'chat',
|
|
778
|
+
abilities: { vision: true },
|
|
779
|
+
// 用户未设置 settings
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const builtinModel = {
|
|
783
|
+
id: 'gpt-4',
|
|
784
|
+
enabled: true,
|
|
785
|
+
type: 'chat' as const,
|
|
786
|
+
settings: { searchImpl: 'tool', searchProvider: 'google' },
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
|
|
790
|
+
vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
|
|
791
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
|
|
792
|
+
|
|
793
|
+
const result = await repo.getEnabledModels();
|
|
794
|
+
|
|
795
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
796
|
+
expect(merged).toBeDefined();
|
|
797
|
+
// 应该使用内置的 settings
|
|
798
|
+
expect(merged?.settings).toEqual({ searchImpl: 'tool', searchProvider: 'google' });
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('should have no settings when both user and builtin have no settings', async () => {
|
|
802
|
+
const mockProviders = [
|
|
803
|
+
{ enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
|
|
804
|
+
];
|
|
805
|
+
|
|
806
|
+
const userModel: EnabledAiModel = {
|
|
807
|
+
id: 'gpt-4',
|
|
808
|
+
providerId: 'openai',
|
|
809
|
+
enabled: true,
|
|
810
|
+
type: 'chat',
|
|
811
|
+
abilities: { vision: true },
|
|
812
|
+
// 用户未设置 settings
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const builtinModel = {
|
|
816
|
+
id: 'gpt-4',
|
|
817
|
+
enabled: true,
|
|
818
|
+
type: 'chat' as const,
|
|
819
|
+
// 内置也无 settings
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
|
|
823
|
+
vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
|
|
824
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
|
|
825
|
+
|
|
826
|
+
const result = await repo.getEnabledModels();
|
|
827
|
+
|
|
828
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
829
|
+
expect(merged).toBeDefined();
|
|
830
|
+
// 无 settings
|
|
831
|
+
expect(merged?.settings).toBeUndefined();
|
|
832
|
+
});
|
|
734
833
|
});
|
|
735
834
|
|
|
736
835
|
describe('getAiProviderModelList', () => {
|
|
@@ -1270,6 +1369,105 @@ describe('AiInfraRepos', () => {
|
|
|
1270
1369
|
// 无 settings
|
|
1271
1370
|
expect(merged?.settings).toBeUndefined();
|
|
1272
1371
|
});
|
|
1372
|
+
|
|
1373
|
+
it('should prefer user settings over builtin settings in getAiProviderModelList', async () => {
|
|
1374
|
+
const providerId = 'openai';
|
|
1375
|
+
|
|
1376
|
+
const userModels: AiProviderModelListItem[] = [
|
|
1377
|
+
{
|
|
1378
|
+
id: 'gpt-4',
|
|
1379
|
+
type: 'chat',
|
|
1380
|
+
enabled: true,
|
|
1381
|
+
abilities: {},
|
|
1382
|
+
settings: { searchImpl: 'params', searchProvider: 'user-provider' },
|
|
1383
|
+
} as any,
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
const builtinModels: AiProviderModelListItem[] = [
|
|
1387
|
+
{
|
|
1388
|
+
id: 'gpt-4',
|
|
1389
|
+
type: 'chat',
|
|
1390
|
+
enabled: true,
|
|
1391
|
+
settings: { searchImpl: 'tool', searchProvider: 'builtin-provider' },
|
|
1392
|
+
} as any,
|
|
1393
|
+
];
|
|
1394
|
+
|
|
1395
|
+
vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
|
|
1396
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
|
|
1397
|
+
|
|
1398
|
+
const result = await repo.getAiProviderModelList(providerId);
|
|
1399
|
+
|
|
1400
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
1401
|
+
expect(merged).toBeDefined();
|
|
1402
|
+
// 应该使用用户的 settings
|
|
1403
|
+
expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'user-provider' });
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it('should use builtin settings when user has no settings in getAiProviderModelList', async () => {
|
|
1407
|
+
const providerId = 'openai';
|
|
1408
|
+
|
|
1409
|
+
const userModels: AiProviderModelListItem[] = [
|
|
1410
|
+
{
|
|
1411
|
+
id: 'gpt-4',
|
|
1412
|
+
type: 'chat',
|
|
1413
|
+
enabled: true,
|
|
1414
|
+
abilities: { vision: true },
|
|
1415
|
+
// 用户未设置 settings
|
|
1416
|
+
},
|
|
1417
|
+
];
|
|
1418
|
+
|
|
1419
|
+
const builtinModels: AiProviderModelListItem[] = [
|
|
1420
|
+
{
|
|
1421
|
+
id: 'gpt-4',
|
|
1422
|
+
type: 'chat',
|
|
1423
|
+
enabled: true,
|
|
1424
|
+
settings: { searchImpl: 'tool', searchProvider: 'google' },
|
|
1425
|
+
} as any,
|
|
1426
|
+
];
|
|
1427
|
+
|
|
1428
|
+
vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
|
|
1429
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
|
|
1430
|
+
|
|
1431
|
+
const result = await repo.getAiProviderModelList(providerId);
|
|
1432
|
+
|
|
1433
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
1434
|
+
expect(merged).toBeDefined();
|
|
1435
|
+
// 应该使用内置的 settings
|
|
1436
|
+
expect(merged?.settings).toEqual({ searchImpl: 'tool', searchProvider: 'google' });
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('should have no settings when both user and builtin have no settings in getAiProviderModelList', async () => {
|
|
1440
|
+
const providerId = 'openai';
|
|
1441
|
+
|
|
1442
|
+
const userModels: AiProviderModelListItem[] = [
|
|
1443
|
+
{
|
|
1444
|
+
id: 'gpt-4',
|
|
1445
|
+
type: 'chat',
|
|
1446
|
+
enabled: true,
|
|
1447
|
+
abilities: { vision: true },
|
|
1448
|
+
// 用户未设置 settings
|
|
1449
|
+
},
|
|
1450
|
+
];
|
|
1451
|
+
|
|
1452
|
+
const builtinModels: AiProviderModelListItem[] = [
|
|
1453
|
+
{
|
|
1454
|
+
id: 'gpt-4',
|
|
1455
|
+
type: 'chat',
|
|
1456
|
+
enabled: true,
|
|
1457
|
+
// 内置也无 settings
|
|
1458
|
+
},
|
|
1459
|
+
];
|
|
1460
|
+
|
|
1461
|
+
vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
|
|
1462
|
+
vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
|
|
1463
|
+
|
|
1464
|
+
const result = await repo.getAiProviderModelList(providerId);
|
|
1465
|
+
|
|
1466
|
+
const merged = result.find((m) => m.id === 'gpt-4');
|
|
1467
|
+
expect(merged).toBeDefined();
|
|
1468
|
+
// 无 settings
|
|
1469
|
+
expect(merged?.settings).toBeUndefined();
|
|
1470
|
+
});
|
|
1273
1471
|
});
|
|
1274
1472
|
|
|
1275
1473
|
describe('getAiProviderRuntimeState', () => {
|
|
@@ -91,6 +91,7 @@ const injectSearchSettings = (providerId: string, item: any) => {
|
|
|
91
91
|
if (item?.settings?.searchImpl || item?.settings?.searchProvider) {
|
|
92
92
|
const next = { ...item } as any;
|
|
93
93
|
if (next.settings) {
|
|
94
|
+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
|
94
95
|
const { searchImpl, searchProvider, ...restSettings } = next.settings;
|
|
95
96
|
next.settings = Object.keys(restSettings).length > 0 ? restSettings : undefined;
|
|
96
97
|
}
|
|
@@ -224,7 +225,7 @@ export class AiInfraRepos {
|
|
|
224
225
|
enabled: typeof user.enabled === 'boolean' ? user.enabled : item.enabled,
|
|
225
226
|
id: item.id,
|
|
226
227
|
providerId: provider.id,
|
|
227
|
-
settings: item.settings,
|
|
228
|
+
settings: user.settings || item.settings,
|
|
228
229
|
sort: user.sort || undefined,
|
|
229
230
|
type: user.type || item.type,
|
|
230
231
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
2
2
|
import { boolean, integer, jsonb, pgTable, primaryKey, text, varchar } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { AiModelSettings } from 'model-bank';
|
|
3
4
|
|
|
4
5
|
import { AiProviderConfig, AiProviderSettings } from '@/types/aiProvider';
|
|
5
6
|
|
|
@@ -64,6 +65,7 @@ export const aiModels = pgTable(
|
|
|
64
65
|
contextWindowTokens: integer('context_window_tokens'),
|
|
65
66
|
source: varchar('source', { enum: ['remote', 'custom', 'builtin'], length: 20 }),
|
|
66
67
|
releasedAt: varchar('released_at', { length: 10 }),
|
|
68
|
+
settings: jsonb('settings').default({}).$type<AiModelSettings>(),
|
|
67
69
|
|
|
68
70
|
...timestamps,
|
|
69
71
|
},
|
|
@@ -24,13 +24,13 @@ const LabsPage = memo(() => {
|
|
|
24
24
|
const [
|
|
25
25
|
isPreferenceInit,
|
|
26
26
|
enableInputMarkdown,
|
|
27
|
-
|
|
27
|
+
enableAssistantMessageGroup,
|
|
28
28
|
// enableGroupChat,
|
|
29
29
|
updateLab,
|
|
30
30
|
] = useUserStore((s) => [
|
|
31
31
|
preferenceSelectors.isPreferenceInit(s),
|
|
32
32
|
labPreferSelectors.enableInputMarkdown(s),
|
|
33
|
-
|
|
33
|
+
labPreferSelectors.enableAssistantMessageGroup(s),
|
|
34
34
|
// labPreferSelectors.enableGroupChat(s),
|
|
35
35
|
s.updateLab,
|
|
36
36
|
]);
|
|
@@ -43,12 +43,13 @@ const LabsPage = memo(() => {
|
|
|
43
43
|
key: 'enableInputMarkdown',
|
|
44
44
|
title: t('features.inputMarkdown.title'),
|
|
45
45
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
{
|
|
47
|
+
checked: enableAssistantMessageGroup,
|
|
48
|
+
cover: 'https://github.com/user-attachments/assets/ba517751-1f3b-4269-979e-f8471e3ebb89',
|
|
49
|
+
desc: t('features.assistantMessageGroup.desc'),
|
|
50
|
+
key: 'enableAssistantMessageGroup',
|
|
51
|
+
title: t('features.assistantMessageGroup.title'),
|
|
52
|
+
},
|
|
52
53
|
// {
|
|
53
54
|
// checked: enableGroupChat,
|
|
54
55
|
// cover: 'https://github.com/user-attachments/assets/72894d24-a96a-4d7c-a823-ff9e6a1a8b6d',
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { AssistantContentBlock, UIChatMessage } from '@lobechat/types';
|
|
2
|
+
import { ActionIconGroup, type ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
|
3
|
+
import { App } from 'antd';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { memo, use, useCallback, useContext, useMemo, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import ShareMessageModal from '@/features/Conversation/components/ShareMessageModal';
|
|
9
|
+
import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
|
|
10
|
+
import { useChatStore } from '@/store/chat';
|
|
11
|
+
import { threadSelectors } from '@/store/chat/selectors';
|
|
12
|
+
import { useSessionStore } from '@/store/session';
|
|
13
|
+
import { sessionSelectors } from '@/store/session/selectors';
|
|
14
|
+
|
|
15
|
+
import { InPortalThreadContext } from '../../../context/InPortalThreadContext';
|
|
16
|
+
import { useChatListActionsBar } from '../../../hooks/useChatListActionsBar';
|
|
17
|
+
|
|
18
|
+
interface GroupActionsProps {
|
|
19
|
+
contentBlock?: AssistantContentBlock;
|
|
20
|
+
data: UIChatMessage;
|
|
21
|
+
id: string;
|
|
22
|
+
index: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }) => {
|
|
26
|
+
const { tools } = data;
|
|
27
|
+
const [isThreadMode, hasThread] = useChatStore((s) => [
|
|
28
|
+
!!s.activeThreadId,
|
|
29
|
+
threadSelectors.hasThreadBySourceMsgId(id)(s),
|
|
30
|
+
]);
|
|
31
|
+
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
|
32
|
+
const [showShareModal, setShareModal] = useState(false);
|
|
33
|
+
|
|
34
|
+
const { edit, delAndRegenerate, copy, divider, del, branching, share } = useChatListActionsBar({
|
|
35
|
+
hasThread,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const hasTools = !!tools;
|
|
39
|
+
|
|
40
|
+
const inPortalThread = useContext(InPortalThreadContext);
|
|
41
|
+
const inThread = isThreadMode || inPortalThread;
|
|
42
|
+
|
|
43
|
+
const items = useMemo(() => {
|
|
44
|
+
if (hasTools) return [delAndRegenerate, copy];
|
|
45
|
+
|
|
46
|
+
return [edit, copy, inThread || isGroupSession ? null : branching].filter(
|
|
47
|
+
Boolean,
|
|
48
|
+
) as ActionIconGroupItemType[];
|
|
49
|
+
}, [inThread, hasTools, isGroupSession]);
|
|
50
|
+
|
|
51
|
+
const { t } = useTranslation('common');
|
|
52
|
+
const searchParams = useSearchParams();
|
|
53
|
+
const topic = searchParams.get('topic');
|
|
54
|
+
const [
|
|
55
|
+
deleteMessage,
|
|
56
|
+
translateMessage,
|
|
57
|
+
delAndRegenerateMessage,
|
|
58
|
+
copyMessage,
|
|
59
|
+
openThreadCreator,
|
|
60
|
+
delAndResendThreadMessage,
|
|
61
|
+
toggleMessageEditing,
|
|
62
|
+
] = useChatStore((s) => [
|
|
63
|
+
s.deleteMessage,
|
|
64
|
+
s.translateMessage,
|
|
65
|
+
s.delAndRegenerateMessage,
|
|
66
|
+
s.copyMessage,
|
|
67
|
+
s.openThreadCreator,
|
|
68
|
+
s.delAndResendThreadMessage,
|
|
69
|
+
s.toggleMessageEditing,
|
|
70
|
+
]);
|
|
71
|
+
const { message } = App.useApp();
|
|
72
|
+
const virtuosoRef = use(VirtuosoContext);
|
|
73
|
+
|
|
74
|
+
const onActionClick = useCallback(
|
|
75
|
+
async (action: ActionIconGroupEvent) => {
|
|
76
|
+
switch (action.key) {
|
|
77
|
+
case 'edit': {
|
|
78
|
+
toggleMessageEditing(id, true);
|
|
79
|
+
|
|
80
|
+
virtuosoRef?.current?.scrollIntoView({ align: 'start', behavior: 'auto', index });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!data) return;
|
|
84
|
+
|
|
85
|
+
switch (action.key) {
|
|
86
|
+
case 'copy': {
|
|
87
|
+
if (!contentBlock) return;
|
|
88
|
+
await copyMessage(id, contentBlock.content);
|
|
89
|
+
message.success(t('copySuccess', { defaultValue: 'Copy Success' }));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'branching': {
|
|
93
|
+
if (!topic) {
|
|
94
|
+
message.warning(t('branchingRequiresSavedTopic'));
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
openThreadCreator(id);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'del': {
|
|
102
|
+
deleteMessage(id);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'delAndRegenerate': {
|
|
107
|
+
if (inPortalThread) {
|
|
108
|
+
delAndResendThreadMessage(id);
|
|
109
|
+
} else {
|
|
110
|
+
delAndRegenerateMessage(id);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'share': {
|
|
116
|
+
setShareModal(true);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (action.keyPath.at(-1) === 'translate') {
|
|
122
|
+
// click the menu data with translate data, the result is:
|
|
123
|
+
// key: 'en-US'
|
|
124
|
+
// keyPath: ['en-US','translate']
|
|
125
|
+
const lang = action.keyPath[0];
|
|
126
|
+
translateMessage(id, lang);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[data, topic],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<ActionIconGroup
|
|
135
|
+
items={items}
|
|
136
|
+
menu={{
|
|
137
|
+
items: [edit, copy, divider, share, divider, delAndRegenerate, del],
|
|
138
|
+
}}
|
|
139
|
+
onActionClick={onActionClick}
|
|
140
|
+
/>
|
|
141
|
+
<ShareMessageModal
|
|
142
|
+
message={data!}
|
|
143
|
+
onCancel={() => {
|
|
144
|
+
setShareModal(false);
|
|
145
|
+
}}
|
|
146
|
+
open={showShareModal}
|
|
147
|
+
/>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export default WithContentId;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { UIChatMessage } from '@lobechat/types';
|
|
2
|
+
import { ActionIconGroup, type ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
|
3
|
+
import { useSearchParams } from 'next/navigation';
|
|
4
|
+
import { memo, useCallback, useContext, useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useChatStore } from '@/store/chat';
|
|
7
|
+
import { threadSelectors } from '@/store/chat/selectors';
|
|
8
|
+
import { useSessionStore } from '@/store/session';
|
|
9
|
+
import { sessionSelectors } from '@/store/session/selectors';
|
|
10
|
+
|
|
11
|
+
import { InPortalThreadContext } from '../../../context/InPortalThreadContext';
|
|
12
|
+
import { useChatListActionsBar } from '../../../hooks/useChatListActionsBar';
|
|
13
|
+
|
|
14
|
+
interface GroupActionsProps {
|
|
15
|
+
data: UIChatMessage;
|
|
16
|
+
id: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const WithoutContentId = memo<GroupActionsProps>(({ id, data }) => {
|
|
20
|
+
const [isThreadMode, hasThread] = useChatStore((s) => [
|
|
21
|
+
!!s.activeThreadId,
|
|
22
|
+
threadSelectors.hasThreadBySourceMsgId(id)(s),
|
|
23
|
+
]);
|
|
24
|
+
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
|
25
|
+
|
|
26
|
+
const { delAndRegenerate, del } = useChatListActionsBar({ hasThread });
|
|
27
|
+
|
|
28
|
+
const inPortalThread = useContext(InPortalThreadContext);
|
|
29
|
+
const inThread = isThreadMode || inPortalThread;
|
|
30
|
+
|
|
31
|
+
const items = useMemo(() => {
|
|
32
|
+
return [delAndRegenerate, del].filter(Boolean) as ActionIconGroupItemType[];
|
|
33
|
+
}, [inThread, isGroupSession]);
|
|
34
|
+
|
|
35
|
+
const searchParams = useSearchParams();
|
|
36
|
+
const topic = searchParams.get('topic');
|
|
37
|
+
|
|
38
|
+
const [deleteMessage, delAndRegenerateMessage, delAndResendThreadMessage] = useChatStore((s) => [
|
|
39
|
+
s.deleteMessage,
|
|
40
|
+
s.delAndRegenerateMessage,
|
|
41
|
+
s.delAndResendThreadMessage,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const onActionClick = useCallback(
|
|
45
|
+
async (action: ActionIconGroupEvent) => {
|
|
46
|
+
if (!data) return;
|
|
47
|
+
|
|
48
|
+
switch (action.key) {
|
|
49
|
+
case 'del': {
|
|
50
|
+
deleteMessage(id);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case 'delAndRegenerate': {
|
|
55
|
+
if (inPortalThread) {
|
|
56
|
+
delAndResendThreadMessage(id);
|
|
57
|
+
} else {
|
|
58
|
+
delAndRegenerateMessage(id);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[data, topic],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return <ActionIconGroup items={items} onActionClick={onActionClick} />;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export default WithoutContentId;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AssistantContentBlock, UIChatMessage } from '@lobechat/types';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import WithContentId from './WithContentId';
|
|
5
|
+
import WithoutContentId from './WithoutContentId';
|
|
6
|
+
|
|
7
|
+
interface GroupActionsProps {
|
|
8
|
+
contentBlock?: AssistantContentBlock;
|
|
9
|
+
contentId?: string;
|
|
10
|
+
data: UIChatMessage;
|
|
11
|
+
id: string;
|
|
12
|
+
index: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GroupActionsBar = memo<GroupActionsProps>(
|
|
16
|
+
({ id, data, contentBlock, index, contentId }) => {
|
|
17
|
+
if (!contentId) return <WithoutContentId data={data} id={id} />;
|
|
18
|
+
|
|
19
|
+
return <WithContentId contentBlock={contentBlock} data={data} id={contentId} index={index} />;
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AssistantContentBlock } from '@lobechat/types';
|
|
2
|
+
import { MarkdownProps } from '@lobehub/ui';
|
|
3
|
+
import { memo, useMemo } from 'react';
|
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
|
5
|
+
|
|
6
|
+
import { LOADING_FLAT } from '@/const/message';
|
|
7
|
+
import { markdownElements } from '@/features/Conversation/MarkdownElements';
|
|
8
|
+
import Reasoning from '@/features/Conversation/Messages/Assistant/Reasoning';
|
|
9
|
+
import { useChatStore } from '@/store/chat';
|
|
10
|
+
import { aiChatSelectors, messageStateSelectors } from '@/store/chat/selectors';
|
|
11
|
+
import { useUserStore } from '@/store/user';
|
|
12
|
+
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
|
13
|
+
|
|
14
|
+
import ImageFileListViewer from '../User/ImageFileListViewer';
|
|
15
|
+
import ErrorContent from './Error';
|
|
16
|
+
import MessageContent from './MessageContent';
|
|
17
|
+
import { Tools } from './Tools';
|
|
18
|
+
|
|
19
|
+
const rehypePlugins = markdownElements.map((element) => element.rehypePlugin).filter(Boolean);
|
|
20
|
+
const remarkPlugins = markdownElements.map((element) => element.remarkPlugin).filter(Boolean);
|
|
21
|
+
|
|
22
|
+
interface ContentBlockProps extends AssistantContentBlock {
|
|
23
|
+
index: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ContentBlock = memo<ContentBlockProps>((props) => {
|
|
27
|
+
const { id, tools, content, imageList, reasoning, error } = props;
|
|
28
|
+
const showImageItems = !!imageList && imageList.length > 0;
|
|
29
|
+
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
|
30
|
+
|
|
31
|
+
const hasTools = tools && tools.length > 0;
|
|
32
|
+
const showReasoning =
|
|
33
|
+
(!!reasoning && reasoning.content?.trim() !== '') || (!reasoning && isReasoning);
|
|
34
|
+
|
|
35
|
+
const { transitionMode, highlighterTheme, mermaidTheme } = useUserStore(
|
|
36
|
+
userGeneralSettingsSelectors.config,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const generating = useChatStore(messageStateSelectors.isMessageGenerating(id));
|
|
40
|
+
|
|
41
|
+
const animated = transitionMode === 'fadeIn' && generating;
|
|
42
|
+
|
|
43
|
+
const components = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
Object.fromEntries(
|
|
46
|
+
markdownElements.map((element) => {
|
|
47
|
+
const Component = element.Component;
|
|
48
|
+
|
|
49
|
+
return [element.tag, (props: any) => <Component {...props} id={id} />];
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
[id],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const markdownProps: Omit<MarkdownProps, 'className' | 'style' | 'children'> = useMemo(
|
|
56
|
+
() => ({
|
|
57
|
+
animated,
|
|
58
|
+
componentProps: {
|
|
59
|
+
highlight: {
|
|
60
|
+
theme: highlighterTheme,
|
|
61
|
+
},
|
|
62
|
+
mermaid: { theme: mermaidTheme },
|
|
63
|
+
},
|
|
64
|
+
components,
|
|
65
|
+
enableCustomFootnotes: true,
|
|
66
|
+
rehypePlugins,
|
|
67
|
+
remarkPlugins,
|
|
68
|
+
}),
|
|
69
|
+
[animated, components, highlighterTheme, mermaidTheme],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (error && (content === LOADING_FLAT || !content))
|
|
73
|
+
return <ErrorContent error={error} id={id} />;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Flexbox gap={8} id={id}>
|
|
77
|
+
{showReasoning && <Reasoning {...reasoning} id={id} />}
|
|
78
|
+
|
|
79
|
+
{/* Content - markdown text */}
|
|
80
|
+
{content && (
|
|
81
|
+
<MessageContent content={content} hasTools={hasTools} markdownProps={markdownProps} />
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* Image files */}
|
|
85
|
+
{showImageItems && <ImageFileListViewer items={imageList} />}
|
|
86
|
+
|
|
87
|
+
{/* Tools */}
|
|
88
|
+
{hasTools && <Tools messageId={id} tools={tools} />}
|
|
89
|
+
</Flexbox>
|
|
90
|
+
);
|
|
91
|
+
});
|