@lobehub/lobehub 2.0.0-next.353 → 2.0.0-next.354
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/.agents/skills/add-provider-doc/SKILL.md +90 -0
- package/.agents/skills/add-setting-env/SKILL.md +106 -0
- package/.agents/skills/debug/SKILL.md +66 -0
- package/.agents/skills/desktop/SKILL.md +78 -0
- package/.agents/skills/desktop/references/feature-implementation.md +99 -0
- package/.agents/skills/desktop/references/local-tools.md +133 -0
- package/.agents/skills/desktop/references/menu-config.md +103 -0
- package/.agents/skills/desktop/references/window-management.md +143 -0
- package/.agents/skills/drizzle/SKILL.md +129 -0
- package/.agents/skills/drizzle/references/db-migrations.md +50 -0
- package/.agents/skills/hotkey/SKILL.md +90 -0
- package/{.cursor/rules/i18n.mdc → .agents/skills/i18n/SKILL.md} +14 -23
- package/.agents/skills/linear/SKILL.md +51 -0
- package/.agents/skills/microcopy/SKILL.md +83 -0
- package/.agents/skills/modal/SKILL.md +102 -0
- package/{.cursor/rules/project-structure.mdc → .agents/skills/project-overview/SKILL.md} +65 -37
- package/.agents/skills/react/SKILL.md +73 -0
- package/.agents/skills/react/references/layout-kit.md +100 -0
- package/.agents/skills/recent-data/SKILL.md +108 -0
- package/.agents/skills/testing/SKILL.md +89 -0
- package/.agents/skills/testing/references/agent-runtime-e2e.md +126 -0
- package/.agents/skills/testing/references/db-model-test.md +124 -0
- package/.agents/skills/testing/references/desktop-controller-test.md +124 -0
- package/.agents/skills/testing/references/electron-ipc-test.md +63 -0
- package/.agents/skills/testing/references/zustand-store-action-test.md +150 -0
- package/.agents/skills/typescript/SKILL.md +52 -0
- package/.agents/skills/zustand/SKILL.md +78 -0
- package/.agents/skills/zustand/references/action-patterns.md +125 -0
- package/.agents/skills/zustand/references/slice-organization.md +125 -0
- package/AGENTS.md +42 -55
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +57 -46
- package/GEMINI.md +47 -39
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +2 -3
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +1 -2
- package/src/libs/pdfjs/index.tsx +25 -0
- package/src/store/test-coverage.md +5 -5
- package/.cursor/rules/add-provider-doc.mdc +0 -183
- package/.cursor/rules/add-setting-env.mdc +0 -175
- package/.cursor/rules/cursor-rules.mdc +0 -28
- package/.cursor/rules/db-migrations.mdc +0 -46
- package/.cursor/rules/debug-usage.mdc +0 -86
- package/.cursor/rules/desktop-controller-tests.mdc +0 -189
- package/.cursor/rules/desktop-feature-implementation.mdc +0 -155
- package/.cursor/rules/desktop-local-tools-implement.mdc +0 -81
- package/.cursor/rules/desktop-menu-configuration.mdc +0 -209
- package/.cursor/rules/desktop-window-management.mdc +0 -301
- package/.cursor/rules/drizzle-schema-style-guide.mdc +0 -218
- package/.cursor/rules/hotkey.mdc +0 -162
- package/.cursor/rules/linear.mdc +0 -53
- package/.cursor/rules/microcopy-cn.mdc +0 -158
- package/.cursor/rules/microcopy-en.mdc +0 -148
- package/.cursor/rules/modal-imperative.mdc +0 -162
- package/.cursor/rules/packages/react-layout-kit.mdc +0 -122
- package/.cursor/rules/project-introduce.mdc +0 -36
- package/.cursor/rules/react.mdc +0 -169
- package/.cursor/rules/recent-data-usage.mdc +0 -139
- package/.cursor/rules/rules-index.mdc +0 -44
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +0 -285
- package/.cursor/rules/testing-guide/db-model-test.mdc +0 -455
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +0 -80
- package/.cursor/rules/testing-guide/testing-guide.mdc +0 -534
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +0 -574
- package/.cursor/rules/typescript.mdc +0 -55
- package/.cursor/rules/zustand-action-patterns.mdc +0 -328
- package/.cursor/rules/zustand-slice-organization.mdc +0 -308
- package/src/libs/pdfjs/pdf.worker.ts +0 -1
- package/src/libs/pdfjs/worker.ts +0 -12
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-event-handler-refs.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-use-latest.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-api-routes.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-defer-await.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-parallel.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-suspense-boundaries.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-barrel-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-conditional.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-defer-third-party.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-dynamic-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-preload.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-localstorage-schema.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-passive-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-swr-dedup.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-batch-dom-css.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-function-results.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-property-access.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-storage.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-combine-iterations.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-early-exit.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-hoist-regexp.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-index-maps.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-length-check-first.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-min-max-loop.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-set-map-lookups.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-tosorted-immutable.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-activity.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-animate-svg-wrapper.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-conditional-render.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-content-visibility.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hoist-jsx.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hydration-no-flicker.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-svg-precision.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-defer-reads.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-derived-state.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-functional-setstate.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-lazy-state-init.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-memo.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-transitions.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-after-nonblocking.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-lru.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-react.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-parallel-fetching.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-serialization.md +0 -0
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description:
|
|
3
|
-
globs: src/store/**
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# LobeChat Zustand Action Patterns
|
|
8
|
-
|
|
9
|
-
## Action Type Hierarchy
|
|
10
|
-
|
|
11
|
-
LobeChat Actions use a layered architecture with clear separation of responsibilities:
|
|
12
|
-
|
|
13
|
-
### 1. Public Actions
|
|
14
|
-
|
|
15
|
-
Main interfaces exposed for UI component consumption:
|
|
16
|
-
|
|
17
|
-
- Naming: Verb form (`createTopic`, `sendMessage`, `updateTopicTitle`)
|
|
18
|
-
- Responsibilities: Parameter validation, flow orchestration, calling internal actions
|
|
19
|
-
- Example: `src/store/chat/slices/topic/action.ts`
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
// Public Action example
|
|
23
|
-
createTopic: async () => {
|
|
24
|
-
// ...
|
|
25
|
-
return topicId;
|
|
26
|
-
},
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### 2. Internal Actions (`internal_*`)
|
|
30
|
-
|
|
31
|
-
Internal implementation details handling core business logic:
|
|
32
|
-
|
|
33
|
-
- Naming: `internal_` prefix + verb (`internal_createTopic`, `internal_updateMessageContent`)
|
|
34
|
-
- Responsibilities: Optimistic updates, service calls, error handling, state synchronization
|
|
35
|
-
- Should not be called directly by UI components
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// Internal Action example - Optimistic update pattern
|
|
39
|
-
internal_createTopic: async (params) => {
|
|
40
|
-
const tmpId = Date.now().toString();
|
|
41
|
-
|
|
42
|
-
// 1. Immediately update frontend state (optimistic update)
|
|
43
|
-
get().internal_dispatchTopic(
|
|
44
|
-
{ type: 'addTopic', value: { ...params, id: tmpId } },
|
|
45
|
-
'internal_createTopic',
|
|
46
|
-
);
|
|
47
|
-
get().internal_updateTopicLoading(tmpId, true);
|
|
48
|
-
|
|
49
|
-
// 2. Call backend service
|
|
50
|
-
const topicId = await topicService.createTopic(params);
|
|
51
|
-
get().internal_updateTopicLoading(tmpId, false);
|
|
52
|
-
|
|
53
|
-
// 3. Refresh data to ensure consistency
|
|
54
|
-
get().internal_updateTopicLoading(topicId, true);
|
|
55
|
-
await get().refreshTopic();
|
|
56
|
-
get().internal_updateTopicLoading(topicId, false);
|
|
57
|
-
|
|
58
|
-
return topicId;
|
|
59
|
-
},
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 3. Dispatch Methods (`internal_dispatch*`)
|
|
63
|
-
|
|
64
|
-
Methods dedicated to handling state updates:
|
|
65
|
-
|
|
66
|
-
- Naming: `internal_dispatch` + entity name (`internal_dispatchTopic`, `internal_dispatchMessage`)
|
|
67
|
-
- Responsibilities: Calling reducers, updating Zustand store, handling state comparison
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// Dispatch Method example
|
|
71
|
-
internal_dispatchTopic: (payload, action) => {
|
|
72
|
-
const nextTopics = topicReducer(topicSelectors.currentTopics(get()), payload);
|
|
73
|
-
const nextMap = { ...get().topicMaps, [get().activeId]: nextTopics };
|
|
74
|
-
|
|
75
|
-
if (isEqual(nextMap, get().topicMaps)) return;
|
|
76
|
-
|
|
77
|
-
set({ topicMaps: nextMap }, false, action ?? n(`dispatchTopic/${payload.type}`));
|
|
78
|
-
},
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## When to Use Reducer Pattern vs. Simple `set`
|
|
82
|
-
|
|
83
|
-
### Use Reducer Pattern When
|
|
84
|
-
|
|
85
|
-
Suitable for complex data structure management, especially:
|
|
86
|
-
|
|
87
|
-
- Managing object lists or maps (e.g., `messagesMap`, `topicMaps`)
|
|
88
|
-
- Scenarios requiring optimistic updates
|
|
89
|
-
- Complex state transition logic
|
|
90
|
-
- Type-safe action payloads needed
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
// Reducer pattern example - Complex message state management
|
|
94
|
-
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
|
|
95
|
-
switch (payload.type) {
|
|
96
|
-
case 'updateMessage': {
|
|
97
|
-
return produce(state, (draftState) => {
|
|
98
|
-
const index = draftState.findIndex((i) => i.id === payload.id);
|
|
99
|
-
if (index < 0) return;
|
|
100
|
-
draftState[index] = merge(draftState[index], {
|
|
101
|
-
...payload.value,
|
|
102
|
-
updatedAt: Date.now(),
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
case 'createMessage': {
|
|
107
|
-
// ...
|
|
108
|
-
}
|
|
109
|
-
// ...other complex state transitions
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Use Simple `set` When
|
|
115
|
-
|
|
116
|
-
Suitable for simple state updates:
|
|
117
|
-
|
|
118
|
-
- Toggling boolean values
|
|
119
|
-
- Updating simple strings/numbers
|
|
120
|
-
- Setting single state fields
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// Simple set example
|
|
124
|
-
updateInputMessage: (message) => {
|
|
125
|
-
if (isEqual(message, get().inputMessage)) return;
|
|
126
|
-
set({ inputMessage: message }, false, n('updateInputMessage'));
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
togglePortal: (open?: boolean) => {
|
|
130
|
-
set({ showPortal: open ?? !get().showPortal }, false, 'togglePortal');
|
|
131
|
-
},
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Optimistic Update Implementation Patterns
|
|
135
|
-
|
|
136
|
-
Optimistic updates are a core pattern in LobeChat for providing smooth user experience:
|
|
137
|
-
|
|
138
|
-
### Standard Optimistic Update Flow
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
// Complete optimistic update example
|
|
142
|
-
internal_updateMessageContent: async (id, content, extra) => {
|
|
143
|
-
const { internal_dispatchMessage, refreshMessages } = get();
|
|
144
|
-
|
|
145
|
-
// 1. Immediately update frontend state (optimistic update)
|
|
146
|
-
internal_dispatchMessage({
|
|
147
|
-
id,
|
|
148
|
-
type: 'updateMessage',
|
|
149
|
-
value: { content },
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// 2. Call backend service
|
|
153
|
-
await messageService.updateMessage(id, {
|
|
154
|
-
content,
|
|
155
|
-
tools: extra?.toolCalls ? internal_transformToolCalls(extra.toolCalls) : undefined,
|
|
156
|
-
// ...other fields
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// 3. Refresh to ensure data consistency
|
|
160
|
-
await refreshMessages();
|
|
161
|
-
},
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Optimistic Update for Create Operations
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
internal_createMessage: async (message, context) => {
|
|
168
|
-
const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
|
|
169
|
-
|
|
170
|
-
let tempId = context?.tempMessageId;
|
|
171
|
-
if (!tempId) {
|
|
172
|
-
// Create temporary message for optimistic update
|
|
173
|
-
tempId = internal_createTmpMessage(message);
|
|
174
|
-
internal_toggleMessageLoading(true, tempId);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const id = await messageService.createMessage(message);
|
|
179
|
-
if (!context?.skipRefresh) {
|
|
180
|
-
await refreshMessages();
|
|
181
|
-
}
|
|
182
|
-
internal_toggleMessageLoading(false, tempId);
|
|
183
|
-
return id;
|
|
184
|
-
} catch (e) {
|
|
185
|
-
internal_toggleMessageLoading(false, tempId);
|
|
186
|
-
// Error handling: update message error state
|
|
187
|
-
internal_dispatchMessage({
|
|
188
|
-
id: tempId,
|
|
189
|
-
type: 'updateMessage',
|
|
190
|
-
value: { error: { type: ChatErrorType.CreateMessageError, message: e.message } },
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Delete Operation Pattern (No Optimistic Update)
|
|
197
|
-
|
|
198
|
-
Delete operations typically don't suit optimistic updates because:
|
|
199
|
-
|
|
200
|
-
- Deletion is destructive; error recovery is complex
|
|
201
|
-
- Users have lower expectations for immediate feedback on deletions
|
|
202
|
-
- Restoring state on deletion failure causes confusion
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
// Standard delete operation pattern - No optimistic update
|
|
206
|
-
removeGenerationTopic: async (id: string) => {
|
|
207
|
-
const { internal_removeGenerationTopic } = get();
|
|
208
|
-
await internal_removeGenerationTopic(id);
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
internal_removeGenerationTopic: async (id: string) => {
|
|
212
|
-
// 1. Show loading state
|
|
213
|
-
get().internal_updateGenerationTopicLoading(id, true);
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
// 2. Directly call backend service
|
|
217
|
-
await generationTopicService.deleteTopic(id);
|
|
218
|
-
|
|
219
|
-
// 3. Refresh data to get latest state
|
|
220
|
-
await get().refreshGenerationTopics();
|
|
221
|
-
} finally {
|
|
222
|
-
// 4. Ensure loading state is cleared (whether success or failure)
|
|
223
|
-
get().internal_updateGenerationTopicLoading(id, false);
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Delete operation characteristics:
|
|
229
|
-
|
|
230
|
-
- Directly call service without pre-updating state
|
|
231
|
-
- Rely on loading state for user feedback
|
|
232
|
-
- Refresh entire list after operation to ensure consistency
|
|
233
|
-
- Use `try/finally` to ensure loading state is always cleaned up
|
|
234
|
-
|
|
235
|
-
## Loading State Management Pattern
|
|
236
|
-
|
|
237
|
-
LobeChat uses a unified loading state management pattern:
|
|
238
|
-
|
|
239
|
-
### Array-based Loading State
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// Define in initialState.ts
|
|
243
|
-
export interface ChatMessageState {
|
|
244
|
-
messageEditingIds: string[]; // Message editing state
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Manage in action
|
|
248
|
-
{
|
|
249
|
-
toggleMessageEditing: (id, editing) => {
|
|
250
|
-
set(
|
|
251
|
-
{ messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
|
|
252
|
-
false,
|
|
253
|
-
'toggleMessageEditing',
|
|
254
|
-
);
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## SWR Integration Pattern
|
|
260
|
-
|
|
261
|
-
LobeChat uses SWR for data fetching and cache management:
|
|
262
|
-
|
|
263
|
-
### Hook-based Data Fetching
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// Define SWR hook in action.ts
|
|
267
|
-
useFetchMessages: (enable, sessionId, activeTopicId) =>
|
|
268
|
-
useClientDataSWR<ChatMessage[]>(
|
|
269
|
-
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
|
|
270
|
-
async ([, sessionId, topicId]) => messageService.getMessages(sessionId, topicId),
|
|
271
|
-
{
|
|
272
|
-
onSuccess: (messages, key) => {
|
|
273
|
-
const nextMap = {
|
|
274
|
-
...get().messagesMap,
|
|
275
|
-
[messageMapKey(sessionId, activeTopicId)]: messages,
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
|
|
279
|
-
|
|
280
|
-
set({ messagesInit: true, messagesMap: nextMap }, false, n('useFetchMessages'));
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
),
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Cache Invalidation and Refresh
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
// Standard data refresh pattern
|
|
290
|
-
refreshMessages: async () => {
|
|
291
|
-
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
|
|
292
|
-
};
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Naming Convention Summary
|
|
296
|
-
|
|
297
|
-
### Action Naming Patterns
|
|
298
|
-
|
|
299
|
-
- Public Actions: Verb form, describing user intent
|
|
300
|
-
- `createTopic`, `sendMessage`, `regenerateMessage`
|
|
301
|
-
- Internal Actions: `internal_` + verb, describing internal operation
|
|
302
|
-
- `internal_createTopic`, `internal_updateMessageContent`
|
|
303
|
-
- Dispatch Methods: `internal_dispatch` + entity name
|
|
304
|
-
- `internal_dispatchTopic`, `internal_dispatchMessage`
|
|
305
|
-
- Toggle Methods: `internal_toggle` + state name
|
|
306
|
-
- `internal_toggleMessageLoading`, `internal_toggleChatLoading`
|
|
307
|
-
|
|
308
|
-
### State Naming Patterns
|
|
309
|
-
|
|
310
|
-
- ID arrays: `[entity]LoadingIds`, `[entity]EditingIds`
|
|
311
|
-
- Map structures: `[entity]Maps`, `[entity]Map`
|
|
312
|
-
- Currently active: `active[Entity]Id`
|
|
313
|
-
- Initialization flags: `[entity]sInit`
|
|
314
|
-
|
|
315
|
-
## Best Practices
|
|
316
|
-
|
|
317
|
-
1. Use optimistic updates appropriately:
|
|
318
|
-
- ✅ Suitable: Create, update operations (frequent user interaction)
|
|
319
|
-
- ❌ Avoid: Delete operations (destructive, complex error recovery)
|
|
320
|
-
2. Loading state management: Use unified loading state arrays to manage concurrent operations
|
|
321
|
-
3. Type safety: Define TypeScript interfaces for all action payloads
|
|
322
|
-
4. SWR integration: Use SWR to manage data fetching and cache invalidation
|
|
323
|
-
5. AbortController: Provide cancellation capability for long-running operations
|
|
324
|
-
6. Operation mode selection:
|
|
325
|
-
- Create/Update: Optimistic update + eventual consistency
|
|
326
|
-
- Delete: Loading state + service call + data refresh
|
|
327
|
-
|
|
328
|
-
This Action organization pattern ensures code consistency, maintainability, and provides excellent user experience.
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description:
|
|
3
|
-
globs: src/store/**
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# LobeChat Zustand Store Slice 组织架构
|
|
8
|
-
|
|
9
|
-
本文档描述了 LobeChat 项目中 Zustand Store 的模块化 Slice 组织方式,展示如何通过分片架构管理复杂的应用状态。
|
|
10
|
-
|
|
11
|
-
## 顶层 Store 结构
|
|
12
|
-
|
|
13
|
-
LobeChat 的 `chat` store (`src/store/chat/`) 采用模块化的 slice 结构来组织状态和逻辑。
|
|
14
|
-
|
|
15
|
-
### 关键聚合文件
|
|
16
|
-
|
|
17
|
-
- `src/store/chat/initialState.ts`: 聚合所有 slice 的初始状态
|
|
18
|
-
- `src/store/chat/store.ts`: 定义顶层的 `ChatStore`,组合所有 slice 的 actions
|
|
19
|
-
- `src/store/chat/selectors.ts`: 统一导出所有 slice 的 selectors
|
|
20
|
-
- `src/store/chat/helpers.ts`: 提供聊天相关的辅助函数
|
|
21
|
-
|
|
22
|
-
### Store 聚合模式
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
// src/store/chat/initialState.ts
|
|
26
|
-
import { ChatTopicState, initialTopicState } from './slices/topic/initialState';
|
|
27
|
-
import { ChatMessageState, initialMessageState } from './slices/message/initialState';
|
|
28
|
-
import { ChatAIChatState, initialAiChatState } from './slices/aiChat/initialState';
|
|
29
|
-
|
|
30
|
-
export type ChatStoreState = ChatTopicState &
|
|
31
|
-
ChatMessageState &
|
|
32
|
-
ChatAIChatState &
|
|
33
|
-
// ...其他 slice states
|
|
34
|
-
|
|
35
|
-
export const initialState: ChatStoreState = {
|
|
36
|
-
...initialMessageState,
|
|
37
|
-
...initialTopicState,
|
|
38
|
-
...initialAiChatState,
|
|
39
|
-
// ...其他 initial slice states
|
|
40
|
-
};
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
// src/store/chat/store.ts
|
|
45
|
-
import { ChatMessageAction, chatMessage } from './slices/message/action';
|
|
46
|
-
import { ChatTopicAction, chatTopic } from './slices/topic/action';
|
|
47
|
-
import { ChatAIChatAction, chatAiChat } from './slices/aiChat/actions';
|
|
48
|
-
|
|
49
|
-
export interface ChatStoreAction
|
|
50
|
-
extends ChatMessageAction,
|
|
51
|
-
ChatTopicAction,
|
|
52
|
-
ChatAIChatAction,
|
|
53
|
-
// ...其他 slice actions
|
|
54
|
-
|
|
55
|
-
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
|
|
56
|
-
...initialState,
|
|
57
|
-
...chatMessage(...params),
|
|
58
|
-
...chatTopic(...params),
|
|
59
|
-
...chatAiChat(...params),
|
|
60
|
-
// ...其他 slice action creators
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export const useChatStore = createWithEqualityFn<ChatStore>()(
|
|
64
|
-
subscribeWithSelector(devtools(createStore)),
|
|
65
|
-
shallow,
|
|
66
|
-
);
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## 单个 Slice 的标准结构
|
|
70
|
-
|
|
71
|
-
每个 slice 位于 `src/store/chat/slices/[sliceName]/` 目录下:
|
|
72
|
-
|
|
73
|
-
```plaintext
|
|
74
|
-
src/store/chat/slices/
|
|
75
|
-
└── [sliceName]/ # 例如 message, topic, aiChat, builtinTool
|
|
76
|
-
├── action.ts # 定义 actions (或者是一个 actions/ 目录)
|
|
77
|
-
├── initialState.ts # 定义 state 结构和初始值
|
|
78
|
-
├── reducer.ts # (可选) 如果使用 reducer 模式
|
|
79
|
-
├── selectors.ts # 定义 selectors
|
|
80
|
-
└── index.ts # (可选) 重新导出模块内容
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 文件职责说明
|
|
84
|
-
|
|
85
|
-
1. `initialState.ts`:
|
|
86
|
-
- 定义 slice 的 TypeScript 状态接口
|
|
87
|
-
- 提供初始状态默认值
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
// 典型的 initialState.ts 结构
|
|
91
|
-
export interface ChatTopicState {
|
|
92
|
-
activeTopicId?: string;
|
|
93
|
-
topicMaps: Record<string, ChatTopic[]>; // 核心数据结构
|
|
94
|
-
topicsInit: boolean;
|
|
95
|
-
topicLoadingIds: string[];
|
|
96
|
-
// ...其他状态字段
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const initialTopicState: ChatTopicState = {
|
|
100
|
-
activeTopicId: undefined,
|
|
101
|
-
topicMaps: {},
|
|
102
|
-
topicsInit: false,
|
|
103
|
-
topicLoadingIds: [],
|
|
104
|
-
// ...其他初始值
|
|
105
|
-
};
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
1. `reducer.ts` (复杂状态使用):
|
|
109
|
-
- 定义纯函数 reducer,处理同步状态转换
|
|
110
|
-
- 使用 `immer` 确保不可变更新
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// 典型的 reducer.ts 结构
|
|
114
|
-
import { produce } from 'immer';
|
|
115
|
-
|
|
116
|
-
interface AddChatTopicAction {
|
|
117
|
-
type: 'addTopic';
|
|
118
|
-
value: CreateTopicParams & { id?: string };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface UpdateChatTopicAction {
|
|
122
|
-
id: string;
|
|
123
|
-
type: 'updateTopic';
|
|
124
|
-
value: Partial<ChatTopic>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction;
|
|
128
|
-
|
|
129
|
-
export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
|
|
130
|
-
switch (payload.type) {
|
|
131
|
-
case 'addTopic': {
|
|
132
|
-
return produce(state, (draftState) => {
|
|
133
|
-
draftState.unshift({
|
|
134
|
-
...payload.value,
|
|
135
|
-
id: payload.value.id ?? Date.now().toString(),
|
|
136
|
-
createdAt: Date.now(),
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
case 'updateTopic': {
|
|
141
|
-
return produce(state, (draftState) => {
|
|
142
|
-
const index = draftState.findIndex((topic) => topic.id === payload.id);
|
|
143
|
-
if (index !== -1) {
|
|
144
|
-
draftState[index] = { ...draftState[index], ...payload.value };
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
default:
|
|
149
|
-
return state;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
1. `selectors.ts`:
|
|
155
|
-
- 提供状态查询和计算函数
|
|
156
|
-
- 供 UI 组件使用的状态订阅接口
|
|
157
|
-
- 重要: 使用 `export const xxxSelectors` 模式聚合所有 selectors
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// 典型的 selectors.ts 结构
|
|
161
|
-
import { ChatStoreState } from '../../initialState';
|
|
162
|
-
|
|
163
|
-
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined => s.topicMaps[s.activeId];
|
|
164
|
-
|
|
165
|
-
const currentActiveTopic = (s: ChatStoreState): ChatTopic | undefined => {
|
|
166
|
-
return currentTopics(s)?.find((topic) => topic.id === s.activeTopicId);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const getTopicById =
|
|
170
|
-
(id: string) =>
|
|
171
|
-
(s: ChatStoreState): ChatTopic | undefined =>
|
|
172
|
-
currentTopics(s)?.find((topic) => topic.id === id);
|
|
173
|
-
|
|
174
|
-
// 核心模式:使用 xxxSelectors 聚合导出
|
|
175
|
-
export const topicSelectors = {
|
|
176
|
-
currentActiveTopic,
|
|
177
|
-
currentTopics,
|
|
178
|
-
getTopicById,
|
|
179
|
-
// ...其他 selectors
|
|
180
|
-
};
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## 特殊 Slice 组织模式
|
|
184
|
-
|
|
185
|
-
### 复杂 Actions 的子目录结构 (aiChat Slice)
|
|
186
|
-
|
|
187
|
-
当 slice 的 actions 过于复杂时,可以拆分到子目录:
|
|
188
|
-
|
|
189
|
-
```plaintext
|
|
190
|
-
src/store/chat/slices/aiChat/
|
|
191
|
-
├── actions/
|
|
192
|
-
│ ├── generateAIChat.ts # AI 对话生成
|
|
193
|
-
│ ├── rag.ts # RAG 检索增强生成
|
|
194
|
-
│ ├── memory.ts # 对话记忆管理
|
|
195
|
-
│ └── index.ts # 聚合所有 actions
|
|
196
|
-
├── initialState.ts
|
|
197
|
-
├── selectors.ts
|
|
198
|
-
└── index.ts
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
参考:`src/store/chat/slices/aiChat/actions/`
|
|
202
|
-
|
|
203
|
-
### 工具类 Slice (builtinTool)
|
|
204
|
-
|
|
205
|
-
管理多种内置工具的状态:
|
|
206
|
-
|
|
207
|
-
```plaintext
|
|
208
|
-
src/store/chat/slices/builtinTool/
|
|
209
|
-
├── actions/
|
|
210
|
-
│ ├── dalle.ts # DALL-E 图像生成
|
|
211
|
-
│ ├── search.ts # 搜索功能
|
|
212
|
-
│ ├── localFile.ts # 本地文件操作
|
|
213
|
-
│ └── index.ts
|
|
214
|
-
├── initialState.ts
|
|
215
|
-
├── selectors.ts
|
|
216
|
-
└── index.ts
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
参考:`src/store/chat/slices/builtinTool/`
|
|
220
|
-
|
|
221
|
-
## 状态设计模式
|
|
222
|
-
|
|
223
|
-
### 1. Map 结构用于关联数据
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
// 以 sessionId 为 key,管理多个会话的数据
|
|
227
|
-
topicMaps: Record<string, ChatTopic[]>;
|
|
228
|
-
messagesMap: Record<string, ChatMessage[]>;
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### 2. 数组用于加载状态管理
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
// 管理多个并发操作的加载状态
|
|
235
|
-
messageLoadingIds: string[]
|
|
236
|
-
topicLoadingIds: string[]
|
|
237
|
-
chatLoadingIds: string[]
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### 3. 可选字段用于当前活动项
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
// 当前激活的实体 ID
|
|
244
|
-
activeId: string
|
|
245
|
-
activeTopicId?: string
|
|
246
|
-
activeThreadId?: string
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## Slice 集成到顶层 Store
|
|
250
|
-
|
|
251
|
-
### 1. 状态聚合
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
// 在 initialState.ts 中
|
|
255
|
-
export type ChatStoreState = ChatTopicState &
|
|
256
|
-
ChatMessageState &
|
|
257
|
-
ChatAIChatState &
|
|
258
|
-
// ...其他 slice states
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### 2. Action 接口聚合
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// 在 store.ts 中
|
|
265
|
-
export interface ChatStoreAction
|
|
266
|
-
extends ChatMessageAction,
|
|
267
|
-
ChatTopicAction,
|
|
268
|
-
ChatAIChatAction,
|
|
269
|
-
// ...其他 slice actions
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### 3. Selector 统一导出
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
// 在 selectors.ts 中 - 统一聚合 selectors
|
|
276
|
-
export { chatSelectors } from './slices/message/selectors';
|
|
277
|
-
export { topicSelectors } from './slices/topic/selectors';
|
|
278
|
-
export { aiChatSelectors } from './slices/aiChat/selectors';
|
|
279
|
-
|
|
280
|
-
// 每个 slice 的 selectors.ts 都使用 xxxSelectors 模式:
|
|
281
|
-
// export const chatSelectors = { ... }
|
|
282
|
-
// export const topicSelectors = { ... }
|
|
283
|
-
// export const aiChatSelectors = { ... }
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## 最佳实践
|
|
287
|
-
|
|
288
|
-
1. Slice 划分原则:
|
|
289
|
-
- 按功能领域划分(message, topic, aiChat 等)
|
|
290
|
-
- 每个 slice 管理相关的状态和操作
|
|
291
|
-
- 避免 slice 之间的强耦合
|
|
292
|
-
|
|
293
|
-
2. 文件命名规范:
|
|
294
|
-
- 使用小驼峰命名 slice 目录
|
|
295
|
-
- 文件名使用一致的模式(action.ts, selectors.ts 等)
|
|
296
|
-
- 复杂 actions 时使用 actions/ 子目录
|
|
297
|
-
|
|
298
|
-
3. 状态结构设计:
|
|
299
|
-
- 扁平化的状态结构,避免深层嵌套
|
|
300
|
-
- 使用 Map 结构管理列表数据
|
|
301
|
-
- 分离加载状态和业务数据
|
|
302
|
-
|
|
303
|
-
4. 类型安全:
|
|
304
|
-
- 为每个 slice 定义清晰的 TypeScript 接口
|
|
305
|
-
- 使用 Zustand 的 StateCreator 确保类型一致性
|
|
306
|
-
- 在顶层聚合时保持类型安全
|
|
307
|
-
|
|
308
|
-
这种模块化的 slice 组织方式使得大型应用的状态管理变得清晰、可维护,并且易于扩展。
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import 'pdfjs-dist/build/pdf.worker.min.mjs';
|
package/src/libs/pdfjs/worker.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { pdfjs } from 'react-pdf';
|
|
4
|
-
|
|
5
|
-
pdfjs.GlobalWorkerOptions.workerSrc = `https://registry.npmmirror.com/pdfjs-dist/${pdfjs.version}/files/build/pdf.worker.min.mjs`;
|
|
6
|
-
|
|
7
|
-
// TODO: Re-enable module worker when fully on Turbopack.
|
|
8
|
-
// if (typeof Worker !== 'undefined' && !pdfjs.GlobalWorkerOptions.workerPort) {
|
|
9
|
-
// pdfjs.GlobalWorkerOptions.workerPort = new Worker(new URL('./pdf.worker.ts', import.meta.url), {
|
|
10
|
-
// type: 'module',
|
|
11
|
-
// });
|
|
12
|
-
// }
|
/package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md
RENAMED
|
File without changes
|
/package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|