@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +10 -0
  3. package/locales/zh-CN/memory.json +11 -0
  4. package/package.json +1 -1
  5. package/packages/builtin-tool-memory/package.json +1 -0
  6. package/packages/builtin-tool-memory/src/client/Inspector/SearchUserMemory/index.tsx +3 -2
  7. package/packages/builtin-tool-memory/src/manifest.ts +4 -1
  8. package/packages/context-engine/src/engine/messages/index.ts +1 -0
  9. package/packages/context-engine/src/engine/messages/types.ts +11 -0
  10. package/packages/database/src/models/userMemory/activity.ts +163 -0
  11. package/packages/database/src/models/userMemory/index.ts +1 -0
  12. package/packages/database/src/models/userMemory/model.ts +175 -1
  13. package/packages/database/src/repositories/userMemory/index.ts +5 -0
  14. package/packages/prompts/src/prompts/userMemory/__snapshots__/formatSearchResults.test.ts.snap +12 -0
  15. package/packages/prompts/src/prompts/userMemory/formatSearchResults.test.ts +41 -0
  16. package/packages/prompts/src/prompts/userMemory/formatSearchResults.ts +56 -2
  17. package/packages/types/src/userMemory/activity.ts +19 -0
  18. package/packages/types/src/userMemory/index.ts +1 -0
  19. package/packages/types/src/userMemory/tools.ts +10 -1
  20. package/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts +19 -0
  21. package/src/app/[variants]/(main)/memory/_layout/Sidebar/Header/Nav.tsx +8 -0
  22. package/src/app/[variants]/(main)/memory/activities/features/ActivityDropdown.tsx +68 -0
  23. package/src/app/[variants]/(main)/memory/activities/features/ActivityRightPanel.tsx +113 -0
  24. package/src/app/[variants]/(main)/memory/activities/features/List/GridView/ActivityCard.tsx +36 -0
  25. package/src/app/[variants]/(main)/memory/activities/features/List/GridView/index.tsx +32 -0
  26. package/src/app/[variants]/(main)/memory/activities/features/List/TimelineView/ActivityCard.tsx +34 -0
  27. package/src/app/[variants]/(main)/memory/activities/features/List/TimelineView/index.tsx +41 -0
  28. package/src/app/[variants]/(main)/memory/activities/features/List/index.tsx +43 -0
  29. package/src/app/[variants]/(main)/memory/activities/index.tsx +125 -0
  30. package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +1 -0
  31. package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
  32. package/src/locales/default/memory.ts +12 -0
  33. package/src/server/routers/lambda/userMemories.test.ts +18 -4
  34. package/src/server/routers/lambda/userMemories.ts +131 -1
  35. package/src/server/routers/lambda/userMemory.ts +34 -1
  36. package/src/server/services/memory/userMemory/extract.ts +1 -0
  37. package/src/services/chat/mecha/contextEngineering.test.ts +1 -0
  38. package/src/services/chat/mecha/memoryManager.ts +2 -0
  39. package/src/services/userMemory/crud.ts +17 -0
  40. package/src/services/userMemory/index.ts +18 -1
  41. package/src/store/userMemory/initialState.ts +4 -1
  42. package/src/store/userMemory/selectors.ts +1 -0
  43. package/src/store/userMemory/slices/activity/action.ts +123 -0
  44. package/src/store/userMemory/slices/activity/index.ts +2 -0
  45. package/src/store/userMemory/slices/activity/initialState.ts +23 -0
  46. package/src/store/userMemory/slices/agent/action.ts +1 -0
  47. package/src/store/userMemory/slices/base/action.ts +27 -4
  48. 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,4 +1,5 @@
1
1
  export * from './base';
2
+ export * from './activity';
2
3
  export * from './experience';
3
4
  export * from './identity';
4
5
  export * from './layers';
@@ -1,6 +1,11 @@
1
1
  import { z } from 'zod';
2
2
 
3
- import { UserMemoryContext, UserMemoryExperience, UserMemoryPreference } from './layers';
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;
@@ -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;