@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.257
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 +18 -0
- package/locales/en-US/plugin.json +1 -0
- package/locales/zh-CN/plugin.json +1 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
- package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
- package/packages/database/src/models/__tests__/agent.test.ts +91 -4
- package/packages/database/src/models/agent.ts +15 -7
- package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
- package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
- package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
- package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
- package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
- package/packages/types/src/agent/agentConfig.ts +8 -8
- package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
- package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
- package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
- package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
- package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
- package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
- package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
- package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
- package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
- package/src/app/[variants]/(main)/page/index.tsx +3 -7
- package/src/components/Editor/AutoSaveHint.tsx +1 -1
- package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
- package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
- package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
- package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
- package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
- package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
- package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
- package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
- package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
- package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
- package/src/features/EditorCanvas/index.ts +9 -0
- package/src/features/PageEditor/Copilot/index.tsx +16 -1
- package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
- package/src/features/PageEditor/Header/index.tsx +15 -18
- package/src/features/PageEditor/Header/useMenu.tsx +12 -9
- package/src/features/PageEditor/PageEditor.tsx +45 -21
- package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
- package/src/features/PageEditor/PageTitle/index.tsx +2 -2
- package/src/features/PageEditor/StoreUpdater.tsx +35 -308
- package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
- package/src/features/PageEditor/store/action.ts +96 -188
- package/src/features/PageEditor/store/initialState.ts +16 -21
- package/src/features/PageEditor/store/selectors.ts +3 -4
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
- package/src/features/PageExplorer/index.tsx +34 -67
- package/src/features/Portal/Artifacts/index.ts +0 -2
- package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
- package/src/features/Portal/Document/Body.tsx +1 -3
- package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
- package/src/features/Portal/Document/Header.tsx +13 -10
- package/src/features/Portal/Document/TodoList.tsx +6 -4
- package/src/features/Portal/Document/Wrapper.tsx +3 -11
- package/src/features/Portal/Document/index.ts +0 -2
- package/src/features/Portal/FilePreview/index.ts +0 -2
- package/src/features/Portal/GroupThread/index.ts +0 -3
- package/src/features/Portal/MessageDetail/index.ts +0 -2
- package/src/features/Portal/Notebook/index.ts +0 -2
- package/src/features/Portal/Plugins/index.ts +0 -2
- package/src/features/Portal/Thread/index.ts +0 -3
- package/src/features/Portal/components/Header.tsx +18 -6
- package/src/features/Portal/router.tsx +34 -97
- package/src/features/Portal/type.ts +0 -2
- package/src/features/RightPanel/index.tsx +11 -2
- package/src/locales/default/plugin.ts +1 -0
- package/src/store/chat/slices/portal/action.test.ts +218 -15
- package/src/store/chat/slices/portal/action.ts +194 -41
- package/src/store/chat/slices/portal/initialState.ts +40 -1
- package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
- package/src/store/chat/slices/portal/selectors.test.ts +119 -17
- package/src/store/chat/slices/portal/selectors.ts +117 -36
- package/src/store/document/index.ts +17 -5
- package/src/store/document/slices/document/action.ts +209 -0
- package/src/store/document/slices/document/index.ts +6 -0
- package/src/store/document/slices/editor/action.test.ts +340 -0
- package/src/store/document/slices/editor/action.ts +133 -149
- package/src/store/document/slices/editor/index.ts +9 -2
- package/src/store/document/slices/editor/initialState.ts +66 -29
- package/src/store/document/slices/editor/reducer.test.ts +217 -0
- package/src/store/document/slices/editor/reducer.ts +67 -0
- package/src/store/document/slices/editor/selectors.test.ts +395 -0
- package/src/store/document/slices/editor/selectors.ts +107 -5
- package/src/store/document/store.ts +12 -13
- package/src/store/file/slices/document/action.ts +19 -188
- package/src/store/file/slices/document/initialState.ts +0 -30
- package/src/store/file/slices/document/selectors.ts +25 -59
- package/src/store/global/initialState.ts +2 -0
- package/src/store/global/selectors/systemStatus.ts +2 -0
- package/src/store/notebook/index.ts +5 -4
- package/src/store/page/index.ts +2 -0
- package/src/store/page/initialState.ts +92 -0
- package/src/store/page/selectors.ts +5 -0
- package/src/store/page/slices/crud/action.ts +477 -0
- package/src/store/page/slices/crud/index.ts +2 -0
- package/src/store/page/slices/crud/initialState.ts +7 -0
- package/src/store/page/slices/internal/action.ts +32 -0
- package/src/store/page/slices/internal/index.ts +2 -0
- package/src/store/page/slices/internal/reducer.ts +105 -0
- package/src/store/page/slices/list/action.ts +206 -0
- package/src/store/page/slices/list/index.ts +3 -0
- package/src/store/page/slices/list/initialState.ts +29 -0
- package/src/store/page/slices/list/selectors.ts +90 -0
- package/src/store/page/slices/selection/action.ts +67 -0
- package/src/store/page/slices/selection/index.ts +2 -0
- package/src/store/page/slices/selection/initialState.ts +11 -0
- package/src/store/page/store.ts +29 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
- package/src/utils/identifier.ts +8 -2
- package/src/features/PageEditor/Body/index.tsx +0 -68
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
- package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
- package/src/features/Portal/Artifacts/useEnable.ts +0 -4
- package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
- package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
- package/src/features/Portal/Document/Title.tsx +0 -54
- package/src/features/Portal/Document/store/action.ts +0 -114
- package/src/features/Portal/Document/store/index.ts +0 -21
- package/src/features/Portal/Document/store/initialState.ts +0 -24
- package/src/features/Portal/Document/useEnable.ts +0 -8
- package/src/features/Portal/FilePreview/useEnable.ts +0 -6
- package/src/features/Portal/GroupThread/hook.ts +0 -9
- package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
- package/src/features/Portal/Notebook/useEnable.ts +0 -6
- package/src/features/Portal/Plugins/useEnable.ts +0 -6
- package/src/features/Portal/Thread/hook.ts +0 -8
- package/src/store/document/slices/notebook/action.ts +0 -119
- package/src/store/document/slices/notebook/index.ts +0 -3
- package/src/store/document/slices/notebook/initialState.ts +0 -12
- package/src/store/document/slices/notebook/selectors.ts +0 -26
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
import type { ChatStoreState } from '@/store/chat';
|
|
5
5
|
|
|
6
|
+
import { PortalViewType } from './initialState';
|
|
6
7
|
import { chatPortalSelectors } from './selectors';
|
|
7
8
|
|
|
8
9
|
describe('chatDockSelectors', () => {
|
|
@@ -10,6 +11,7 @@ describe('chatDockSelectors', () => {
|
|
|
10
11
|
const state = {
|
|
11
12
|
showPortal: false,
|
|
12
13
|
portalToolMessage: undefined,
|
|
14
|
+
portalStack: [],
|
|
13
15
|
dbMessagesMap: {},
|
|
14
16
|
activeAgentId: 'test-id',
|
|
15
17
|
activeTopicId: undefined,
|
|
@@ -19,6 +21,82 @@ describe('chatDockSelectors', () => {
|
|
|
19
21
|
return state;
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
describe('currentView', () => {
|
|
25
|
+
it('should return null when stack is empty', () => {
|
|
26
|
+
expect(chatPortalSelectors.currentView(createState())).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return the top view from stack', () => {
|
|
30
|
+
const state = createState({
|
|
31
|
+
portalStack: [
|
|
32
|
+
{ type: PortalViewType.Notebook },
|
|
33
|
+
{ type: PortalViewType.Document, documentId: 'doc-1' },
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
expect(chatPortalSelectors.currentView(state)).toEqual({
|
|
37
|
+
type: PortalViewType.Document,
|
|
38
|
+
documentId: 'doc-1',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('currentViewType', () => {
|
|
44
|
+
it('should return null when stack is empty', () => {
|
|
45
|
+
expect(chatPortalSelectors.currentViewType(createState())).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return the type of top view', () => {
|
|
49
|
+
const state = createState({
|
|
50
|
+
portalStack: [{ type: PortalViewType.Notebook }],
|
|
51
|
+
});
|
|
52
|
+
expect(chatPortalSelectors.currentViewType(state)).toBe(PortalViewType.Notebook);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('canGoBack', () => {
|
|
57
|
+
it('should return false when stack has 0 or 1 views', () => {
|
|
58
|
+
expect(chatPortalSelectors.canGoBack(createState())).toBe(false);
|
|
59
|
+
expect(
|
|
60
|
+
chatPortalSelectors.canGoBack(
|
|
61
|
+
createState({ portalStack: [{ type: PortalViewType.Notebook }] }),
|
|
62
|
+
),
|
|
63
|
+
).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return true when stack has more than 1 view', () => {
|
|
67
|
+
const state = createState({
|
|
68
|
+
portalStack: [
|
|
69
|
+
{ type: PortalViewType.Notebook },
|
|
70
|
+
{ type: PortalViewType.Document, documentId: 'doc-1' },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
expect(chatPortalSelectors.canGoBack(state)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('showArtifactUI', () => {
|
|
78
|
+
it('should return false when current view is not Artifact', () => {
|
|
79
|
+
expect(chatPortalSelectors.showArtifactUI(createState())).toBe(false);
|
|
80
|
+
expect(
|
|
81
|
+
chatPortalSelectors.showArtifactUI(
|
|
82
|
+
createState({ portalStack: [{ type: PortalViewType.Notebook }] }),
|
|
83
|
+
),
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should return true when current view is Artifact', () => {
|
|
88
|
+
const state = createState({
|
|
89
|
+
portalStack: [
|
|
90
|
+
{
|
|
91
|
+
type: PortalViewType.Artifact,
|
|
92
|
+
artifact: { id: 'test', title: 'Test', type: 'text' },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
expect(chatPortalSelectors.showArtifactUI(state)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
22
100
|
describe('showDock', () => {
|
|
23
101
|
it('should return the showDock state', () => {
|
|
24
102
|
expect(chatPortalSelectors.showPortal(createState({ showPortal: true }))).toBe(true);
|
|
@@ -27,12 +105,16 @@ describe('chatDockSelectors', () => {
|
|
|
27
105
|
});
|
|
28
106
|
|
|
29
107
|
describe('toolUIMessageId', () => {
|
|
30
|
-
it('should return undefined when
|
|
108
|
+
it('should return undefined when no ToolUI view on stack', () => {
|
|
31
109
|
expect(chatPortalSelectors.toolMessageId(createState())).toBeUndefined();
|
|
32
110
|
});
|
|
33
111
|
|
|
34
|
-
it('should return the
|
|
35
|
-
const state = createState({
|
|
112
|
+
it('should return the messageId when ToolUI view is on stack', () => {
|
|
113
|
+
const state = createState({
|
|
114
|
+
portalStack: [
|
|
115
|
+
{ type: PortalViewType.ToolUI, messageId: 'test-id', identifier: 'test' },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
36
118
|
expect(chatPortalSelectors.toolMessageId(state)).toBe('test-id');
|
|
37
119
|
});
|
|
38
120
|
});
|
|
@@ -40,7 +122,9 @@ describe('chatDockSelectors', () => {
|
|
|
40
122
|
describe('isMessageToolUIOpen', () => {
|
|
41
123
|
it('should return false when id does not match or showDock is false', () => {
|
|
42
124
|
const state = createState({
|
|
43
|
-
|
|
125
|
+
portalStack: [
|
|
126
|
+
{ type: PortalViewType.ToolUI, messageId: 'test-id', identifier: 'test' },
|
|
127
|
+
],
|
|
44
128
|
showPortal: false,
|
|
45
129
|
});
|
|
46
130
|
expect(chatPortalSelectors.isPluginUIOpen('test-id')(state)).toBe(false);
|
|
@@ -49,7 +133,9 @@ describe('chatDockSelectors', () => {
|
|
|
49
133
|
|
|
50
134
|
it('should return true when id matches and showDock is true', () => {
|
|
51
135
|
const state = createState({
|
|
52
|
-
|
|
136
|
+
portalStack: [
|
|
137
|
+
{ type: PortalViewType.ToolUI, messageId: 'test-id', identifier: 'test' },
|
|
138
|
+
],
|
|
53
139
|
showPortal: true,
|
|
54
140
|
});
|
|
55
141
|
expect(chatPortalSelectors.isPluginUIOpen('test-id')(state)).toBe(true);
|
|
@@ -57,45 +143,61 @@ describe('chatDockSelectors', () => {
|
|
|
57
143
|
});
|
|
58
144
|
|
|
59
145
|
describe('showToolUI', () => {
|
|
60
|
-
it('should return false when
|
|
146
|
+
it('should return false when no ToolUI view on stack', () => {
|
|
61
147
|
expect(chatPortalSelectors.showPluginUI(createState())).toBe(false);
|
|
62
148
|
});
|
|
63
149
|
|
|
64
|
-
it('should return true when
|
|
65
|
-
const state = createState({
|
|
150
|
+
it('should return true when ToolUI view is on stack', () => {
|
|
151
|
+
const state = createState({
|
|
152
|
+
portalStack: [
|
|
153
|
+
{ type: PortalViewType.ToolUI, messageId: 'test-id', identifier: 'test' },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
66
156
|
expect(chatPortalSelectors.showPluginUI(state)).toBe(true);
|
|
67
157
|
});
|
|
68
158
|
});
|
|
69
159
|
|
|
70
160
|
describe('toolUIIdentifier', () => {
|
|
71
|
-
it('should return undefined when
|
|
161
|
+
it('should return undefined when no ToolUI view on stack', () => {
|
|
72
162
|
expect(chatPortalSelectors.toolUIIdentifier(createState())).toBeUndefined();
|
|
73
163
|
});
|
|
74
164
|
|
|
75
|
-
it('should return the identifier when
|
|
76
|
-
const state = createState({
|
|
165
|
+
it('should return the identifier when ToolUI view is on stack', () => {
|
|
166
|
+
const state = createState({
|
|
167
|
+
portalStack: [
|
|
168
|
+
{ type: PortalViewType.ToolUI, messageId: 'test-id', identifier: 'test' },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
77
171
|
expect(chatPortalSelectors.toolUIIdentifier(state)).toBe('test');
|
|
78
172
|
});
|
|
79
173
|
});
|
|
80
174
|
|
|
81
175
|
describe('showFilePreview', () => {
|
|
82
|
-
it('should return false when
|
|
176
|
+
it('should return false when no FilePreview view on stack', () => {
|
|
83
177
|
expect(chatPortalSelectors.showFilePreview(createState())).toBe(false);
|
|
84
178
|
});
|
|
85
179
|
|
|
86
|
-
it('should return true when
|
|
87
|
-
const state = createState({
|
|
180
|
+
it('should return true when FilePreview view is on stack', () => {
|
|
181
|
+
const state = createState({
|
|
182
|
+
portalStack: [
|
|
183
|
+
{ type: PortalViewType.FilePreview, file: { fileId: 'file-id', chunkText: 'chunk' } },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
88
186
|
expect(chatPortalSelectors.showFilePreview(state)).toBe(true);
|
|
89
187
|
});
|
|
90
188
|
});
|
|
91
189
|
|
|
92
190
|
describe('previewFileId', () => {
|
|
93
|
-
it('should return undefined when
|
|
191
|
+
it('should return undefined when no FilePreview view on stack', () => {
|
|
94
192
|
expect(chatPortalSelectors.previewFileId(createState())).toBeUndefined();
|
|
95
193
|
});
|
|
96
194
|
|
|
97
|
-
it('should return the fileId when
|
|
98
|
-
const state = createState({
|
|
195
|
+
it('should return the fileId when FilePreview view is on stack', () => {
|
|
196
|
+
const state = createState({
|
|
197
|
+
portalStack: [
|
|
198
|
+
{ type: PortalViewType.FilePreview, file: { fileId: 'file-id', chunkText: 'chunk' } },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
99
201
|
expect(chatPortalSelectors.previewFileId(state)).toBe('file-id');
|
|
100
202
|
});
|
|
101
203
|
});
|
|
@@ -1,35 +1,66 @@
|
|
|
1
1
|
import { ARTIFACT_TAG_CLOSED_REGEX, ARTIFACT_TAG_REGEX } from '@/const/plugin';
|
|
2
2
|
import type { ChatStoreState } from '@/store/chat';
|
|
3
|
+
import { type PortalArtifact } from '@/types/artifact';
|
|
3
4
|
|
|
4
5
|
import { dbMessageSelectors } from '../message/selectors';
|
|
6
|
+
import { type PortalFile, type PortalViewData, PortalViewType } from './initialState';
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
// ============== Core Stack Selectors ==============
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
+
const currentView = (s: ChatStoreState): PortalViewData | null => {
|
|
11
|
+
const { portalStack } = s;
|
|
12
|
+
return portalStack.at(-1) ?? null;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
|
-
const
|
|
15
|
+
const currentViewType = (s: ChatStoreState): PortalViewType | null => {
|
|
16
|
+
return currentView(s)?.type ?? null;
|
|
17
|
+
};
|
|
12
18
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
const canGoBack = (s: ChatStoreState): boolean => {
|
|
20
|
+
return s.portalStack.length > 1;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const stackDepth = (s: ChatStoreState): number => {
|
|
24
|
+
return s.portalStack.length;
|
|
25
|
+
};
|
|
17
26
|
|
|
18
|
-
const
|
|
19
|
-
const previewFileId = (s: ChatStoreState) => s.portalFile?.fileId;
|
|
20
|
-
const chunkText = (s: ChatStoreState) => s.portalFile?.chunkText;
|
|
27
|
+
const showPortal = (s: ChatStoreState) => s.showPortal;
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
// ============== View Type Guards ==============
|
|
30
|
+
|
|
31
|
+
const showArtifactUI = (s: ChatStoreState) => currentViewType(s) === PortalViewType.Artifact;
|
|
32
|
+
const showDocument = (s: ChatStoreState) => currentViewType(s) === PortalViewType.Document;
|
|
33
|
+
const showNotebook = (s: ChatStoreState) => currentViewType(s) === PortalViewType.Notebook;
|
|
34
|
+
const showFilePreview = (s: ChatStoreState) => currentViewType(s) === PortalViewType.FilePreview;
|
|
35
|
+
const showMessageDetail = (s: ChatStoreState) =>
|
|
36
|
+
currentViewType(s) === PortalViewType.MessageDetail;
|
|
37
|
+
const showPluginUI = (s: ChatStoreState) => currentViewType(s) === PortalViewType.ToolUI;
|
|
38
|
+
|
|
39
|
+
// ============== Data Extractors ==============
|
|
40
|
+
|
|
41
|
+
// Helper to extract data from current view
|
|
42
|
+
const getViewData = <T extends PortalViewType>(
|
|
43
|
+
s: ChatStoreState,
|
|
44
|
+
type: T,
|
|
45
|
+
): Extract<PortalViewData, { type: T }> | null => {
|
|
46
|
+
const view = currentView(s);
|
|
47
|
+
if (view?.type === type) {
|
|
48
|
+
return view as Extract<PortalViewData, { type: T }>;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
const
|
|
53
|
+
// Artifact selectors
|
|
54
|
+
const currentArtifact = (s: ChatStoreState): PortalArtifact | undefined => {
|
|
55
|
+
const view = getViewData(s, PortalViewType.Artifact);
|
|
56
|
+
return view?.artifact;
|
|
57
|
+
};
|
|
26
58
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const artifactCodeLanguage = (s: ChatStoreState) => s.portalArtifact?.language;
|
|
59
|
+
const artifactTitle = (s: ChatStoreState) => currentArtifact(s)?.title;
|
|
60
|
+
const artifactIdentifier = (s: ChatStoreState) => currentArtifact(s)?.identifier || '';
|
|
61
|
+
const artifactMessageId = (s: ChatStoreState) => currentArtifact(s)?.id;
|
|
62
|
+
const artifactType = (s: ChatStoreState) => currentArtifact(s)?.type;
|
|
63
|
+
const artifactCodeLanguage = (s: ChatStoreState) => currentArtifact(s)?.language;
|
|
33
64
|
|
|
34
65
|
const artifactMessageContent = (id: string) => (s: ChatStoreState) => {
|
|
35
66
|
const message = dbMessageSelectors.getDbMessageById(id)(s);
|
|
@@ -54,37 +85,87 @@ const isArtifactTagClosed = (id: string) => (s: ChatStoreState) => {
|
|
|
54
85
|
return ARTIFACT_TAG_CLOSED_REGEX.test(content || '');
|
|
55
86
|
};
|
|
56
87
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
// Document selectors
|
|
89
|
+
const portalDocumentId = (s: ChatStoreState): string | undefined => {
|
|
90
|
+
const view = getViewData(s, PortalViewType.Document);
|
|
91
|
+
return view?.documentId;
|
|
92
|
+
};
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
// File Preview selectors
|
|
95
|
+
const currentFile = (s: ChatStoreState): PortalFile | undefined => {
|
|
96
|
+
const view = getViewData(s, PortalViewType.FilePreview);
|
|
97
|
+
return view?.file;
|
|
98
|
+
};
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
const previewFileId = (s: ChatStoreState) => currentFile(s)?.fileId;
|
|
101
|
+
const chunkText = (s: ChatStoreState) => currentFile(s)?.chunkText;
|
|
67
102
|
|
|
68
|
-
|
|
103
|
+
// Message Detail selectors
|
|
104
|
+
const messageDetailId = (s: ChatStoreState): string | undefined => {
|
|
105
|
+
const view = getViewData(s, PortalViewType.MessageDetail);
|
|
106
|
+
return view?.messageId;
|
|
107
|
+
};
|
|
69
108
|
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
// Tool UI / Plugin selectors
|
|
110
|
+
const currentToolUI = (
|
|
111
|
+
s: ChatStoreState,
|
|
112
|
+
): { identifier: string; messageId: string } | undefined => {
|
|
113
|
+
const view = getViewData(s, PortalViewType.ToolUI);
|
|
114
|
+
if (view) {
|
|
115
|
+
return { identifier: view.identifier, messageId: view.messageId };
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
};
|
|
72
119
|
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
const toolMessageId = (s: ChatStoreState) => currentToolUI(s)?.messageId;
|
|
121
|
+
const toolUIIdentifier = (s: ChatStoreState) => currentToolUI(s)?.identifier;
|
|
122
|
+
const isPluginUIOpen = (id: string) => (s: ChatStoreState) =>
|
|
123
|
+
toolMessageId(s) === id && showPortal(s);
|
|
75
124
|
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
126
|
+
export const chatPortalSelectors = {
|
|
127
|
+
// Core stack selectors
|
|
128
|
+
currentView,
|
|
129
|
+
currentViewType,
|
|
130
|
+
canGoBack,
|
|
131
|
+
stackDepth,
|
|
132
|
+
showPortal,
|
|
78
133
|
|
|
134
|
+
// View type guards
|
|
79
135
|
showArtifactUI,
|
|
136
|
+
showDocument,
|
|
137
|
+
showNotebook,
|
|
138
|
+
showFilePreview,
|
|
139
|
+
showMessageDetail,
|
|
140
|
+
showPluginUI,
|
|
141
|
+
|
|
142
|
+
// Artifact data
|
|
143
|
+
currentArtifact,
|
|
80
144
|
artifactTitle,
|
|
81
145
|
artifactIdentifier,
|
|
82
146
|
artifactMessageId,
|
|
83
147
|
artifactType,
|
|
148
|
+
artifactCodeLanguage,
|
|
84
149
|
artifactCode,
|
|
85
150
|
artifactMessageContent,
|
|
86
|
-
artifactCodeLanguage,
|
|
87
151
|
isArtifactTagClosed,
|
|
152
|
+
|
|
153
|
+
// Document data
|
|
154
|
+
portalDocumentId,
|
|
155
|
+
|
|
156
|
+
// File preview data
|
|
157
|
+
currentFile,
|
|
158
|
+
previewFileId,
|
|
159
|
+
chunkText,
|
|
160
|
+
|
|
161
|
+
// Message detail data
|
|
162
|
+
messageDetailId,
|
|
163
|
+
|
|
164
|
+
// Tool UI data
|
|
165
|
+
currentToolUI,
|
|
166
|
+
toolMessageId,
|
|
167
|
+
toolUIIdentifier,
|
|
168
|
+
isPluginUIOpen,
|
|
88
169
|
};
|
|
89
170
|
|
|
90
171
|
export * from './selectors/thread';
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
// Selectors
|
|
2
2
|
export { editorSelectors } from './slices/editor';
|
|
3
|
-
export { notebookSelectors } from './slices/notebook';
|
|
4
3
|
|
|
5
4
|
// Store
|
|
6
|
-
export type {
|
|
5
|
+
export type { DocumentState, DocumentStore, DocumentStoreAction } from './store';
|
|
7
6
|
export { getDocumentStoreState, useDocumentStore } from './store';
|
|
8
7
|
|
|
9
|
-
// Re-export slice types
|
|
10
|
-
export type {
|
|
11
|
-
|
|
8
|
+
// Re-export document slice types
|
|
9
|
+
export type {
|
|
10
|
+
DocumentAction,
|
|
11
|
+
InitDocumentParams,
|
|
12
|
+
UseFetchDocumentOptions,
|
|
13
|
+
} from './slices/document';
|
|
14
|
+
|
|
15
|
+
// Re-export editor slice types
|
|
16
|
+
export type {
|
|
17
|
+
DocumentSourceType,
|
|
18
|
+
EditorAction,
|
|
19
|
+
EditorContentState,
|
|
20
|
+
EditorState,
|
|
21
|
+
SaveMetadata,
|
|
22
|
+
} from './slices/editor';
|
|
23
|
+
export { createInitialEditorContentState } from './slices/editor';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { EDITOR_DEBOUNCE_TIME, EDITOR_MAX_WAIT } from '@lobechat/const';
|
|
4
|
+
import { type DocumentItem } from '@lobechat/database/schemas';
|
|
5
|
+
import { type IEditor } from '@lobehub/editor';
|
|
6
|
+
import { debounce } from 'es-toolkit/compat';
|
|
7
|
+
import type { SWRResponse } from 'swr';
|
|
8
|
+
import { type StateCreator } from 'zustand/vanilla';
|
|
9
|
+
|
|
10
|
+
import { useClientDataSWRWithSync } from '@/libs/swr';
|
|
11
|
+
import { documentService } from '@/services/document';
|
|
12
|
+
import { setNamespace } from '@/utils/storeDebug';
|
|
13
|
+
|
|
14
|
+
import type { DocumentStore } from '../../store';
|
|
15
|
+
import { type DocumentSourceType } from '../editor/initialState';
|
|
16
|
+
|
|
17
|
+
const n = setNamespace('document/document');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parameters for initializing a document with editor
|
|
21
|
+
*/
|
|
22
|
+
export interface InitDocumentParams {
|
|
23
|
+
/**
|
|
24
|
+
* Whether auto-save is enabled. Defaults to true.
|
|
25
|
+
* Set to false if the consumer handles saving themselves.
|
|
26
|
+
*/
|
|
27
|
+
autoSave?: boolean;
|
|
28
|
+
content?: string | null;
|
|
29
|
+
documentId: string;
|
|
30
|
+
editor: IEditor;
|
|
31
|
+
editorData?: unknown;
|
|
32
|
+
sourceType: DocumentSourceType;
|
|
33
|
+
topicId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for useFetchDocument hook
|
|
38
|
+
*/
|
|
39
|
+
export interface UseFetchDocumentOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Whether auto-save is enabled. Defaults to true.
|
|
42
|
+
*/
|
|
43
|
+
autoSave?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Editor instance to load content into
|
|
46
|
+
*/
|
|
47
|
+
editor?: IEditor;
|
|
48
|
+
/**
|
|
49
|
+
* Source type for the document. Defaults to 'page'.
|
|
50
|
+
*/
|
|
51
|
+
sourceType?: DocumentSourceType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DocumentAction {
|
|
55
|
+
/**
|
|
56
|
+
* Close a document and remove it from state
|
|
57
|
+
*/
|
|
58
|
+
closeDocument: (documentId: string) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Flush any pending debounced save for a document
|
|
61
|
+
*/
|
|
62
|
+
flushSave: (documentId?: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Initialize a document with editor - stores state only.
|
|
65
|
+
* Content is loaded into editor via onEditorInit when Editor component is ready.
|
|
66
|
+
*/
|
|
67
|
+
initDocumentWithEditor: (params: InitDocumentParams) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Trigger a debounced save for the specified document
|
|
70
|
+
*/
|
|
71
|
+
triggerDebouncedSave: (documentId: string) => void;
|
|
72
|
+
/**
|
|
73
|
+
* SWR hook to fetch document and initialize in DocumentStore
|
|
74
|
+
*/
|
|
75
|
+
useFetchDocument: (
|
|
76
|
+
documentId: string | undefined,
|
|
77
|
+
options?: UseFetchDocumentOptions,
|
|
78
|
+
) => SWRResponse<DocumentItem | null>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const createDocumentSlice: StateCreator<
|
|
82
|
+
DocumentStore,
|
|
83
|
+
[['zustand/devtools', never]],
|
|
84
|
+
[],
|
|
85
|
+
DocumentAction
|
|
86
|
+
> = (set, get) => {
|
|
87
|
+
// Store debounced save functions per document - inside store closure so `get` is always correct
|
|
88
|
+
const debouncedSaves = new Map<string, ReturnType<typeof debounce>>();
|
|
89
|
+
|
|
90
|
+
const getOrCreateDebouncedSave = (documentId: string) => {
|
|
91
|
+
if (!debouncedSaves.has(documentId)) {
|
|
92
|
+
const debouncedFn = debounce(
|
|
93
|
+
async () => {
|
|
94
|
+
try {
|
|
95
|
+
await get().performSave(documentId);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[DocumentStore] Failed to auto-save:', error);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
EDITOR_DEBOUNCE_TIME,
|
|
101
|
+
{ leading: false, maxWait: EDITOR_MAX_WAIT, trailing: true },
|
|
102
|
+
);
|
|
103
|
+
debouncedSaves.set(documentId, debouncedFn);
|
|
104
|
+
}
|
|
105
|
+
return debouncedSaves.get(documentId)!;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const cleanupDebouncedSave = (documentId: string) => {
|
|
109
|
+
const fn = debouncedSaves.get(documentId);
|
|
110
|
+
if (fn) {
|
|
111
|
+
fn.cancel();
|
|
112
|
+
debouncedSaves.delete(documentId);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
closeDocument: (documentId) => {
|
|
118
|
+
// Flush any pending saves before closing
|
|
119
|
+
const save = debouncedSaves.get(documentId);
|
|
120
|
+
if (save) {
|
|
121
|
+
save.flush();
|
|
122
|
+
cleanupDebouncedSave(documentId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { activeDocumentId, internal_dispatchDocument } = get();
|
|
126
|
+
|
|
127
|
+
// Delete document via reducer
|
|
128
|
+
internal_dispatchDocument({ id: documentId, type: 'deleteDocument' });
|
|
129
|
+
|
|
130
|
+
// Update activeDocumentId if needed
|
|
131
|
+
if (activeDocumentId === documentId) {
|
|
132
|
+
set({ activeDocumentId: undefined }, false, n('closeDocument:clearActive'));
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
flushSave: (documentId) => {
|
|
137
|
+
const id = documentId || get().activeDocumentId;
|
|
138
|
+
if (id) {
|
|
139
|
+
const save = debouncedSaves.get(id);
|
|
140
|
+
save?.flush();
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
initDocumentWithEditor: (params) => {
|
|
145
|
+
const { documentId, sourceType, content, editorData, topicId, autoSave, editor } = params;
|
|
146
|
+
|
|
147
|
+
const { internal_dispatchDocument } = get();
|
|
148
|
+
|
|
149
|
+
// Add or update document via reducer
|
|
150
|
+
internal_dispatchDocument({
|
|
151
|
+
id: documentId,
|
|
152
|
+
type: 'addDocument',
|
|
153
|
+
value: {
|
|
154
|
+
autoSave,
|
|
155
|
+
content: content ?? undefined,
|
|
156
|
+
editorData,
|
|
157
|
+
lastSavedContent: content ?? undefined,
|
|
158
|
+
sourceType,
|
|
159
|
+
topicId,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Update activeDocumentId and editor
|
|
164
|
+
set({ activeDocumentId: documentId, editor }, false, n('initDocumentWithEditor:setActive'));
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
triggerDebouncedSave: (documentId) => {
|
|
168
|
+
const save = getOrCreateDebouncedSave(documentId);
|
|
169
|
+
save();
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
useFetchDocument: (documentId, options = {}) => {
|
|
173
|
+
const { autoSave = true, editor, sourceType = 'page' } = options;
|
|
174
|
+
const swrKey = documentId && editor ? ['document/editor', documentId] : null;
|
|
175
|
+
|
|
176
|
+
return useClientDataSWRWithSync<DocumentItem | null>(
|
|
177
|
+
swrKey,
|
|
178
|
+
async () => {
|
|
179
|
+
// documentId is guaranteed to be defined when swrKey is not null
|
|
180
|
+
const document = await documentService.getDocumentById(documentId!);
|
|
181
|
+
if (!document) {
|
|
182
|
+
console.warn(`[useFetchDocument] Document not found: ${documentId}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return document;
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
focusThrottleInterval: 20_000,
|
|
190
|
+
onData: (document) => {
|
|
191
|
+
// Both documentId and editor are guaranteed to be defined when this callback is called
|
|
192
|
+
if (!document || !documentId || !editor) return;
|
|
193
|
+
|
|
194
|
+
// Initialize document with editor
|
|
195
|
+
get().initDocumentWithEditor({
|
|
196
|
+
autoSave,
|
|
197
|
+
content: document.content,
|
|
198
|
+
documentId,
|
|
199
|
+
editor,
|
|
200
|
+
editorData: document.editorData,
|
|
201
|
+
sourceType,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
revalidateOnFocus: true,
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
};
|