@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.256

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 (140) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/plugin.json +1 -0
  4. package/locales/zh-CN/plugin.json +1 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
  7. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
  8. package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
  9. package/packages/database/src/models/__tests__/agent.test.ts +91 -4
  10. package/packages/database/src/models/agent.ts +15 -7
  11. package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
  12. package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
  13. package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
  14. package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
  15. package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
  16. package/packages/types/src/agent/agentConfig.ts +8 -8
  17. package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
  18. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
  19. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
  20. package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
  21. package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
  22. package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
  23. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
  24. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
  25. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
  26. package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
  27. package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
  28. package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
  29. package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
  30. package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
  31. package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
  32. package/src/app/[variants]/(main)/page/index.tsx +3 -7
  33. package/src/components/Editor/AutoSaveHint.tsx +1 -1
  34. package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
  35. package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
  36. package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
  37. package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
  38. package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
  39. package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
  40. package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
  41. package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
  42. package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
  43. package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
  44. package/src/features/EditorCanvas/index.ts +9 -0
  45. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  46. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  47. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  48. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  49. package/src/features/PageEditor/Header/index.tsx +15 -18
  50. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  51. package/src/features/PageEditor/PageEditor.tsx +45 -21
  52. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  53. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  54. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  55. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  56. package/src/features/PageEditor/store/action.ts +96 -188
  57. package/src/features/PageEditor/store/initialState.ts +16 -21
  58. package/src/features/PageEditor/store/selectors.ts +3 -4
  59. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  60. package/src/features/PageExplorer/index.tsx +34 -67
  61. package/src/features/Portal/Artifacts/index.ts +0 -2
  62. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  63. package/src/features/Portal/Document/Body.tsx +1 -3
  64. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  65. package/src/features/Portal/Document/Header.tsx +13 -10
  66. package/src/features/Portal/Document/TodoList.tsx +6 -4
  67. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  68. package/src/features/Portal/Document/index.ts +0 -2
  69. package/src/features/Portal/FilePreview/index.ts +0 -2
  70. package/src/features/Portal/GroupThread/index.ts +0 -3
  71. package/src/features/Portal/MessageDetail/index.ts +0 -2
  72. package/src/features/Portal/Notebook/index.ts +0 -2
  73. package/src/features/Portal/Plugins/index.ts +0 -2
  74. package/src/features/Portal/Thread/index.ts +0 -3
  75. package/src/features/Portal/components/Header.tsx +18 -6
  76. package/src/features/Portal/router.tsx +34 -97
  77. package/src/features/Portal/type.ts +0 -2
  78. package/src/locales/default/plugin.ts +1 -0
  79. package/src/store/chat/slices/portal/action.test.ts +218 -15
  80. package/src/store/chat/slices/portal/action.ts +194 -41
  81. package/src/store/chat/slices/portal/initialState.ts +40 -1
  82. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  83. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  84. package/src/store/chat/slices/portal/selectors.ts +117 -36
  85. package/src/store/document/index.ts +17 -5
  86. package/src/store/document/slices/document/action.ts +209 -0
  87. package/src/store/document/slices/document/index.ts +6 -0
  88. package/src/store/document/slices/editor/action.test.ts +340 -0
  89. package/src/store/document/slices/editor/action.ts +133 -149
  90. package/src/store/document/slices/editor/index.ts +9 -2
  91. package/src/store/document/slices/editor/initialState.ts +66 -29
  92. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  93. package/src/store/document/slices/editor/reducer.ts +67 -0
  94. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  95. package/src/store/document/slices/editor/selectors.ts +107 -5
  96. package/src/store/document/store.ts +12 -13
  97. package/src/store/file/slices/document/action.ts +19 -188
  98. package/src/store/file/slices/document/initialState.ts +0 -30
  99. package/src/store/file/slices/document/selectors.ts +25 -59
  100. package/src/store/notebook/index.ts +5 -4
  101. package/src/store/page/index.ts +2 -0
  102. package/src/store/page/initialState.ts +92 -0
  103. package/src/store/page/selectors.ts +5 -0
  104. package/src/store/page/slices/crud/action.ts +477 -0
  105. package/src/store/page/slices/crud/index.ts +2 -0
  106. package/src/store/page/slices/crud/initialState.ts +7 -0
  107. package/src/store/page/slices/internal/action.ts +32 -0
  108. package/src/store/page/slices/internal/index.ts +2 -0
  109. package/src/store/page/slices/internal/reducer.ts +105 -0
  110. package/src/store/page/slices/list/action.ts +206 -0
  111. package/src/store/page/slices/list/index.ts +3 -0
  112. package/src/store/page/slices/list/initialState.ts +29 -0
  113. package/src/store/page/slices/list/selectors.ts +90 -0
  114. package/src/store/page/slices/selection/action.ts +67 -0
  115. package/src/store/page/slices/selection/index.ts +2 -0
  116. package/src/store/page/slices/selection/initialState.ts +11 -0
  117. package/src/store/page/store.ts +29 -0
  118. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  119. package/src/utils/identifier.ts +8 -2
  120. package/src/features/PageEditor/Body/index.tsx +0 -68
  121. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  122. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  123. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  124. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  125. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  126. package/src/features/Portal/Document/Title.tsx +0 -54
  127. package/src/features/Portal/Document/store/action.ts +0 -114
  128. package/src/features/Portal/Document/store/index.ts +0 -21
  129. package/src/features/Portal/Document/store/initialState.ts +0 -24
  130. package/src/features/Portal/Document/useEnable.ts +0 -8
  131. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  132. package/src/features/Portal/GroupThread/hook.ts +0 -9
  133. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  134. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  135. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  136. package/src/features/Portal/Thread/hook.ts +0 -8
  137. package/src/store/document/slices/notebook/action.ts +0 -119
  138. package/src/store/document/slices/notebook/index.ts +0 -3
  139. package/src/store/document/slices/notebook/initialState.ts +0 -12
  140. 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 dockToolMessage is not set', () => {
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 id when dockToolMessage is set', () => {
35
- const state = createState({ portalToolMessage: { id: 'test-id', identifier: 'test' } });
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
- portalToolMessage: { id: 'test-id', identifier: 'test' },
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
- portalToolMessage: { id: 'test-id', identifier: 'test' },
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 dockToolMessage is not set', () => {
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 dockToolMessage is set', () => {
65
- const state = createState({ portalToolMessage: { id: 'test-id', identifier: 'test' } });
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 dockToolMessage is not set', () => {
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 dockToolMessage is set', () => {
76
- const state = createState({ portalToolMessage: { id: 'test-id', identifier: 'test' } });
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 portalFile is not set', () => {
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 portalFile is set', () => {
87
- const state = createState({ portalFile: { fileId: 'file-id', chunkText: 'chunk' } });
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 portalFile is not set', () => {
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 portalFile is set', () => {
98
- const state = createState({ portalFile: { fileId: 'file-id', chunkText: 'chunk' } });
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
- const showPortal = (s: ChatStoreState) => s.showPortal;
8
+ // ============== Core Stack Selectors ==============
7
9
 
