@lobehub/lobehub 2.0.0-next.263 → 2.0.0-next.265
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/manual-build-desktop.yml +16 -37
- package/CHANGELOG.md +52 -0
- package/apps/desktop/native-deps.config.mjs +19 -3
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +13 -0
- package/apps/desktop/src/main/core/browser/Browser.ts +14 -0
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +32 -0
- package/apps/desktop/src/main/utils/permissions.ts +86 -22
- package/changelog/v1.json +18 -0
- package/package.json +2 -2
- package/packages/database/src/models/__tests__/agent.test.ts +165 -4
- package/packages/database/src/models/agent.ts +46 -0
- package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
- package/packages/database/src/repositories/agentGroup/index.ts +150 -0
- package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
- package/packages/database/src/repositories/home/index.ts +48 -67
- package/pnpm-workspace.yaml +1 -0
- package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
- package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
- package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
- package/src/features/ChatInput/InputEditor/index.tsx +1 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
- package/src/server/routers/lambda/agent.ts +15 -0
- package/src/server/routers/lambda/agentGroup.ts +16 -0
- package/src/services/agent.ts +11 -0
- package/src/services/chatGroup/index.ts +11 -0
- package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
- package/src/store/home/slices/sidebarUI/action.ts +37 -9
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
- package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
|
@@ -36,6 +36,86 @@ describe('HomeRepository', () => {
|
|
|
36
36
|
expect(result.groups).toEqual([]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('should return non-virtual agents without agentsToSessions relationship', async () => {
|
|
40
|
+
// Create an agent without session relationship (e.g., duplicated agent)
|
|
41
|
+
const agentId = 'standalone-agent';
|
|
42
|
+
|
|
43
|
+
await clientDB.insert(Schema.agents).values({
|
|
44
|
+
id: agentId,
|
|
45
|
+
userId,
|
|
46
|
+
title: 'Standalone Agent',
|
|
47
|
+
description: 'Agent without session',
|
|
48
|
+
pinned: false,
|
|
49
|
+
virtual: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
53
|
+
|
|
54
|
+
// Agent should appear in ungrouped list even without agentsToSessions
|
|
55
|
+
expect(result.ungrouped).toHaveLength(1);
|
|
56
|
+
expect(result.ungrouped[0].id).toBe(agentId);
|
|
57
|
+
expect(result.ungrouped[0].title).toBe('Standalone Agent');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return pinned non-virtual agents without agentsToSessions relationship', async () => {
|
|
61
|
+
// Create a pinned agent without session relationship
|
|
62
|
+
const agentId = 'pinned-standalone';
|
|
63
|
+
|
|
64
|
+
await clientDB.insert(Schema.agents).values({
|
|
65
|
+
id: agentId,
|
|
66
|
+
userId,
|
|
67
|
+
title: 'Pinned Standalone Agent',
|
|
68
|
+
pinned: true,
|
|
69
|
+
virtual: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
73
|
+
|
|
74
|
+
// Agent should appear in pinned list
|
|
75
|
+
expect(result.pinned).toHaveLength(1);
|
|
76
|
+
expect(result.pinned[0].id).toBe(agentId);
|
|
77
|
+
expect(result.pinned[0].pinned).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return mixed agents with and without session relationships', async () => {
|
|
81
|
+
// Agent with session
|
|
82
|
+
await clientDB.transaction(async (tx) => {
|
|
83
|
+
await tx.insert(Schema.agents).values({
|
|
84
|
+
id: 'with-session',
|
|
85
|
+
userId,
|
|
86
|
+
title: 'Agent With Session',
|
|
87
|
+
pinned: false,
|
|
88
|
+
virtual: false,
|
|
89
|
+
});
|
|
90
|
+
await tx.insert(Schema.sessions).values({
|
|
91
|
+
id: 'session-1',
|
|
92
|
+
slug: 'session-1',
|
|
93
|
+
userId,
|
|
94
|
+
});
|
|
95
|
+
await tx.insert(Schema.agentsToSessions).values({
|
|
96
|
+
agentId: 'with-session',
|
|
97
|
+
sessionId: 'session-1',
|
|
98
|
+
userId,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Agent without session (e.g., duplicated)
|
|
103
|
+
await clientDB.insert(Schema.agents).values({
|
|
104
|
+
id: 'without-session',
|
|
105
|
+
userId,
|
|
106
|
+
title: 'Agent Without Session',
|
|
107
|
+
pinned: false,
|
|
108
|
+
virtual: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
112
|
+
|
|
113
|
+
// Both agents should appear
|
|
114
|
+
expect(result.ungrouped).toHaveLength(2);
|
|
115
|
+
expect(result.ungrouped.map((a) => a.id)).toContain('with-session');
|
|
116
|
+
expect(result.ungrouped.map((a) => a.id)).toContain('without-session');
|
|
117
|
+
});
|
|
118
|
+
|
|
39
119
|
it('should return agents with pinned status from agents table', async () => {
|
|
40
120
|
// Create an agent with pinned=true
|
|
41
121
|
const agentId = 'agent-1';
|
|
@@ -380,6 +460,16 @@ describe('HomeRepository', () => {
|
|
|
380
460
|
sessionId: 'session-search-unpinned',
|
|
381
461
|
userId,
|
|
382
462
|
});
|
|
463
|
+
|
|
464
|
+
// Agent without session (e.g., duplicated agent)
|
|
465
|
+
await tx.insert(Schema.agents).values({
|
|
466
|
+
id: 'search-standalone',
|
|
467
|
+
userId,
|
|
468
|
+
title: 'Standalone Searchable Agent',
|
|
469
|
+
description: 'A standalone agent without session',
|
|
470
|
+
pinned: false,
|
|
471
|
+
virtual: false,
|
|
472
|
+
});
|
|
383
473
|
});
|
|
384
474
|
});
|
|
385
475
|
|
|
@@ -388,6 +478,24 @@ describe('HomeRepository', () => {
|
|
|
388
478
|
expect(result).toEqual([]);
|
|
389
479
|
});
|
|
390
480
|
|
|
481
|
+
it('should search agents without agentsToSessions relationship', async () => {
|
|
482
|
+
const result = await homeRepo.searchAgents('Standalone');
|
|
483
|
+
|
|
484
|
+
expect(result).toHaveLength(1);
|
|
485
|
+
expect(result[0].id).toBe('search-standalone');
|
|
486
|
+
expect(result[0].title).toBe('Standalone Searchable Agent');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should search and return mixed agents with and without session relationships', async () => {
|
|
490
|
+
// Search for "Searchable" should return all 3 agents
|
|
491
|
+
const result = await homeRepo.searchAgents('Searchable');
|
|
492
|
+
|
|
493
|
+
expect(result).toHaveLength(3);
|
|
494
|
+
expect(result.map((a) => a.id)).toContain('search-pinned');
|
|
495
|
+
expect(result.map((a) => a.id)).toContain('search-unpinned');
|
|
496
|
+
expect(result.map((a) => a.id)).toContain('search-standalone');
|
|
497
|
+
});
|
|
498
|
+
|
|
391
499
|
it('should search agents by title and return correct pinned status', async () => {
|
|
392
500
|
const result = await homeRepo.searchAgents('Searchable Pinned');
|
|
393
501
|
|
|
@@ -407,15 +515,19 @@ describe('HomeRepository', () => {
|
|
|
407
515
|
it('should return multiple matching agents with correct pinned status', async () => {
|
|
408
516
|
const result = await homeRepo.searchAgents('Searchable');
|
|
409
517
|
|
|
410
|
-
|
|
518
|
+
// 3 agents: search-pinned, search-unpinned, search-standalone
|
|
519
|
+
expect(result).toHaveLength(3);
|
|
411
520
|
|
|
412
521
|
const pinnedAgent = result.find((a) => a.id === 'search-pinned');
|
|
413
522
|
const unpinnedAgent = result.find((a) => a.id === 'search-unpinned');
|
|
523
|
+
const standaloneAgent = result.find((a) => a.id === 'search-standalone');
|
|
414
524
|
|
|
415
525
|
expect(pinnedAgent).toBeDefined();
|
|
416
526
|
expect(pinnedAgent!.pinned).toBe(true);
|
|
417
527
|
expect(unpinnedAgent).toBeDefined();
|
|
418
528
|
expect(unpinnedAgent!.pinned).toBe(false);
|
|
529
|
+
expect(standaloneAgent).toBeDefined();
|
|
530
|
+
expect(standaloneAgent!.pinned).toBe(false);
|
|
419
531
|
});
|
|
420
532
|
|
|
421
533
|
it('should not return virtual agents in search', async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SidebarAgentItem, SidebarAgentListResponse, SidebarGroup } from '@lobechat/types';
|
|
2
2
|
import { cleanObject } from '@lobechat/utils';
|
|
3
|
-
import { and, desc, eq, ilike, inArray, or } from 'drizzle-orm';
|
|
3
|
+
import { and, desc, eq, ilike, inArray, not, or } from 'drizzle-orm';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
agents,
|
|
@@ -36,11 +36,7 @@ export class HomeRepository {
|
|
|
36
36
|
* Get sidebar agent list with pinned, grouped, and ungrouped items
|
|
37
37
|
*/
|
|
38
38
|
async getSidebarAgentList(): Promise<SidebarAgentListResponse> {
|
|
39
|
-
// 1. Query all agents (non-virtual) with their session info
|
|
40
|
-
// Note: We query both agents.pinned and sessions.pinned for backward compatibility
|
|
41
|
-
// agents.pinned takes priority, falling back to sessions.pinned for legacy data
|
|
42
|
-
// Note: We query both agents.sessionGroupId and sessions.groupId for backward compatibility
|
|
43
|
-
// agents.sessionGroupId takes priority, falling back to sessions.groupId for legacy data
|
|
39
|
+
// 1. Query all agents (non-virtual) with their session info (if exists)
|
|
44
40
|
const agentList = await this.db
|
|
45
41
|
.select({
|
|
46
42
|
agentSessionGroupId: agents.sessionGroupId,
|
|
@@ -55,9 +51,9 @@ export class HomeRepository {
|
|
|
55
51
|
updatedAt: agents.updatedAt,
|
|
56
52
|
})
|
|
57
53
|
.from(agents)
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.where(and(eq(agents.userId, this.userId), eq(agents.virtual,
|
|
54
|
+
.leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
|
|
55
|
+
.leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
|
|
56
|
+
.where(and(eq(agents.userId, this.userId), not(eq(agents.virtual, true))))
|
|
61
57
|
.orderBy(desc(agents.updatedAt));
|
|
62
58
|
|
|
63
59
|
// 2. Query all chatGroups (group chats)
|
|
@@ -75,32 +71,7 @@ export class HomeRepository {
|
|
|
75
71
|
.orderBy(desc(chatGroups.updatedAt));
|
|
76
72
|
|
|
77
73
|
// 2.1 Query member avatars for each chat group
|
|
78
|
-
const
|
|
79
|
-
const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
|
|
80
|
-
|
|
81
|
-
if (chatGroupIds.length > 0) {
|
|
82
|
-
const memberAvatars = await this.db
|
|
83
|
-
.select({
|
|
84
|
-
avatar: agents.avatar,
|
|
85
|
-
backgroundColor: agents.backgroundColor,
|
|
86
|
-
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
87
|
-
})
|
|
88
|
-
.from(chatGroupsAgents)
|
|
89
|
-
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
90
|
-
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
91
|
-
.orderBy(chatGroupsAgents.order);
|
|
92
|
-
|
|
93
|
-
for (const member of memberAvatars) {
|
|
94
|
-
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
95
|
-
if (member.avatar) {
|
|
96
|
-
existing.push({
|
|
97
|
-
avatar: member.avatar,
|
|
98
|
-
background: member.backgroundColor ?? undefined,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
74
|
+
const memberAvatarsMap = await this.getChatGroupMemberAvatars(chatGroupList.map((g) => g.id));
|
|
104
75
|
|
|
105
76
|
// 3. Query all sessionGroups (user-defined folders)
|
|
106
77
|
const groupList = await this.db
|
|
@@ -125,7 +96,7 @@ export class HomeRepository {
|
|
|
125
96
|
id: string;
|
|
126
97
|
pinned: boolean | null;
|
|
127
98
|
sessionGroupId: string | null;
|
|
128
|
-
sessionId: string;
|
|
99
|
+
sessionId: string | null;
|
|
129
100
|
sessionPinned: boolean | null;
|
|
130
101
|
title: string | null;
|
|
131
102
|
updatedAt: Date;
|
|
@@ -217,7 +188,6 @@ export class HomeRepository {
|
|
|
217
188
|
const searchPattern = `%${keyword.toLowerCase()}%`;
|
|
218
189
|
|
|
219
190
|
// 1. Search agents by title or description
|
|
220
|
-
// Note: We query both agents.pinned and sessions.pinned for backward compatibility
|
|
221
191
|
const agentResults = await this.db
|
|
222
192
|
.select({
|
|
223
193
|
avatar: agents.avatar,
|
|
@@ -230,12 +200,12 @@ export class HomeRepository {
|
|
|
230
200
|
updatedAt: agents.updatedAt,
|
|
231
201
|
})
|
|
232
202
|
.from(agents)
|
|
233
|
-
.
|
|
234
|
-
.
|
|
203
|
+
.leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
|
|
204
|
+
.leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
|
|
235
205
|
.where(
|
|
236
206
|
and(
|
|
237
207
|
eq(agents.userId, this.userId),
|
|
238
|
-
eq(agents.virtual,
|
|
208
|
+
not(eq(agents.virtual, true)),
|
|
239
209
|
or(ilike(agents.title, searchPattern), ilike(agents.description, searchPattern)),
|
|
240
210
|
),
|
|
241
211
|
)
|
|
@@ -260,35 +230,11 @@ export class HomeRepository {
|
|
|
260
230
|
.orderBy(desc(chatGroups.updatedAt));
|
|
261
231
|
|
|
262
232
|
// 2.1 Query member avatars for matching chat groups
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (chatGroupIds.length > 0) {
|
|
267
|
-
const memberAvatars = await this.db
|
|
268
|
-
.select({
|
|
269
|
-
avatar: agents.avatar,
|
|
270
|
-
backgroundColor: agents.backgroundColor,
|
|
271
|
-
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
272
|
-
})
|
|
273
|
-
.from(chatGroupsAgents)
|
|
274
|
-
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
275
|
-
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
276
|
-
.orderBy(chatGroupsAgents.order);
|
|
277
|
-
|
|
278
|
-
for (const member of memberAvatars) {
|
|
279
|
-
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
280
|
-
if (member.avatar) {
|
|
281
|
-
existing.push({
|
|
282
|
-
avatar: member.avatar,
|
|
283
|
-
background: member.backgroundColor ?? undefined,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
233
|
+
const memberAvatarsMap = await this.getChatGroupMemberAvatars(
|
|
234
|
+
chatGroupResults.map((g) => g.id),
|
|
235
|
+
);
|
|
289
236
|
|
|
290
237
|
// 3. Combine and format results
|
|
291
|
-
// For pinned status: agents.pinned takes priority, fallback to sessions.pinned for backward compatibility
|
|
292
238
|
const results: SidebarAgentItem[] = [
|
|
293
239
|
...agentResults.map((a) =>
|
|
294
240
|
cleanObject({
|
|
@@ -320,4 +266,39 @@ export class HomeRepository {
|
|
|
320
266
|
|
|
321
267
|
return results;
|
|
322
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Query member avatars for chat groups
|
|
272
|
+
*/
|
|
273
|
+
private async getChatGroupMemberAvatars(
|
|
274
|
+
chatGroupIds: string[],
|
|
275
|
+
): Promise<Map<string, Array<{ avatar: string; background?: string }>>> {
|
|
276
|
+
const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
|
|
277
|
+
|
|
278
|
+
if (chatGroupIds.length === 0) return memberAvatarsMap;
|
|
279
|
+
|
|
280
|
+
const memberAvatars = await this.db
|
|
281
|
+
.select({
|
|
282
|
+
avatar: agents.avatar,
|
|
283
|
+
backgroundColor: agents.backgroundColor,
|
|
284
|
+
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
285
|
+
})
|
|
286
|
+
.from(chatGroupsAgents)
|
|
287
|
+
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
288
|
+
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
289
|
+
.orderBy(chatGroupsAgents.order);
|
|
290
|
+
|
|
291
|
+
for (const member of memberAvatars) {
|
|
292
|
+
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
293
|
+
if (member.avatar) {
|
|
294
|
+
existing.push({
|
|
295
|
+
avatar: member.avatar,
|
|
296
|
+
background: member.backgroundColor ?? undefined,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return memberAvatarsMap;
|
|
303
|
+
}
|
|
323
304
|
}
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -11,10 +11,10 @@ import { useSendMenuItems } from './useSendMenuItems';
|
|
|
11
11
|
const leftActions: ActionKeys[] = [
|
|
12
12
|
'model',
|
|
13
13
|
'search',
|
|
14
|
-
'typo',
|
|
15
14
|
'fileUpload',
|
|
15
|
+
'tools',
|
|
16
16
|
'---',
|
|
17
|
-
['
|
|
17
|
+
['typo', 'params', 'clear'],
|
|
18
18
|
'mainToken',
|
|
19
19
|
];
|
|
20
20
|
|
|
@@ -13,8 +13,8 @@ import { useGlobalStore } from '@/store/global';
|
|
|
13
13
|
import { useHomeStore } from '@/store/home';
|
|
14
14
|
|
|
15
15
|
import Actions from '../Item/Actions';
|
|
16
|
-
import { useDropdownMenu } from '../Item/useDropdownMenu';
|
|
17
16
|
import Editing from './Editing';
|
|
17
|
+
import { useGroupDropdownMenu } from './useDropdownMenu';
|
|
18
18
|
|
|
19
19
|
interface GroupItemProps {
|
|
20
20
|
className?: string;
|
|
@@ -85,13 +85,9 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
|
|
|
85
85
|
return <GroupAvatar avatars={(avatar as any) || []} size={22} />;
|
|
86
86
|
}, [isUpdating, avatar]);
|
|
87
87
|
|
|
88
|
-
const dropdownMenu =
|
|
89
|
-
group: undefined,
|
|
88
|
+
const dropdownMenu = useGroupDropdownMenu({
|
|
90
89
|
id,
|
|
91
|
-
openCreateGroupModal: () => {}, // Groups don't need this
|
|
92
|
-
parentType: 'group',
|
|
93
90
|
pinned: pinned ?? false,
|
|
94
|
-
sessionType: 'group',
|
|
95
91
|
toggleEditing,
|
|
96
92
|
});
|
|
97
93
|
|
package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Icon, type MenuProps } from '@lobehub/ui';
|
|
2
|
+
import { App } from 'antd';
|
|
3
|
+
import { LucideCopy, Pen, PictureInPicture2Icon, Pin, PinOff, Trash } from 'lucide-react';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import { useGlobalStore } from '@/store/global';
|
|
8
|
+
import { useHomeStore } from '@/store/home';
|
|
9
|
+
|
|
10
|
+
interface UseGroupDropdownMenuParams {
|
|
11
|
+
id: string;
|
|
12
|
+
pinned: boolean;
|
|
13
|
+
toggleEditing: (visible?: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useGroupDropdownMenu = ({
|
|
17
|
+
id,
|
|
18
|
+
pinned,
|
|
19
|
+
toggleEditing,
|
|
20
|
+
}: UseGroupDropdownMenuParams): (() => MenuProps['items']) => {
|
|
21
|
+
const { t } = useTranslation('chat');
|
|
22
|
+
const { modal, message } = App.useApp();
|
|
23
|
+
|
|
24
|
+
const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow);
|
|
25
|
+
const [pinAgentGroup, duplicateAgentGroup, removeAgentGroup] = useHomeStore((s) => [
|
|
26
|
+
s.pinAgentGroup,
|
|
27
|
+
s.duplicateAgentGroup,
|
|
28
|
+
s.removeAgentGroup,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
return useMemo(
|
|
32
|
+
() => () =>
|
|
33
|
+
[
|
|
34
|
+
{
|
|
35
|
+
icon: <Icon icon={pinned ? PinOff : Pin} />,
|
|
36
|
+
key: 'pin',
|
|
37
|
+
label: t(pinned ? 'pinOff' : 'pin'),
|
|
38
|
+
onClick: () => pinAgentGroup(id, !pinned),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
icon: <Icon icon={Pen} />,
|
|
42
|
+
key: 'rename',
|
|
43
|
+
label: t('rename', { ns: 'common' }),
|
|
44
|
+
onClick: (info: any) => {
|
|
45
|
+
info.domEvent?.stopPropagation();
|
|
46
|
+
toggleEditing(true);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
icon: <Icon icon={LucideCopy} />,
|
|
51
|
+
key: 'duplicate',
|
|
52
|
+
label: t('duplicate', { ns: 'common' }),
|
|
53
|
+
onClick: ({ domEvent }: any) => {
|
|
54
|
+
domEvent.stopPropagation();
|
|
55
|
+
duplicateAgentGroup(id);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
icon: <Icon icon={PictureInPicture2Icon} />,
|
|
60
|
+
key: 'openInNewWindow',
|
|
61
|
+
label: t('openInNewWindow'),
|
|
62
|
+
onClick: ({ domEvent }: any) => {
|
|
63
|
+
domEvent.stopPropagation();
|
|
64
|
+
openAgentInNewWindow(id);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{ type: 'divider' },
|
|
68
|
+
{
|
|
69
|
+
danger: true,
|
|
70
|
+
icon: <Icon icon={Trash} />,
|
|
71
|
+
key: 'delete',
|
|
72
|
+
label: t('delete', { ns: 'common' }),
|
|
73
|
+
onClick: ({ domEvent }: any) => {
|
|
74
|
+
domEvent.stopPropagation();
|
|
75
|
+
modal.confirm({
|
|
76
|
+
centered: true,
|
|
77
|
+
okButtonProps: { danger: true },
|
|
78
|
+
onOk: async () => {
|
|
79
|
+
await removeAgentGroup(id);
|
|
80
|
+
message.success(t('confirmRemoveGroupSuccess'));
|
|
81
|
+
},
|
|
82
|
+
title: t('confirmRemoveChatGroupItemAlert'),
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
] as MenuProps['items'],
|
|
87
|
+
[
|
|
88
|
+
t,
|
|
89
|
+
pinned,
|
|
90
|
+
pinAgentGroup,
|
|
91
|
+
id,
|
|
92
|
+
toggleEditing,
|
|
93
|
+
duplicateAgentGroup,
|
|
94
|
+
openAgentInNewWindow,
|
|
95
|
+
modal,
|
|
96
|
+
removeAgentGroup,
|
|
97
|
+
message,
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -15,9 +15,9 @@ import { useHomeStore } from '@/store/home';
|
|
|
15
15
|
|
|
16
16
|
import { useAgentModal } from '../../ModalProvider';
|
|
17
17
|
import Actions from '../Item/Actions';
|
|
18
|
-
import { useDropdownMenu } from '../Item/useDropdownMenu';
|
|
19
18
|
import Avatar from './Avatar';
|
|
20
19
|
import Editing from './Editing';
|
|
20
|
+
import { useAgentDropdownMenu } from './useDropdownMenu';
|
|
21
21
|
|
|
22
22
|
interface AgentItemProps {
|
|
23
23
|
className?: string;
|
|
@@ -96,13 +96,11 @@ const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
|
|
|
96
96
|
return <Avatar avatar={typeof avatar === 'string' ? avatar : undefined} />;
|
|
97
97
|
}, [isUpdating, avatar]);
|
|
98
98
|
|
|
99
|
-
const dropdownMenu =
|
|
99
|
+
const dropdownMenu = useAgentDropdownMenu({
|
|
100
100
|
group: undefined, // TODO: pass group from parent if needed
|
|
101
101
|
id,
|
|
102
102
|
openCreateGroupModal: handleOpenCreateGroupModal,
|
|
103
|
-
parentType: 'agent',
|
|
104
103
|
pinned: pinned ?? false,
|
|
105
|
-
sessionType: 'agent',
|
|
106
104
|
toggleEditing,
|
|
107
105
|
});
|
|
108
106
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { SessionDefaultGroup } from '@lobechat/types';
|
|
2
|
+
import { Icon, type MenuProps } from '@lobehub/ui';
|
|
3
|
+
import { App } from 'antd';
|
|
4
|
+
import isEqual from 'fast-deep-equal';
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
FolderInputIcon,
|
|
8
|
+
LucideCopy,
|
|
9
|
+
LucidePlus,
|
|
10
|
+
Pen,
|
|
11
|
+
PictureInPicture2Icon,
|
|
12
|
+
Pin,
|
|
13
|
+
PinOff,
|
|
14
|
+
Trash,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { useTranslation } from 'react-i18next';
|
|
18
|
+
|
|
19
|
+
import { useGlobalStore } from '@/store/global';
|
|
20
|
+
import { useHomeStore } from '@/store/home';
|
|
21
|
+
import { homeAgentListSelectors } from '@/store/home/selectors';
|
|
22
|
+
|
|
23
|
+
interface UseAgentDropdownMenuParams {
|
|
24
|
+
group: string | undefined;
|
|
25
|
+
id: string;
|
|
26
|
+
openCreateGroupModal: () => void;
|
|
27
|
+
pinned: boolean;
|
|
28
|
+
toggleEditing: (visible?: boolean) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const useAgentDropdownMenu = ({
|
|
32
|
+
group,
|
|
33
|
+
id,
|
|
34
|
+
openCreateGroupModal,
|
|
35
|
+
pinned,
|
|
36
|
+
toggleEditing,
|
|
37
|
+
}: UseAgentDropdownMenuParams): (() => MenuProps['items']) => {
|
|
38
|
+
const { t } = useTranslation('chat');
|
|
39
|
+
const { modal, message } = App.useApp();
|
|
40
|
+
|
|
41
|
+
const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow);
|
|
42
|
+
const sessionCustomGroups = useHomeStore(homeAgentListSelectors.agentGroups, isEqual);
|
|
43
|
+
const [pinAgent, duplicateAgent, updateAgentGroup, removeAgent] = useHomeStore((s) => [
|
|
44
|
+
s.pinAgent,
|
|
45
|
+
s.duplicateAgent,
|
|
46
|
+
s.updateAgentGroup,
|
|
47
|
+
s.removeAgent,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const isDefault = group === SessionDefaultGroup.Default;
|
|
51
|
+
|
|
52
|
+
return useMemo(
|
|
53
|
+
() => () =>
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
icon: <Icon icon={pinned ? PinOff : Pin} />,
|
|
57
|
+
key: 'pin',
|
|
58
|
+
label: t(pinned ? 'pinOff' : 'pin'),
|
|
59
|
+
onClick: () => pinAgent(id, !pinned),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
icon: <Icon icon={Pen} />,
|
|
63
|
+
key: 'rename',
|
|
64
|
+
label: t('rename', { ns: 'common' }),
|
|
65
|
+
onClick: (info: any) => {
|
|
66
|
+
info.domEvent?.stopPropagation();
|
|
67
|
+
toggleEditing(true);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
icon: <Icon icon={LucideCopy} />,
|
|
72
|
+
key: 'duplicate',
|
|
73
|
+
label: t('duplicate', { ns: 'common' }),
|
|
74
|
+
onClick: ({ domEvent }: any) => {
|
|
75
|
+
domEvent.stopPropagation();
|
|
76
|
+
duplicateAgent(id);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
icon: <Icon icon={PictureInPicture2Icon} />,
|
|
81
|
+
key: 'openInNewWindow',
|
|
82
|
+
label: t('openInNewWindow'),
|
|
83
|
+
onClick: ({ domEvent }: any) => {
|
|
84
|
+
domEvent.stopPropagation();
|
|
85
|
+
openAgentInNewWindow(id);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{ type: 'divider' },
|
|
89
|
+
{
|
|
90
|
+
children: [
|
|
91
|
+
...sessionCustomGroups.map(({ id: groupId, name }) => ({
|
|
92
|
+
icon: group === groupId ? <Icon icon={Check} /> : <div />,
|
|
93
|
+
key: groupId,
|
|
94
|
+
label: name,
|
|
95
|
+
onClick: () => updateAgentGroup(id, groupId),
|
|
96
|
+
})),
|
|
97
|
+
{
|
|
98
|
+
icon: isDefault ? <Icon icon={Check} /> : <div />,
|
|
99
|
+
key: 'defaultList',
|
|
100
|
+
label: t('defaultList'),
|
|
101
|
+
onClick: () => updateAgentGroup(id, SessionDefaultGroup.Default),
|
|
102
|
+
},
|
|
103
|
+
{ type: 'divider' as const },
|
|
104
|
+
{
|
|
105
|
+
icon: <Icon icon={LucidePlus} />,
|
|
106
|
+
key: 'createGroup',
|
|
107
|
+
label: <div>{t('sessionGroup.createGroup')}</div>,
|
|
108
|
+
onClick: ({ domEvent }: any) => {
|
|
109
|
+
domEvent.stopPropagation();
|
|
110
|
+
openCreateGroupModal();
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
icon: <Icon icon={FolderInputIcon} />,
|
|
115
|
+
key: 'moveGroup',
|
|
116
|
+
label: t('sessionGroup.moveGroup'),
|
|
117
|
+
},
|
|
118
|
+
{ type: 'divider' },
|
|
119
|
+
{
|
|
120
|
+
danger: true,
|
|
121
|
+
icon: <Icon icon={Trash} />,
|
|
122
|
+
key: 'delete',
|
|
123
|
+
label: t('delete', { ns: 'common' }),
|
|
124
|
+
onClick: ({ domEvent }: any) => {
|
|
125
|
+
domEvent.stopPropagation();
|
|
126
|
+
modal.confirm({
|
|
127
|
+
centered: true,
|
|
128
|
+
okButtonProps: { danger: true },
|
|
129
|
+
onOk: async () => {
|
|
130
|
+
await removeAgent(id);
|
|
131
|
+
message.success(t('confirmRemoveSessionSuccess'));
|
|
132
|
+
},
|
|
133
|
+
title: t('confirmRemoveSessionItemAlert'),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
] as MenuProps['items'],
|
|
138
|
+
[
|
|
139
|
+
pinned,
|
|
140
|
+
id,
|
|
141
|
+
toggleEditing,
|
|
142
|
+
sessionCustomGroups,
|
|
143
|
+
group,
|
|
144
|
+
isDefault,
|
|
145
|
+
openCreateGroupModal,
|
|
146
|
+
message,
|
|
147
|
+
],
|
|
148
|
+
);
|
|
149
|
+
};
|
|
@@ -12,7 +12,7 @@ import ModeHeader from './ModeHeader';
|
|
|
12
12
|
import StarterList from './StarterList';
|
|
13
13
|
import { useSend } from './useSend';
|
|
14
14
|
|
|
15
|
-
const leftActions: ActionKeys[] = ['model', 'search', 'fileUpload'];
|
|
15
|
+
const leftActions: ActionKeys[] = ['model', 'search', 'fileUpload', 'tools'];
|
|
16
16
|
|
|
17
17
|
const InputArea = () => {
|
|
18
18
|
const { loading, send, inboxAgentId } = useSend();
|
|
@@ -36,7 +36,7 @@ const styles = createStaticStyles(({ css }) => ({
|
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
38
|
const useIsEditorInit = (editor: IEditor) => {
|
|
39
|
-
const [isEditInit, setEditInit] = useState<boolean>(!!editor
|
|
39
|
+
const [isEditInit, setEditInit] = useState<boolean>(!!editor?.getLexicalEditor());
|
|
40
40
|
|
|
41
41
|
useEffect(() => {
|
|
42
42
|
if (!editor) return;
|