@lobehub/chat 0.148.8 → 0.148.10
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/package.json +2 -2
- package/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx +11 -5
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +6 -4
- package/src/app/chat/features/SessionListContent/List/AddButton.tsx +4 -9
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +21 -16
- package/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +8 -2
- package/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx +9 -1
- package/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx +12 -3
- package/src/app/home/Redirect.tsx +2 -2
- package/src/app/welcome/(desktop)/features/Footer.tsx +1 -1
- package/src/app/welcome/features/Banner/index.tsx +5 -9
- package/src/config/modelProviders/ollama.ts +38 -38
- package/src/database/client/models/__tests__/session.test.ts +2 -2
- package/src/database/client/models/session.ts +4 -14
- package/src/libs/agent-runtime/zeroone/index.test.ts +16 -16
- package/src/locales/default/chat.ts +6 -2
- package/src/locales/default/welcome.ts +1 -0
- package/src/migrations/FromV3ToV4/fixtures/ollama-output-v4.json +0 -1
- package/src/services/config.ts +5 -21
- package/src/services/message/client.ts +10 -0
- package/src/services/message/index.ts +1 -13
- package/src/services/message/type.ts +3 -0
- package/src/services/session/client.ts +3 -0
- package/src/services/session/type.ts +8 -2
- package/src/services/topic/client.ts +1 -1
- package/src/services/topic/type.ts +3 -2
- package/src/store/global/slices/settings/actions/llm.test.ts +0 -1
- package/src/store/global/slices/settings/selectors/modelProvider.test.ts +1 -1
- package/src/store/session/slices/session/action.ts +74 -76
- package/src/store/session/slices/session/initialState.ts +8 -1
- package/src/store/session/slices/session/reducers.test.ts +79 -0
- package/src/store/session/slices/session/reducers.ts +61 -0
- package/src/store/session/slices/sessionGroup/action.test.ts +9 -0
- package/src/store/session/slices/sessionGroup/action.ts +25 -6
- package/src/store/session/slices/sessionGroup/reducer.test.ts +86 -0
- package/src/store/session/slices/sessionGroup/reducer.ts +56 -0
- package/src/store/session/slices/sessionGroup/selectors.ts +1 -5
- package/src/types/service.ts +7 -0
- package/src/types/session.ts +5 -7
- package/src/utils/uuid.ts +4 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { LobeAgentConfig } from '@/types/agent';
|
|
5
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
6
|
+
|
|
7
|
+
import { SessionDispatch, sessionsReducer } from './reducers';
|
|
8
|
+
|
|
9
|
+
describe('sessionsReducer', () => {
|
|
10
|
+
const mockSession = {
|
|
11
|
+
id: nanoid(),
|
|
12
|
+
config: {
|
|
13
|
+
model: 'gpt-3.5-turbo',
|
|
14
|
+
} as any,
|
|
15
|
+
meta: {
|
|
16
|
+
title: 'Test Agent',
|
|
17
|
+
description: 'A test agent',
|
|
18
|
+
avatar: '',
|
|
19
|
+
},
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
const initialState: LobeSessions = [];
|
|
23
|
+
|
|
24
|
+
it('should add a new session', () => {
|
|
25
|
+
const addAction: SessionDispatch = {
|
|
26
|
+
session: mockSession,
|
|
27
|
+
type: 'addSession',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const newState = sessionsReducer(initialState, addAction);
|
|
31
|
+
|
|
32
|
+
expect(newState).toHaveLength(1);
|
|
33
|
+
expect(newState[0]).toMatchObject({
|
|
34
|
+
...mockSession,
|
|
35
|
+
createdAt: expect.any(Date),
|
|
36
|
+
updatedAt: expect.any(Date),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should remove an existing session', () => {
|
|
41
|
+
const state: LobeSessions = [mockSession];
|
|
42
|
+
const removeAction: SessionDispatch = {
|
|
43
|
+
id: mockSession.id,
|
|
44
|
+
type: 'removeSession',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const newState = sessionsReducer(state, removeAction);
|
|
48
|
+
|
|
49
|
+
expect(newState).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should update an existing session', () => {
|
|
53
|
+
const state: LobeSessions = [mockSession];
|
|
54
|
+
const updateAction: SessionDispatch = {
|
|
55
|
+
id: mockSession.id,
|
|
56
|
+
type: 'updateSession',
|
|
57
|
+
value: { group: 'abc' },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newState = sessionsReducer(state, updateAction);
|
|
61
|
+
|
|
62
|
+
expect(newState).toHaveLength(1);
|
|
63
|
+
expect(newState[0]).toMatchObject({
|
|
64
|
+
...mockSession,
|
|
65
|
+
group: 'abc',
|
|
66
|
+
updatedAt: expect.any(Date),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return the same state for unknown action', () => {
|
|
71
|
+
const state: LobeSessions = [mockSession];
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
const unknownAction: SessionDispatch = { type: 'unknown' };
|
|
74
|
+
|
|
75
|
+
const newState = sessionsReducer(state, unknownAction);
|
|
76
|
+
|
|
77
|
+
expect(newState).toEqual(state);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { LobeAgentSession, LobeSessions } from '@/types/session';
|
|
4
|
+
|
|
5
|
+
interface AddSession {
|
|
6
|
+
session: LobeAgentSession;
|
|
7
|
+
type: 'addSession';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RemoveSession {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'removeSession';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UpdateSession {
|
|
16
|
+
id: string;
|
|
17
|
+
type: 'updateSession';
|
|
18
|
+
value: Partial<LobeAgentSession>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SessionDispatch = AddSession | RemoveSession | UpdateSession;
|
|
22
|
+
|
|
23
|
+
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
|
|
24
|
+
switch (payload.type) {
|
|
25
|
+
case 'addSession': {
|
|
26
|
+
return produce(state, (draft) => {
|
|
27
|
+
const { session } = payload;
|
|
28
|
+
if (!session) return;
|
|
29
|
+
|
|
30
|
+
// TODO: 后续将 Date 类型做个迁移,就可以移除这里的 ignore 了
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
draft.unshift({ ...session, createdAt: new Date(), updatedAt: new Date() });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'removeSession': {
|
|
37
|
+
return produce(state, (draftState) => {
|
|
38
|
+
const index = draftState.findIndex((item) => item.id === payload.id);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
draftState.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'updateSession': {
|
|
46
|
+
return produce(state, (draftState) => {
|
|
47
|
+
const { value, id } = payload;
|
|
48
|
+
const index = draftState.findIndex((item) => item.id === id);
|
|
49
|
+
|
|
50
|
+
if (index !== -1) {
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
draftState[index] = { ...draftState[index], ...value, updatedAt: new Date() };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
default: {
|
|
58
|
+
return produce(state, () => {});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -8,6 +8,15 @@ afterEach(() => {
|
|
|
8
8
|
vi.restoreAllMocks();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
12
|
+
message: {
|
|
13
|
+
loading: vi.fn(),
|
|
14
|
+
success: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
destroy: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
11
20
|
describe('createSessionGroupSlice', () => {
|
|
12
21
|
describe('addSessionGroup', () => {
|
|
13
22
|
it('should add a session group and refresh sessions', async () => {
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { t } from 'i18next';
|
|
1
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
2
3
|
|
|
4
|
+
import { message } from '@/components/AntdStaticMethods';
|
|
3
5
|
import { sessionService } from '@/services/session';
|
|
4
6
|
import { SessionStore } from '@/store/session';
|
|
5
7
|
import { SessionGroupItem } from '@/types/session';
|
|
6
8
|
|
|
9
|
+
import { SessionGroupsDispatch, sessionGroupsReducer } from './reducer';
|
|
10
|
+
|
|
11
|
+
/* eslint-disable typescript-sort-keys/interface */
|
|
7
12
|
export interface SessionGroupAction {
|
|
8
13
|
addSessionGroup: (name: string) => Promise<string>;
|
|
9
14
|
clearSessionGroups: () => Promise<void>;
|
|
10
15
|
removeSessionGroup: (id: string) => Promise<void>;
|
|
11
|
-
updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
|
|
12
16
|
updateSessionGroupName: (id: string, name: string) => Promise<void>;
|
|
13
17
|
updateSessionGroupSort: (items: SessionGroupItem[]) => Promise<void>;
|
|
18
|
+
internal_dispatchSessionGroups: (payload: SessionGroupsDispatch) => void;
|
|
14
19
|
}
|
|
20
|
+
/* eslint-enable */
|
|
15
21
|
|
|
16
22
|
export const createSessionGroupSlice: StateCreator<
|
|
17
23
|
SessionStore,
|
|
@@ -36,11 +42,6 @@ export const createSessionGroupSlice: StateCreator<
|
|
|
36
42
|
await sessionService.removeSessionGroup(id);
|
|
37
43
|
await get().refreshSessions();
|
|
38
44
|
},
|
|
39
|
-
updateSessionGroupId: async (sessionId, group) => {
|
|
40
|
-
await sessionService.updateSession(sessionId, { group });
|
|
41
|
-
|
|
42
|
-
await get().refreshSessions();
|
|
43
|
-
},
|
|
44
45
|
|
|
45
46
|
updateSessionGroupName: async (id, name) => {
|
|
46
47
|
await sessionService.updateSessionGroup(id, { name });
|
|
@@ -48,7 +49,25 @@ export const createSessionGroupSlice: StateCreator<
|
|
|
48
49
|
},
|
|
49
50
|
updateSessionGroupSort: async (items) => {
|
|
50
51
|
const sortMap = items.map((item, index) => ({ id: item.id, sort: index }));
|
|
52
|
+
|
|
53
|
+
get().internal_dispatchSessionGroups({ sortMap, type: 'updateSessionGroupOrder' });
|
|
54
|
+
|
|
55
|
+
message.loading({
|
|
56
|
+
content: t('sessionGroup.sorting', { ns: 'chat' }),
|
|
57
|
+
duration: 0,
|
|
58
|
+
key: 'updateSessionGroupSort',
|
|
59
|
+
});
|
|
60
|
+
|
|
51
61
|
await sessionService.updateSessionGroupOrder(sortMap);
|
|
62
|
+
message.destroy('updateSessionGroupSort');
|
|
63
|
+
message.success(t('sessionGroup.sortSuccess', { ns: 'chat' }));
|
|
64
|
+
|
|
52
65
|
await get().refreshSessions();
|
|
53
66
|
},
|
|
67
|
+
|
|
68
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
69
|
+
internal_dispatchSessionGroups: (payload) => {
|
|
70
|
+
const nextSessionGroups = sessionGroupsReducer(get().sessionGroups, payload);
|
|
71
|
+
get().internal_processSessions(get().sessions, nextSessionGroups, 'updateSessionGroups');
|
|
72
|
+
},
|
|
54
73
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { SessionGroupItem } from '@/types/session';
|
|
5
|
+
|
|
6
|
+
import { sessionGroupsReducer } from './reducer';
|
|
7
|
+
|
|
8
|
+
describe('sessionGroupsReducer', () => {
|
|
9
|
+
const initialState: SessionGroupItem[] = [
|
|
10
|
+
{
|
|
11
|
+
id: nanoid(),
|
|
12
|
+
name: 'Group 1',
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
updatedAt: Date.now(),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: nanoid(),
|
|
18
|
+
name: 'Group 2',
|
|
19
|
+
createdAt: Date.now(),
|
|
20
|
+
updatedAt: Date.now(),
|
|
21
|
+
sort: 1,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
it('should add a new session group item', () => {
|
|
26
|
+
const newItem: SessionGroupItem = {
|
|
27
|
+
id: nanoid(),
|
|
28
|
+
name: 'New Group',
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
updatedAt: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = sessionGroupsReducer(initialState, {
|
|
34
|
+
type: 'addSessionGroupItem',
|
|
35
|
+
item: newItem,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result).toHaveLength(3);
|
|
39
|
+
expect(result).toContainEqual(newItem);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should delete a session group item', () => {
|
|
43
|
+
const itemToDelete = initialState[0].id;
|
|
44
|
+
|
|
45
|
+
const result = sessionGroupsReducer(initialState, {
|
|
46
|
+
type: 'deleteSessionGroupItem',
|
|
47
|
+
id: itemToDelete,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result).toHaveLength(1);
|
|
51
|
+
expect(result).not.toContainEqual(expect.objectContaining({ id: itemToDelete }));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should update a session group item', () => {
|
|
55
|
+
const itemToUpdate = initialState[0].id;
|
|
56
|
+
const updatedItem = { name: 'Updated Group' };
|
|
57
|
+
|
|
58
|
+
const result = sessionGroupsReducer(initialState, {
|
|
59
|
+
type: 'updateSessionGroupItem',
|
|
60
|
+
id: itemToUpdate,
|
|
61
|
+
item: updatedItem,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toHaveLength(2);
|
|
65
|
+
expect(result).toContainEqual(expect.objectContaining({ id: itemToUpdate, ...updatedItem }));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should update session group order', () => {
|
|
69
|
+
const sortMap = [
|
|
70
|
+
{ id: initialState[1].id, sort: 0 },
|
|
71
|
+
{ id: initialState[0].id, sort: 1 },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const result = sessionGroupsReducer(initialState, { type: 'updateSessionGroupOrder', sortMap });
|
|
75
|
+
|
|
76
|
+
expect(result).toHaveLength(2);
|
|
77
|
+
expect(result[0].id).toBe(initialState[1].id);
|
|
78
|
+
expect(result[1].id).toBe(initialState[0].id);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return the initial state for unknown action', () => {
|
|
82
|
+
const result = sessionGroupsReducer(initialState, { type: 'unknown' } as any);
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual(initialState);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { SessionGroupItem } from '@/types/session';
|
|
2
|
+
|
|
3
|
+
export type AddSessionGroupAction = { item: SessionGroupItem; type: 'addSessionGroupItem' };
|
|
4
|
+
export type DeleteSessionGroupAction = { id: string; type: 'deleteSessionGroupItem' };
|
|
5
|
+
export type UpdateSessionGroupAction = {
|
|
6
|
+
id: string;
|
|
7
|
+
item: Partial<SessionGroupItem>;
|
|
8
|
+
type: 'updateSessionGroupItem';
|
|
9
|
+
};
|
|
10
|
+
export type UpdateSessionGroupOrderAction = {
|
|
11
|
+
sortMap: { id: string; sort?: number }[];
|
|
12
|
+
type: 'updateSessionGroupOrder';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SessionGroupsDispatch =
|
|
16
|
+
| AddSessionGroupAction
|
|
17
|
+
| DeleteSessionGroupAction
|
|
18
|
+
| UpdateSessionGroupAction
|
|
19
|
+
| UpdateSessionGroupOrderAction;
|
|
20
|
+
|
|
21
|
+
export const sessionGroupsReducer = (
|
|
22
|
+
state: SessionGroupItem[],
|
|
23
|
+
payload: SessionGroupsDispatch,
|
|
24
|
+
): SessionGroupItem[] => {
|
|
25
|
+
switch (payload.type) {
|
|
26
|
+
case 'addSessionGroupItem': {
|
|
27
|
+
return [...state, payload.item];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case 'deleteSessionGroupItem': {
|
|
31
|
+
return state.filter((item) => item.id !== payload.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'updateSessionGroupItem': {
|
|
35
|
+
return state.map((item) => {
|
|
36
|
+
if (item.id === payload.id) {
|
|
37
|
+
return { ...item, ...payload.item };
|
|
38
|
+
}
|
|
39
|
+
return item;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'updateSessionGroupOrder': {
|
|
44
|
+
return state
|
|
45
|
+
.map((item) => {
|
|
46
|
+
const sort = payload.sortMap.find((i) => i.id === item.id)?.sort;
|
|
47
|
+
return { ...item, sort };
|
|
48
|
+
})
|
|
49
|
+
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
default: {
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { SessionStore } from '@/store/session';
|
|
2
2
|
|
|
3
|
-
const sessionGroupItems = (s: SessionStore) =>
|
|
4
|
-
s.customSessionGroups.map((group) => ({
|
|
5
|
-
id: group.id,
|
|
6
|
-
name: group.name,
|
|
7
|
-
}));
|
|
3
|
+
const sessionGroupItems = (s: SessionStore) => s.sessionGroups;
|
|
8
4
|
|
|
9
5
|
const getGroupById = (id: string) => (s: SessionStore) =>
|
|
10
6
|
sessionGroupItems(s).find((group) => group.id === id);
|
package/src/types/session.ts
CHANGED
|
@@ -44,15 +44,13 @@ export interface LobeAgentSettings {
|
|
|
44
44
|
|
|
45
45
|
export type LobeSessions = LobeAgentSession[];
|
|
46
46
|
|
|
47
|
-
export interface CustomSessionGroup {
|
|
47
|
+
export interface CustomSessionGroup extends SessionGroupItem {
|
|
48
48
|
children: LobeSessions;
|
|
49
|
-
id: SessionGroupId;
|
|
50
|
-
name: string;
|
|
51
49
|
}
|
|
52
50
|
|
|
51
|
+
export type LobeSessionGroups = SessionGroupItem[];
|
|
52
|
+
|
|
53
53
|
export interface ChatSessionList {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
default: LobeSessions;
|
|
57
|
-
pinned: LobeSessions;
|
|
54
|
+
sessionGroups: LobeSessionGroups;
|
|
55
|
+
sessions: LobeSessions;
|
|
58
56
|
}
|
package/src/utils/uuid.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// generate('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 16); //=> "4f90d13a42"
|
|
2
2
|
import { customAlphabet } from 'nanoid/non-secure';
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
'1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
6
|
-
|
|
7
|
-
);
|
|
4
|
+
export const createNanoId = (size = 8) =>
|
|
5
|
+
customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', size);
|
|
6
|
+
|
|
7
|
+
export const nanoid = createNanoId();
|
|
8
8
|
|
|
9
9
|
export { v4 as uuid } from 'uuid';
|