8
- const showMessageDetail = (s: ChatStoreState) => !!s.portalMessageDetail;
9
- const messageDetailId = (s: ChatStoreState) => s.portalMessageDetail;
10
+ const currentView = (s: ChatStoreState): PortalViewData | null => {
11
+ const { portalStack } = s;
12
+ return portalStack.at(-1) ?? null;
13
+ };
10
14
 
11
- const showPluginUI = (s: ChatStoreState) => !!s.portalToolMessage;
15
+ const currentViewType = (s: ChatStoreState): PortalViewType | null => {
16
+ return currentView(s)?.type ?? null;
17
+ };
12
18
 
13
- const toolMessageId = (s: ChatStoreState) => s.portalToolMessage?.id;
14
- const isPluginUIOpen = (id: string) => (s: ChatStoreState) =>
15
- toolMessageId(s) === id && showPortal(s);
16
- const toolUIIdentifier = (s: ChatStoreState) => s.portalToolMessage?.identifier;
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 showFilePreview = (s: ChatStoreState) => !!s.portalFile;
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
- const showNotebook = (s: ChatStoreState) => !!s.showNotebook;
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
- const showDocument = (s: ChatStoreState) => !!s.portalDocumentId;
25
- const portalDocumentId = (s: ChatStoreState) => s.portalDocumentId;
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 showArtifactUI = (s: ChatStoreState) => !!s.portalArtifact;
28
- const artifactTitle = (s: ChatStoreState) => s.portalArtifact?.title;
29
- const artifactIdentifier = (s: ChatStoreState) => s.portalArtifact?.identifier || '';
30
- const artifactMessageId = (s: ChatStoreState) => s.portalArtifact?.id;
31
- const artifactType = (s: ChatStoreState) => s.portalArtifact?.type;
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
- /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
58
- export const chatPortalSelectors = {
59
- isPluginUIOpen,
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
- previewFileId,
62
- showFilePreview,
63
- chunkText,
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
- messageDetailId,
66
- showMessageDetail,
100
+ const previewFileId = (s: ChatStoreState) => currentFile(s)?.fileId;
101
+ const chunkText = (s: ChatStoreState) => currentFile(s)?.chunkText;
67
102
 
68
- showNotebook,
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
- showDocument,
71
- portalDocumentId,
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
- showPluginUI,
74
- showPortal,
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
- toolMessageId,
77
- toolUIIdentifier,
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 { DocumentAction, DocumentState, DocumentStore } from './store';
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 { EditorAction, EditorState } from './slices/editor';
11
- export type { NotebookAction, NotebookState } from './slices/notebook';
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
+ };
@@ -0,0 +1,6 @@
1
+ export {
2
+ createDocumentSlice,
3
+ type DocumentAction,
4
+ type InitDocumentParams,
5
+ type UseFetchDocumentOptions,
6
+ } from './action';