@lobehub/lobehub 2.0.0-next.370 → 2.0.0-next.372
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 +10 -0
- package/locales/zh-CN/memory.json +11 -0
- package/package.json +1 -1
- package/packages/builtin-tool-memory/package.json +1 -0
- package/packages/builtin-tool-memory/src/client/Inspector/SearchUserMemory/index.tsx +3 -2
- package/packages/builtin-tool-memory/src/manifest.ts +4 -1
- package/packages/context-engine/src/engine/messages/index.ts +1 -0
- package/packages/context-engine/src/engine/messages/types.ts +11 -0
- package/packages/database/src/models/userMemory/activity.ts +163 -0
- package/packages/database/src/models/userMemory/index.ts +1 -0
- package/packages/database/src/models/userMemory/model.ts +175 -1
- package/packages/database/src/repositories/userMemory/index.ts +5 -0
- package/packages/prompts/src/prompts/userMemory/__snapshots__/formatSearchResults.test.ts.snap +12 -0
- package/packages/prompts/src/prompts/userMemory/formatSearchResults.test.ts +41 -0
- package/packages/prompts/src/prompts/userMemory/formatSearchResults.ts +56 -2
- package/packages/types/src/userMemory/activity.ts +19 -0
- package/packages/types/src/userMemory/index.ts +1 -0
- package/packages/types/src/userMemory/tools.ts +10 -1
- package/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts +19 -0
- package/src/app/[variants]/(main)/memory/_layout/Sidebar/Header/Nav.tsx +8 -0
- package/src/app/[variants]/(main)/memory/activities/features/ActivityDropdown.tsx +68 -0
- package/src/app/[variants]/(main)/memory/activities/features/ActivityRightPanel.tsx +113 -0
- package/src/app/[variants]/(main)/memory/activities/features/List/GridView/ActivityCard.tsx +36 -0
- package/src/app/[variants]/(main)/memory/activities/features/List/GridView/index.tsx +32 -0
- package/src/app/[variants]/(main)/memory/activities/features/List/TimelineView/ActivityCard.tsx +34 -0
- package/src/app/[variants]/(main)/memory/activities/features/List/TimelineView/index.tsx +41 -0
- package/src/app/[variants]/(main)/memory/activities/features/List/index.tsx +43 -0
- package/src/app/[variants]/(main)/memory/activities/index.tsx +125 -0
- package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +1 -0
- package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
- package/src/locales/default/memory.ts +12 -0
- package/src/server/routers/lambda/userMemories.test.ts +18 -4
- package/src/server/routers/lambda/userMemories.ts +131 -1
- package/src/server/routers/lambda/userMemory.ts +34 -1
- package/src/server/services/memory/userMemory/extract.ts +1 -0
- package/src/services/chat/mecha/contextEngineering.test.ts +1 -0
- package/src/services/chat/mecha/memoryManager.ts +2 -0
- package/src/services/userMemory/crud.ts +17 -0
- package/src/services/userMemory/index.ts +18 -1
- package/src/store/userMemory/initialState.ts +4 -1
- package/src/store/userMemory/selectors.ts +1 -0
- package/src/store/userMemory/slices/activity/action.ts +123 -0
- package/src/store/userMemory/slices/activity/index.ts +2 -0
- package/src/store/userMemory/slices/activity/initialState.ts +23 -0
- package/src/store/userMemory/slices/agent/action.ts +1 -0
- package/src/store/userMemory/slices/base/action.ts +27 -4
- package/src/store/userMemory/store.ts +3 -0
|
@@ -8,6 +8,7 @@ describe('formatMemorySearchResults', () => {
|
|
|
8
8
|
const result = formatMemorySearchResults({
|
|
9
9
|
query: 'test query',
|
|
10
10
|
results: {
|
|
11
|
+
activities: [],
|
|
11
12
|
contexts: [],
|
|
12
13
|
experiences: [],
|
|
13
14
|
preferences: [],
|
|
@@ -21,6 +22,7 @@ describe('formatMemorySearchResults', () => {
|
|
|
21
22
|
const result = formatMemorySearchResults({
|
|
22
23
|
query: 'project',
|
|
23
24
|
results: {
|
|
25
|
+
activities: [],
|
|
24
26
|
contexts: [
|
|
25
27
|
{
|
|
26
28
|
accessedAt: new Date('2024-01-01'),
|
|
@@ -48,10 +50,46 @@ describe('formatMemorySearchResults', () => {
|
|
|
48
50
|
expect(result).toMatchSnapshot();
|
|
49
51
|
});
|
|
50
52
|
|
|
53
|
+
it('should format activity memories with scheduling and feedback details', () => {
|
|
54
|
+
const result = formatMemorySearchResults({
|
|
55
|
+
query: 'meeting',
|
|
56
|
+
results: {
|
|
57
|
+
activities: [
|
|
58
|
+
{
|
|
59
|
+
createdAt: new Date('2024-01-01'),
|
|
60
|
+
endsAt: new Date('2024-01-01T11:00:00Z'),
|
|
61
|
+
feedback: 'Great alignment',
|
|
62
|
+
id: 'act-1',
|
|
63
|
+
metadata: null,
|
|
64
|
+
narrative: 'Covered Q1 roadmap and risks',
|
|
65
|
+
notes: 'Share slides after the call',
|
|
66
|
+
startsAt: new Date('2024-01-01T10:00:00Z'),
|
|
67
|
+
status: 'scheduled',
|
|
68
|
+
timezone: 'UTC',
|
|
69
|
+
type: 'meeting',
|
|
70
|
+
updatedAt: new Date('2024-01-01'),
|
|
71
|
+
userMemoryId: null,
|
|
72
|
+
associatedLocations: [],
|
|
73
|
+
associatedObjects: [],
|
|
74
|
+
associatedSubjects: [],
|
|
75
|
+
tags: ['meeting', 'Q1'],
|
|
76
|
+
accessedAt: new Date('2024-01-01'),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
contexts: [],
|
|
80
|
+
experiences: [],
|
|
81
|
+
preferences: [],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result).toMatchSnapshot();
|
|
86
|
+
});
|
|
87
|
+
|
|
51
88
|
it('should format experience memories with full content', () => {
|
|
52
89
|
const result = formatMemorySearchResults({
|
|
53
90
|
query: 'debugging',
|
|
54
91
|
results: {
|
|
92
|
+
activities: [],
|
|
55
93
|
contexts: [],
|
|
56
94
|
experiences: [
|
|
57
95
|
{
|
|
@@ -82,6 +120,7 @@ describe('formatMemorySearchResults', () => {
|
|
|
82
120
|
const result = formatMemorySearchResults({
|
|
83
121
|
query: 'code style',
|
|
84
122
|
results: {
|
|
123
|
+
activities: [],
|
|
85
124
|
contexts: [],
|
|
86
125
|
experiences: [],
|
|
87
126
|
preferences: [
|
|
@@ -109,6 +148,7 @@ describe('formatMemorySearchResults', () => {
|
|
|
109
148
|
const result = formatMemorySearchResults({
|
|
110
149
|
query: 'work',
|
|
111
150
|
results: {
|
|
151
|
+
activities: [],
|
|
112
152
|
contexts: [
|
|
113
153
|
{
|
|
114
154
|
accessedAt: new Date('2024-01-01'),
|
|
@@ -171,6 +211,7 @@ describe('formatMemorySearchResults', () => {
|
|
|
171
211
|
const result = formatMemorySearchResults({
|
|
172
212
|
query: 'test',
|
|
173
213
|
results: {
|
|
214
|
+
activities: [],
|
|
174
215
|
contexts: [
|
|
175
216
|
{
|
|
176
217
|
accessedAt: new Date('2024-01-01'),
|
|
@@ -4,9 +4,16 @@ import type { SearchMemoryResult } from '@lobechat/types';
|
|
|
4
4
|
* Search result item interfaces matching the SearchMemoryResult type
|
|
5
5
|
*/
|
|
6
6
|
type ContextResult = SearchMemoryResult['contexts'][number];
|
|
7
|
+
type ActivityResult = SearchMemoryResult['activities'][number];
|
|
7
8
|
type ExperienceResult = SearchMemoryResult['experiences'][number];
|
|
8
9
|
type PreferenceResult = SearchMemoryResult['preferences'][number];
|
|
9
10
|
|
|
11
|
+
const formatDate = (value?: string | Date | null) => {
|
|
12
|
+
if (!value) return value;
|
|
13
|
+
|
|
14
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
15
|
+
};
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Format a single context memory item for search results
|
|
12
19
|
* Format: attributes for metadata, description as text content
|
|
@@ -64,6 +71,48 @@ const formatContextResult = (item: ContextResult): string => {
|
|
|
64
71
|
return ` <context ${attrs.join(' ')}>${content}</context>`;
|
|
65
72
|
};
|
|
66
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Format a single activity memory item for search results
|
|
76
|
+
* Includes scheduling attributes and feedback
|
|
77
|
+
*/
|
|
78
|
+
const formatActivityResult = (item: ActivityResult): string => {
|
|
79
|
+
const attrs: string[] = [`id="${item.id}"`];
|
|
80
|
+
|
|
81
|
+
if (item.type) {
|
|
82
|
+
attrs.push(`type="${item.type}"`);
|
|
83
|
+
}
|
|
84
|
+
if (item.status) {
|
|
85
|
+
attrs.push(`status="${item.status}"`);
|
|
86
|
+
}
|
|
87
|
+
const startsAt = formatDate(item.startsAt);
|
|
88
|
+
if (startsAt) {
|
|
89
|
+
attrs.push(`startsAt="${startsAt}"`);
|
|
90
|
+
}
|
|
91
|
+
const endsAt = formatDate(item.endsAt);
|
|
92
|
+
if (endsAt) {
|
|
93
|
+
attrs.push(`endsAt="${endsAt}"`);
|
|
94
|
+
}
|
|
95
|
+
if (item.timezone) {
|
|
96
|
+
attrs.push(`timezone="${item.timezone}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const children: string[] = [];
|
|
100
|
+
|
|
101
|
+
if (item.feedback) {
|
|
102
|
+
children.push(` <feedback>${item.feedback}</feedback>`);
|
|
103
|
+
}
|
|
104
|
+
if (item.narrative) {
|
|
105
|
+
children.push(` <narrative>${item.narrative}</narrative>`);
|
|
106
|
+
}
|
|
107
|
+
if (item.notes) {
|
|
108
|
+
children.push(` <notes>${item.notes}</notes>`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const content = children.length > 0 ? `\n${children.join('\n')}\n ` : '';
|
|
112
|
+
|
|
113
|
+
return ` <activity ${attrs.join(' ')}>${content}</activity>`;
|
|
114
|
+
};
|
|
115
|
+
|
|
67
116
|
/**
|
|
68
117
|
* Format a single experience memory item for search results
|
|
69
118
|
* Format: attributes for metadata, situation and keyLearning as child elements
|
|
@@ -129,8 +178,8 @@ export const formatMemorySearchResults = ({
|
|
|
129
178
|
query,
|
|
130
179
|
results,
|
|
131
180
|
}: FormatSearchResultsOptions): string => {
|
|
132
|
-
const { contexts, experiences, preferences } = results;
|
|
133
|
-
const total = contexts.length + experiences.length + preferences.length;
|
|
181
|
+
const { activities, contexts, experiences, preferences } = results;
|
|
182
|
+
const total = activities.length + contexts.length + experiences.length + preferences.length;
|
|
134
183
|
|
|
135
184
|
if (total === 0) {
|
|
136
185
|
return `<memories query="${query}">
|
|
@@ -146,6 +195,11 @@ export const formatMemorySearchResults = ({
|
|
|
146
195
|
sections.push(`<contexts count="${contexts.length}">\n${contextsXml}\n</contexts>`);
|
|
147
196
|
}
|
|
148
197
|
|
|
198
|
+
if (activities.length > 0) {
|
|
199
|
+
const activitiesXml = activities.map(formatActivityResult).join('\n');
|
|
200
|
+
sections.push(`<activities count="${activities.length}">\n${activitiesXml}\n</activities>`);
|
|
201
|
+
}
|
|
202
|
+
|
|
149
203
|
// Add experiences section
|
|
150
204
|
if (experiences.length > 0) {
|
|
151
205
|
const experiencesXml = experiences.map(formatExperienceResult).join('\n');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BaseListItem, BaseListParams, BaseListResult } from './shared';
|
|
2
|
+
|
|
3
|
+
export type ActivityListSort = 'capturedAt' | 'startsAt';
|
|
4
|
+
|
|
5
|
+
export interface ActivityListParams extends BaseListParams {
|
|
6
|
+
sort?: ActivityListSort;
|
|
7
|
+
status?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ActivityListItem extends BaseListItem {
|
|
11
|
+
endsAt: Date | null;
|
|
12
|
+
narrative: string | null;
|
|
13
|
+
notes: string | null;
|
|
14
|
+
startsAt: Date | null;
|
|
15
|
+
status: string | null;
|
|
16
|
+
timezone: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ActivityListResult = BaseListResult<ActivityListItem>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
UserMemoryActivity,
|
|
5
|
+
UserMemoryContext,
|
|
6
|
+
UserMemoryExperience,
|
|
7
|
+
UserMemoryPreference,
|
|
8
|
+
} from './layers';
|
|
4
9
|
|
|
5
10
|
export const searchMemorySchema = z.object({
|
|
6
11
|
// TODO: we need to dynamically fetch the available categories/types from the backend
|
|
@@ -8,6 +13,7 @@ export const searchMemorySchema = z.object({
|
|
|
8
13
|
// memoryType: z.string().optional(),
|
|
9
14
|
query: z.string(),
|
|
10
15
|
topK: z.object({
|
|
16
|
+
activities: z.coerce.number().int().min(0),
|
|
11
17
|
contexts: z.coerce.number().int().min(0),
|
|
12
18
|
experiences: z.coerce.number().int().min(0),
|
|
13
19
|
preferences: z.coerce.number().int().min(0),
|
|
@@ -17,6 +23,9 @@ export const searchMemorySchema = z.object({
|
|
|
17
23
|
export type SearchMemoryParams = z.infer<typeof searchMemorySchema>;
|
|
18
24
|
|
|
19
25
|
export interface SearchMemoryResult {
|
|
26
|
+
activities: Array<
|
|
27
|
+
Omit<UserMemoryActivity, 'userId' | 'narrativeVector' | 'feedbackVector'>
|
|
28
|
+
>;
|
|
20
29
|
contexts: Array<Omit<UserMemoryContext, 'userId' | 'titleVector' | 'descriptionVector'>>;
|
|
21
30
|
experiences: Array<
|
|
22
31
|
Omit<UserMemoryExperience, 'userId' | 'actionVector' | 'situationVector' | 'keyLearningVector'>
|
|
@@ -82,6 +82,7 @@ export const POST = async (req: Request) => {
|
|
|
82
82
|
const searchResult = await model.searchWithEmbedding({
|
|
83
83
|
embedding,
|
|
84
84
|
limits: {
|
|
85
|
+
activities: topK,
|
|
85
86
|
contexts: topK,
|
|
86
87
|
experiences: topK,
|
|
87
88
|
preferences: topK,
|
|
@@ -102,6 +103,9 @@ export const POST = async (req: Request) => {
|
|
|
102
103
|
...searchResult.preferences
|
|
103
104
|
.map((preference) => preference.userMemoryId)
|
|
104
105
|
.filter((id): id is string => !!id),
|
|
106
|
+
...searchResult.activities
|
|
107
|
+
.map((activity) => activity.userMemoryId)
|
|
108
|
+
.filter((id): id is string => !!id),
|
|
105
109
|
...identities
|
|
106
110
|
.map((identity) => identity.userMemoryId)
|
|
107
111
|
.filter((id): id is string => !!id),
|
|
@@ -179,10 +183,25 @@ export const POST = async (req: Request) => {
|
|
|
179
183
|
})
|
|
180
184
|
.filter(Boolean);
|
|
181
185
|
|
|
186
|
+
const activityItems = searchResult.activities
|
|
187
|
+
.map((activity) => {
|
|
188
|
+
const memory = activity.userMemoryId ? memoryMap.get(activity.userMemoryId) : undefined;
|
|
189
|
+
if (!memory) return undefined;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
activity,
|
|
193
|
+
id: activity.userMemoryId,
|
|
194
|
+
layer: LayersEnum.Activity,
|
|
195
|
+
memory,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
|
|
182
200
|
const items = [
|
|
183
201
|
...contextItems.slice(0, topK),
|
|
184
202
|
...experienceItems.slice(0, topK),
|
|
185
203
|
...preferenceItems.slice(0, topK),
|
|
204
|
+
...activityItems.slice(0, topK),
|
|
186
205
|
...identityItems,
|
|
187
206
|
];
|
|
188
207
|
console.log('[locomo-dev-search] compiled items');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Flexbox } from '@lobehub/ui';
|
|
4
4
|
import {
|
|
5
|
+
CalendarClockIcon,
|
|
5
6
|
BrainCircuitIcon,
|
|
6
7
|
BubblesIcon,
|
|
7
8
|
HeartPulseIcon,
|
|
@@ -26,6 +27,7 @@ interface Item {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
enum MemoryTabKey {
|
|
30
|
+
Activities = 'activities',
|
|
29
31
|
Contexts = 'contexts',
|
|
30
32
|
Experiences = 'experiences',
|
|
31
33
|
Home = 'home',
|
|
@@ -85,6 +87,12 @@ const Nav = memo(() => {
|
|
|
85
87
|
title: t('tab.experiences'),
|
|
86
88
|
url: '/memory/experiences',
|
|
87
89
|
},
|
|
90
|
+
{
|
|
91
|
+
icon: CalendarClockIcon,
|
|
92
|
+
key: MemoryTabKey.Activities,
|
|
93
|
+
title: t('tab.activities'),
|
|
94
|
+
url: '/memory/activities',
|
|
95
|
+
},
|
|
88
96
|
],
|
|
89
97
|
[t],
|
|
90
98
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ActionIcon, type ActionIconProps, DropdownMenu } from '@lobehub/ui';
|
|
2
|
+
import { App } from 'antd';
|
|
3
|
+
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
|
|
4
|
+
import { type KeyboardEvent, type MouseEvent, memo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import { useUserMemoryStore } from '@/store/userMemory';
|
|
8
|
+
|
|
9
|
+
interface ActivityDropdownProps {
|
|
10
|
+
id: string;
|
|
11
|
+
size?: ActionIconProps['size'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ActivityDropdown = memo<ActivityDropdownProps>(({ id, size = 'small' }) => {
|
|
15
|
+
const { t } = useTranslation(['memory', 'common']);
|
|
16
|
+
const { modal } = App.useApp();
|
|
17
|
+
|
|
18
|
+
const activities = useUserMemoryStore((s) => s.activities);
|
|
19
|
+
const deleteActivity = useUserMemoryStore((s) => s.deleteActivity);
|
|
20
|
+
const setEditingMemory = useUserMemoryStore((s) => s.setEditingMemory);
|
|
21
|
+
|
|
22
|
+
const handleMenuClick = (info: { domEvent: MouseEvent | KeyboardEvent; key: string }) => {
|
|
23
|
+
info.domEvent.stopPropagation();
|
|
24
|
+
|
|
25
|
+
if (info.key === 'edit') {
|
|
26
|
+
const activity = activities.find((item) => item.id === id);
|
|
27
|
+
if (activity) {
|
|
28
|
+
setEditingMemory(id, activity.narrative || activity.notes || '', 'activity');
|
|
29
|
+
}
|
|
30
|
+
} else if (info.key === 'delete') {
|
|
31
|
+
modal.confirm({
|
|
32
|
+
cancelText: t('cancel', { ns: 'common' }),
|
|
33
|
+
content: t('activity.deleteConfirm'),
|
|
34
|
+
okButtonProps: { danger: true },
|
|
35
|
+
okText: t('confirm', { ns: 'common' }),
|
|
36
|
+
onOk: async () => {
|
|
37
|
+
await deleteActivity(id);
|
|
38
|
+
},
|
|
39
|
+
title: t('activity.deleteTitle'),
|
|
40
|
+
type: 'warning',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const menuItems = [
|
|
46
|
+
{
|
|
47
|
+
icon: <Pencil size={14} />,
|
|
48
|
+
key: 'edit',
|
|
49
|
+
label: t('activity.actions.edit'),
|
|
50
|
+
onClick: handleMenuClick,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
danger: true,
|
|
54
|
+
icon: <Trash2 size={14} />,
|
|
55
|
+
key: 'delete',
|
|
56
|
+
label: t('activity.actions.delete'),
|
|
57
|
+
onClick: handleMenuClick,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<DropdownMenu items={menuItems}>
|
|
63
|
+
<ActionIcon icon={MoreHorizontal} size={size} />
|
|
64
|
+
</DropdownMenu>
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export default ActivityDropdown;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Flexbox, Tag, Text } from '@lobehub/ui';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { memo, useMemo } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import CateTag from '@/app/[variants]/(main)/memory/features/CateTag';
|
|
9
|
+
import DetailLoading from '@/app/[variants]/(main)/memory/features/DetailLoading';
|
|
10
|
+
import DetailPanel from '@/app/[variants]/(main)/memory/features/DetailPanel';
|
|
11
|
+
import HashTags from '@/app/[variants]/(main)/memory/features/HashTags';
|
|
12
|
+
import HighlightedContent from '@/app/[variants]/(main)/memory/features/HighlightedContent';
|
|
13
|
+
import SourceLink from '@/app/[variants]/(main)/memory/features/SourceLink';
|
|
14
|
+
import Time from '@/app/[variants]/(main)/memory/features/Time';
|
|
15
|
+
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
16
|
+
import { useQueryState } from '@/hooks/useQueryParam';
|
|
17
|
+
import { useUserMemoryStore } from '@/store/userMemory';
|
|
18
|
+
import { LayersEnum } from '@/types/userMemory';
|
|
19
|
+
|
|
20
|
+
import ActivityDropdown from './ActivityDropdown';
|
|
21
|
+
|
|
22
|
+
const formatTime = (value?: Date | string | null) => {
|
|
23
|
+
if (!value) return null;
|
|
24
|
+
const time = dayjs(value);
|
|
25
|
+
if (!time.isValid()) return null;
|
|
26
|
+
return time.format('YYYY-MM-DD HH:mm');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ActivityRightPanel = memo(() => {
|
|
30
|
+
const { t } = useTranslation('memory');
|
|
31
|
+
const [activityId] = useQueryState('activityId', { clearOnDefault: true });
|
|
32
|
+
const useFetchMemoryDetail = useUserMemoryStore((s) => s.useFetchMemoryDetail);
|
|
33
|
+
|
|
34
|
+
const { data: activity, isLoading } = useFetchMemoryDetail(activityId, LayersEnum.Activity);
|
|
35
|
+
|
|
36
|
+
const schedule = useMemo(() => {
|
|
37
|
+
if (!activity) return null;
|
|
38
|
+
const start = formatTime(activity.startsAt);
|
|
39
|
+
const end = formatTime(activity.endsAt);
|
|
40
|
+
if (!start && !end) return null;
|
|
41
|
+
if (start && end) return `${start} → ${end}`;
|
|
42
|
+
return start || end;
|
|
43
|
+
}, [activity]);
|
|
44
|
+
|
|
45
|
+
if (!activityId) return null;
|
|
46
|
+
|
|
47
|
+
let content;
|
|
48
|
+
if (isLoading) content = <DetailLoading />;
|
|
49
|
+
if (activity) {
|
|
50
|
+
const capturedAt =
|
|
51
|
+
activity.startsAt ||
|
|
52
|
+
activity.capturedAt ||
|
|
53
|
+
activity.updatedAt ||
|
|
54
|
+
activity.createdAt ||
|
|
55
|
+
undefined;
|
|
56
|
+
|
|
57
|
+
content = (
|
|
58
|
+
<>
|
|
59
|
+
<CateTag cate={activity.type} />
|
|
60
|
+
<Text
|
|
61
|
+
as={'h1'}
|
|
62
|
+
fontSize={20}
|
|
63
|
+
style={{
|
|
64
|
+
lineHeight: 1.4,
|
|
65
|
+
marginBottom: 0,
|
|
66
|
+
}}
|
|
67
|
+
weight={'bold'}
|
|
68
|
+
>
|
|
69
|
+
{activity.title || t('activity.defaultType')}
|
|
70
|
+
</Text>
|
|
71
|
+
<Flexbox align="center" gap={16} horizontal justify="space-between">
|
|
72
|
+
{activity.status && <Tag>{activity.status}</Tag>}
|
|
73
|
+
<SourceLink source={activity.source} />
|
|
74
|
+
</Flexbox>
|
|
75
|
+
<Flexbox align="center" gap={16} horizontal justify="space-between">
|
|
76
|
+
<Time capturedAt={capturedAt} />
|
|
77
|
+
{activity.timezone && (
|
|
78
|
+
<Text fontSize={12} type="secondary">
|
|
79
|
+
{activity.timezone}
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
</Flexbox>
|
|
83
|
+
|
|
84
|
+
{schedule && <HighlightedContent>{schedule}</HighlightedContent>}
|
|
85
|
+
{activity.narrative && (
|
|
86
|
+
<HighlightedContent title={t('activity.narrative')}>{activity.narrative}</HighlightedContent>
|
|
87
|
+
)}
|
|
88
|
+
{activity.notes && (
|
|
89
|
+
<HighlightedContent title={t('activity.notes')}>{activity.notes}</HighlightedContent>
|
|
90
|
+
)}
|
|
91
|
+
{activity.feedback && (
|
|
92
|
+
<HighlightedContent title={t('activity.feedback')}>{activity.feedback}</HighlightedContent>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<HashTags hashTags={activity.tags} />
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<DetailPanel
|
|
102
|
+
header={{
|
|
103
|
+
right: activityId ? (
|
|
104
|
+
<ActivityDropdown id={activityId} size={DESKTOP_HEADER_ICON_SIZE} />
|
|
105
|
+
) : undefined,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</DetailPanel>
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export default ActivityRightPanel;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ActivityListItem } from '@lobechat/types';
|
|
2
|
+
import { Tag } from '@lobehub/ui';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
import GridCard from '@/app/[variants]/(main)/memory/features/GridView/GridCard';
|
|
7
|
+
|
|
8
|
+
import ActivityDropdown from '../../ActivityDropdown';
|
|
9
|
+
|
|
10
|
+
interface ActivityCardProps {
|
|
11
|
+
activity: ActivityListItem;
|
|
12
|
+
onClick: (activity: ActivityListItem) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ActivityCard = memo<ActivityCardProps>(({ activity, onClick }) => {
|
|
16
|
+
const { t } = useTranslation('memory');
|
|
17
|
+
const capturedAt =
|
|
18
|
+
activity.startsAt || activity.capturedAt || activity.updatedAt || activity.createdAt;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<GridCard
|
|
22
|
+
actions={<ActivityDropdown id={activity.id} />}
|
|
23
|
+
badges={activity.status ? <Tag>{activity.status}</Tag> : undefined}
|
|
24
|
+
capturedAt={capturedAt}
|
|
25
|
+
cate={activity.type}
|
|
26
|
+
hashTags={activity.tags}
|
|
27
|
+
onClick={() => onClick(activity)}
|
|
28
|
+
title={activity.title || t('activity.defaultType')}
|
|
29
|
+
titleAddon={activity.timezone}
|
|
30
|
+
>
|
|
31
|
+
{activity.narrative || activity.notes || t('activity.defaultType')}
|
|
32
|
+
</GridCard>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export default ActivityCard;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ActivityListItem } from '@lobechat/types';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useUserMemoryStore } from '@/store/userMemory';
|
|
5
|
+
|
|
6
|
+
import { GridView } from '../../../../features/GridView';
|
|
7
|
+
import ActivityCard from './ActivityCard';
|
|
8
|
+
|
|
9
|
+
interface GridViewProps {
|
|
10
|
+
activities: ActivityListItem[];
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
onClick: (activity: ActivityListItem) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ActivitiesGridView = memo<GridViewProps>(({ activities, isLoading, onClick }) => {
|
|
16
|
+
const loadMoreActivities = useUserMemoryStore((s) => s.loadMoreActivities);
|
|
17
|
+
const activitiesHasMore = useUserMemoryStore((s) => s.activitiesHasMore);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<GridView
|
|
21
|
+
hasMore={activitiesHasMore}
|
|
22
|
+
isLoading={isLoading}
|
|
23
|
+
items={activities}
|
|
24
|
+
onLoadMore={loadMoreActivities}
|
|
25
|
+
renderItem={(activity: ActivityListItem) => (
|
|
26
|
+
<ActivityCard activity={activity} onClick={() => onClick(activity)} />
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default ActivitiesGridView;
|
package/src/app/[variants]/(main)/memory/activities/features/List/TimelineView/ActivityCard.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActivityListItem } from '@lobechat/types';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
import TimeLineCard from '@/app/[variants]/(main)/memory/features/TimeLineView/TimeLineCard';
|
|
6
|
+
|
|
7
|
+
import ActivityDropdown from '../../ActivityDropdown';
|
|
8
|
+
|
|
9
|
+
interface ActivityCardProps {
|
|
10
|
+
activity: ActivityListItem;
|
|
11
|
+
onClick: (activity: ActivityListItem) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ActivityCard = memo<ActivityCardProps>(({ activity, onClick }) => {
|
|
15
|
+
const { t } = useTranslation('memory');
|
|
16
|
+
const capturedAt =
|
|
17
|
+
activity.startsAt || activity.capturedAt || activity.updatedAt || activity.createdAt;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<TimeLineCard
|
|
21
|
+
actions={<ActivityDropdown id={activity.id} />}
|
|
22
|
+
capturedAt={capturedAt}
|
|
23
|
+
cate={activity.type}
|
|
24
|
+
hashTags={activity.tags}
|
|
25
|
+
onClick={() => onClick(activity)}
|
|
26
|
+
title={activity.title || t('activity.defaultType')}
|
|
27
|
+
titleAddon={activity.status}
|
|
28
|
+
>
|
|
29
|
+
{activity.narrative || activity.notes || activity.status || t('activity.defaultType')}
|
|
30
|
+
</TimeLineCard>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default ActivityCard;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ActivityListItem } from '@lobechat/types';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useUserMemoryStore } from '@/store/userMemory';
|
|
7
|
+
|
|
8
|
+
import { TimelineView as GenericTimelineView } from '../../../../features/TimeLineView';
|
|
9
|
+
import { PeriodHeader, TimelineItemWrapper } from '../../../../features/TimeLineView/PeriodGroup';
|
|
10
|
+
import ActivityCard from './ActivityCard';
|
|
11
|
+
|
|
12
|
+
interface ActivityTimelineViewProps {
|
|
13
|
+
activities: ActivityListItem[];
|
|
14
|
+
isLoading?: boolean;
|
|
15
|
+
onCardClick: (activity: ActivityListItem) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ActivityTimelineView = memo<ActivityTimelineViewProps>(
|
|
19
|
+
({ activities, isLoading, onCardClick }) => {
|
|
20
|
+
const loadMoreActivities = useUserMemoryStore((s) => s.loadMoreActivities);
|
|
21
|
+
const activitiesHasMore = useUserMemoryStore((s) => s.activitiesHasMore);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<GenericTimelineView
|
|
25
|
+
data={activities}
|
|
26
|
+
groupBy="day"
|
|
27
|
+
hasMore={activitiesHasMore}
|
|
28
|
+
isLoading={isLoading}
|
|
29
|
+
onLoadMore={loadMoreActivities}
|
|
30
|
+
renderHeader={(periodKey: string) => <PeriodHeader groupBy="day" periodKey={periodKey} />}
|
|
31
|
+
renderItem={(activity: ActivityListItem) => (
|
|
32
|
+
<TimelineItemWrapper>
|
|
33
|
+
<ActivityCard activity={activity} onClick={onCardClick} />
|
|
34
|
+
</TimelineItemWrapper>
|
|
35
|
+
)}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export default ActivityTimelineView;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
import MemoryEmpty from '@/app/[variants]/(main)/memory/features/MemoryEmpty';
|
|
5
|
+
import { useQueryState } from '@/hooks/useQueryParam';
|
|
6
|
+
import { useGlobalStore } from '@/store/global';
|
|
7
|
+
import { useUserMemoryStore } from '@/store/userMemory';
|
|
8
|
+
|
|
9
|
+
import { type ViewMode } from '../../../features/ViewModeSwitcher';
|
|
10
|
+
import GridView from './GridView';
|
|
11
|
+
import TimelineView from './TimelineView';
|
|
12
|
+
|
|
13
|
+
interface ActivitiesListProps {
|
|
14
|
+
isLoading?: boolean;
|
|
15
|
+
searchValue?: string;
|
|
16
|
+
viewMode: ViewMode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ActivitiesList = memo<ActivitiesListProps>(({ isLoading, searchValue, viewMode }) => {
|
|
20
|
+
const { t } = useTranslation(['memory', 'common']);
|
|
21
|
+
const [, setActivityId] = useQueryState('activityId', { clearOnDefault: true });
|
|
22
|
+
const toggleRightPanel = useGlobalStore((s) => s.toggleRightPanel);
|
|
23
|
+
const activities = useUserMemoryStore((s) => s.activities);
|
|
24
|
+
|
|
25
|
+
const handleCardClick = (activity: any) => {
|
|
26
|
+
setActivityId(activity.id);
|
|
27
|
+
toggleRightPanel(true);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const isEmpty = activities.length === 0;
|
|
31
|
+
|
|
32
|
+
if (isEmpty) {
|
|
33
|
+
return <MemoryEmpty search={Boolean(searchValue)} title={t('activity.empty')} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return viewMode === 'timeline' ? (
|
|
37
|
+
<TimelineView activities={activities} isLoading={isLoading} onCardClick={handleCardClick} />
|
|
38
|
+
) : (
|
|
39
|
+
<GridView activities={activities} isLoading={isLoading} onClick={handleCardClick} />
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default ActivitiesList;
|