@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -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/Copilot/index.tsx +16 -1
  46. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  47. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  48. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  49. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  50. package/src/features/PageEditor/Header/index.tsx +15 -18
  51. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  52. package/src/features/PageEditor/PageEditor.tsx +45 -21
  53. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  54. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  55. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  56. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  57. package/src/features/PageEditor/store/action.ts +96 -188
  58. package/src/features/PageEditor/store/initialState.ts +16 -21
  59. package/src/features/PageEditor/store/selectors.ts +3 -4
  60. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  61. package/src/features/PageExplorer/index.tsx +34 -67
  62. package/src/features/Portal/Artifacts/index.ts +0 -2
  63. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  64. package/src/features/Portal/Document/Body.tsx +1 -3
  65. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  66. package/src/features/Portal/Document/Header.tsx +13 -10
  67. package/src/features/Portal/Document/TodoList.tsx +6 -4
  68. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  69. package/src/features/Portal/Document/index.ts +0 -2
  70. package/src/features/Portal/FilePreview/index.ts +0 -2
  71. package/src/features/Portal/GroupThread/index.ts +0 -3
  72. package/src/features/Portal/MessageDetail/index.ts +0 -2
  73. package/src/features/Portal/Notebook/index.ts +0 -2
  74. package/src/features/Portal/Plugins/index.ts +0 -2
  75. package/src/features/Portal/Thread/index.ts +0 -3
  76. package/src/features/Portal/components/Header.tsx +18 -6
  77. package/src/features/Portal/router.tsx +34 -97
  78. package/src/features/Portal/type.ts +0 -2
  79. package/src/features/RightPanel/index.tsx +11 -2
  80. package/src/locales/default/plugin.ts +1 -0
  81. package/src/store/chat/slices/portal/action.test.ts +218 -15
  82. package/src/store/chat/slices/portal/action.ts +194 -41
  83. package/src/store/chat/slices/portal/initialState.ts +40 -1
  84. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  85. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  86. package/src/store/chat/slices/portal/selectors.ts +117 -36
  87. package/src/store/document/index.ts +17 -5
  88. package/src/store/document/slices/document/action.ts +209 -0
  89. package/src/store/document/slices/document/index.ts +6 -0
  90. package/src/store/document/slices/editor/action.test.ts +340 -0
  91. package/src/store/document/slices/editor/action.ts +133 -149
  92. package/src/store/document/slices/editor/index.ts +9 -2
  93. package/src/store/document/slices/editor/initialState.ts +66 -29
  94. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  95. package/src/store/document/slices/editor/reducer.ts +67 -0
  96. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  97. package/src/store/document/slices/editor/selectors.ts +107 -5
  98. package/src/store/document/store.ts +12 -13
  99. package/src/store/file/slices/document/action.ts +19 -188
  100. package/src/store/file/slices/document/initialState.ts +0 -30
  101. package/src/store/file/slices/document/selectors.ts +25 -59
  102. package/src/store/global/initialState.ts +2 -0
  103. package/src/store/global/selectors/systemStatus.ts +2 -0
  104. package/src/store/notebook/index.ts +5 -4
  105. package/src/store/page/index.ts +2 -0
  106. package/src/store/page/initialState.ts +92 -0
  107. package/src/store/page/selectors.ts +5 -0
  108. package/src/store/page/slices/crud/action.ts +477 -0
  109. package/src/store/page/slices/crud/index.ts +2 -0
  110. package/src/store/page/slices/crud/initialState.ts +7 -0
  111. package/src/store/page/slices/internal/action.ts +32 -0
  112. package/src/store/page/slices/internal/index.ts +2 -0
  113. package/src/store/page/slices/internal/reducer.ts +105 -0
  114. package/src/store/page/slices/list/action.ts +206 -0
  115. package/src/store/page/slices/list/index.ts +3 -0
  116. package/src/store/page/slices/list/initialState.ts +29 -0
  117. package/src/store/page/slices/list/selectors.ts +90 -0
  118. package/src/store/page/slices/selection/action.ts +67 -0
  119. package/src/store/page/slices/selection/index.ts +2 -0
  120. package/src/store/page/slices/selection/initialState.ts +11 -0
  121. package/src/store/page/store.ts +29 -0
  122. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  123. package/src/utils/identifier.ts +8 -2
  124. package/src/features/PageEditor/Body/index.tsx +0 -68
  125. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  126. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  127. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  128. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  129. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  130. package/src/features/Portal/Document/Title.tsx +0 -54
  131. package/src/features/Portal/Document/store/action.ts +0 -114
  132. package/src/features/Portal/Document/store/index.ts +0 -21
  133. package/src/features/Portal/Document/store/initialState.ts +0 -24
  134. package/src/features/Portal/Document/useEnable.ts +0 -8
  135. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  136. package/src/features/Portal/GroupThread/hook.ts +0 -9
  137. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  138. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  139. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  140. package/src/features/Portal/Thread/hook.ts +0 -8
  141. package/src/store/document/slices/notebook/action.ts +0 -119
  142. package/src/store/document/slices/notebook/index.ts +0 -3
  143. package/src/store/document/slices/notebook/initialState.ts +0 -12
  144. package/src/store/document/slices/notebook/selectors.ts +0 -26
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import { type IEditor } from '@lobehub/editor';
4
+ import { Alert } from '@lobehub/ui';
5
+ import { Skeleton } from 'antd';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { createStoreUpdater } from 'zustand-utils';
9
+
10
+ import { useSaveDocumentHotkey } from '@/hooks/useHotkeys';
11
+ import { useDocumentStore } from '@/store/document';
12
+ import { editorSelectors } from '@/store/document/slices/editor';
13
+
14
+ import type { EditorCanvasProps } from './EditorCanvas';
15
+ import InternalEditor from './InternalEditor';
16
+
17
+ /**
18
+ * Loading skeleton for the editor
19
+ */
20
+ const EditorSkeleton = memo(() => (
21
+ <div style={{ paddingBlock: 24 }}>
22
+ <Skeleton active paragraph={{ rows: 8 }} />
23
+ </div>
24
+ ));
25
+
26
+ /**
27
+ * Error display for fetch failures
28
+ */
29
+ const EditorError = memo<{ error: Error }>(({ error }) => {
30
+ const { t } = useTranslation('file');
31
+
32
+ return (
33
+ <Alert
34
+ description={error.message || t('pageEditor.loadError', 'Failed to load document')}
35
+ showIcon
36
+ style={{ margin: 16 }}
37
+ title={t('pageEditor.error', 'Error')}
38
+ type="error"
39
+ />
40
+ );
41
+ });
42
+
43
+ export interface DocumentIdModeProps extends EditorCanvasProps {
44
+ documentId: string;
45
+ editor: IEditor | undefined;
46
+ }
47
+
48
+ /**
49
+ * EditorCanvas with documentId mode - handles data fetching internally
50
+ */
51
+ const DocumentIdMode = memo<DocumentIdModeProps>(
52
+ ({
53
+ editor,
54
+ documentId,
55
+ autoSave = true,
56
+ sourceType = 'page',
57
+ onContentChange,
58
+ style,
59
+ ...editorProps
60
+ }) => {
61
+ const { t } = useTranslation('file');
62
+
63
+ const storeUpdater = createStoreUpdater(useDocumentStore);
64
+ storeUpdater('activeDocumentId', documentId);
65
+ storeUpdater('editor', editor);
66
+
67
+ // Get document store actions
68
+ const [onEditorInit, handleContentChangeStore, useFetchDocument, flushSave] = useDocumentStore(
69
+ (s) => [s.onEditorInit, s.handleContentChange, s.useFetchDocument, s.flushSave],
70
+ );
71
+
72
+ useSaveDocumentHotkey(flushSave);
73
+
74
+ // Use SWR hook for document fetching (auto-initializes via onSuccess in DocumentStore)
75
+ const { error } = useFetchDocument(documentId, { autoSave, editor, sourceType });
76
+
77
+ // Check loading state via selector (document not yet in store)
78
+ const isLoading = useDocumentStore(editorSelectors.isDocumentLoading(documentId));
79
+
80
+ // Handle content change
81
+ const handleChange = () => {
82
+ handleContentChangeStore();
83
+ onContentChange?.();
84
+ };
85
+
86
+ // Show loading state
87
+ if (isLoading) {
88
+ return <EditorSkeleton />;
89
+ }
90
+
91
+ if (!editor) return null;
92
+
93
+ return (
94
+ <>
95
+ {error && <EditorError error={error as Error} />}
96
+ <InternalEditor
97
+ editor={editor}
98
+ onContentChange={handleChange}
99
+ onInit={onEditorInit}
100
+ placeholder={editorProps.placeholder || t('pageEditor.editorPlaceholder')}
101
+ style={style}
102
+ {...editorProps}
103
+ />
104
+ </>
105
+ );
106
+ },
107
+ );
108
+
109
+ DocumentIdMode.displayName = 'DocumentIdMode';
110
+
111
+ export default DocumentIdMode;
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import { type IEditor, type SlashOptions } from '@lobehub/editor';
4
+ import { type ChatInputActionsProps } from '@lobehub/editor/react';
5
+ import { Editor } from '@lobehub/editor/react';
6
+ import { type CSSProperties, memo } from 'react';
7
+
8
+ import DocumentIdMode from './DocumentIdMode';
9
+ import EditorDataMode from './EditorDataMode';
10
+ import { EditorErrorBoundary } from './ErrorBoundary';
11
+ import InternalEditor from './InternalEditor';
12
+
13
+ /**
14
+ * Plugin type for the editor
15
+ * Allows any array of plugins that the Editor component accepts
16
+ */
17
+ type EditorPlugins = Parameters<typeof Editor>[0]['plugins'];
18
+
19
+ export interface EditorCanvasProps {
20
+ /**
21
+ * Whether to enable auto-save in DocumentStore. Defaults to true.
22
+ * Only applies when documentId is provided.
23
+ */
24
+ autoSave?: boolean;
25
+
26
+ /**
27
+ * Document ID to load from server.
28
+ * When provided, component will use useSWR to fetch document data.
29
+ */
30
+ documentId?: string;
31
+
32
+ /**
33
+ * Editor data to render directly (skip fetch).
34
+ * Use this when you already have the content and don't need to fetch.
35
+ */
36
+ editorData?: {
37
+ content?: string;
38
+ editorData?: unknown;
39
+ };
40
+
41
+ /**
42
+ * Extra plugins to prepend to BASE_PLUGINS (e.g., ReactLiteXmlPlugin)
43
+ */
44
+ extraPlugins?: EditorPlugins;
45
+
46
+ /**
47
+ * Whether to show the floating toolbar. Defaults to true.
48
+ */
49
+ floatingToolbar?: boolean;
50
+
51
+ /**
52
+ * Content change handler
53
+ */
54
+ onContentChange?: () => void;
55
+
56
+ /**
57
+ * Editor initialization handler
58
+ */
59
+ onInit?: (editor: IEditor) => void;
60
+
61
+ /**
62
+ * Placeholder text for empty editor
63
+ */
64
+ placeholder?: string;
65
+
66
+ /**
67
+ * Custom plugins for the editor. If provided, replaces BASE_PLUGINS entirely.
68
+ * Use this when you need complete control over plugins.
69
+ */
70
+ plugins?: EditorPlugins;
71
+
72
+ /**
73
+ * Slash menu items
74
+ */
75
+ slashItems?: SlashOptions['items'];
76
+
77
+ /**
78
+ * Source type for DocumentStore. Defaults to 'page'.
79
+ */
80
+ sourceType?: 'page' | 'notebook';
81
+
82
+ /**
83
+ * Custom styles for the editor
84
+ */
85
+ style?: CSSProperties;
86
+
87
+ /**
88
+ * Extra items to add to the floating toolbar (e.g., "Ask Copilot" button)
89
+ */
90
+ toolbarExtraItems?: ChatInputActionsProps['items'];
91
+ }
92
+
93
+ export interface EditorCanvasWithEditorProps extends EditorCanvasProps {
94
+ /**
95
+ * Editor instance
96
+ */
97
+ editor: IEditor | undefined;
98
+ }
99
+
100
+ /**
101
+ * EditorCanvas component that accepts editor as a prop
102
+ *
103
+ * Three modes of operation:
104
+ * 1. documentId mode: Pass documentId, component fetches data via useSWR, shows loading/error states
105
+ * 2. editorData mode: Pass editorData directly, skips fetch and renders immediately
106
+ * 3. Basic mode: No documentId or editorData, just renders the editor (original behavior)
107
+ *
108
+ * Features:
109
+ * - Internal ErrorBoundary for graceful error handling
110
+ * - Loading skeleton during fetch (documentId mode)
111
+ * - Error state display for fetch failures (documentId mode)
112
+ * - Auto-save integration with DocumentStore (documentId mode)
113
+ * - AutoSave hint display (documentId mode)
114
+ */
115
+ export const EditorCanvas = memo<EditorCanvasWithEditorProps>(
116
+ ({ editor, documentId, editorData, ...props }) => {
117
+ // documentId mode - fetch and render with loading/error states
118
+ if (documentId) {
119
+ return (
120
+ <EditorErrorBoundary>
121
+ <DocumentIdMode documentId={documentId} editor={editor} {...props} />
122
+ </EditorErrorBoundary>
123
+ );
124
+ }
125
+
126
+ // editorData mode - render with provided data
127
+ if (editorData) {
128
+ return (
129
+ <EditorErrorBoundary>
130
+ <EditorDataMode editor={editor} editorData={editorData} {...props} />
131
+ </EditorErrorBoundary>
132
+ );
133
+ }
134
+
135
+ // Basic mode - original behavior
136
+ if (!editor) return null;
137
+
138
+ return (
139
+ <EditorErrorBoundary>
140
+ <InternalEditor editor={editor} {...props} />
141
+ </EditorErrorBoundary>
142
+ );
143
+ },
144
+ );
145
+
146
+ EditorCanvas.displayName = 'EditorCanvas';
147
+
148
+ export default EditorCanvas;
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { type IEditor } from '@lobehub/editor';
4
+ import { memo, useEffect, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import type { EditorCanvasProps } from './EditorCanvas';
8
+ import InternalEditor from './InternalEditor';
9
+
10
+ export interface EditorDataModeProps extends EditorCanvasProps {
11
+ editor: IEditor | undefined;
12
+ editorData: NonNullable<EditorCanvasProps['editorData']>;
13
+ }
14
+
15
+ /**
16
+ * EditorCanvas with editorData mode - uses provided data directly
17
+ */
18
+ const EditorDataMode = memo<EditorDataModeProps>(
19
+ ({ editor, editorData, onContentChange, style, ...editorProps }) => {
20
+ const { t } = useTranslation('file');
21
+ const [isInitialized, setIsInitialized] = useState(false);
22
+
23
+ // Load content into editor on mount
24
+ useEffect(() => {
25
+ if (!editor || isInitialized) return;
26
+
27
+ const hasValidEditorData =
28
+ editorData.editorData &&
29
+ typeof editorData.editorData === 'object' &&
30
+ Object.keys(editorData.editorData as object).length > 0;
31
+
32
+ try {
33
+ if (hasValidEditorData) {
34
+ editor.setDocument('json', JSON.stringify(editorData.editorData));
35
+ } else if (editorData.content?.trim()) {
36
+ editor.setDocument('markdown', editorData.content, { keepId: true });
37
+ } else {
38
+ console.error('[EditorCanvas] load content error:', editorData);
39
+ }
40
+
41
+ setIsInitialized(true);
42
+ } catch (err) {
43
+ console.error('[EditorCanvas] Failed to load content:', err);
44
+ }
45
+ }, [editorData, editor, isInitialized]);
46
+
47
+ if (!editor) return null;
48
+
49
+ return (
50
+ <div style={{ position: 'relative', ...style }}>
51
+ <InternalEditor
52
+ editor={editor}
53
+ onContentChange={onContentChange}
54
+ placeholder={editorProps.placeholder || t('pageEditor.editorPlaceholder')}
55
+ {...editorProps}
56
+ />
57
+ </div>
58
+ );
59
+ },
60
+ );
61
+
62
+ EditorDataMode.displayName = 'EditorDataMode';
63
+
64
+ export default EditorDataMode;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { Alert } from '@lobehub/ui';
4
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
5
+
6
+ interface EditorErrorBoundaryState {
7
+ error: Error | null;
8
+ hasError: boolean;
9
+ }
10
+
11
+ interface EditorErrorBoundaryProps {
12
+ children: ReactNode;
13
+ fallback?: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * ErrorBoundary for EditorCanvas component.
18
+ * Catches rendering errors in the editor and displays a fallback error UI
19
+ * instead of crashing the entire page.
20
+ */
21
+ export class EditorErrorBoundary extends Component<
22
+ EditorErrorBoundaryProps,
23
+ EditorErrorBoundaryState
24
+ > {
25
+ public state: EditorErrorBoundaryState = {
26
+ error: null,
27
+ hasError: false,
28
+ };
29
+
30
+ public static getDerivedStateFromError(error: Error): Partial<EditorErrorBoundaryState> {
31
+ return { error, hasError: true };
32
+ }
33
+
34
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
35
+ console.error('[EditorErrorBoundary] Caught error in editor render:', {
36
+ componentStack: errorInfo.componentStack,
37
+ error: error.message,
38
+ stack: error.stack,
39
+ });
40
+ }
41
+
42
+ public render() {
43
+ if (this.state.hasError) {
44
+ if (this.props.fallback) {
45
+ return this.props.fallback;
46
+ }
47
+
48
+ return (
49
+ <Alert
50
+ message={this.state.error?.message || 'An unknown error occurred in the editor'}
51
+ showIcon
52
+ style={{
53
+ margin: 16,
54
+ overflow: 'hidden',
55
+ position: 'relative',
56
+ width: '100%',
57
+ }}
58
+ title="Editor Error"
59
+ type="error"
60
+ />
61
+ );
62
+ }
63
+
64
+ return this.props.children;
65
+ }
66
+ }
@@ -0,0 +1,245 @@
1
+ 'use client';
2
+
3
+ import {
4
+ HotkeyEnum,
5
+ INSERT_HEADING_COMMAND,
6
+ getHotkeyById,
7
+ type IEditor,
8
+ } from '@lobehub/editor';
9
+ import {
10
+ ChatInputActions,
11
+ type ChatInputActionsProps,
12
+ type EditorState,
13
+ FloatActions,
14
+ } from '@lobehub/editor/react';
15
+ import { Block } from '@lobehub/ui';
16
+ import { cssVar } from 'antd-style';
17
+ import {
18
+ BoldIcon,
19
+ CodeXmlIcon,
20
+ Heading1Icon,
21
+ Heading2Icon,
22
+ Heading3Icon,
23
+ ItalicIcon,
24
+ LinkIcon,
25
+ ListIcon,
26
+ ListOrderedIcon,
27
+ ListTodoIcon,
28
+ MessageSquareQuote,
29
+ Redo2Icon,
30
+ SigmaIcon,
31
+ SquareDashedBottomCodeIcon,
32
+ StrikethroughIcon,
33
+ UnderlineIcon,
34
+ Undo2Icon,
35
+ } from 'lucide-react';
36
+ import { type CSSProperties, memo, useMemo } from 'react';
37
+ import { useTranslation } from 'react-i18next';
38
+
39
+ export interface InlineToolbarProps {
40
+ className?: string;
41
+ editor?: IEditor;
42
+ editorState?: EditorState;
43
+ /**
44
+ * Extra items to prepend to the toolbar (e.g., "Ask Copilot" button)
45
+ */
46
+ extraItems?: ChatInputActionsProps['items'];
47
+ floating?: boolean;
48
+ style?: CSSProperties;
49
+ }
50
+
51
+ const InlineToolbar = memo<InlineToolbarProps>(
52
+ ({ floating, style, className, editor, editorState, extraItems }) => {
53
+ const { t } = useTranslation('editor');
54
+
55
+ const items: ChatInputActionsProps['items'] = useMemo(() => {
56
+ if (!editorState) return [];
57
+
58
+ const baseItems = [
59
+ // Extra items (like "Ask Copilot") come first
60
+ ...(extraItems || []),
61
+ extraItems?.length ? { type: 'divider' as const } : null,
62
+ !floating && {
63
+ disabled: !editorState.canUndo,
64
+ icon: Undo2Icon,
65
+ key: 'undo',
66
+ label: t('typobar.undo', 'Undo'),
67
+ onClick: editorState.undo,
68
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Undo).keys },
69
+ },
70
+ !floating && {
71
+ disabled: !editorState.canRedo,
72
+ icon: Redo2Icon,
73
+ key: 'redo',
74
+ label: t('typobar.redo', 'Redo'),
75
+ onClick: editorState.redo,
76
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Redo).keys },
77
+ },
78
+ !floating && {
79
+ type: 'divider',
80
+ },
81
+ {
82
+ active: editorState.isBold,
83
+ icon: BoldIcon,
84
+ key: 'bold',
85
+ label: t('typobar.bold'),
86
+ onClick: editorState.bold,
87
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
88
+ },
89
+ {
90
+ active: editorState.isItalic,
91
+ icon: ItalicIcon,
92
+ key: 'italic',
93
+ label: t('typobar.italic'),
94
+ onClick: editorState.italic,
95
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
96
+ },
97
+ {
98
+ active: editorState.isUnderline,
99
+ icon: UnderlineIcon,
100
+ key: 'underline',
101
+ label: t('typobar.underline'),
102
+ onClick: editorState.underline,
103
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
104
+ },
105
+ {
106
+ active: editorState.isStrikethrough,
107
+ icon: StrikethroughIcon,
108
+ key: 'strikethrough',
109
+ label: t('typobar.strikethrough'),
110
+ onClick: editorState.strikethrough,
111
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
112
+ },
113
+ {
114
+ type: 'divider',
115
+ },
116
+ !floating && {
117
+ icon: Heading1Icon,
118
+ key: 'h1',
119
+ label: t('slash.h1'),
120
+ onClick: () => {
121
+ if (editor) {
122
+ editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h1' });
123
+ }
124
+ },
125
+ },
126
+ !floating && {
127
+ icon: Heading2Icon,
128
+ key: 'h2',
129
+ label: t('slash.h2'),
130
+ onClick: () => {
131
+ if (editor) {
132
+ editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h2' });
133
+ }
134
+ },
135
+ },
136
+ !floating && {
137
+ icon: Heading3Icon,
138
+ key: 'h3',
139
+ label: t('slash.h3'),
140
+ onClick: () => {
141
+ if (editor) {
142
+ editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h3' });
143
+ }
144
+ },
145
+ },
146
+ !floating && {
147
+ type: 'divider',
148
+ },
149
+ {
150
+ icon: ListIcon,
151
+ key: 'bulletList',
152
+ label: t('typobar.bulletList'),
153
+ onClick: editorState.bulletList,
154
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
155
+ },
156
+ {
157
+ icon: ListOrderedIcon,
158
+ key: 'numberlist',
159
+ label: t('typobar.numberList'),
160
+ onClick: editorState.numberList,
161
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
162
+ },
163
+ {
164
+ icon: ListTodoIcon,
165
+ key: 'tasklist',
166
+ label: t('typobar.taskList'),
167
+ onClick: editorState.checkList,
168
+ },
169
+ {
170
+ type: 'divider',
171
+ },
172
+ {
173
+ active: editorState.isBlockquote,
174
+ icon: MessageSquareQuote,
175
+ key: 'blockquote',
176
+ label: t('typobar.blockquote'),
177
+ onClick: editorState.blockquote,
178
+ },
179
+ {
180
+ icon: LinkIcon,
181
+ key: 'link',
182
+ label: t('typobar.link'),
183
+ onClick: editorState.insertLink,
184
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Link).keys },
185
+ },
186
+ {
187
+ icon: SigmaIcon,
188
+ key: 'math',
189
+ label: t('typobar.tex'),
190
+ onClick: editorState.insertMath,
191
+ },
192
+ {
193
+ type: 'divider',
194
+ },
195
+ {
196
+ active: editorState.isCode,
197
+ icon: CodeXmlIcon,
198
+ key: 'code',
199
+ label: t('typobar.code'),
200
+ onClick: editorState.code,
201
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
202
+ },
203
+ !floating && {
204
+ icon: SquareDashedBottomCodeIcon,
205
+ key: 'codeblock',
206
+ label: t('typobar.codeblock'),
207
+ onClick: editorState.codeblock,
208
+ },
209
+ ];
210
+
211
+ return baseItems.filter(Boolean) as ChatInputActionsProps['items'];
212
+ }, [editor, editorState, extraItems, floating, t]);
213
+
214
+ if (!editorState) return null;
215
+
216
+ // Floating toolbar - just return the actions
217
+ if (floating) return <FloatActions className={className} items={items} style={style} />;
218
+
219
+ // Fixed toolbar - wrap in a styled container
220
+ return (
221
+ <Block
222
+ className={className}
223
+ padding={4}
224
+ shadow
225
+ style={{
226
+ background: cssVar.colorBgElevated,
227
+ borderRadius: 8,
228
+ marginBottom: 16,
229
+ marginTop: 16,
230
+ position: 'sticky',
231
+ top: 12,
232
+ zIndex: 10,
233
+ ...style,
234
+ }}
235
+ variant={'outlined'}
236
+ >
237
+ <ChatInputActions items={items} />
238
+ </Block>
239
+ );
240
+ },
241
+ );
242
+
243
+ InlineToolbar.displayName = 'InlineToolbar';
244
+
245
+ export default InlineToolbar;