diff --git a/.changeset/twelve-laws-press copy 2.md b/.changeset/twelve-laws-press copy 2.md new file mode 100644 index 0000000000..1cd08454df --- /dev/null +++ b/.changeset/twelve-laws-press copy 2.md @@ -0,0 +1,20 @@ +--- +'@udecode/slate': patch +--- + +- Fix `editor.api.last(at, { level: 0 })` when editor has no children, it should return `undefined`. Fixes error on cmd+a > backspace. +- Fix `editor.tf.removeNodes` when `previousEmptyBlock` is true without `at` option, it should return early. + - Fixes #3960 +- Add `RangeApi.contains` to check if a range fully contains another range (both start and end points). +- Add `editor.api.isSelected(target, { contains?: boolean })` to check if a path or range is selected by the current selection. When `contains` is true, checks if selection fully contains the target. +- `editor.tf.insertText` now support both legacy slate transforms `editor.insertText` and `Transforms.insertText`: + - `editor.insertText` -> `editor.tf.insertText` without `at` option. In addition, `marks: false` option can be used to exclude current marks. Default is `true`. + - `Transforms.insertText` -> `editor.tf.insertText` with `at` option +- Add `editor.api.next` option `from`: + - `from?: 'after' | 'child'` (default: `'after'`): Determines where to start traversing from + - `'after'`: Start from the point after the current location + - `'child'`: Start from the first child of the current path. `at` must be a path. +- Add `editor.api.previous` option `from`: + - `from?: 'before' | 'parent'` (default: `'before'`): Determines where to start traversing from + - `'before'`: Start from the point before the current location + - `'parent'`: Start from the parent of the current path. `at` must be a path. diff --git a/.changeset/twelve-laws-press copy 3.md b/.changeset/twelve-laws-press copy 3.md new file mode 100644 index 0000000000..2248d4aaa0 --- /dev/null +++ b/.changeset/twelve-laws-press copy 3.md @@ -0,0 +1,13 @@ +--- +'@udecode/plate-markdown': minor +--- + +- `editor.api.markdown.deserialize`: + - Improve support for indented lists: nested lists, mixed ordered and unordered lists + - Fix: markdown codeblock without language should not set `lang: undefined` to the node + - Add options: + - `memoize`: Enable block-level memoization with `_memo` property, so it is compatible with `PlateStatic` memoization. + - `parse`: Filter out specific markdown token types (e.g. 'space') + - `processor`: Customize the markdown processor +- Add `parseMarkdownBlocks`: Extract and filter markdown tokens using marked lexer +- Fix `editor.api.markdown.serialize` indenting should be 3 spaces instead of 2. diff --git a/.changeset/twelve-laws-press copy 4.md b/.changeset/twelve-laws-press copy 4.md new file mode 100644 index 0000000000..e90487bef5 --- /dev/null +++ b/.changeset/twelve-laws-press copy 4.md @@ -0,0 +1,7 @@ +--- +'@udecode/plate-selection': minor +--- + +- Fix: after deleting all blocks using block selection, focus the empty editor. +- Feature: shift+up/down block selection to expand/shrink selection. Supports nested blocks. +- Feature: up/down support nested blocks diff --git a/.changeset/twelve-laws-press copy 5.md b/.changeset/twelve-laws-press copy 5.md new file mode 100644 index 0000000000..01444cf3f2 --- /dev/null +++ b/.changeset/twelve-laws-press copy 5.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-comments': patch +--- + +- Fix `insertComment` - it should not deselect the editor after inserting a comment. diff --git a/.changeset/twelve-laws-press copy.md b/.changeset/twelve-laws-press copy.md new file mode 100644 index 0000000000..4f15cb4ff3 --- /dev/null +++ b/.changeset/twelve-laws-press copy.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-core': patch +--- + +- Fix `tf.reset` missing `options` argument. Fixes editor reset on select all > backspace using `ResetNodePlugin`. +- `PlateStatic` element and leaf rendering is now memoized with `React.memo` so you can safely update `editor.children`. For elements, it compares the `element` reference or `element._memo` value. The latter can be used to memoize based on the markdown string instead of the `element` reference. For example, `deserializeMd` with `memoize: true` will set `element._memo` for that purpose. diff --git a/.changeset/twelve-laws-press.md b/.changeset/twelve-laws-press.md new file mode 100644 index 0000000000..2305db61e1 --- /dev/null +++ b/.changeset/twelve-laws-press.md @@ -0,0 +1,13 @@ +--- +'@udecode/plate-ai': minor +--- + +Breaking changes (experimental): + +- `AIChatPlugin`: Remove `createAIEditor` option +- Fix `editor.tf.replaceSelection`: + - Improved single block selection case with full range check + - Fixed text properties inheritance when replacing selection + - In block selection mode, select the replaced blocks +- Add `useAIChatEditor`: Creates an editor, registers in the AI chat plugin, and deserializes the + content into `editor.children` with block-level memoization. diff --git a/.gitignore b/.gitignore index d78c881327..d0bec05e63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/ignore-files/ for more about ignoring files. +apps/www/src/app/api/ai + **/tsconfig.tsbuildinfo .yarn/* diff --git a/apps/www/content/docs/cn/plugin-methods.mdx b/apps/www/content/docs/cn/plugin-methods.mdx index 3896398f24..e69f07616e 100644 --- a/apps/www/content/docs/cn/plugin-methods.mdx +++ b/apps/www/content/docs/cn/plugin-methods.mdx @@ -315,13 +315,13 @@ const MyPlugin = createPlatePlugin({ transforms: { insertText(text) { // 自定义逻辑 - console.log('Inserting:', text); + console.info('Inserting:', text); // 调用原始方法 insertText(text); // 后续逻辑 - console.log('Inserted:', text); + console.info('Inserted:', text); }, }, api: { diff --git a/apps/www/content/docs/en/ai.mdx b/apps/www/content/docs/en/ai.mdx index e54e64d20e..a223a5165a 100644 --- a/apps/www/content/docs/en/ai.mdx +++ b/apps/www/content/docs/en/ai.mdx @@ -13,7 +13,7 @@ docs: - Combobox menu with predefined commands: - Generate: continue writing, add summary, explain - - Edit: improve writing, make it longer or shorter, fix spelling & grammar, simplify language + - Edit: improve writing, emojify, make it longer or shorter, fix spelling & grammar, simplify language - Three trigger modes: - Cursor mode: trigger at block end - Selection mode: trigger with selected text @@ -442,3 +442,52 @@ Special undo operation for AI changes: - Undoes the last operation if it was AI-generated - Removes the redo stack entry to prevent redoing AI operations + +### useAIChatEditor + +A hook that creates an editor for AI chat responses, registers it in the AI chat plugin, and deserializes markdown content with block-level memoization. + + + +The markdown content to deserialize into the editor. + + + + +Enable block-level memoization with `_memo` property. Defaults to true. + + +Options for the markdown token parser. Can filter out specific token types. + + +Function to customize the markdown processor. + + +Additional options passed to `usePlateEditor`. + + + + + + +The configured editor instance with deserialized content. + + + +```tsx +const AIChatEditor = ({ content }: { content: string }) => { + const aiEditor = useAIChatEditor(content, { + plugins: [ + // Your editor plugins + MarkdownPlugin, + // etc... + ], + // Optional markdown parser options + parser: { + exclude: ['space'], + }, + }); + + return ; +}; +``` diff --git a/apps/www/content/docs/en/block-selection.mdx b/apps/www/content/docs/en/block-selection.mdx index e1d92245c7..eff3f888c4 100644 --- a/apps/www/content/docs/en/block-selection.mdx +++ b/apps/www/content/docs/en/block-selection.mdx @@ -222,6 +222,18 @@ A set of IDs for the currently selected blocks. - **Default:** `new Set()` + + +(Internal) The ID of the anchor block in the current selection. Used for shift-based selection. + +- **Default:** `null` + + + +Function to determine if a block element is selectable. + +- **Default:** `() => true` + ### BlockMenuPlugin @@ -371,3 +383,64 @@ Returns fragment prop for the currently selected blocks. ### useSelectionArea A hook that initializes and manages the selection area functionality. + +### editor.api.blockSelection.isSelectable + +Checks if a block element is selectable. + + + +The block element to check. + + +The path to the block element. + + + + + +Returns true if the block is selectable. + + + +### editor.api.blockSelection.moveSelection + +Moves the selection up or down to the next selectable block. + + + +The direction to move the selection. + + + +When moving up: +- Gets the previous selectable block from the top-most selected block +- Sets it as the new anchor +- Clears previous selection and selects only this block + +When moving down: +- Gets the next selectable block from the bottom-most selected block +- Sets it as the new anchor +- Clears previous selection and selects only this block + +### editor.api.blockSelection.shiftSelection + +Expands or shrinks the selection based on the anchor block. + + + +The direction to expand/shrink the selection. + + + +For SHIFT + DOWN: +- If anchor is top-most: Expands down by adding block below bottom-most +- Otherwise: Shrinks from top-most (unless top-most is the anchor) + +For SHIFT + UP: +- If anchor is bottom-most: Expands up by adding block above top-most +- Otherwise: Shrinks from bottom-most (unless bottom-most is the anchor) + +The anchor block always remains selected. If no anchor is set, it defaults to: +- Bottom-most block for SHIFT + UP +- Top-most block for SHIFT + DOWN diff --git a/apps/www/content/docs/en/components/changelog.mdx b/apps/www/content/docs/en/components/changelog.mdx index 77d32fdb6f..9ad92eb045 100644 --- a/apps/www/content/docs/en/components/changelog.mdx +++ b/apps/www/content/docs/en/components/changelog.mdx @@ -8,25 +8,36 @@ Since Plate UI is not a component library, a changelog is maintained here. Use the [CLI](https://platejs.org/docs/components/cli) to install the latest version of the components. -## December 2024 #17 +## January 2025 #18 -### December 28 #17.8 +### January 12 #18.2 -- `export-toolbar-button`: add `katex` support -- `plate-element`: remove `relative` className -- All components using the `PlateElement` have had redundant `relative` class names removed. -### December 27 #17.7 +- `ai-plugins`: remove `createAIEditor`, it's now created in `ai-chat-editor` +- `ai-chat-editor`: just use `useAIChatEditor` (v42.1) +- `ai-menu`: avoid collisions, remove `aiEditorRef` +- `command`: add `focus-visible:outline-none` +- `editor-static`: update `aiChat` padding +- `transforms`: fix `insertBlock` used by slash commands: it should insert a new block if the newly inserted block is of the same type as the command. +- `block-selection-plugins`: update `BlockSelectionPlugin` -- `fixed-toolbar-buttons`: add `font-size-toolbar-button` -- `floating-toolbar`: add `inline-equation-toolbar-button` -- `turn-into-dropdown-menu`: Fix: after turn into other block, the editor should regain focus. -- `insert-dropdown-menu`: add `inline equation` and `equation` & fix the focus issue -- `slash-input-element`: add `equation` and `inline equation` - -### December 25 #17.6 +```tsx +BlockSelectionPlugin.configure(({ editor }) => ({ + options: { + enableContextMenu: true, + isSelectable: (element, path) => { + return ( + !['code_line', 'column', 'td'].includes(element.type) && + !editor.api.block({ above: true, at: path, match: { type: 'tr' } }) + ); + }, + }, +})) +``` -v42 + +### January 8 #18.1 +- v42 migration - `table-element`, `table-element-static` - Move icons to `table-icons` - Remove `colgroup`, col width is now set in `table-cell-element` @@ -44,6 +55,21 @@ v42 - `table-dropdown-menu`: new insert table interface. - `column-group-element`: fix `ColumnFloatingToolbar` onColumnChange +## December 2024 #17 + +### December 28 #17.8 + +- `export-toolbar-button`: add `katex` support +- `plate-element`: remove `relative` className +- All components using the `PlateElement` have had redundant `relative` class names removed. +### December 27 #17.7 + +- `fixed-toolbar-buttons`: add `font-size-toolbar-button` +- `floating-toolbar`: add `inline-equation-toolbar-button` +- `turn-into-dropdown-menu`: Fix: after turn into other block, the editor should regain focus. +- `insert-dropdown-menu`: add `inline equation` and `equation` & fix the focus issue +- `slash-input-element`: add `equation` and `inline equation` + ### December 23 #17.5 - `table-element`: fix selection diff --git a/apps/www/content/docs/en/markdown.mdx b/apps/www/content/docs/en/markdown.mdx index 1351cc7320..73b89089db 100644 --- a/apps/www/content/docs/en/markdown.mdx +++ b/apps/www/content/docs/en/markdown.mdx @@ -42,6 +42,7 @@ const value = editor.api.markdown.deserialize('**Hello world!**'); ### Slate to Markdown Currently supported plugins: paragraph, link, list, heading, italic, bold and code. +List indentation uses 3 spaces instead of 2. ```tsx const editor = createPlateEditor({ @@ -114,6 +115,19 @@ Converts a Markdown string to a Slate value. The Markdown string to be deserialized. + + + +Enable block-level memoization with `_memo` property, making it compatible with `PlateStatic` memoization. + + +Options for the token parser. Can filter out specific markdown token types (e.g. 'space'). + + +A function that allows you to customize the markdown processor. + + + @@ -140,4 +154,30 @@ The Slate nodes to serialize. If not provided, the entire editor value will be u A Markdown string representing the serialized Slate content. + + +### parseMarkdownBlocks + +Extracts and filters markdown tokens using marked lexer. + + + +The Markdown string to parse into tokens. + + + + +Array of token types to exclude (e.g. ['space', 'hr']). + + +Whether to trim end of the content. Defaults to true. + + + + + + + +An array of markdown tokens. + \ No newline at end of file diff --git a/apps/www/content/docs/en/plugin.mdx b/apps/www/content/docs/en/plugin.mdx index 515a6bdc51..14065225c8 100644 --- a/apps/www/content/docs/en/plugin.mdx +++ b/apps/www/content/docs/en/plugin.mdx @@ -176,13 +176,13 @@ const CustomPlugin = createPlatePlugin({ transforms: { deleteForward(options) { // Custom logic before deletion - console.log('Deleting forward...'); + console.info('Deleting forward...'); // Call original transform deleteForward(options); // Custom logic after deletion - console.log('Deleted forward'); + console.info('Deleted forward'); }, }, // Override API methods diff --git a/apps/www/public/r/styles/default/ai-demo.json b/apps/www/public/r/styles/default/ai-demo.json index ed61067ec9..3c36b4e0c6 100644 --- a/apps/www/public/r/styles/default/ai-demo.json +++ b/apps/www/public/r/styles/default/ai-demo.json @@ -23,7 +23,7 @@ "type": "registry:example" }, { - "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const aiValue: any = (\n \n AI Menu\n Generate and refine content with AI.\n Access the AI menu in many ways:\n \n Press \"⌘ + J\".\n \n \n Select text and click \"Ask AI\" in the floating toolbar\n \n \n Right-click a block and select \"Ask AI\"\n \n \n Press space in an empty block. Try it out:\n \n \n \n \n Once opened, you can:\n \n Search commands in the input field:\n \n \n Use arrow keys to navigate, Enter to select\n \n \n Generating commands:\n \n \n Continue writing\n \n \n Add a summary\n \n \n Explain\n \n \n Generating suggestions:\n \n \n Accept\n \n \n Discard\n \n \n Try again\n \n \n Editing commands:\n \n \n Improve writing\n \n \n Make it longer or shorter\n \n \n Fix spelling & grammar\n \n \n Simplify language\n \n Editing suggestions:\n \n Replace the selection\n \n \n Insert below\n \n \n Discard\n \n \n Try again\n \n \n Note: chat history is preserved until the menu is closed.\n \n \n);\n", + "content": "import { jsx } from '@udecode/plate-test-utils';\n\njsx;\n\nexport const aiValue: any = (\n \n AI Menu\n Generate and refine content with AI.\n Access the AI menu in many ways:\n \n Press \"⌘ + J\".\n \n \n Select text and click \"Ask AI\" in the floating toolbar\n \n \n Right-click a block and select \"Ask AI\"\n \n \n Press space in an empty block. Try it out:\n \n \n \n \n Once opened, you can:\n \n Search commands in the input field:\n \n \n Use arrow keys to navigate, Enter to select\n \n \n Generating commands:\n \n \n Continue writing\n \n \n Add a summary\n \n \n Explain\n \n \n Generating suggestions:\n \n \n Accept\n \n \n Discard\n \n \n Try again\n \n \n Editing commands:\n \n \n Improve writing\n \n \n Emojify\n \n \n Make it longer or shorter\n \n \n Fix spelling & grammar\n \n \n Simplify language\n \n Editing suggestions:\n \n Replace the selection\n \n \n Insert below\n \n \n Discard\n \n \n Try again\n \n \n Note: chat history is preserved until the menu is closed.\n \n \n);\n", "path": "example/values/ai-value.tsx", "target": "components/ai-value.tsx", "type": "registry:example" diff --git a/apps/www/public/r/styles/default/ai-menu.json b/apps/www/public/r/styles/default/ai-menu.json index afc7afe4fc..cc4606eeae 100644 --- a/apps/www/public/r/styles/default/ai-menu.json +++ b/apps/www/public/r/styles/default/ai-menu.json @@ -18,39 +18,31 @@ "title": "AI Menu" } ], - "examples": [ - "ai-demo", - "ai-pro" - ], + "examples": ["ai-demo", "ai-pro"], "label": "New", "title": "AI Menu" }, "files": [ { - "content": "'use client';\n\nimport * as React from 'react';\n\nimport { type NodeEntry, type SlateEditor, isHotkey } from '@udecode/plate';\nimport { useEditorPlugin, useHotkeys } from '@udecode/plate/react';\nimport { AIChatPlugin, useEditorChat } from '@udecode/plate-ai/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const aiEditorRef = React.useRef(null);\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState(\n null\n );\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: NodeEntry[]) => {\n show(editor.api.toDOMNode(blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const [ancestor] = editor.api.block({ highest: true })!;\n\n if (!editor.api.isAt({ end: true }) && !editor.api.isEmpty(ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.addSelectedRow(ancestor.id as string);\n }\n\n show(editor.api.toDOMNode(ancestor)!);\n },\n onOpenSelection: () => {\n show(editor.api.toDOMNode(editor.api.blocks().at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n \n \n\n {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n avoidCollisions={false}\n side=\"bottom\"\n >\n \n {mode === 'chat' && isSelecting && messages.length > 0 && (\n \n )}\n\n {isLoading ? (\n
\n \n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n
\n ) : (\n {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n data-plate-focus\n autoFocus\n />\n )}\n\n {!isLoading && (\n \n \n \n )}\n \n \n
\n );\n}\n", + "content": "'use client';\n\nimport * as React from 'react';\n\nimport { AIChatPlugin, useEditorChat } from '@udecode/plate-ai/react';\nimport {\n type SlateEditor,\n type TElement,\n type TNodeEntry,\n getAncestorNode,\n getBlocks,\n isElementEmpty,\n isHotkey,\n isSelectionAtBlockEnd,\n} from '@udecode/plate-common';\nimport {\n toDOMNode,\n useEditorPlugin,\n useHotkeys,\n} from '@udecode/plate-common/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const aiEditorRef = React.useRef(null);\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState(\n null\n );\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: TNodeEntry[]) => {\n show(toDOMNode(editor, blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const ancestor = getAncestorNode(editor)?.[0] as TElement;\n\n if (!isSelectionAtBlockEnd(editor) && !isElementEmpty(editor, ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.addSelectedRow(ancestor.id as string);\n }\n\n show(toDOMNode(editor, ancestor)!);\n },\n onOpenSelection: () => {\n show(toDOMNode(editor, getBlocks(editor).at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n \n \n\n {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n avoidCollisions={false}\n side=\"bottom\"\n >\n \n {mode === 'chat' && isSelecting && messages.length > 0 && (\n \n )}\n\n {isLoading ? (\n
\n \n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n
\n ) : (\n {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n data-plate-focus\n autoFocus\n />\n )}\n\n {!isLoading && (\n \n \n \n )}\n \n \n
\n );\n}\n", "path": "plate-ui/ai-menu.tsx", "target": "components/plate-ui/ai-menu.tsx", "type": "registry:ui" }, { - "content": "'use client';\n\nimport { useEffect, useMemo } from 'react';\n\nimport { type SlateEditor, NodeApi } from '@udecode/plate';\nimport { type PlateEditor, useEditorPlugin } from '@udecode/plate/react';\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport { useIsSelecting } from '@udecode/plate-selection/react';\nimport {\n Album,\n BadgeHelp,\n Check,\n CornerUpLeft,\n FeatherIcon,\n ListEnd,\n ListMinus,\n ListPlus,\n PenLine,\n Wand,\n X,\n} from 'lucide-react';\n\nimport { CommandGroup, CommandItem } from './command';\n\nexport type EditorChatState =\n | 'cursorCommand'\n | 'cursorSuggestion'\n | 'selectionCommand'\n | 'selectionSuggestion';\n\nexport const aiChatItems = {\n accept: {\n icon: ,\n label: 'Accept',\n value: 'accept',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIChatPlugin).aiChat.accept();\n editor.tf.focus({ edge: 'end' });\n },\n },\n continueWrite: {\n icon: ,\n label: 'Continue writing',\n value: 'continueWrite',\n onSelect: ({ editor }) => {\n const ancestorNode = editor.api.block({ highest: true });\n\n if (!ancestorNode) return;\n\n const isEmpty = NodeApi.string(ancestorNode[0]).trim().length === 0;\n\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: isEmpty\n ? `\n{editor}\n\nStart writing a new paragraph AFTER ONLY ONE SENTENCE`\n : 'Continue writing AFTER ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',\n });\n },\n },\n discard: {\n icon: ,\n label: 'Discard',\n shortcut: 'Escape',\n value: 'discard',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIPlugin).ai.undo();\n editor.getApi(AIChatPlugin).aiChat.hide();\n },\n },\n explain: {\n icon: ,\n label: 'Explain',\n value: 'explain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: {\n default: 'Explain {editor}',\n selecting: 'Explain',\n },\n });\n },\n },\n fixSpelling: {\n icon: ,\n label: 'Fix spelling & grammar',\n value: 'fixSpelling',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Fix spelling and grammar',\n });\n },\n },\n improveWriting: {\n icon: ,\n label: 'Improve writing',\n value: 'improveWriting',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Improve the writing',\n });\n },\n },\n insertBelow: {\n icon: ,\n label: 'Insert below',\n value: 'insertBelow',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.insertBelow(aiEditor);\n },\n },\n makeLonger: {\n icon: ,\n label: 'Make longer',\n value: 'makeLonger',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make longer',\n });\n },\n },\n makeShorter: {\n icon: ,\n label: 'Make shorter',\n value: 'makeShorter',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make shorter',\n });\n },\n },\n replace: {\n icon: ,\n label: 'Replace selection',\n value: 'replace',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);\n },\n },\n simplifyLanguage: {\n icon: ,\n label: 'Simplify language',\n value: 'simplifyLanguage',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Simplify the language',\n });\n },\n },\n summarize: {\n icon: ,\n label: 'Add a summary',\n value: 'summarize',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: {\n default: 'Summarize {editor}',\n selecting: 'Summarize',\n },\n });\n },\n },\n tryAgain: {\n icon: ,\n label: 'Try again',\n value: 'tryAgain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.reload();\n },\n },\n} satisfies Record<\n string,\n {\n icon: React.ReactNode;\n label: string;\n value: string;\n component?: React.ComponentType<{ menuState: EditorChatState }>;\n filterItems?: boolean;\n items?: { label: string; value: string }[];\n shortcut?: string;\n onSelect?: ({\n aiEditor,\n editor,\n }: {\n aiEditor: SlateEditor;\n editor: PlateEditor;\n }) => void;\n }\n>;\n\nconst menuStateItems: Record<\n EditorChatState,\n {\n items: (typeof aiChatItems)[keyof typeof aiChatItems][];\n heading?: string;\n }[]\n> = {\n cursorCommand: [\n {\n items: [\n aiChatItems.continueWrite,\n aiChatItems.summarize,\n aiChatItems.explain,\n ],\n },\n ],\n cursorSuggestion: [\n {\n items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],\n },\n ],\n selectionCommand: [\n {\n items: [\n aiChatItems.improveWriting,\n aiChatItems.makeLonger,\n aiChatItems.makeShorter,\n aiChatItems.fixSpelling,\n aiChatItems.simplifyLanguage,\n ],\n },\n ],\n selectionSuggestion: [\n {\n items: [\n aiChatItems.replace,\n aiChatItems.insertBelow,\n aiChatItems.discard,\n aiChatItems.tryAgain,\n ],\n },\n ],\n};\n\nexport const AIMenuItems = ({\n aiEditorRef,\n setValue,\n}: {\n aiEditorRef: React.MutableRefObject;\n setValue: (value: string) => void;\n}) => {\n const { editor, useOption } = useEditorPlugin(AIChatPlugin);\n const { messages } = useOption('chat');\n const isSelecting = useIsSelecting();\n\n const menuState = useMemo(() => {\n if (messages && messages.length > 0) {\n return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';\n }\n\n return isSelecting ? 'selectionCommand' : 'cursorCommand';\n }, [isSelecting, messages]);\n\n const menuGroups = useMemo(() => {\n const items = menuStateItems[menuState];\n\n return items;\n }, [menuState]);\n\n useEffect(() => {\n if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {\n setValue(menuGroups[0].items[0].value);\n }\n }, [menuGroups, setValue]);\n\n return (\n <>\n {menuGroups.map((group, index) => (\n \n {group.items.map((menuItem) => (\n {\n menuItem.onSelect?.({\n aiEditor: aiEditorRef.current!,\n editor: editor,\n });\n }}\n >\n {menuItem.icon}\n {menuItem.label}\n \n ))}\n \n ))}\n \n );\n};\n", + "content": "'use client';\n\nimport { useEffect, useMemo } from 'react';\n\nimport { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';\nimport {\n type SlateEditor,\n getAncestorNode,\n getEndPoint,\n getNodeString,\n} from '@udecode/plate-common';\nimport {\n type PlateEditor,\n focusEditor,\n useEditorPlugin,\n} from '@udecode/plate-common/react';\nimport { useIsSelecting } from '@udecode/plate-selection/react';\nimport {\n Album,\n BadgeHelp,\n Check,\n CornerUpLeft,\n FeatherIcon,\n ListEnd,\n ListMinus,\n ListPlus,\n PenLine,\n Wand,\n X,\n} from 'lucide-react';\n\nimport { CommandGroup, CommandItem } from './command';\n\nexport type EditorChatState =\n | 'cursorCommand'\n | 'cursorSuggestion'\n | 'selectionCommand'\n | 'selectionSuggestion';\n\nexport const aiChatItems = {\n accept: {\n icon: ,\n label: 'Accept',\n value: 'accept',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIChatPlugin).aiChat.accept();\n focusEditor(editor, getEndPoint(editor, editor.selection!));\n },\n },\n continueWrite: {\n icon: ,\n label: 'Continue writing',\n value: 'continueWrite',\n onSelect: ({ editor }) => {\n const ancestorNode = getAncestorNode(editor);\n\n if (!ancestorNode) return;\n\n const isEmpty = getNodeString(ancestorNode[0]).trim().length === 0;\n\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: isEmpty\n ? `\n{editor}\n\nStart writing a new paragraph AFTER ONLY ONE SENTENCE`\n : 'Continue writing AFTER ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',\n });\n },\n },\n discard: {\n icon: ,\n label: 'Discard',\n shortcut: 'Escape',\n value: 'discard',\n onSelect: ({ editor }) => {\n editor.getTransforms(AIPlugin).ai.undo();\n editor.getApi(AIChatPlugin).aiChat.hide();\n },\n },\n emojify: {\n icon: ,\n label: 'Emojify',\n value: 'emojify',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Emojify',\n });\n },\n },\n explain: {\n icon: ,\n label: 'Explain',\n value: 'explain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: {\n default: 'Explain {editor}',\n selecting: 'Explain',\n },\n });\n },\n },\n fixSpelling: {\n icon: ,\n label: 'Fix spelling & grammar',\n value: 'fixSpelling',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Fix spelling and grammar',\n });\n },\n },\n improveWriting: {\n icon: ,\n label: 'Improve writing',\n value: 'improveWriting',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Improve the writing',\n });\n },\n },\n insertBelow: {\n icon: ,\n label: 'Insert below',\n value: 'insertBelow',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.insertBelow(aiEditor);\n },\n },\n makeLonger: {\n icon: ,\n label: 'Make longer',\n value: 'makeLonger',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make longer',\n });\n },\n },\n makeShorter: {\n icon: ,\n label: 'Make shorter',\n value: 'makeShorter',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Make shorter',\n });\n },\n },\n replace: {\n icon: ,\n label: 'Replace selection',\n value: 'replace',\n onSelect: ({ aiEditor, editor }) => {\n void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);\n },\n },\n simplifyLanguage: {\n icon: ,\n label: 'Simplify language',\n value: 'simplifyLanguage',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n prompt: 'Simplify the language',\n });\n },\n },\n summarize: {\n icon: ,\n label: 'Add a summary',\n value: 'summarize',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.submit({\n mode: 'insert',\n prompt: {\n default: 'Summarize {editor}',\n selecting: 'Summarize',\n },\n });\n },\n },\n tryAgain: {\n icon: ,\n label: 'Try again',\n value: 'tryAgain',\n onSelect: ({ editor }) => {\n void editor.getApi(AIChatPlugin).aiChat.reload();\n },\n },\n} satisfies Record<\n string,\n {\n icon: React.ReactNode;\n label: string;\n value: string;\n component?: React.ComponentType<{ menuState: EditorChatState }>;\n filterItems?: boolean;\n items?: { label: string; value: string }[];\n shortcut?: string;\n onSelect?: ({\n aiEditor,\n editor,\n }: {\n aiEditor: SlateEditor;\n editor: PlateEditor;\n }) => void;\n }\n>;\n\nconst menuStateItems: Record<\n EditorChatState,\n {\n items: (typeof aiChatItems)[keyof typeof aiChatItems][];\n heading?: string;\n }[]\n> = {\n cursorCommand: [\n {\n items: [\n aiChatItems.continueWrite,\n aiChatItems.summarize,\n aiChatItems.explain,\n ],\n },\n ],\n cursorSuggestion: [\n {\n items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],\n },\n ],\n selectionCommand: [\n {\n items: [\n aiChatItems.improveWriting,\n aiChatItems.makeLonger,\n aiChatItems.makeShorter,\n aiChatItems.fixSpelling,\n aiChatItems.simplifyLanguage,\n ],\n },\n ],\n selectionSuggestion: [\n {\n items: [\n aiChatItems.replace,\n aiChatItems.insertBelow,\n aiChatItems.discard,\n aiChatItems.tryAgain,\n ],\n },\n ],\n};\n\nexport const AIMenuItems = ({\n aiEditorRef,\n setValue,\n}: {\n aiEditorRef: React.MutableRefObject;\n setValue: (value: string) => void;\n}) => {\n const { editor, useOption } = useEditorPlugin(AIChatPlugin);\n const { messages } = useOption('chat');\n const isSelecting = useIsSelecting();\n\n const menuState = useMemo(() => {\n if (messages && messages.length > 0) {\n return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';\n }\n\n return isSelecting ? 'selectionCommand' : 'cursorCommand';\n }, [isSelecting, messages]);\n\n const menuGroups = useMemo(() => {\n const items = menuStateItems[menuState];\n\n return items;\n }, [menuState]);\n\n useEffect(() => {\n if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {\n setValue(menuGroups[0].items[0].value);\n }\n }, [menuGroups, setValue]);\n\n return (\n <>\n {menuGroups.map((group, index) => (\n \n {group.items.map((menuItem) => (\n {\n menuItem.onSelect?.({\n aiEditor: aiEditorRef.current!,\n editor: editor,\n });\n }}\n >\n {menuItem.icon}\n {menuItem.label}\n \n ))}\n \n ))}\n \n );\n};\n", "path": "plate-ui/ai-menu-items.tsx", "target": "components/plate-ui/ai-menu-items.tsx", "type": "registry:ui" }, { - "content": "'use client';\n\nimport React, { memo } from 'react';\n\nimport { withProps } from '@udecode/cn';\nimport {\n type SlateEditor,\n BaseParagraphPlugin,\n SlateLeaf,\n} from '@udecode/plate';\nimport { useEditorPlugin } from '@udecode/plate/react';\nimport { AIChatPlugin, useLastAssistantMessage } from '@udecode/plate-ai/react';\nimport {\n BaseBoldPlugin,\n BaseCodePlugin,\n BaseItalicPlugin,\n BaseStrikethroughPlugin,\n BaseUnderlinePlugin,\n} from '@udecode/plate-basic-marks';\nimport { BaseBlockquotePlugin } from '@udecode/plate-block-quote';\nimport {\n BaseCodeBlockPlugin,\n BaseCodeLinePlugin,\n BaseCodeSyntaxPlugin,\n} from '@udecode/plate-code-block';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule';\nimport { BaseLinkPlugin } from '@udecode/plate-link';\nimport { deserializeMd } from '@udecode/plate-markdown';\n\nimport { BlockquoteElementStatic } from './blockquote-element-static';\nimport { CodeBlockElementStatic } from './code-block-element-static';\nimport { CodeLeafStatic } from './code-leaf-static';\nimport { CodeLineElementStatic } from './code-line-element-static';\nimport { CodeSyntaxLeafStatic } from './code-syntax-leaf-static';\nimport { EditorStatic } from './editor-static';\nimport { HeadingElementStatic } from './heading-element-static';\nimport { HrElementStatic } from './hr-element-static';\nimport { LinkElementStatic } from './link-element-static';\nimport { ParagraphElementStatic } from './paragraph-element-static';\n\nconst staticComponents = {\n [BaseBlockquotePlugin.key]: BlockquoteElementStatic,\n [BaseBoldPlugin.key]: withProps(SlateLeaf, { as: 'strong' }),\n [BaseCodeBlockPlugin.key]: CodeBlockElementStatic,\n [BaseCodeLinePlugin.key]: CodeLineElementStatic,\n [BaseCodePlugin.key]: CodeLeafStatic,\n [BaseCodeSyntaxPlugin.key]: CodeSyntaxLeafStatic,\n [BaseHorizontalRulePlugin.key]: HrElementStatic,\n [BaseItalicPlugin.key]: withProps(SlateLeaf, { as: 'em' }),\n [BaseLinkPlugin.key]: LinkElementStatic,\n [BaseParagraphPlugin.key]: ParagraphElementStatic,\n [BaseStrikethroughPlugin.key]: withProps(SlateLeaf, { as: 's' }),\n [BaseUnderlinePlugin.key]: withProps(SlateLeaf, { as: 'u' }),\n [HEADING_KEYS.h1]: withProps(HeadingElementStatic, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElementStatic, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElementStatic, { variant: 'h3' }),\n};\n\nexport const AIChatEditor = memo(\n ({\n aiEditorRef,\n }: {\n aiEditorRef: React.MutableRefObject;\n }) => {\n const { getOptions } = useEditorPlugin(AIChatPlugin);\n const lastAssistantMessage = useLastAssistantMessage();\n const content = lastAssistantMessage?.content ?? '';\n\n const aiEditor = React.useMemo(() => {\n const editor = getOptions().createAIEditor();\n\n const fragment = deserializeMd(editor, content);\n editor.children =\n fragment.length > 0 ? fragment : editor.api.create.value();\n\n return editor;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n React.useEffect(() => {\n if (aiEditor && content) {\n aiEditorRef.current = aiEditor;\n\n setTimeout(() => {\n aiEditor.tf.setValue(deserializeMd(aiEditor, content));\n }, 0);\n }\n }, [aiEditor, aiEditorRef, content]);\n\n if (!content) return null;\n\n return (\n \n );\n }\n);\n", + "content": "'use client';\n\nimport React, { memo } from 'react';\n\nimport { withProps } from '@udecode/cn';\nimport { AIChatPlugin, useLastAssistantMessage } from '@udecode/plate-ai/react';\nimport {\n BaseBoldPlugin,\n BaseCodePlugin,\n BaseItalicPlugin,\n BaseStrikethroughPlugin,\n BaseUnderlinePlugin,\n} from '@udecode/plate-basic-marks';\nimport { BaseBlockquotePlugin } from '@udecode/plate-block-quote';\nimport {\n BaseCodeBlockPlugin,\n BaseCodeLinePlugin,\n BaseCodeSyntaxPlugin,\n} from '@udecode/plate-code-block';\nimport { useEditorPlugin } from '@udecode/plate-common/react';\nimport {\n type SlateEditor,\n BaseParagraphPlugin,\n SlateLeaf,\n} from '@udecode/plate-common';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule';\nimport { BaseLinkPlugin } from '@udecode/plate-link';\nimport { deserializeMd } from '@udecode/plate-markdown';\n\nimport { BlockquoteElementStatic } from './blockquote-element-static';\nimport { CodeBlockElementStatic } from './code-block-element-static';\nimport { CodeLeafStatic } from './code-leaf-static';\nimport { CodeLineElementStatic } from './code-line-element-static';\nimport { CodeSyntaxLeafStatic } from './code-syntax-leaf-static';\nimport { EditorStatic } from './editor-static';\nimport { HeadingElementStatic } from './heading-element-static';\nimport { HrElementStatic } from './hr-element-static';\nimport { LinkElementStatic } from './link-element-static';\nimport { ParagraphElementStatic } from './paragraph-element-static';\n\nconst staticComponents = {\n [BaseBlockquotePlugin.key]: BlockquoteElementStatic,\n [BaseBoldPlugin.key]: withProps(SlateLeaf, { as: 'strong' }),\n [BaseCodeBlockPlugin.key]: CodeBlockElementStatic,\n [BaseCodeLinePlugin.key]: CodeLineElementStatic,\n [BaseCodePlugin.key]: CodeLeafStatic,\n [BaseCodeSyntaxPlugin.key]: CodeSyntaxLeafStatic,\n [BaseHorizontalRulePlugin.key]: HrElementStatic,\n [BaseItalicPlugin.key]: withProps(SlateLeaf, { as: 'em' }),\n [BaseLinkPlugin.key]: LinkElementStatic,\n [BaseParagraphPlugin.key]: ParagraphElementStatic,\n [BaseStrikethroughPlugin.key]: withProps(SlateLeaf, { as: 's' }),\n [BaseUnderlinePlugin.key]: withProps(SlateLeaf, { as: 'u' }),\n [HEADING_KEYS.h1]: withProps(HeadingElementStatic, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElementStatic, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElementStatic, { variant: 'h3' }),\n};\n\nexport const AIChatEditor = memo(\n ({\n aiEditorRef,\n }: {\n aiEditorRef: React.MutableRefObject;\n }) => {\n const { getOptions } = useEditorPlugin(AIChatPlugin);\n const lastAssistantMessage = useLastAssistantMessage();\n const content = lastAssistantMessage?.content ?? '';\n\n const aiEditor = React.useMemo(() => {\n const editor = getOptions().createAIEditor();\n\n const fragment = deserializeMd(editor, content);\n editor.children =\n fragment.length > 0 ? fragment : editor.api.create.value();\n\n return editor;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n React.useEffect(() => {\n if (aiEditor && content) {\n aiEditorRef.current = aiEditor;\n\n setTimeout(() => {\n aiEditor.tf.setValue(deserializeMd(aiEditor, content));\n }, 0);\n }\n }, [aiEditor, aiEditorRef, content]);\n\n if (!content) return null;\n\n return (\n \n );\n }\n);\n", "path": "plate-ui/ai-chat-editor.tsx", "target": "components/plate-ui/ai-chat-editor.tsx", "type": "registry:ui" } ], "name": "ai-menu", - "registryDependencies": [ - "use-chat", - "command", - "popover", - "editor" - ], + "registryDependencies": ["use-chat", "command", "popover", "editor"], "type": "registry:ui" -} \ No newline at end of file +} diff --git a/apps/www/public/r/styles/default/command.json b/apps/www/public/r/styles/default/command.json index 79a1de577b..bf42f004c2 100644 --- a/apps/www/public/r/styles/default/command.json +++ b/apps/www/public/r/styles/default/command.json @@ -11,7 +11,7 @@ }, "files": [ { - "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { DialogProps } from '@radix-ui/react-dialog';\n\nimport { Command as CommandPrimitive } from '@udecode/cmdk';\nimport {\n cn,\n createPrimitiveElement,\n withCn,\n withRef,\n withVariants,\n} from '@udecode/cn';\nimport { cva } from 'class-variance-authority';\nimport { Search } from 'lucide-react';\n\nimport { Dialog, DialogContent, DialogTitle } from './dialog';\nimport { inputVariants } from './input';\n\nconst commandVariants = cva(\n 'flex size-full flex-col rounded-md bg-popover text-popover-foreground',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n combobox: 'overflow-visible bg-transparent has-[[data-readonly]]:w-fit',\n default: 'overflow-hidden',\n },\n },\n }\n);\n\nexport const Command = withVariants(CommandPrimitive, commandVariants, [\n 'variant',\n]);\n\nexport function CommandDialog({ children, ...props }: DialogProps) {\n return (\n \n \n Command Dialog\n \n {children}\n \n \n \n );\n}\n\nexport const CommandInput = withRef(\n ({ className, ...props }, ref) => (\n
\n \n \n
\n )\n);\n\nexport const InputCommand = withVariants(\n CommandPrimitive.Input,\n inputVariants,\n ['variant']\n);\n\nexport const CommandList = withCn(\n CommandPrimitive.List,\n 'max-h-[500px] overflow-y-auto overflow-x-hidden'\n);\n\nexport const CommandEmpty = withCn(\n CommandPrimitive.Empty,\n 'py-6 text-center text-sm'\n);\n\nexport const CommandGroup = withCn(\n CommandPrimitive.Group,\n 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground'\n);\n\nexport const CommandSeparator = withCn(\n CommandPrimitive.Separator,\n '-mx-1 h-px bg-border'\n);\n\nexport const CommandItem = withCn(\n CommandPrimitive.Item,\n 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'\n);\n\nexport const CommandShortcut = withCn(\n createPrimitiveElement('span'),\n 'ml-auto text-xs tracking-widest text-muted-foreground'\n);\n", + "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { DialogProps } from '@radix-ui/react-dialog';\n\nimport { Command as CommandPrimitive } from '@udecode/cmdk';\nimport {\n cn,\n createPrimitiveElement,\n withCn,\n withRef,\n withVariants,\n} from '@udecode/cn';\nimport { cva } from 'class-variance-authority';\nimport { Search } from 'lucide-react';\n\nimport { Dialog, DialogContent, DialogTitle } from './dialog';\nimport { inputVariants } from './input';\n\nconst commandVariants = cva(\n 'flex size-full flex-col rounded-md bg-popover text-popover-foreground focus-visible:outline-none',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n combobox: 'overflow-visible bg-transparent has-[[data-readonly]]:w-fit',\n default: 'overflow-hidden',\n },\n },\n }\n);\n\nexport const Command = withVariants(CommandPrimitive, commandVariants, [\n 'variant',\n]);\n\nexport function CommandDialog({ children, ...props }: DialogProps) {\n return (\n \n \n Command Dialog\n \n {children}\n \n \n \n );\n}\n\nexport const CommandInput = withRef(\n ({ className, ...props }, ref) => (\n
\n \n \n
\n )\n);\n\nexport const InputCommand = withVariants(\n CommandPrimitive.Input,\n inputVariants,\n ['variant']\n);\n\nexport const CommandList = withCn(\n CommandPrimitive.List,\n 'max-h-[500px] overflow-y-auto overflow-x-hidden'\n);\n\nexport const CommandEmpty = withCn(\n CommandPrimitive.Empty,\n 'py-6 text-center text-sm'\n);\n\nexport const CommandGroup = withCn(\n CommandPrimitive.Group,\n 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground'\n);\n\nexport const CommandSeparator = withCn(\n CommandPrimitive.Separator,\n '-mx-1 h-px bg-border'\n);\n\nexport const CommandItem = withCn(\n CommandPrimitive.Item,\n 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'\n);\n\nexport const CommandShortcut = withCn(\n createPrimitiveElement('span'),\n 'ml-auto text-xs tracking-widest text-muted-foreground'\n);\n", "path": "plate-ui/command.tsx", "target": "components/plate-ui/command.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/editor.json b/apps/www/public/r/styles/default/editor.json index 56dab36e18..38b6e446e7 100644 --- a/apps/www/public/r/styles/default/editor.json +++ b/apps/www/public/r/styles/default/editor.json @@ -7,11 +7,7 @@ "route": "https://pro.platejs.org/docs/components/editor" } ], - "examples": [ - "editor-default", - "editor-disabled", - "editor-full-width" - ] + "examples": ["editor-default", "editor-disabled", "editor-full-width"] }, "files": [ { @@ -30,4 +26,4 @@ "name": "editor", "registryDependencies": [], "type": "registry:ui" -} \ No newline at end of file +} diff --git a/apps/www/src/registry/default/components/editor/plugins/ai-plugins.tsx b/apps/www/src/registry/default/components/editor/plugins/ai-plugins.tsx index 5c62393203..2a14f29fdc 100644 --- a/apps/www/src/registry/default/components/editor/plugins/ai-plugins.tsx +++ b/apps/www/src/registry/default/components/editor/plugins/ai-plugins.tsx @@ -2,76 +2,12 @@ import React from 'react'; -import { BaseParagraphPlugin, createSlateEditor } from '@udecode/plate'; import { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react'; -import { - BaseBoldPlugin, - BaseCodePlugin, - BaseItalicPlugin, - BaseStrikethroughPlugin, - BaseUnderlinePlugin, -} from '@udecode/plate-basic-marks'; -import { BaseBlockquotePlugin } from '@udecode/plate-block-quote'; -import { - BaseCodeBlockPlugin, - BaseCodeLinePlugin, - BaseCodeSyntaxPlugin, -} from '@udecode/plate-code-block'; -import { BaseHeadingPlugin, HEADING_LEVELS } from '@udecode/plate-heading'; -import { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule'; -import { BaseIndentListPlugin } from '@udecode/plate-indent-list'; -import { BaseLinkPlugin } from '@udecode/plate-link'; import { MarkdownPlugin } from '@udecode/plate-markdown'; import { AIMenu } from '@/registry/default/plate-ui/ai-menu'; -import { - TodoLiStatic, - TodoMarkerStatic, -} from '@/registry/default/plate-ui/indent-todo-marker-static'; import { cursorOverlayPlugin } from './cursor-overlay-plugin'; -const createAIEditor = () => { - const editor = createSlateEditor({ - id: 'ai', - plugins: [ - BaseBlockquotePlugin, - BaseBoldPlugin, - BaseCodeBlockPlugin, - BaseCodeLinePlugin, - BaseCodePlugin, - BaseCodeSyntaxPlugin, - BaseItalicPlugin, - BaseStrikethroughPlugin, - BaseUnderlinePlugin, - BaseHeadingPlugin, - BaseHorizontalRulePlugin, - BaseLinkPlugin, - BaseParagraphPlugin, - BaseIndentListPlugin.extend({ - inject: { - targetPlugins: [ - BaseParagraphPlugin.key, - ...HEADING_LEVELS, - BaseBlockquotePlugin.key, - BaseCodeBlockPlugin.key, - ], - }, - options: { - listStyleTypes: { - todo: { - liComponent: TodoLiStatic, - markerComponent: TodoMarkerStatic, - type: 'todo', - }, - }, - }, - }), - MarkdownPlugin.configure({ options: { indentList: true } }), - ], - }); - - return editor; -}; const systemCommon = `\ You are an advanced AI-powered note-taking assistant, designed to enhance productivity and creativity in note management. @@ -160,7 +96,6 @@ export const aiPlugins = [ AIPlugin, AIChatPlugin.configure({ options: { - createAIEditor, promptTemplate: ({ isBlockSelecting, isSelecting }) => { return isBlockSelecting ? PROMPT_TEMPLATES.userBlockSelecting diff --git a/apps/www/src/registry/default/components/editor/plugins/block-selection-plugins.ts b/apps/www/src/registry/default/components/editor/plugins/block-selection-plugins.ts index 246efe37b7..7d3df81978 100644 --- a/apps/www/src/registry/default/components/editor/plugins/block-selection-plugins.ts +++ b/apps/www/src/registry/default/components/editor/plugins/block-selection-plugins.ts @@ -3,15 +3,17 @@ import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; export const blockSelectionPlugins = [ - BlockSelectionPlugin.configure({ - inject: { - excludeBelowPlugins: ['tr'], - excludePlugins: ['table', 'code_line', 'column_group', 'column'], - }, + BlockSelectionPlugin.configure(({ editor }) => ({ options: { enableContextMenu: true, + isSelectable: (element, path) => { + return ( + !['code_line', 'column', 'td'].includes(element.type) && + !editor.api.block({ above: true, at: path, match: { type: 'tr' } }) + ); + }, }, - }), + })), ] as const; export const blockSelectionReadOnlyPlugin = BlockSelectionPlugin.configure({ diff --git a/apps/www/src/registry/default/components/editor/transforms.ts b/apps/www/src/registry/default/components/editor/transforms.ts index f4641de400..6cead3b48e 100644 --- a/apps/www/src/registry/default/components/editor/transforms.ts +++ b/apps/www/src/registry/default/components/editor/transforms.ts @@ -109,22 +109,20 @@ const insertInlineMap: Record< export const insertBlock = (editor: PlateEditor, type: string) => { editor.tf.withoutNormalizing(() => { + const block = editor.api.block(); + + if (!block) return; if (type in insertBlockMap) { insertBlockMap[type](editor, type); } else { - const path = editor.api.block()?.[1]; - - if (!path) return; - - const at = PathApi.next(path); - editor.tf.insertNodes(editor.api.create.block({ type }), { - at, + at: PathApi.next(block[1]), select: true, }); } - - editor.tf.removeNodes({ previousEmptyBlock: true }); + if (getBlockType(block[0]) !== type) { + editor.tf.removeNodes({ previousEmptyBlock: true }); + } }); }; diff --git a/apps/www/src/registry/default/components/editor/use-chat.ts b/apps/www/src/registry/default/components/editor/use-chat.ts index ff25738d24..e6ac3d6530 100644 --- a/apps/www/src/registry/default/components/editor/use-chat.ts +++ b/apps/www/src/registry/default/components/editor/use-chat.ts @@ -46,29 +46,58 @@ const fakeStreamText = ({ chunkCount?: number; streamProtocol?: 'data' | 'text'; } = {}) => { - const chunks = Array.from({ length: chunkCount }, () => ({ - delay: faker.number.int({ max: 150, min: 50 }), - texts: faker.lorem.words({ max: 3, min: 1 }) + ' ', - })); + // Create 3 blocks with different lengths + const blocks = [ + Array.from({ length: chunkCount }, () => ({ + delay: faker.number.int({ max: 100, min: 30 }), + texts: faker.lorem.words({ max: 3, min: 1 }) + ' ', + })), + Array.from({ length: chunkCount + 2 }, () => ({ + delay: faker.number.int({ max: 100, min: 30 }), + texts: faker.lorem.words({ max: 3, min: 1 }) + ' ', + })), + Array.from({ length: chunkCount + 4 }, () => ({ + delay: faker.number.int({ max: 100, min: 30 }), + texts: faker.lorem.words({ max: 3, min: 1 }) + ' ', + })), + ]; + const encoder = new TextEncoder(); return new ReadableStream({ async start(controller) { - for (const chunk of chunks) { - await new Promise((resolve) => setTimeout(resolve, chunk.delay)); - - if (streamProtocol === 'text') { - controller.enqueue(encoder.encode(chunk.texts)); - } else { - controller.enqueue( - encoder.encode(`0:${JSON.stringify(chunk.texts)}\n`) - ); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + + // Stream the block content + for (const chunk of block) { + await new Promise((resolve) => setTimeout(resolve, chunk.delay)); + + if (streamProtocol === 'text') { + controller.enqueue(encoder.encode(chunk.texts)); + } else { + controller.enqueue( + encoder.encode(`0:${JSON.stringify(chunk.texts)}\n`) + ); + } + } + + // Add double newline after each block except the last one + if (i < blocks.length - 1) { + if (streamProtocol === 'text') { + controller.enqueue(encoder.encode('\n\n')); + } else { + controller.enqueue(encoder.encode(`0:${JSON.stringify('\n\n')}\n`)); + } } } if (streamProtocol === 'data') { controller.enqueue( - `d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":${chunks.length}}}\n` + `d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":${blocks.reduce( + (sum, block) => sum + block.length, + 0 + )}}}\n` ); } diff --git a/apps/www/src/registry/default/example/controlled-demo.tsx b/apps/www/src/registry/default/example/controlled-demo.tsx index 25d7e76943..9d9e9ecaa7 100644 --- a/apps/www/src/registry/default/example/controlled-demo.tsx +++ b/apps/www/src/registry/default/example/controlled-demo.tsx @@ -45,7 +45,6 @@ export default function ControlledEditorDemo() {