From 216532d3501c65d4c572f44f196b86e4db1d2ec1 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 15 Aug 2024 09:02:18 +0800 Subject: [PATCH 001/127] docs --- .../docs/components/floating-toolbar.mdx | 5 + .../content/docs/components/image-element.mdx | 5 + apps/www/src/__registry__/index.tsx | 18 +++ apps/www/src/app/docs/layout.tsx | 2 +- .../src/components/component-preview-pro.tsx | 153 ++++++++++++++++++ apps/www/src/components/mdx-components.tsx | 2 + apps/www/src/components/sidebar-nav.tsx | 15 ++ apps/www/src/config/customizer-components.ts | 2 + apps/www/src/config/docs.ts | 11 ++ apps/www/src/config/potion-components.ts | 67 ++++++++ .../default/example/image-pro-demo.tsx | 11 ++ .../default/example/toolbar-pro-demo.tsx | 11 ++ apps/www/src/registry/example.ts | 10 ++ apps/www/src/types/nav.ts | 3 + 14 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 apps/www/src/components/component-preview-pro.tsx create mode 100644 apps/www/src/config/potion-components.ts create mode 100644 apps/www/src/registry/default/example/image-pro-demo.tsx create mode 100644 apps/www/src/registry/default/example/toolbar-pro-demo.tsx diff --git a/apps/www/content/docs/components/floating-toolbar.mdx b/apps/www/content/docs/components/floating-toolbar.mdx index abbd75ab0d..fcf1d17a29 100644 --- a/apps/www/content/docs/components/floating-toolbar.mdx +++ b/apps/www/content/docs/components/floating-toolbar.mdx @@ -61,6 +61,11 @@ Update the import paths to match your project setup. ## Examples +### Basic + + +### Pro + \ No newline at end of file diff --git a/apps/www/content/docs/components/image-element.mdx b/apps/www/content/docs/components/image-element.mdx index 0232a45a8c..b471ea557d 100644 --- a/apps/www/content/docs/components/image-element.mdx +++ b/apps/www/content/docs/components/image-element.mdx @@ -66,6 +66,11 @@ Update the import paths to match your project setup. ## Examples +### Basic + + +### Pro + \ No newline at end of file diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 387824dab5..53e1e6d181 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -839,6 +839,24 @@ export const Index: Record = { subcategory: "undefined", component: React.lazy(() => import('@/registry/default/plate-ui/resizable')), }, + 'toolbar-pro-demo': { + name: 'toolbar-pro-demo', + type: 'components:example', + registryDependencies: [], + files: ['registry/default/example/toolbar-pro-demo.tsx'], + category: "undefined", + subcategory: "undefined", + component: React.lazy(() => import('@/registry/default/example/toolbar-pro-demo')), + }, + 'image-pro-demo': { + name: 'image-pro-demo', + type: 'components:example', + registryDependencies: [], + files: ['registry/default/example/image-pro-demo.tsx'], + category: "undefined", + subcategory: "undefined", + component: React.lazy(() => import('@/registry/default/example/image-pro-demo')), + }, 'editor-default': { name: 'editor-default', type: 'components:example', diff --git a/apps/www/src/app/docs/layout.tsx b/apps/www/src/app/docs/layout.tsx index 7126e9a836..3f0ce4eb77 100644 --- a/apps/www/src/app/docs/layout.tsx +++ b/apps/www/src/app/docs/layout.tsx @@ -9,7 +9,7 @@ interface DocsLayoutProps { export default function DocsLayout({ children }: DocsLayoutProps) { return (
-
+
+ ); +} diff --git a/apps/www/src/components/mdx-components.tsx b/apps/www/src/components/mdx-components.tsx index 9d7789fd30..808bbc5a6b 100644 --- a/apps/www/src/components/mdx-components.tsx +++ b/apps/www/src/components/mdx-components.tsx @@ -29,6 +29,7 @@ import { Code } from './code'; import { CodeBlockWrapper } from './code-block-wrapper'; import { ComponentExample } from './component-example'; import { ComponentPreview } from './component-preview'; +import { ComponentPreviewPro } from './component-preview-pro'; import { ComponentSource } from './component-source'; import { HydrateAtoms } from './context/hydrate-atoms'; import { FrameworkDocs } from './framework-docs'; @@ -80,6 +81,7 @@ const components = { ), ComponentExample, ComponentPreview, + ComponentPreviewPro, ComponentSource, FrameworkDocs: ({ className, diff --git a/apps/www/src/components/sidebar-nav.tsx b/apps/www/src/components/sidebar-nav.tsx index 739fc8a5d0..3660bfe417 100644 --- a/apps/www/src/components/sidebar-nav.tsx +++ b/apps/www/src/components/sidebar-nav.tsx @@ -9,6 +9,8 @@ import { cn } from '@udecode/cn'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { Icons } from './icons'; + export interface DocsSidebarNavProps { config: DocsConfig; } @@ -64,11 +66,24 @@ export function DocsSidebarNavItems({ target={item.external ? '_blank' : ''} > {item.title} + {item.isExternalLink && ( + + )} {item.label && ( {item.label} )} + {item.new && ( + + New + + )} + {item.pro && ( + + Pro + + )} {item.items?.map((subItem, subIndex) => ( + ); +} diff --git a/apps/www/src/registry/default/example/toolbar-pro-demo.tsx b/apps/www/src/registry/default/example/toolbar-pro-demo.tsx new file mode 100644 index 0000000000..890850093e --- /dev/null +++ b/apps/www/src/registry/default/example/toolbar-pro-demo.tsx @@ -0,0 +1,11 @@ +import { PRO_HOST } from '../../../config/potion-components'; + +export default function ToolbarProDemo() { + return ( + + ); +} diff --git a/apps/www/src/registry/example.ts b/apps/www/src/registry/example.ts index 5d0d3dc4a9..0daf839b9a 100644 --- a/apps/www/src/registry/example.ts +++ b/apps/www/src/registry/example.ts @@ -1,6 +1,16 @@ import type { Registry } from './schema'; export const example: Registry = [ + { + files: ['example/toolbar-pro-demo.tsx'], + name: 'toolbar-pro-demo', + type: 'components:example', + }, + { + files: ['example/image-pro-demo.tsx'], + name: 'image-pro-demo', + type: 'components:example', + }, { files: ['example/editor-default.tsx'], name: 'editor-default', diff --git a/apps/www/src/types/nav.ts b/apps/www/src/types/nav.ts index 8d5e86376c..02ecb15332 100644 --- a/apps/www/src/types/nav.ts +++ b/apps/www/src/types/nav.ts @@ -6,7 +6,10 @@ export interface NavItem { external?: boolean; href?: string; icon?: keyof typeof Icons; + isExternalLink?: boolean; label?: string; + new?: boolean; + pro?: boolean; } export interface NavItemWithChildren extends NavItem { From b0851c6ded44b90d9585d4cc518f208c738bb494 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 10 Sep 2024 16:40:26 +0800 Subject: [PATCH 002/127] fix --- .../docs/components/floating-toolbar.mdx | 2 +- .../content/docs/components/image-element.mdx | 2 +- apps/www/src/__registry__/index.tsx | 17 +++-------- apps/www/src/config/potion-components.ts | 2 +- .../default/example/pro-iframe-demo.tsx | 11 +++++++ .../default/example/toolbar-pro-demo.tsx | 11 ------- apps/www/src/registry/example.ts | 9 ++---- yarn.lock | 30 +++++++++---------- 8 files changed, 35 insertions(+), 49 deletions(-) create mode 100644 apps/www/src/registry/default/example/pro-iframe-demo.tsx delete mode 100644 apps/www/src/registry/default/example/toolbar-pro-demo.tsx diff --git a/apps/www/content/docs/components/floating-toolbar.mdx b/apps/www/content/docs/components/floating-toolbar.mdx index fcf1d17a29..22b18b0b89 100644 --- a/apps/www/content/docs/components/floating-toolbar.mdx +++ b/apps/www/content/docs/components/floating-toolbar.mdx @@ -68,4 +68,4 @@ Update the import paths to match your project setup. ### Pro - \ No newline at end of file + \ No newline at end of file diff --git a/apps/www/content/docs/components/image-element.mdx b/apps/www/content/docs/components/image-element.mdx index b471ea557d..ee2ef57c33 100644 --- a/apps/www/content/docs/components/image-element.mdx +++ b/apps/www/content/docs/components/image-element.mdx @@ -73,4 +73,4 @@ Update the import paths to match your project setup. ### Pro - \ No newline at end of file + \ No newline at end of file diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 53e1e6d181..3eb1069783 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -839,23 +839,14 @@ export const Index: Record = { subcategory: "undefined", component: React.lazy(() => import('@/registry/default/plate-ui/resizable')), }, - 'toolbar-pro-demo': { - name: 'toolbar-pro-demo', + 'pro-iframe-demo': { + name: 'pro-iframe-demo', type: 'components:example', registryDependencies: [], - files: ['registry/default/example/toolbar-pro-demo.tsx'], + files: ['registry/default/example/pro-iframe-demo.tsx'], category: "undefined", subcategory: "undefined", - component: React.lazy(() => import('@/registry/default/example/toolbar-pro-demo')), - }, - 'image-pro-demo': { - name: 'image-pro-demo', - type: 'components:example', - registryDependencies: [], - files: ['registry/default/example/image-pro-demo.tsx'], - category: "undefined", - subcategory: "undefined", - component: React.lazy(() => import('@/registry/default/example/image-pro-demo')), + component: React.lazy(() => import('@/registry/default/example/pro-iframe-demo')), }, 'editor-default': { name: 'editor-default', diff --git a/apps/www/src/config/potion-components.ts b/apps/www/src/config/potion-components.ts index b2d7975b79..b166218771 100644 --- a/apps/www/src/config/potion-components.ts +++ b/apps/www/src/config/potion-components.ts @@ -1,4 +1,4 @@ -export const PRO_HOST = 'http://localhost:3000'; +export const PRO_HOST = 'https://pro.platejs.org'; export const potionComponents = { aiMenu: { diff --git a/apps/www/src/registry/default/example/pro-iframe-demo.tsx b/apps/www/src/registry/default/example/pro-iframe-demo.tsx new file mode 100644 index 0000000000..39ae324ea4 --- /dev/null +++ b/apps/www/src/registry/default/example/pro-iframe-demo.tsx @@ -0,0 +1,11 @@ +import { PRO_HOST } from '../../../config/potion-components'; + +export default function ProIframeDemo({ component }: { component: string }) { + return ( + + ); +} diff --git a/apps/www/src/registry/default/example/toolbar-pro-demo.tsx b/apps/www/src/registry/default/example/toolbar-pro-demo.tsx deleted file mode 100644 index 890850093e..0000000000 --- a/apps/www/src/registry/default/example/toolbar-pro-demo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PRO_HOST } from '../../../config/potion-components'; - -export default function ToolbarProDemo() { - return ( - - ); -} diff --git a/apps/www/src/registry/example.ts b/apps/www/src/registry/example.ts index 0daf839b9a..aa3f4af3a9 100644 --- a/apps/www/src/registry/example.ts +++ b/apps/www/src/registry/example.ts @@ -2,13 +2,8 @@ import type { Registry } from './schema'; export const example: Registry = [ { - files: ['example/toolbar-pro-demo.tsx'], - name: 'toolbar-pro-demo', - type: 'components:example', - }, - { - files: ['example/image-pro-demo.tsx'], - name: 'image-pro-demo', + files: ['example/pro-iframe-demo.tsx'], + name: 'pro-iframe-demo', type: 'components:example', }, { diff --git a/yarn.lock b/yarn.lock index 4e5dd3dcad..386d30f9aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5951,7 +5951,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-break@npm:36.0.0, @udecode/plate-break@workspace:^, @udecode/plate-break@workspace:packages/break": +"@udecode/plate-break@npm:36.3.8, @udecode/plate-break@workspace:^, @udecode/plate-break@workspace:packages/break": version: 0.0.0-use.local resolution: "@udecode/plate-break@workspace:packages/break" dependencies: @@ -6226,7 +6226,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-floating@npm:36.3.2, @udecode/plate-floating@workspace:^, @udecode/plate-floating@workspace:packages/floating": +"@udecode/plate-floating@npm:36.3.8, @udecode/plate-floating@workspace:^, @udecode/plate-floating@workspace:packages/floating": version: 0.0.0-use.local resolution: "@udecode/plate-floating@workspace:packages/floating" dependencies: @@ -6409,12 +6409,12 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-link@npm:36.3.2, @udecode/plate-link@workspace:^, @udecode/plate-link@workspace:packages/link": +"@udecode/plate-link@npm:36.3.8, @udecode/plate-link@workspace:^, @udecode/plate-link@workspace:packages/link": version: 0.0.0-use.local resolution: "@udecode/plate-link@workspace:packages/link" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-floating": "npm:36.3.2" + "@udecode/plate-floating": "npm:36.3.8" "@udecode/plate-normalizers": "npm:36.0.0" peerDependencies: "@udecode/plate-common": ">=36.3.7" @@ -6613,13 +6613,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-serializer-csv@npm:36.3.5, @udecode/plate-serializer-csv@workspace:^, @udecode/plate-serializer-csv@workspace:packages/serializer-csv": +"@udecode/plate-serializer-csv@npm:36.3.8, @udecode/plate-serializer-csv@workspace:^, @udecode/plate-serializer-csv@workspace:packages/serializer-csv": version: 0.0.0-use.local resolution: "@udecode/plate-serializer-csv@workspace:packages/serializer-csv" dependencies: "@types/papaparse": "npm:^5.3.14" "@udecode/plate-common": "workspace:^" - "@udecode/plate-table": "npm:36.3.5" + "@udecode/plate-table": "npm:36.3.8" papaparse: "npm:^5.4.1" peerDependencies: "@udecode/plate-common": ">=36.3.7" @@ -6632,7 +6632,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-serializer-docx@npm:36.3.5, @udecode/plate-serializer-docx@workspace:^, @udecode/plate-serializer-docx@workspace:packages/serializer-docx": +"@udecode/plate-serializer-docx@npm:36.3.8, @udecode/plate-serializer-docx@workspace:^, @udecode/plate-serializer-docx@workspace:packages/serializer-docx": version: 0.0.0-use.local resolution: "@udecode/plate-serializer-docx@workspace:packages/serializer-docx" dependencies: @@ -6642,7 +6642,7 @@ __metadata: "@udecode/plate-indent-list": "npm:36.3.3" "@udecode/plate-media": "npm:36.2.2" "@udecode/plate-paragraph": "npm:36.0.0" - "@udecode/plate-table": "npm:36.3.5" + "@udecode/plate-table": "npm:36.3.8" validator: "npm:^13.11.0" peerDependencies: "@udecode/plate-common": ">=36.3.7" @@ -6744,7 +6744,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-table@npm:36.3.5, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": +"@udecode/plate-table@npm:36.3.8, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": version: 0.0.0-use.local resolution: "@udecode/plate-table@workspace:packages/table" dependencies: @@ -6886,14 +6886,14 @@ __metadata: "@udecode/plate-basic-elements": "npm:36.0.12" "@udecode/plate-basic-marks": "npm:36.0.0" "@udecode/plate-block-quote": "npm:36.0.0" - "@udecode/plate-break": "npm:36.0.0" + "@udecode/plate-break": "npm:36.3.8" "@udecode/plate-code-block": "npm:36.0.0" "@udecode/plate-combobox": "npm:36.0.0" "@udecode/plate-comments": "npm:36.0.0" "@udecode/plate-common": "npm:36.3.7" "@udecode/plate-diff": "npm:36.0.0" "@udecode/plate-find-replace": "npm:36.0.0" - "@udecode/plate-floating": "npm:36.3.2" + "@udecode/plate-floating": "npm:36.3.8" "@udecode/plate-font": "npm:36.0.0" "@udecode/plate-heading": "npm:36.0.12" "@udecode/plate-highlight": "npm:36.0.0" @@ -6902,7 +6902,7 @@ __metadata: "@udecode/plate-indent-list": "npm:36.3.3" "@udecode/plate-kbd": "npm:36.0.0" "@udecode/plate-line-height": "npm:36.0.0" - "@udecode/plate-link": "npm:36.3.2" + "@udecode/plate-link": "npm:36.3.8" "@udecode/plate-list": "npm:36.0.0" "@udecode/plate-media": "npm:36.2.2" "@udecode/plate-mention": "npm:36.0.0" @@ -6912,13 +6912,13 @@ __metadata: "@udecode/plate-reset-node": "npm:36.0.0" "@udecode/plate-resizable": "npm:36.0.0" "@udecode/plate-select": "npm:36.0.0" - "@udecode/plate-serializer-csv": "npm:36.3.5" - "@udecode/plate-serializer-docx": "npm:36.3.5" + "@udecode/plate-serializer-csv": "npm:36.3.8" + "@udecode/plate-serializer-docx": "npm:36.3.8" "@udecode/plate-serializer-html": "npm:36.0.0" "@udecode/plate-serializer-md": "npm:36.0.7" "@udecode/plate-suggestion": "npm:36.0.0" "@udecode/plate-tabbable": "npm:36.0.0" - "@udecode/plate-table": "npm:36.3.5" + "@udecode/plate-table": "npm:36.3.8" "@udecode/plate-toggle": "npm:36.3.6" "@udecode/plate-trailing-block": "npm:36.0.0" peerDependencies: From 88eba6fcfd3d1902735e09befddbaf8dfc3a51c8 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 10 Sep 2024 18:38:58 +0800 Subject: [PATCH 003/127] toc --- apps/www/src/config/docs.ts | 3 +-- apps/www/src/config/potion-components.ts | 13 +++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts index 12eae24e7d..1389877416 100644 --- a/apps/www/src/config/docs.ts +++ b/apps/www/src/config/docs.ts @@ -54,8 +54,7 @@ export const docsConfig: DocsConfig = { potionComponents.mediaController, potionComponents.slashMenu, potionComponents.toolbar, - potionComponents.tocElement, - potionComponents.tocSideBar, + potionComponents.toc, customizerComponents.editor, customizerComponents.alignDropdownMenu, customizerComponents.avatar, diff --git a/apps/www/src/config/potion-components.ts b/apps/www/src/config/potion-components.ts index b166218771..60184f44e3 100644 --- a/apps/www/src/config/potion-components.ts +++ b/apps/www/src/config/potion-components.ts @@ -43,19 +43,12 @@ export const potionComponents = { pro: true, title: 'Slash Menu', }, - tocElement: { - href: `${PRO_HOST}/docs/components/toc-element`, + toc: { + href: `${PRO_HOST}/docs/components/toc`, isExternalLink: true, new: true, pro: true, - title: 'TOC Element', - }, - tocSideBar: { - href: `${PRO_HOST}/docs/components/toc-sidebar`, - isExternalLink: true, - new: true, - pro: true, - title: 'TOC Sidebar', + title: 'Table of contents', }, toolbar: { href: `${PRO_HOST}/docs/components/toolbar`, From ecd125c33d03b32331f56493f17f61e9b25c6816 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 10 Sep 2024 19:41:47 +0800 Subject: [PATCH 004/127] Docs --- README.md | 11 +++---- .../content/docs/components/image-element.mdx | 3 +- apps/www/content/docs/getting-started.mdx | 11 +++---- .../src/components/component-preview-pro.tsx | 30 +++++++++++++++---- apps/www/src/config/potion-components.ts | 18 +++++------ apps/www/src/config/site.ts | 1 + .../default/example/pro-iframe-demo.tsx | 4 +-- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 39f81de7fb..525cadba56 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,12 @@ Plate You can choose one of the following templates to get started: -| Option | NextJS | Tailwind | Plate | Plugins | -| --------------------------------------------------------------------------------- | ------ | -------- | ----- | ------- | -| [Plate playground template](https://github.com/udecode/plate-playground-template) | ✅ | ✅ | ✅ | ✅ | -| [Plate minimal template](https://github.com/udecode/plate-template) | ✅ | ✅ | ✅ | | -| [NextJS template](https://platejs.org/docs/components/installation/next) | ✅ | ✅ | | | +| Option | NextJS | Plate | Plugins |AI & Backend | +| --------------------------------------------------------------------------------- | ------ | ----- | ------- |-------- | +| [Notion clone template](https://pro.platejs.org/docs/templates/potion) | ✅ | ✅ | ✅ |✅ | +| [Plate playground template](https://github.com/udecode/plate-playground-template) | ✅ | ✅ | ✅ | | +| [Plate minimal template](https://github.com/udecode/plate-template) | ✅ | ✅ | | | +| [NextJS template](https://platejs.org/docs/components/installation/next) | ✅ | | | | ## Documentation diff --git a/apps/www/content/docs/components/image-element.mdx b/apps/www/content/docs/components/image-element.mdx index 7eacc9321d..3ebb9559b1 100644 --- a/apps/www/content/docs/components/image-element.mdx +++ b/apps/www/content/docs/components/image-element.mdx @@ -66,10 +66,9 @@ Update the import paths to match your project setup. ## Examples -### Basic - + ### Pro diff --git a/apps/www/content/docs/getting-started.mdx b/apps/www/content/docs/getting-started.mdx index d921ef0562..c1b19272ee 100644 --- a/apps/www/content/docs/getting-started.mdx +++ b/apps/www/content/docs/getting-started.mdx @@ -13,11 +13,12 @@ description: A quick tutorial to get you up and running with Plate. You can choose one of the following templates to get started: -| Option | NextJS | Tailwind | Plate | Plugins | -| ------------------------------------------------------------------------------------------------------------------ | ------ | -------- | ----- | ------- | -| Plate playground template | ✅ | ✅ | ✅ | ✅ | -| Plate minimal template | ✅ | ✅ | ✅ | | -| NextJS template | ✅ | ✅ | | | +| Option | NextJS | Plate | Plugins | AI & Backend | +| ------------------------------------------------------------------------------------------------------------------ | ------ | ----- | ------- | --- | +| Notion clone template | ✅ | ✅ | ✅ | ✅ |✅ | +| Plate playground template | ✅ | ✅ | ✅ | | +| Plate minimal template | ✅ | ✅ | | | +| NextJS template | ✅ | | | | For an existing project, jump to the next step. diff --git a/apps/www/src/components/component-preview-pro.tsx b/apps/www/src/components/component-preview-pro.tsx index 2a14d2494d..53781af443 100644 --- a/apps/www/src/components/component-preview-pro.tsx +++ b/apps/www/src/components/component-preview-pro.tsx @@ -3,11 +3,14 @@ import * as React from 'react'; import { cn } from '@udecode/cn'; +import Link from 'next/link'; import { Index } from '@/__registry__'; import { useConfig } from '@/hooks/use-config'; +import { buttonVariants } from '@/registry/default/plate-ui/button'; import { styles } from '@/registry/styles'; +import { siteConfig } from '../config/site'; import { CopyButton } from './copy-button'; import { Icons } from './icons'; import { StyleSwitcher } from './style-switcher'; @@ -77,19 +80,34 @@ export function ComponentPreviewPro({
{!hideCode && ( - + Preview - - Code - + + Get the code -> + + {/* */} )}
diff --git a/apps/www/src/config/potion-components.ts b/apps/www/src/config/potion-components.ts index 60184f44e3..7f344c2cb2 100644 --- a/apps/www/src/config/potion-components.ts +++ b/apps/www/src/config/potion-components.ts @@ -1,57 +1,57 @@ -export const PRO_HOST = 'https://pro.platejs.org'; +import { siteConfig } from './site'; export const potionComponents = { aiMenu: { - href: `${PRO_HOST}/docs/components/ai-menu`, + href: `${siteConfig.links.potion}/docs/components/ai-menu`, isExternalLink: true, new: true, pro: true, title: 'AI Menu', }, contextMenu: { - href: `${PRO_HOST}/docs/components/context-menu`, + href: `${siteConfig.links.potion}/docs/components/context-menu`, isExternalLink: true, new: true, pro: true, title: 'Context Menu', }, equation: { - href: `${PRO_HOST}/docs/components/equation`, + href: `${siteConfig.links.potion}/docs/components/equation`, isExternalLink: true, new: true, pro: true, title: 'Equation', }, inlineEquation: { - href: `${PRO_HOST}/docs/components/inline-equation`, + href: `${siteConfig.links.potion}/docs/components/inline-equation`, isExternalLink: true, new: true, pro: true, title: 'Inline Equation', }, mediaController: { - href: `${PRO_HOST}/docs/components/media-controller`, + href: `${siteConfig.links.potion}/docs/components/media-controller`, isExternalLink: true, new: true, pro: true, title: 'Media Controller', }, slashMenu: { - href: `${PRO_HOST}/docs/components/slash-menu`, + href: `${siteConfig.links.potion}/docs/components/slash-menu`, isExternalLink: true, new: true, pro: true, title: 'Slash Menu', }, toc: { - href: `${PRO_HOST}/docs/components/toc`, + href: `${siteConfig.links.potion}/docs/components/toc`, isExternalLink: true, new: true, pro: true, title: 'Table of contents', }, toolbar: { - href: `${PRO_HOST}/docs/components/toolbar`, + href: `${siteConfig.links.potion}/docs/components/toolbar`, isExternalLink: true, new: true, pro: true, diff --git a/apps/www/src/config/site.ts b/apps/www/src/config/site.ts index 398786b485..830bf92377 100644 --- a/apps/www/src/config/site.ts +++ b/apps/www/src/config/site.ts @@ -4,6 +4,7 @@ export const siteConfig = { links: { discord: 'https://discord.gg/mAZRuBzGM3', github: 'https://github.com/udecode/plate', + potion: 'https://pro.platejs.org', profile: 'https://github.com/zbeyens', twitter: 'https://twitter.com/zbeyens', }, diff --git a/apps/www/src/registry/default/example/pro-iframe-demo.tsx b/apps/www/src/registry/default/example/pro-iframe-demo.tsx index 39ae324ea4..a0740341e0 100644 --- a/apps/www/src/registry/default/example/pro-iframe-demo.tsx +++ b/apps/www/src/registry/default/example/pro-iframe-demo.tsx @@ -1,10 +1,10 @@ -import { PRO_HOST } from '../../../config/potion-components'; +import { siteConfig } from '../../../config/site'; export default function ProIframeDemo({ component }: { component: string }) { return ( ); From 253ddadf0bd28d988e1b56b5b3c0705093a1cec9 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 10 Sep 2024 20:11:41 +0800 Subject: [PATCH 005/127] docs --- .../src/components/component-preview-pro.tsx | 2 +- apps/www/src/components/doc-page-layout.tsx | 2 + apps/www/src/components/open-in-plus.tsx | 37 +++++++++++++++++ apps/www/src/components/sidebar-nav.tsx | 13 +----- apps/www/src/config/potion-components.ts | 40 ++++++++----------- apps/www/src/config/site.ts | 2 +- .../default/example/pro-iframe-demo.tsx | 2 +- 7 files changed, 60 insertions(+), 38 deletions(-) create mode 100644 apps/www/src/components/open-in-plus.tsx diff --git a/apps/www/src/components/component-preview-pro.tsx b/apps/www/src/components/component-preview-pro.tsx index 53781af443..08827315c7 100644 --- a/apps/www/src/components/component-preview-pro.tsx +++ b/apps/www/src/components/component-preview-pro.tsx @@ -95,7 +95,7 @@ export function ComponentPreviewPro({ 'hover:ring-2 hover:ring-primary hover:ring-offset-2', 'transition-all duration-300 ease-out' )} - href={`${siteConfig.links.potion}/pricing`} + href={`${siteConfig.links.platePlus}/pricing`} target="_blank" >
+
diff --git a/apps/www/src/components/open-in-plus.tsx b/apps/www/src/components/open-in-plus.tsx new file mode 100644 index 0000000000..ab624ecd3f --- /dev/null +++ b/apps/www/src/components/open-in-plus.tsx @@ -0,0 +1,37 @@ +import { cn } from '@udecode/cn'; +import Link from 'next/link'; + +import { Button } from '@/registry/default/plate-ui/button'; + +import { siteConfig } from '../config/site'; + +export function OpenInPlus({ className }: { className?: string }) { + return ( +
+
+ Get all-access to Plate Plus for AI and Backend Support +
+
Enhance your editing experience with advanced features.
+
+ Plate Plus offers AI-powered assistance and robust backend solutions to + elevate your content creation. +
+ + + Get all-access to Plate Plus + +
+ ); +} diff --git a/apps/www/src/components/sidebar-nav.tsx b/apps/www/src/components/sidebar-nav.tsx index 22678d3f49..70ad56c00a 100644 --- a/apps/www/src/components/sidebar-nav.tsx +++ b/apps/www/src/components/sidebar-nav.tsx @@ -73,22 +73,13 @@ export function DocsSidebarNavItems({ {item.label} )} - {item.new && ( - - New - - )} - {item.pro && ( - - Pro - - )} {item.items?.map((subItem, subIndex) => ( ); From d14f9a15ac778320947000b2af8fcb89d577f4bf Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 10 Sep 2024 20:55:41 +0800 Subject: [PATCH 006/127] style --- apps/www/src/components/component-preview-pro.tsx | 3 ++- apps/www/src/components/open-in-plus.tsx | 13 +++++-------- apps/www/src/components/sidebar-nav.tsx | 13 ++++++++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/www/src/components/component-preview-pro.tsx b/apps/www/src/components/component-preview-pro.tsx index 08827315c7..5b86124b01 100644 --- a/apps/www/src/components/component-preview-pro.tsx +++ b/apps/www/src/components/component-preview-pro.tsx @@ -93,7 +93,8 @@ export function ComponentPreviewPro({ 'group relative flex justify-start gap-2 overflow-hidden whitespace-pre rounded-sm', 'dark:bg-muted dark:text-foreground', 'hover:ring-2 hover:ring-primary hover:ring-offset-2', - 'transition-all duration-300 ease-out' + 'transition-all duration-300 ease-out', + 'mb-2 h-8' )} href={`${siteConfig.links.platePlus}/pricing`} target="_blank" diff --git a/apps/www/src/components/open-in-plus.tsx b/apps/www/src/components/open-in-plus.tsx index ab624ecd3f..60bc9225dc 100644 --- a/apps/www/src/components/open-in-plus.tsx +++ b/apps/www/src/components/open-in-plus.tsx @@ -14,15 +14,12 @@ export function OpenInPlus({ className }: { className?: string }) { )} >
- Get all-access to Plate Plus for AI and Backend Support -
-
Enhance your editing experience with advanced features.
-
- Plate Plus offers AI-powered assistance and robust backend solutions to - elevate your content creation. + Build your editor even faster
+
Complete, deployable AI-powered template with backend.
+
All components included. Customizable and extensible.
- Get all-access to Plate Plus + Get all-access
); diff --git a/apps/www/src/components/sidebar-nav.tsx b/apps/www/src/components/sidebar-nav.tsx index 70ad56c00a..8b982d2aa1 100644 --- a/apps/www/src/components/sidebar-nav.tsx +++ b/apps/www/src/components/sidebar-nav.tsx @@ -74,12 +74,23 @@ export function DocsSidebarNavItems({ className={cn( 'ml-2 rounded-md bg-secondary px-1.5 py-0.5 text-xs leading-none text-foreground no-underline group-hover:no-underline', item.label === 'New' && 'bg-[#adfa1d] dark:text-background', - item.label === 'Plus' && 'bg-primary text-background' + item.label === 'Plus' && + 'bg-primary text-primary-foreground' )} > {item.label} )} + {item.new && ( + + New + + )} {item.items?.map((subItem, subIndex) => ( Date: Tue, 10 Sep 2024 21:38:58 +0800 Subject: [PATCH 007/127] lint --- apps/www/src/registry/default/example/image-pro-demo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/src/registry/default/example/image-pro-demo.tsx b/apps/www/src/registry/default/example/image-pro-demo.tsx index 0a2fb3ad03..e7e01cc6f8 100644 --- a/apps/www/src/registry/default/example/image-pro-demo.tsx +++ b/apps/www/src/registry/default/example/image-pro-demo.tsx @@ -1,10 +1,10 @@ -import { PRO_HOST } from '../../../config/potion-components'; +import { siteConfig } from '../../../config/site'; export default function ImageProDemo() { return ( ); From 6618351e8b486b638ced1a3e89f041389e0771f1 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Wed, 25 Sep 2024 18:19:51 +0800 Subject: [PATCH 008/127] wip --- apps/www/content/docs/ai.mdx | 115 +++++++++++++++++++++++++++++++++++ apps/www/src/config/docs.ts | 4 ++ 2 files changed, 119 insertions(+) create mode 100644 apps/www/content/docs/ai.mdx diff --git a/apps/www/content/docs/ai.mdx b/apps/www/content/docs/ai.mdx new file mode 100644 index 0000000000..075070dd16 --- /dev/null +++ b/apps/www/content/docs/ai.mdx @@ -0,0 +1,115 @@ +--- +title: AI +description: allows you to select from a list of AI commands. +docs: + - route: https://pro.platejs.org/docs/components/ai + title: AIMenu +--- + + + + + +## Features + +- Provides an AI-powered menu. +- Offers a selection of AI commands to enhance content creation and editing. +- Seamlessly integrates AI assistance within the editor interface. + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## Plugins + +### AlignPlugin + +## API + +### setAlign + +Sets the alignment for the specified block elements in the editor. + + + +The editor instance. + + + + +The alignment value. + + + + + + +## API Components + +### useAlignDropdownMenuState + + + + The alignment value. + + + +### useAlignDropdownMenu + + + + The alignment value. + + + + + + Props for the radio group. + + + The alignment value. + + + Callback to set the alignment value. + + + + diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts index 1389877416..9778da4602 100644 --- a/apps/www/src/config/docs.ts +++ b/apps/www/src/config/docs.ts @@ -248,6 +248,10 @@ export const docsConfig: DocsConfig = { }, { items: [ + { + href: '/docs/ai', + title: 'AI', + }, { href: '/docs/alignment', title: 'Alignment', From 83147ff7c40cb4a9fe7b7f2e3277efdb95d7d33c Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 26 Sep 2024 12:04:09 +0800 Subject: [PATCH 009/127] docs --- apps/www/content/docs/ai.mdx | 63 +------------------------- apps/www/content/docs/callout.mdx | 54 ++++++++++++++++++++++ apps/www/content/docs/context-menu.mdx | 54 ++++++++++++++++++++++ apps/www/content/docs/copilot.mdx | 54 ++++++++++++++++++++++ apps/www/src/config/doc-pro.ts | 52 +++++++++++++++++++++ apps/www/src/config/docs.ts | 6 +-- 6 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 apps/www/content/docs/callout.mdx create mode 100644 apps/www/content/docs/context-menu.mdx create mode 100644 apps/www/content/docs/copilot.mdx create mode 100644 apps/www/src/config/doc-pro.ts diff --git a/apps/www/content/docs/ai.mdx b/apps/www/content/docs/ai.mdx index 075070dd16..7d6a639fb6 100644 --- a/apps/www/content/docs/ai.mdx +++ b/apps/www/content/docs/ai.mdx @@ -6,7 +6,7 @@ docs: title: AIMenu --- - + @@ -50,66 +50,5 @@ const editor = usePlateEditor({ }); ``` -## Plugins - -### AlignPlugin - ## API -### setAlign - -Sets the alignment for the specified block elements in the editor. - - - -The editor instance. - - - - -The alignment value. - - - - - - -## API Components - -### useAlignDropdownMenuState - - - - The alignment value. - - - -### useAlignDropdownMenu - - - - The alignment value. - - - - - - Props for the radio group. - - - The alignment value. - - - Callback to set the alignment value. - - - - diff --git a/apps/www/content/docs/callout.mdx b/apps/www/content/docs/callout.mdx new file mode 100644 index 0000000000..acea01d03c --- /dev/null +++ b/apps/www/content/docs/callout.mdx @@ -0,0 +1,54 @@ +--- +title: Callout +description: allows you to select from a list of AI commands. +docs: + - route: https://pro.platejs.org/docs/components/callout + title: Callout +--- + + + + + +## Features + +- Provides an AI-powered menu. +- Offers a selection of AI commands to enhance content creation and editing. +- Seamlessly integrates AI assistance within the editor interface. + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## API + diff --git a/apps/www/content/docs/context-menu.mdx b/apps/www/content/docs/context-menu.mdx new file mode 100644 index 0000000000..acea01d03c --- /dev/null +++ b/apps/www/content/docs/context-menu.mdx @@ -0,0 +1,54 @@ +--- +title: Callout +description: allows you to select from a list of AI commands. +docs: + - route: https://pro.platejs.org/docs/components/callout + title: Callout +--- + + + + + +## Features + +- Provides an AI-powered menu. +- Offers a selection of AI commands to enhance content creation and editing. +- Seamlessly integrates AI assistance within the editor interface. + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## API + diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx new file mode 100644 index 0000000000..8314ef19ba --- /dev/null +++ b/apps/www/content/docs/copilot.mdx @@ -0,0 +1,54 @@ +--- +title: Copilot +description: allows you to select from a list of AI commands. +docs: + - route: https://pro.platejs.org/docs/components/copilot + title: Copilot +--- + + + + + +## Features + +- Provides an AI-powered menu. +- Offers a selection of AI commands to enhance content creation and editing. +- Seamlessly integrates AI assistance within the editor interface. + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## API + diff --git a/apps/www/src/config/doc-pro.ts b/apps/www/src/config/doc-pro.ts new file mode 100644 index 0000000000..91f75998b6 --- /dev/null +++ b/apps/www/src/config/doc-pro.ts @@ -0,0 +1,52 @@ +export const docPro = [ + { + href: '/docs/ai', + new: true, + title: 'AI', + }, + { + href: '/docs/copilot', + new: true, + title: 'Copilot', + }, + { + href: '/docs/callout', + new: true, + title: 'Callout', + }, + { + href: '/docs/context-menu', + new: true, + title: 'Context Menu', + }, + { + href: '/docs/equation', + new: true, + title: 'Equation', + }, + { + href: '/docs/media-toolbar', + new: true, + title: 'Media Toolbar', + }, + { + href: '/docs/slash-menu', + new: true, + title: 'Slash Menu', + }, + { + href: '/docs/toc', + new: true, + title: 'Table of Contents', + }, + { + href: '/docs/toolbar', + new: true, + title: 'Toolbar', + }, + { + href: '/docs/upload', + new: true, + title: 'Upload', + }, +]; diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts index 9778da4602..531d312748 100644 --- a/apps/www/src/config/docs.ts +++ b/apps/www/src/config/docs.ts @@ -2,6 +2,7 @@ import type { MainNavItem, SidebarNavItem } from '@/types/nav'; import { customizerComponents } from '@/config/customizer-components'; +import { docPro } from './doc-pro'; import { potionComponents } from './potion-components'; export interface DocsConfig { @@ -248,10 +249,7 @@ export const docsConfig: DocsConfig = { }, { items: [ - { - href: '/docs/ai', - title: 'AI', - }, + ...docPro, { href: '/docs/alignment', title: 'Alignment', From d4556ef613b9a4abfe39d2aa38649dc06b5b6ede Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 26 Sep 2024 12:15:52 +0800 Subject: [PATCH 010/127] fix --- apps/www/src/registry/registry-examples.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/src/registry/registry-examples.ts b/apps/www/src/registry/registry-examples.ts index b7658d7c16..012d04b4b2 100644 --- a/apps/www/src/registry/registry-examples.ts +++ b/apps/www/src/registry/registry-examples.ts @@ -4,7 +4,7 @@ export const examples: Registry = [ { files: ['example/pro-iframe-demo.tsx'], name: 'pro-iframe-demo', - type: 'components:example', + type: 'registry:example', }, { files: ['example/editor-default.tsx'], From a33857e3045cbcea9c23f87e3ba663847d6cf19e Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 26 Sep 2024 18:03:53 +0800 Subject: [PATCH 011/127] Copilot --- apps/www/content/docs/context-menu.mdx | 8 +- apps/www/content/docs/copilot.mdx | 2 +- apps/www/content/docs/equation.mdx | 52 +++++ apps/www/content/docs/media-toolbar.mdx | 52 +++++ apps/www/content/docs/slash-menu.mdx | 21 ++ apps/www/content/docs/toc.mdx | 21 ++ apps/www/content/docs/upload.mdx | 22 ++ apps/www/package.json | 1 + .../src/components/component-preview-pro.tsx | 4 +- apps/www/src/components/sidebar-nav.tsx | 3 +- apps/www/src/config/doc-pro.ts | 23 +- .../lib/plate/demo/plugins/copilotPlugin.tsx | 33 +++ .../lib/plate/demo/plugins/tabbablePlugin.ts | 9 +- .../default/example/playground-demo.tsx | 4 +- .../default/example/pro-iframe-demo.tsx | 16 +- .../plate-ui/ai-copilot-hover-card.tsx | 11 + packages/ai/.npmignore | 3 + packages/ai/README.md | 7 + packages/ai/package.json | 67 ++++++ packages/ai/src/index.ts | 5 + packages/ai/src/lib/BaseAIPlugin.ts | 26 +++ packages/ai/src/lib/index.ts | 6 + packages/ai/src/lib/withTriggerAIMenu.ts | 75 ++++++ packages/ai/src/react/ai/AIPlugin.ts | 185 +++++++++++++++ packages/ai/src/react/ai/index.ts | 6 + packages/ai/src/react/ai/utils/index.ts | 5 + .../react/ai/utils/updateMenuAnchorByPath.ts | 27 +++ .../ai/src/react/copilot/CopilotPlugin.tsx | 214 ++++++++++++++++++ .../src/react/copilot/generateCopilotText.ts | 73 ++++++ .../ai/src/react/copilot/getCopilotSystem.ts | 6 + packages/ai/src/react/copilot/index.ts | 10 + .../ai/src/react/copilot/injectCopilot.tsx | 48 ++++ .../ai/src/react/copilot/onKeyDownCopilot.ts | 56 +++++ packages/ai/src/react/copilot/utils/index.ts | 5 + .../src/react/copilot/utils/withoutAbort.ts | 9 + packages/ai/src/react/index.ts | 6 + packages/ai/tsconfig.build.json | 8 + packages/ai/tsconfig.json | 5 + yarn.lock | 17 ++ 39 files changed, 1121 insertions(+), 30 deletions(-) create mode 100644 apps/www/content/docs/equation.mdx create mode 100644 apps/www/content/docs/media-toolbar.mdx create mode 100644 apps/www/content/docs/slash-menu.mdx create mode 100644 apps/www/content/docs/toc.mdx create mode 100644 apps/www/content/docs/upload.mdx create mode 100644 apps/www/src/lib/plate/demo/plugins/copilotPlugin.tsx create mode 100644 apps/www/src/registry/default/plate-ui/ai-copilot-hover-card.tsx create mode 100644 packages/ai/.npmignore create mode 100644 packages/ai/README.md create mode 100644 packages/ai/package.json create mode 100644 packages/ai/src/index.ts create mode 100644 packages/ai/src/lib/BaseAIPlugin.ts create mode 100644 packages/ai/src/lib/index.ts create mode 100644 packages/ai/src/lib/withTriggerAIMenu.ts create mode 100644 packages/ai/src/react/ai/AIPlugin.ts create mode 100644 packages/ai/src/react/ai/index.ts create mode 100644 packages/ai/src/react/ai/utils/index.ts create mode 100644 packages/ai/src/react/ai/utils/updateMenuAnchorByPath.ts create mode 100644 packages/ai/src/react/copilot/CopilotPlugin.tsx create mode 100644 packages/ai/src/react/copilot/generateCopilotText.ts create mode 100644 packages/ai/src/react/copilot/getCopilotSystem.ts create mode 100644 packages/ai/src/react/copilot/index.ts create mode 100644 packages/ai/src/react/copilot/injectCopilot.tsx create mode 100644 packages/ai/src/react/copilot/onKeyDownCopilot.ts create mode 100644 packages/ai/src/react/copilot/utils/index.ts create mode 100644 packages/ai/src/react/copilot/utils/withoutAbort.ts create mode 100644 packages/ai/src/react/index.ts create mode 100644 packages/ai/tsconfig.build.json create mode 100644 packages/ai/tsconfig.json diff --git a/apps/www/content/docs/context-menu.mdx b/apps/www/content/docs/context-menu.mdx index acea01d03c..def4235e8e 100644 --- a/apps/www/content/docs/context-menu.mdx +++ b/apps/www/content/docs/context-menu.mdx @@ -1,12 +1,12 @@ --- -title: Callout +title: Context Menu description: allows you to select from a list of AI commands. docs: - - route: https://pro.platejs.org/docs/components/callout - title: Callout + - route: https://pro.platejs.org/docs/components/context-menu + title: Context Menu --- - + diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index 8314ef19ba..bd8fdd8319 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -6,7 +6,7 @@ docs: title: Copilot --- - + diff --git a/apps/www/content/docs/equation.mdx b/apps/www/content/docs/equation.mdx new file mode 100644 index 0000000000..a68e6867a9 --- /dev/null +++ b/apps/www/content/docs/equation.mdx @@ -0,0 +1,52 @@ +--- +title: Equation +description: allows you to select from a list of AI commands. +docs: + - route: https://pro.platejs.org/docs/components/equation + title: Equation +--- + + + + + +## Features + +- Allows you to insert equations into your editor. + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## API + diff --git a/apps/www/content/docs/media-toolbar.mdx b/apps/www/content/docs/media-toolbar.mdx new file mode 100644 index 0000000000..e73b0dfc01 --- /dev/null +++ b/apps/www/content/docs/media-toolbar.mdx @@ -0,0 +1,52 @@ +--- +title: Media Toolbar +description: allows you to insert images into your editor. +docs: + - route: https://pro.platejs.org/docs/components/media-toolbar + title: Media Toolbar +--- + + + + + +## Features + +- todo + + + +## Installation + +```bash +npm install @udecode/plate-ai +``` + +## Usage + +```tsx +// ... +import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; + +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + ...commonPlugins, + SelectionOverlayPlugin, + MarkdownPlugin.configure({ options: { indentList: true } }), + AIPlugin.configure({ + options: { + scrollContainerSelector: '#scroll_container', + }, + render: { aboveEditable: AIMenu }, + }), + ], + value: aiValue, +}); +``` + +## API + diff --git a/apps/www/content/docs/slash-menu.mdx b/apps/www/content/docs/slash-menu.mdx new file mode 100644 index 0000000000..1b9999f0de --- /dev/null +++ b/apps/www/content/docs/slash-menu.mdx @@ -0,0 +1,21 @@ +--- +title: Slash Menu +description: Allows you to insert various elements into your editor using a slash command. +docs: + - route: https://pro.platejs.org/docs/components/slash-menu + title: Slash Menu +--- + + + + + +## Features + +- Quick insertion of various elements using slash commands +- Customizable menu options +- Seamless integration with the editor + + + +## Installation diff --git a/apps/www/content/docs/toc.mdx b/apps/www/content/docs/toc.mdx new file mode 100644 index 0000000000..7ed4389d76 --- /dev/null +++ b/apps/www/content/docs/toc.mdx @@ -0,0 +1,21 @@ +--- +title: Table of Contents +description: Automatically generates a table of contents for your document. +docs: + - route: https://pro.platejs.org/docs/components/toc + title: Table of Contents +--- + + + + + +## Features + +- Automatically generates a table of contents based on document headings +- Customizable depth and styling options +- Clickable links for easy navigation within the document + + + +## Installation diff --git a/apps/www/content/docs/upload.mdx b/apps/www/content/docs/upload.mdx new file mode 100644 index 0000000000..293a3d7426 --- /dev/null +++ b/apps/www/content/docs/upload.mdx @@ -0,0 +1,22 @@ +--- +title: Upload +description: Allows you to upload files and images into your editor. +docs: + - route: https://pro.platejs.org/docs/components/upload + title: Upload +--- + + + + + +## Features + +- Easy file and image uploading functionality +- Support for various file types +- Customizable upload options +- Seamless integration with the editor + + + +## Installation diff --git a/apps/www/package.json b/apps/www/package.json index 9267366071..5a26d41e4e 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -69,6 +69,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@udecode/cn": "workspace:^", "@udecode/plate": "workspace:^", + "@udecode/plate-ai": "workspace:^", "@udecode/plate-alignment": "workspace:^", "@udecode/plate-autoformat": "workspace:^", "@udecode/plate-basic-elements": "workspace:^", diff --git a/apps/www/src/components/component-preview-pro.tsx b/apps/www/src/components/component-preview-pro.tsx index 5b86124b01..92fe94a173 100644 --- a/apps/www/src/components/component-preview-pro.tsx +++ b/apps/www/src/components/component-preview-pro.tsx @@ -8,7 +8,7 @@ import Link from 'next/link'; import { Index } from '@/__registry__'; import { useConfig } from '@/hooks/use-config'; import { buttonVariants } from '@/registry/default/plate-ui/button'; -import { styles } from '@/registry/styles'; +import { styles } from '@/registry/registry-styles'; import { siteConfig } from '../config/site'; import { CopyButton } from './copy-button'; @@ -129,9 +129,9 @@ export function ComponentPreviewPro({ {/* /> */} {/* ) : null} */} diff --git a/apps/www/src/components/sidebar-nav.tsx b/apps/www/src/components/sidebar-nav.tsx index e4e8c10a78..70585e5958 100644 --- a/apps/www/src/components/sidebar-nav.tsx +++ b/apps/www/src/components/sidebar-nav.tsx @@ -75,7 +75,8 @@ export function DocsSidebarNavItems({ 'ml-2 rounded-md bg-secondary px-1.5 py-0.5 text-xs leading-none text-foreground no-underline group-hover:no-underline', item.label === 'New' && 'bg-[#adfa1d] dark:text-background', item.label === 'Plus' && - 'bg-primary text-primary-foreground' + 'bg-primary text-primary-foreground', + item.label === 'Free' && 'bg-emerald-500 text-white' )} > {item.label} diff --git a/apps/www/src/config/doc-pro.ts b/apps/www/src/config/doc-pro.ts index 91f75998b6..15e3645ec1 100644 --- a/apps/www/src/config/doc-pro.ts +++ b/apps/www/src/config/doc-pro.ts @@ -1,52 +1,47 @@ export const docPro = [ { href: '/docs/ai', - new: true, + label: 'New', title: 'AI', }, { href: '/docs/copilot', - new: true, + label: 'New', title: 'Copilot', }, { href: '/docs/callout', - new: true, + label: 'Plus', title: 'Callout', }, { href: '/docs/context-menu', - new: true, + label: 'Plus', title: 'Context Menu', }, { href: '/docs/equation', - new: true, + label: 'Plus', title: 'Equation', }, { href: '/docs/media-toolbar', - new: true, + label: 'Plus', title: 'Media Toolbar', }, { href: '/docs/slash-menu', - new: true, + label: 'New', title: 'Slash Menu', }, { href: '/docs/toc', - new: true, + label: 'Plus', title: 'Table of Contents', }, - { - href: '/docs/toolbar', - new: true, - title: 'Toolbar', - }, { href: '/docs/upload', - new: true, + label: 'Plus', title: 'Upload', }, ]; diff --git a/apps/www/src/lib/plate/demo/plugins/copilotPlugin.tsx b/apps/www/src/lib/plate/demo/plugins/copilotPlugin.tsx new file mode 100644 index 0000000000..a849c0b060 --- /dev/null +++ b/apps/www/src/lib/plate/demo/plugins/copilotPlugin.tsx @@ -0,0 +1,33 @@ +import { CopilotPlugin } from '@udecode/plate-ai/react'; +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; +import { ParagraphPlugin } from '@udecode/plate-core/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; + +import { AiCopilotHoverCard } from '@/registry/default/plate-ui/ai-copilot-hover-card'; +import { MENTIONABLES } from '@/registry/default/plate-ui/mention-input-element'; + +export const copilotPlugin = CopilotPlugin.configure({ + options: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fetchSuggestion: async ({ abortSignal, prompt }) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(MENTIONABLES[Math.floor(Math.random() * 41)].text); + }, 100); + }); + }, + hoverCard: AiCopilotHoverCard, + query: { + allow: [ + ParagraphPlugin.key, + BlockquotePlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + HEADING_KEYS.h4, + HEADING_KEYS.h5, + HEADING_KEYS.h6, + ], + }, + }, +}); diff --git a/apps/www/src/lib/plate/demo/plugins/tabbablePlugin.ts b/apps/www/src/lib/plate/demo/plugins/tabbablePlugin.ts index cc24c808be..9038954a0c 100644 --- a/apps/www/src/lib/plate/demo/plugins/tabbablePlugin.ts +++ b/apps/www/src/lib/plate/demo/plugins/tabbablePlugin.ts @@ -1,5 +1,9 @@ import { CodeBlockPlugin } from '@udecode/plate-code-block/react'; -import { isSelectionAtBlockStart, someNode } from '@udecode/plate-common'; +import { + isSelectionAtBlockEnd, + isSelectionAtBlockStart, + someNode, +} from '@udecode/plate-common'; import { createPlatePlugin } from '@udecode/plate-common/react'; import { IndentListPlugin } from '@udecode/plate-indent-list/react'; import { ListItemPlugin } from '@udecode/plate-list/react'; @@ -18,7 +22,8 @@ export const tabbablePlugin = TabbablePlugin.extend({ }).configure(({ editor }) => ({ options: { query: () => { - if (isSelectionAtBlockStart(editor)) return false; + if (isSelectionAtBlockStart(editor) || isSelectionAtBlockEnd(editor)) + return false; return !someNode(editor, { match: (n) => { diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 6b6b5e10f5..176f2740b8 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -69,6 +69,7 @@ import { settingsStore } from '@/components/context/settings-store'; import { PlaygroundFixedToolbarButtons } from '@/components/plate-ui/playground-fixed-toolbar-buttons'; import { PlaygroundFloatingToolbarButtons } from '@/components/plate-ui/playground-floating-toolbar-buttons'; import { getAutoformatOptions } from '@/lib/plate/demo/plugins/autoformatOptions'; +import { copilotPlugin } from '@/lib/plate/demo/plugins/copilotPlugin'; import { createPlateUI } from '@/plate/create-plate-ui'; import { editableProps } from '@/plate/demo/editableProps'; import { isEnabled } from '@/plate/demo/is-enabled'; @@ -118,6 +119,8 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { plugins: overridePlugins, }, plugins: [ + //ai + copilotPlugin, // Nodes HeadingPlugin, BlockquotePlugin, @@ -155,7 +158,6 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { TodoListPlugin, TogglePlugin, ExcalidrawPlugin, - // Marks BoldPlugin, ItalicPlugin, diff --git a/apps/www/src/registry/default/example/pro-iframe-demo.tsx b/apps/www/src/registry/default/example/pro-iframe-demo.tsx index 5f0c578f0e..128a59f274 100644 --- a/apps/www/src/registry/default/example/pro-iframe-demo.tsx +++ b/apps/www/src/registry/default/example/pro-iframe-demo.tsx @@ -1,11 +1,17 @@ -import { siteConfig } from '../../../config/site'; +import { siteConfig } from '@/config/site'; +import { cn } from '@/registry/default/lib/utils'; -export default function ProIframeDemo({ component }: { component: string }) { +export default function ProIframeDemo({ + component, +}: { + component: string; + height: string; +}) { return ( + src={`${siteConfig.links.platePlus}/iframe/${component}`} + /> ); } diff --git a/apps/www/src/registry/default/plate-ui/ai-copilot-hover-card.tsx b/apps/www/src/registry/default/plate-ui/ai-copilot-hover-card.tsx new file mode 100644 index 0000000000..cccf54f2b6 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-copilot-hover-card.tsx @@ -0,0 +1,11 @@ +import type { CopilotHoverCardProps } from '@udecode/plate-ai/react'; + +export const AiCopilotHoverCard = ({ + suggestionText, +}: CopilotHoverCardProps) => { + return ( + + {suggestionText} + + ); +}; diff --git a/packages/ai/.npmignore b/packages/ai/.npmignore new file mode 100644 index 0000000000..7d3b305b17 --- /dev/null +++ b/packages/ai/.npmignore @@ -0,0 +1,3 @@ +__tests__ +__test-utils__ +__mocks__ diff --git a/packages/ai/README.md b/packages/ai/README.md new file mode 100644 index 0000000000..eb8febea7a --- /dev/null +++ b/packages/ai/README.md @@ -0,0 +1,7 @@ +# Plate alignment + +Visit https://platejs.org/docs/alignment to view the documentation. + +## License + +[MIT](../../LICENSE) diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 0000000000..9645519921 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,67 @@ +{ + "name": "@udecode/plate-ai", + "version": "38.0.1", + "description": "Text AI plugin for Plate", + "keywords": [ + "plate", + "plugin", + "slate" + ], + "homepage": "https://platejs.org", + "bugs": { + "url": "https://github.com/udecode/plate/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/udecode/plate.git", + "directory": "packages/ai" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "module": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./react": { + "types": "./dist/react/index.d.ts", + "import": "./dist/react/index.mjs", + "module": "./dist/react/index.mjs", + "require": "./dist/react/index.js" + } + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "brl": "yarn p:brl", + "build": "yarn p:build", + "build:watch": "yarn p:build:watch", + "clean": "yarn p:clean", + "lint": "yarn p:lint", + "lint:fix": "yarn p:lint:fix", + "test": "yarn p:test", + "test:watch": "yarn p:test:watch", + "typecheck": "yarn p:typecheck" + }, + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=38.0.6", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.103.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.108.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 0000000000..e7cccc036f --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './lib/index'; diff --git a/packages/ai/src/lib/BaseAIPlugin.ts b/packages/ai/src/lib/BaseAIPlugin.ts new file mode 100644 index 0000000000..7cdb51690a --- /dev/null +++ b/packages/ai/src/lib/BaseAIPlugin.ts @@ -0,0 +1,26 @@ +import type { TriggerComboboxPluginOptions } from '@udecode/plate-combobox'; + +import { + type PluginConfig, + type SlateEditor, + type TNodeEntry, + createTSlatePlugin, +} from '@udecode/plate-common'; + +import { withTriggerAIMenu } from './withTriggerAIMenu'; + +export type BaseAIOptions = { + onOpenAI?: (editor: SlateEditor, nodeEntry: TNodeEntry) => void; +} & TriggerComboboxPluginOptions; + +export type BaseAIPluginConfig = PluginConfig<'ai', BaseAIOptions>; + +export const BaseAIPlugin = createTSlatePlugin({ + key: 'ai', + extendEditor: withTriggerAIMenu, + options: { + scrollContainerSelector: '#scroll_container', + trigger: ' ', + triggerPreviousCharPattern: /^\s?$/, + }, +}); diff --git a/packages/ai/src/lib/index.ts b/packages/ai/src/lib/index.ts new file mode 100644 index 0000000000..99b9131797 --- /dev/null +++ b/packages/ai/src/lib/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './BaseAIPlugin'; +export * from './withTriggerAIMenu'; diff --git a/packages/ai/src/lib/withTriggerAIMenu.ts b/packages/ai/src/lib/withTriggerAIMenu.ts new file mode 100644 index 0000000000..a03c00d092 --- /dev/null +++ b/packages/ai/src/lib/withTriggerAIMenu.ts @@ -0,0 +1,75 @@ +import type { ExtendEditor } from '@udecode/plate-core'; + +import { + getEditorString, + getNodeString, + getPointBefore, + getRange, +} from '@udecode/slate'; +import { getAncestorNode } from '@udecode/slate-utils'; + +import type { BaseAIPluginConfig } from './BaseAIPlugin'; + +export const withTriggerAIMenu: ExtendEditor = ({ + editor, + ...ctx +}) => { + const { insertText } = editor; + + const matchesTrigger = (text: string) => { + const { trigger } = ctx.getOptions(); + + if (trigger instanceof RegExp) { + return trigger.test(text); + } + if (Array.isArray(trigger)) { + return trigger.includes(text); + } + + return text === trigger; + }; + + editor.insertText = (text) => { + const { triggerPreviousCharPattern, triggerQuery } = ctx.getOptions(); + + if ( + !editor.selection || + !matchesTrigger(text) || + (triggerQuery && !triggerQuery(editor)) + ) { + return insertText(text); + } + + // Make sure an input is created at the beginning of line or after a whitespace + const previousChar = getEditorString( + editor, + getRange( + editor, + editor.selection, + getPointBefore(editor, editor.selection) + ) + ); + + const matchesPreviousCharPattern = + triggerPreviousCharPattern?.test(previousChar); + + if (matchesPreviousCharPattern) { + const nodeEntry = getAncestorNode(editor); + + if (!nodeEntry) return insertText(text); + + const [node] = nodeEntry; + + // Make sure can only open menu in the first point + if (getNodeString(node).length > 0) return insertText(text); + + const { onOpenAI } = ctx.getOptions(); + + if (onOpenAI) return onOpenAI(editor, nodeEntry); + } + + return insertText(text); + }; + + return editor; +}; diff --git a/packages/ai/src/react/ai/AIPlugin.ts b/packages/ai/src/react/ai/AIPlugin.ts new file mode 100644 index 0000000000..08f95f3fba --- /dev/null +++ b/packages/ai/src/react/ai/AIPlugin.ts @@ -0,0 +1,185 @@ +import type { ExtendConfig } from '@udecode/plate-core'; +import type { NodeEntry, Path } from 'slate'; + +import { + type PlateEditor, + toDOMNode, + toTPlatePlugin, +} from '@udecode/plate-common/react'; + +import { type BaseAIPluginConfig, BaseAIPlugin } from '../../lib'; + +export const KEY_AI = 'ai'; + +export interface AIPlugin { + scrollContainerSelector?: string; + trigger?: RegExp | string[] | string; + triggerPreviousCharPattern?: RegExp; +} + +export type AISelectors = { + isOpen: (editorId: string) => boolean; +}; + +export type AIApi = { + abort: () => void; + clearLast: () => void; + focusMenu: () => void; + hide: () => void; + setAnchorElement: (dom: HTMLElement) => void; + show: (editorId: string, dom: HTMLElement, nodeEntry: NodeEntry) => void; +}; + +export type AIActionGroup = { + group?: string; + value?: string; +}; + +export type AIPluginConfig = ExtendConfig< + BaseAIPluginConfig, + { + abortController: AbortController | null; + action: AIActionGroup | null; + aiEditor: PlateEditor | null; + aiState: 'done' | 'generating' | 'idle' | 'requesting'; + anchorDom: HTMLElement | null; + curNodeEntry: NodeEntry | null; + initNodeEntry: NodeEntry | null; + lastGenerate: string | null; + lastPrompt: string | null; + lastWorkPath: Path | null; + menuType: 'selection' | 'space' | null; + openEditorId: string | null; + scrollContainerSelector: string; + store: any | null; + } & AIApi & + AISelectors, + { + ai: AIApi; + } +>; + +export const AIPlugin = toTPlatePlugin(BaseAIPlugin, { + options: { + abortController: null, + action: null, + aiEditor: null, + aiState: 'idle', + anchorDom: null, + curNodeEntry: null, + initNodeEntry: null, + lastGenerate: null, + lastPrompt: null, + lastWorkPath: null, + menuType: null, + openEditorId: null, + store: null, + }, +}) + .extendOptions(({ getOptions }) => ({ + isOpen: (editorId: string) => { + const { openEditorId, store } = getOptions(); + const anchorElement = store?.getState().anchorElement; + const isAnchor = !!anchorElement && document.contains(anchorElement); + + return !!editorId && openEditorId === editorId && isAnchor; + }, + })) + .extendApi< + Required> + >(({ getOptions, setOptions }) => ({ + clearLast: () => { + setOptions({ + lastGenerate: null, + lastPrompt: null, + lastWorkPath: null, + }); + }, + focusMenu: () => { + const { store } = getOptions(); + + setTimeout(() => { + const searchInput = document.querySelector( + '#__potion_ai_menu_searchRef' + ) as HTMLInputElement; + + if (store) { + store.setAutoFocusOnShow(true); + store.setInitialFocus('first'); + searchInput?.focus(); + } + }, 0); + }, + setAnchorElement: (dom: HTMLElement) => { + const { store } = getOptions(); + + if (store) { + store.setAnchorElement(dom); + } + }, + })) + .extendApi>>( + ({ api, getOptions, setOption }) => ({ + abort: () => { + const { abortController } = getOptions(); + + abortController?.abort(); + setOption('aiState', 'idle'); + setTimeout(() => { + api.ai.focusMenu(); + }, 0); + }, + hide: () => { + setOption('openEditorId', null); + getOptions().store?.setAnchorElement(null); + }, + show: (editorId: string, dom: HTMLElement, nodeEntry: NodeEntry) => { + const { store } = getOptions(); + + setOption('openEditorId', editorId); + api.ai.clearLast(); + setOption('initNodeEntry', nodeEntry); + api.ai.setAnchorElement(dom); + store?.show(); + api.ai.focusMenu(); + }, + }) + ) + .extend(({ api, getOptions, setOptions }) => ({ + options: { + onOpenAI(editor, [node, path]) { + // NOTE: toDOMNode is dependent on the React make it to an options if want to support other frame. + const dom = toDOMNode(editor, node); + + if (!dom) return; + + const { scrollContainerSelector } = getOptions(); + + // TODO popup animation + if (scrollContainerSelector) { + const scrollContainer = document.querySelector( + scrollContainerSelector + ); + + if (!scrollContainer) return; + + // Make sure when popup in very bottom the menu within the viewport range. + const rect = dom.getBoundingClientRect(); + const windowHeight = window.innerHeight; + const distanceToBottom = windowHeight - rect.bottom; + + // 261 is height of the menu. + if (distanceToBottom < 261) { + // TODO: scroll animation + scrollContainer.scrollTop += 261 - distanceToBottom; + } + } + + api.ai.show(editor.id, dom, [node, path]); + setOptions({ + aiState: 'idle', + menuType: 'space', + }); + }, + }, + })); diff --git a/packages/ai/src/react/ai/index.ts b/packages/ai/src/react/ai/index.ts new file mode 100644 index 0000000000..e460f9c7c1 --- /dev/null +++ b/packages/ai/src/react/ai/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './AIPlugin'; +export * from './utils/index'; diff --git a/packages/ai/src/react/ai/utils/index.ts b/packages/ai/src/react/ai/utils/index.ts new file mode 100644 index 0000000000..c75e5e81cc --- /dev/null +++ b/packages/ai/src/react/ai/utils/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './updateMenuAnchorByPath'; diff --git a/packages/ai/src/react/ai/utils/updateMenuAnchorByPath.ts b/packages/ai/src/react/ai/utils/updateMenuAnchorByPath.ts new file mode 100644 index 0000000000..e6c953c282 --- /dev/null +++ b/packages/ai/src/react/ai/utils/updateMenuAnchorByPath.ts @@ -0,0 +1,27 @@ +import type { PlateEditor } from '@udecode/plate-core/react'; +import type { Path } from 'slate'; + +import { getAncestorNode } from '@udecode/plate-common'; +import { toDOMNode } from '@udecode/plate-common/react'; + +import { AIPlugin } from '../AIPlugin'; + +export const updateMenuAnchorByPath = (editor: PlateEditor, path: Path) => { + // FIX: replace make the anchor disappear + editor.setOptions(AIPlugin, { + openEditorId: editor.id, + }); + + const nodeEntry = getAncestorNode(editor, path); + + if (nodeEntry) { + setTimeout(() => { + const dom = toDOMNode(editor, nodeEntry[0]); + + if (dom) { + editor.getApi(AIPlugin).ai.setAnchorElement(dom); + editor.setOption(AIPlugin, 'curNodeEntry', nodeEntry); + } + }, 0); + } +}; diff --git a/packages/ai/src/react/copilot/CopilotPlugin.tsx b/packages/ai/src/react/copilot/CopilotPlugin.tsx new file mode 100644 index 0000000000..0826c19bb1 --- /dev/null +++ b/packages/ai/src/react/copilot/CopilotPlugin.tsx @@ -0,0 +1,214 @@ +import React from 'react'; + +import type { InsertTextOperation } from 'slate'; + +import { + type PluginConfig, + type QueryNodeOptions, + withoutMergingHistory, +} from '@udecode/plate-common'; +import { + ParagraphPlugin, + createTPlatePlugin, +} from '@udecode/plate-common/react'; + +import { generateCopilotTextDebounce } from './generateCopilotText'; +import { InjectCopilot } from './injectCopilot'; +import { onKeyDownCopilot } from './onKeyDownCopilot'; +import { withoutAbort } from './utils/withoutAbort'; + +export interface CopilotHoverCardProps { + suggestionText: string; +} + +export interface FetchCopilotSuggestionProps { + abortSignal: AbortController; + prompt: string; +} + +export type CopilotPluginConfig = PluginConfig< + 'copilot', + { + hoverCard: (props: CopilotHoverCardProps) => JSX.Element; + abortController?: AbortController | null; + completedNodeId?: string | null; + copilotState?: 'completed' | 'idle'; + enableDebounce?: boolean; + enableShortCut?: boolean; + fetchSuggestion?: (props: FetchCopilotSuggestionProps) => Promise; + query?: QueryNodeOptions; + shouldAbort?: boolean; + suggestionText?: string | null; + } & CopilotApi & + CopilotSelectors +>; + +type CopilotSelectors = { + isCompleted?: (id: string) => boolean; +}; + +type CopilotApi = { + abortCopilot?: () => void; + setCopilot?: (id: string, text: string) => void; +}; + +export const CopilotPlugin = createTPlatePlugin({ + key: 'copilot', + options: { + copilotState: 'idle', + enableDebounce: false, + hoverCard: (props) => {props.suggestionText}, + query: { + allow: [ParagraphPlugin.key], + }, + shouldAbort: true, + }, +}) + .extendOptions>(({ getOptions }) => ({ + isCompleted: (id) => getOptions().completedNodeId === id, + })) + .extendApi>(({ getOptions, setOptions }) => ({ + abortCopilot: () => { + const { abortController } = getOptions(); + + abortController?.abort(); + + setOptions({ + abortController: null, + completedNodeId: null, + copilotState: 'idle', + }); + }, + setCopilot: (id, text) => { + setOptions({ + completedNodeId: id, + copilotState: 'completed', + suggestionText: text, + }); + }, + })) + .extend({ + extendEditor: ({ api, editor, getOptions, setOptions }) => { + type CopilotBatch = (typeof editor.history.undos)[number] & { + shouldAbort: boolean; + }; + + const { apply, insertText, redo, setSelection, undo, writeHistory } = + editor; + + editor.undo = () => { + if (getOptions().copilotState === 'idle') return undo(); + + const topUndo = editor.history.undos.at(-1) as CopilotBatch; + const oldText = getOptions().suggestionText; + + if ( + topUndo && + topUndo.shouldAbort === false && + topUndo.operations[0].type === 'insert_text' && + oldText + ) { + withoutAbort(editor, () => { + const shouldInsertText = ( + topUndo.operations[0] as InsertTextOperation + ).text; + + const newText = shouldInsertText + oldText; + setOptions({ suggestionText: newText }); + + undo(); + }); + + return; + } + + return undo(); + }; + + editor.redo = () => { + if (getOptions().copilotState === 'idle') return redo(); + + const topRedo = editor.history.redos.at(-1) as CopilotBatch; + const oldText = getOptions().suggestionText; + + if ( + topRedo && + topRedo.shouldAbort === false && + topRedo.operations[0].type === 'insert_text' && + oldText + ) { + withoutAbort(editor, () => { + const shouldRemoveText = ( + topRedo.operations[0] as InsertTextOperation + ).text; + + const newText = oldText.slice(shouldRemoveText.length); + setOptions({ suggestionText: newText }); + + redo(); + }); + + return; + } + + return redo(); + }; + + editor.writeHistory = (stacks, batch) => { + if (getOptions().copilotState === 'idle') + return writeHistory(stacks, batch); + + const { shouldAbort } = getOptions(); + batch.shouldAbort = shouldAbort; + + return writeHistory(stacks, batch); + }; + + editor.insertText = (text) => { + if (getOptions().copilotState === 'idle') return insertText(text); + + const oldText = getOptions().suggestionText; + + if (text.length === 1 && text === oldText?.at(0)) { + withoutAbort(editor, () => { + withoutMergingHistory(editor, () => { + const newText = oldText?.slice(1); + setOptions({ suggestionText: newText }); + insertText(text); + }); + }); + + return; + } + + insertText(text); + }; + + editor.apply = (operation) => { + const { shouldAbort } = getOptions(); + + if (shouldAbort) { + api.copilot.abortCopilot(); + } + + apply(operation); + }; + + editor.setSelection = (selection) => { + if (getOptions().enableDebounce) { + void generateCopilotTextDebounce(editor, { isDebounce: true }); + } + + return setSelection(selection); + }; + + return editor; + }, + render: { + // TODO: type + belowNodes: InjectCopilot as any, + }, + handlers: { + onKeyDown: onKeyDownCopilot as any, + }, + }); diff --git a/packages/ai/src/react/copilot/generateCopilotText.ts b/packages/ai/src/react/copilot/generateCopilotText.ts new file mode 100644 index 0000000000..b7acba42f4 --- /dev/null +++ b/packages/ai/src/react/copilot/generateCopilotText.ts @@ -0,0 +1,73 @@ +import type { KeyboardEvent } from 'react'; + +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { + getAncestorNode, + getNodeString, + isEndPoint, + isExpanded, +} from '@udecode/plate-common'; +// TODO:AI +import { debounce } from 'lodash'; + +import { AIPlugin } from '../ai'; +import { CopilotPlugin } from './CopilotPlugin'; + +export const generateCopilotText = async ( + editor: PlateEditor, + options: { + event?: KeyboardEvent; + isDebounce?: boolean; + } +) => { + const { copilotState, fetchSuggestion, query } = + editor.getOptions(CopilotPlugin); + + if (copilotState === 'completed') return; + + // TODO:AI + const aiState = editor.getOption(AIPlugin, 'aiState'); + + if (aiState !== 'idle') return; + + const nodeEntry = getAncestorNode(editor); + + if (!nodeEntry) return; + if (isExpanded(editor.selection)) return; + + const isEnd = isEndPoint(editor, editor.selection?.focus, nodeEntry[1]); + + if (!isEnd) return; + + options.event?.preventDefault(); + + const [node] = nodeEntry; + + if (!query?.allow?.includes(node.type as string)) return; + + const prompt = getNodeString(node); + + if (prompt.length === 0) return; + + const abortController = new AbortController(); + + const { abortCopilot, setCopilot } = editor.getApi(CopilotPlugin).copilot; + + // abort the last request + abortCopilot(); + + editor.setOptions(CopilotPlugin, { abortController }); + + const suggestion = await fetchSuggestion?.({ + abortSignal: abortController, + prompt, + }); + + return setCopilot( + node.id, + suggestion ?? 'Can not get suggestion did you config fetchSuggestion?' + ); +}; + +export const generateCopilotTextDebounce = debounce(generateCopilotText, 500); diff --git a/packages/ai/src/react/copilot/getCopilotSystem.ts b/packages/ai/src/react/copilot/getCopilotSystem.ts new file mode 100644 index 0000000000..402d5ff768 --- /dev/null +++ b/packages/ai/src/react/copilot/getCopilotSystem.ts @@ -0,0 +1,6 @@ +export const getCopilotSystem = (): string => { + return `Please continue writing a sentence based on the prompt. + Important Note: Please do not answer any questions. Directly provide the continuation of the content without including any part of the prompt and + don't write more than one sentence: finish current sentence or write a new one + `; +}; diff --git a/packages/ai/src/react/copilot/index.ts b/packages/ai/src/react/copilot/index.ts new file mode 100644 index 0000000000..e159a4ed3f --- /dev/null +++ b/packages/ai/src/react/copilot/index.ts @@ -0,0 +1,10 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './CopilotPlugin'; +export * from './generateCopilotText'; +export * from './getCopilotSystem'; +export * from './injectCopilot'; +export * from './onKeyDownCopilot'; +export * from './utils/index'; diff --git a/packages/ai/src/react/copilot/injectCopilot.tsx b/packages/ai/src/react/copilot/injectCopilot.tsx new file mode 100644 index 0000000000..10196f752d --- /dev/null +++ b/packages/ai/src/react/copilot/injectCopilot.tsx @@ -0,0 +1,48 @@ +import React, { useMemo } from 'react'; + +import { getAncestorNode } from '@udecode/plate-common'; +import { + type NodeWrapperComponentProps, + findNodePath, + useEditorPlugin, +} from '@udecode/plate-common/react'; + +import { type CopilotPluginConfig, CopilotPlugin } from './CopilotPlugin'; + +export const InjectCopilot = ( + injectProps: NodeWrapperComponentProps +) => { + const { element } = injectProps; + const { editor, getOptions } = useEditorPlugin(CopilotPlugin); + + const isCompleted = editor.useOption( + CopilotPlugin, + 'isCompleted', + element.id as string + ); + + const nodeType = useMemo(() => { + const path = findNodePath(editor, element); + + if (!path) return; + + const node = getAncestorNode(editor, path); + + return node?.[0].type; + }, [editor, element]); + + const { hoverCard: HoverCard, query, suggestionText } = getOptions(); + + if (query?.allow?.includes(nodeType as string)) { + return function Component({ children }: { children: React.ReactNode }) { + return ( + + {children} + {isCompleted && suggestionText && ( + + )} + + ); + }; + } +}; diff --git a/packages/ai/src/react/copilot/onKeyDownCopilot.ts b/packages/ai/src/react/copilot/onKeyDownCopilot.ts new file mode 100644 index 0000000000..f2d2fb3284 --- /dev/null +++ b/packages/ai/src/react/copilot/onKeyDownCopilot.ts @@ -0,0 +1,56 @@ +import { + insertText, + isHotkey, + withoutMergingHistory, +} from '@udecode/plate-common'; +import { type KeyboardHandler, Hotkeys } from '@udecode/plate-common/react'; + +import { type CopilotPluginConfig, CopilotPlugin } from './CopilotPlugin'; +import { generateCopilotText } from './generateCopilotText'; +import { withoutAbort } from './utils/withoutAbort'; + +export const onKeyDownCopilot: KeyboardHandler = ({ + editor, + event, + getOptions, + setOptions, +}) => { + if (event.defaultPrevented) return; + + const { + copilotState: state, + enableShortCut = true, + suggestionText: completionText, + } = getOptions(); + + if (state === 'completed' && Hotkeys.isTab(editor, event)) { + event.preventDefault(); + withoutMergingHistory(editor, () => { + insertText(editor, completionText!); + }); + } + if (isHotkey('ctrl+space')(event) && enableShortCut) { + void generateCopilotText(editor, { event, isDebounce: false }); + } + if (isHotkey('cmd+right')(event) && state === 'completed') { + event.preventDefault(); + const text = completionText!; + // TODO: support Chinese. + const firstWord = /^\s*\S+/.exec(text)?.[0] || ''; + const remainingText = text.slice(firstWord.length); + + setOptions({ suggestionText: remainingText }); + + withoutAbort(editor, () => { + withoutMergingHistory(editor, () => { + insertText(editor, firstWord); + }); + }); + } + if (state === 'completed' && isHotkey('escape')(event)) { + event.preventDefault(); + event.stopPropagation(); + + return editor.getApi(CopilotPlugin).copilot.abortCopilot(); + } +}; diff --git a/packages/ai/src/react/copilot/utils/index.ts b/packages/ai/src/react/copilot/utils/index.ts new file mode 100644 index 0000000000..c7c7868302 --- /dev/null +++ b/packages/ai/src/react/copilot/utils/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './withoutAbort'; diff --git a/packages/ai/src/react/copilot/utils/withoutAbort.ts b/packages/ai/src/react/copilot/utils/withoutAbort.ts new file mode 100644 index 0000000000..55c36254ee --- /dev/null +++ b/packages/ai/src/react/copilot/utils/withoutAbort.ts @@ -0,0 +1,9 @@ +import type { PlateEditor } from '@udecode/plate-common/react'; + +import { CopilotPlugin } from '..'; + +export const withoutAbort = (editor: PlateEditor, fn: () => void) => { + editor.setOptions(CopilotPlugin, { shouldAbort: false }); + fn(); + editor.setOptions(CopilotPlugin, { shouldAbort: true }); +}; diff --git a/packages/ai/src/react/index.ts b/packages/ai/src/react/index.ts new file mode 100644 index 0000000000..4b6a3dbc5f --- /dev/null +++ b/packages/ai/src/react/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './ai/index'; +export * from './copilot/index'; diff --git a/packages/ai/tsconfig.build.json b/packages/ai/tsconfig.build.json new file mode 100644 index 0000000000..425481e027 --- /dev/null +++ b/packages/ai/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../config/tsconfig.build.json", + "compilerOptions": { + "declarationDir": "./dist", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 0000000000..ad83d092a5 --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../config/tsconfig.base.json", + "include": ["src"], + "exclude": [] +} diff --git a/yarn.lock b/yarn.lock index c08fefdd91..546f2e9aca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5883,6 +5883,22 @@ __metadata: languageName: unknown linkType: soft +"@udecode/plate-ai@workspace:^, @udecode/plate-ai@workspace:packages/ai": + version: 0.0.0-use.local + resolution: "@udecode/plate-ai@workspace:packages/ai" + dependencies: + lodash: "npm:^4.17.21" + peerDependencies: + "@udecode/plate-common": ">=38.0.6" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + languageName: unknown + linkType: soft + "@udecode/plate-alignment@npm:38.0.1, @udecode/plate-alignment@workspace:^, @udecode/plate-alignment@workspace:packages/alignment": version: 0.0.0-use.local resolution: "@udecode/plate-alignment@workspace:packages/alignment" @@ -21604,6 +21620,7 @@ __metadata: "@types/react-syntax-highlighter": "npm:^15.5.13" "@udecode/cn": "workspace:^" "@udecode/plate": "workspace:^" + "@udecode/plate-ai": "workspace:^" "@udecode/plate-alignment": "workspace:^" "@udecode/plate-autoformat": "workspace:^" "@udecode/plate-basic-elements": "workspace:^" From 22966329782dfb9830ec585708866957b84a4d4b Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 26 Sep 2024 21:45:38 +0800 Subject: [PATCH 012/127] lint --- packages/ai/src/lib/withTriggerAIMenu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/lib/withTriggerAIMenu.ts b/packages/ai/src/lib/withTriggerAIMenu.ts index a03c00d092..44e18acc6c 100644 --- a/packages/ai/src/lib/withTriggerAIMenu.ts +++ b/packages/ai/src/lib/withTriggerAIMenu.ts @@ -1,12 +1,12 @@ import type { ExtendEditor } from '@udecode/plate-core'; import { + getAncestorNode, getEditorString, getNodeString, getPointBefore, getRange, -} from '@udecode/slate'; -import { getAncestorNode } from '@udecode/slate-utils'; +} from '@udecode/plate-common'; import type { BaseAIPluginConfig } from './BaseAIPlugin'; From ab17880c5e0b46ff8d2f5478029b2894eb3ee781 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Thu, 26 Sep 2024 22:54:03 +0800 Subject: [PATCH 013/127] ci --- packages/ai/package.json | 1 + packages/ai/src/react/copilot/generateCopilotText.ts | 2 +- packages/ai/src/react/copilot/getCopilotSystem.ts | 6 ------ packages/ai/src/react/copilot/index.ts | 1 - yarn.lock | 1 + 5 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 packages/ai/src/react/copilot/getCopilotSystem.ts diff --git a/packages/ai/package.json b/packages/ai/package.json index 9645519921..a6c0ca6de6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -50,6 +50,7 @@ "typecheck": "yarn p:typecheck" }, "dependencies": { + "@udecode/plate-combobox": "38.0.1", "lodash": "^4.17.21" }, "peerDependencies": { diff --git a/packages/ai/src/react/copilot/generateCopilotText.ts b/packages/ai/src/react/copilot/generateCopilotText.ts index b7acba42f4..f29f425966 100644 --- a/packages/ai/src/react/copilot/generateCopilotText.ts +++ b/packages/ai/src/react/copilot/generateCopilotText.ts @@ -9,7 +9,7 @@ import { isExpanded, } from '@udecode/plate-common'; // TODO:AI -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import { AIPlugin } from '../ai'; import { CopilotPlugin } from './CopilotPlugin'; diff --git a/packages/ai/src/react/copilot/getCopilotSystem.ts b/packages/ai/src/react/copilot/getCopilotSystem.ts deleted file mode 100644 index 402d5ff768..0000000000 --- a/packages/ai/src/react/copilot/getCopilotSystem.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const getCopilotSystem = (): string => { - return `Please continue writing a sentence based on the prompt. - Important Note: Please do not answer any questions. Directly provide the continuation of the content without including any part of the prompt and - don't write more than one sentence: finish current sentence or write a new one - `; -}; diff --git a/packages/ai/src/react/copilot/index.ts b/packages/ai/src/react/copilot/index.ts index e159a4ed3f..97be77d466 100644 --- a/packages/ai/src/react/copilot/index.ts +++ b/packages/ai/src/react/copilot/index.ts @@ -4,7 +4,6 @@ export * from './CopilotPlugin'; export * from './generateCopilotText'; -export * from './getCopilotSystem'; export * from './injectCopilot'; export * from './onKeyDownCopilot'; export * from './utils/index'; diff --git a/yarn.lock b/yarn.lock index 546f2e9aca..8ea38a97b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5887,6 +5887,7 @@ __metadata: version: 0.0.0-use.local resolution: "@udecode/plate-ai@workspace:packages/ai" dependencies: + "@udecode/plate-combobox": "npm:38.0.1" lodash: "npm:^4.17.21" peerDependencies: "@udecode/plate-common": ">=38.0.6" From dd55c66ac8b25cf43de719efb4ef9079a6bbc36b Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Fri, 27 Sep 2024 16:07:14 +0800 Subject: [PATCH 014/127] fix --- packages/ai/src/react/copilot/generateCopilotText.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ai/src/react/copilot/generateCopilotText.ts b/packages/ai/src/react/copilot/generateCopilotText.ts index f29f425966..2cc00a5515 100644 --- a/packages/ai/src/react/copilot/generateCopilotText.ts +++ b/packages/ai/src/react/copilot/generateCopilotText.ts @@ -8,7 +8,6 @@ import { isEndPoint, isExpanded, } from '@udecode/plate-common'; -// TODO:AI import debounce from 'lodash/debounce'; import { AIPlugin } from '../ai'; @@ -26,7 +25,6 @@ export const generateCopilotText = async ( if (copilotState === 'completed') return; - // TODO:AI const aiState = editor.getOption(AIPlugin, 'aiState'); if (aiState !== 'idle') return; From 0c4b714c2dc4662eb039d601ac11c8bac375ecfb Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Fri, 27 Sep 2024 18:25:21 +0800 Subject: [PATCH 015/127] docs --- apps/www/content/docs/copilot.mdx | 274 +++++++++++++++++- apps/www/src/config/customizer-plugins.ts | 9 + .../lib/plate/demo/values/copilotValue.tsx | 34 +++ 3 files changed, 301 insertions(+), 16 deletions(-) create mode 100644 apps/www/src/lib/plate/demo/values/copilotValue.tsx diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index bd8fdd8319..7b58cc1750 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -1,21 +1,21 @@ --- title: Copilot -description: allows you to select from a list of AI commands. +description: AI-powered assistant that provides intelligent suggestions and autocompletion for enhanced content creation. docs: - - route: https://pro.platejs.org/docs/components/copilot - title: Copilot + - route: https://pro.platejs.org/docs/components/copilot-free + title: Copilot Hover Card --- - + ## Features -- Provides an AI-powered menu. -- Offers a selection of AI commands to enhance content creation and editing. -- Seamlessly integrates AI assistance within the editor interface. - +- Offers intelligent autocompletion to enhance content creation and editing. +- Provides two modes: **debounce** and **shortcut**. + 1. Shortcut mode is the default, triggered by pressing `Control+Space`. + 2. Debounce mode can be automatically triggered when the selection changes or during typing. ## Installation @@ -28,7 +28,7 @@ npm install @udecode/plate-ai ```tsx // ... -import { AIPlugin } from '@/registry/default/plate-pro/ai/ai/src/react/AIPlugin'; +import { CopilotPlugin } from '@udecode/plate-ai/react'; const editor = usePlateEditor({ id: 'ai-demo', @@ -37,18 +37,260 @@ const editor = usePlateEditor({ }, plugins: [ ...commonPlugins, - SelectionOverlayPlugin, - MarkdownPlugin.configure({ options: { indentList: true } }), - AIPlugin.configure({ - options: { - scrollContainerSelector: '#scroll_container', + CopilotPlugin.configure({ + options: { + fetchSuggestion: async ({ abortSignal, prompt }) => { + + // Currently, we are using a mock function to simulate the api request + return new Promise((resolve) => { + setTimeout(() => { + resolve(MENTIONABLES[Math.floor(Math.random() * 41)].text); + }, 100); + }); + }, + hoverCard: AiCopilotHoverCard, + query: { + allow: [ + ParagraphPlugin.key, + BlockquotePlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + HEADING_KEYS.h4, + HEADING_KEYS.h5, + HEADING_KEYS.h6, + ], }, - render: { aboveEditable: AIMenu }, - }), + }, +}); ], value: aiValue, }); ``` +## Integrate with your backend + +`options.fetchSuggestion` is an asynchronous function that you need to implement to fetch suggestions from your backend. This function is crucial for integrating the Copilot feature with your own AI model or service. + +The function receives an object with two properties: + +- `abortSignal`: An `AbortSignal` object that allows you to cancel the request if needed. This is particularly useful in debounce mode to abort the request if the user continues typing or changes the selection. +- `prompt`: A string containing the text of the node where the user's cursor is currently positioned. This serves as the context for generating suggestions. + +The function should return a Promise that resolves to a string. This string will be the suggestion inserted into the editor. + +Here's a more detailed example of how you might implement this function: + +```ts + + fetchSuggestion: async ({ abortSignal, prompt }) => { + const system = `Please continue writing a sentence based on the prompt. + Important Note: Please do not answer any questions. Directly provide the continuation + of the content without including any part of the prompt and don't write more than one sentence: + finish current sentence or write a new one` + + const response = await fetch('https://your-api-endpoint.com/api/v1/generate-text', { + method: 'POST', + body: JSON.stringify({ prompt,system }), + // pass the abortSignal to the fetch request + signal: abortSignal, + }); + + const data = await response.json(); + + + // data.suggestion should be a string + return data.suggestion; +}, + +``` + +## Copilot state + +The Copilot plugin maintains its own state to manage the suggestion process. The state can be either 'idle' or 'completed'. Here's a breakdown of what each state means: +```ts +type CopilotState = 'idle' | 'completed'; +``` + +- `idle`: This is the default state. It indicates that the Copilot is not currently providing a suggestion or has finished processing the previous suggestion. +- `completed`: This state indicates that the Copilot has generated a suggestion and it's ready to be inserted into the editor. You'll see this suggestion as gray text in the editor, which can be accepted by pressing the Tab key or rejected by continuing to type. + +You can access the current state of the Copilot using: +```ts +const copilotState = editor.getOptions(CopilotPlugin).copilotState; +``` + + + +## Conflict with Tabbable Plugin + +When using both the [Tabbable Plugin](https://platejs.org/docs/tabbable) and the Copilot feature in your editor, you may encounter conflicts with the `Tab` key functionality. This is because both features utilize the same key binding. + +To resolve this conflict and ensure smooth operation of both features, you can configure the Tabbable Plugin to be disabled when the cursor is at the end of a paragraph. This allows the Copilot feature to function as intended when you press `Tab` at the end of a line. + +Here's how you can modify the Tabbable Plugin configuration: + +1. Visit the [Tabbable Plugin documentation](https://platejs.org/docs/tabbable#conflicts-with-other-plugins) for more details on handling conflicts. +2. Implement the following configuration to disable the Tabbable Plugin when the cursor is at the end of a paragraph: + +```tsx +TabbablePlugin.configure(({ editor }) => ({ + options: { + query: () => { + // return false when cursor is in the end of the paragraph + if (isSelectionAtBlockStart(editor) || isSelectionAtBlockEnd(editor)) + return false; + + return !someNode(editor, { + match: (n) => { + return !!( + n.type && + ([ + CodeBlockPlugin.key, + ListItemPlugin.key, + TablePlugin.key, + ].includes(n.type as any) || + n[IndentListPlugin.key]) + ); + }, + }); + }, + }, +})); +``` + + + +## Plate Plus +In Plate Plus, We using debounce mode by default. That's mean you will see the suggestion automatically without pressing `Control+Space`. + +We also provide a new style of hover card that is more user-friendly.You can hover on the suggestion to see the hover card. + + +All of the backend setup is available in [Potion template](https://pro.platejs.org/docs/templates/potion). + + + + + + +Options for the selection area. Example: + +```ts +{ + boundaries: ['#selection-demo #scroll_container'], + container: ['#selection-demo #scroll_container'], + selectables: ['#selection-demo #scroll_container .slate-selectable'], + selectionAreaClass: 'slate-selection-area', +} +``` + + + +The padding-right of the editor. + + + +Enables or disables the context menu for block selection. + +- **Default:** `false` + + + +Indicates whether block selection is currently active. + +- **Default:** `false` + + + +A function to handle the **`keydown`** event when selecting. + + + +Options for querying nodes during block selection. + +- **Default:** `{ maxLevel: 1 }` + + + +A set of IDs for the currently selected blocks. + +- **Default:** `new Set()` + + + +### BlockContextMenuPlugin + +This plugin is used by `BlockSelectionPlugin` and doesn't need to be added manually. + ## API +### editor.getApi(CopilotPlugin).copilot.abortCopilot(); + +Aborts the ongoing API request and removes any suggestion text currently displayed. + + + + This function does not return a value. + + + + +### editor.api.blockSelection.getSelectedBlocks + +Gets the selected blocks in the editor. + + + + An array of selected block entries. + + + +### editor.getApi(CopilotPlugin).copilot.setCopilot + +Sets the suggestion text to the editor. + + + + The ID of the node to set the suggestion text. + + + The suggestion text to set. + + + + +## Utils functions + +### withoutAbort +Temporarily disables the abort functionality of the Copilot plugin, +by default any apply will cause the Copilot to remove the suggestion text and abort the request. + + + + The Plate editor instance. + + + The function to execute without abort. + + + + + + This function does not return a value. + + + +Usage example: +```ts +import { withoutAbort } from '@udecode/plate-ai/react'; + +withoutAbort(editor, () => { + // Perform operations without the risk of abort + // For example, setting copilot suggestions +}); +``` diff --git a/apps/www/src/config/customizer-plugins.ts b/apps/www/src/config/customizer-plugins.ts index aceecbd6b7..9434758945 100644 --- a/apps/www/src/config/customizer-plugins.ts +++ b/apps/www/src/config/customizer-plugins.ts @@ -1,3 +1,4 @@ +import { CopilotPlugin } from '@udecode/plate-ai/react'; import { AlignPlugin } from '@udecode/plate-alignment/react'; import { AutoformatPlugin } from '@udecode/plate-autoformat/react'; import { @@ -39,6 +40,7 @@ import { TogglePlugin } from '@udecode/plate-toggle/react'; import { TrailingBlockPlugin } from '@udecode/plate-trailing-block'; import { columnValue } from '@/lib/plate/demo/values/columnValue'; +import { copilotValue } from '@/lib/plate/demo/values/copilotValue'; import { DragOverCursorPlugin } from '@/plate/demo/plugins/DragOverCursorPlugin'; import { alignValue } from '@/plate/demo/values/alignValue'; import { autoformatValue } from '@/plate/demo/values/autoformatValue'; @@ -140,6 +142,13 @@ export const customizerPlugins = { route: '/docs/comments', value: commentsValue, }, + copilot: { + id: 'copilot', + label: 'Copilot', + plugins: [CopilotPlugin.key], + route: '/docs/copilot', + value: copilotValue, + }, csv: { id: 'csv', label: 'CSV', diff --git a/apps/www/src/lib/plate/demo/values/copilotValue.tsx b/apps/www/src/lib/plate/demo/values/copilotValue.tsx new file mode 100644 index 0000000000..f006dc8318 --- /dev/null +++ b/apps/www/src/lib/plate/demo/values/copilotValue.tsx @@ -0,0 +1,34 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@udecode/plate-test-utils'; + +jsx; + +export const copilotValue: any = ( + + 🤖 Copilot + + Position your cursor at the + end of a paragraph + where you want to add or modify text. + + + Press Control + Space to trigger Copilot. + + + Choose from the suggested completions: + + + Tab: + Accept the entire suggested completion + + + Control + Right Arrow + : Complete one character at a time + + + Escape + : Cancel the Copilot + + +); From d36ef3b7dc126f030f4197dfcdb816a68da85c11 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Fri, 27 Sep 2024 18:27:15 +0800 Subject: [PATCH 016/127] docs --- apps/www/content/docs/copilot.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index 7b58cc1750..d4214b976a 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -113,7 +113,7 @@ type CopilotState = 'idle' | 'completed'; ``` - `idle`: This is the default state. It indicates that the Copilot is not currently providing a suggestion or has finished processing the previous suggestion. -- `completed`: This state indicates that the Copilot has generated a suggestion and it's ready to be inserted into the editor. You'll see this suggestion as gray text in the editor, which can be accepted by pressing the Tab key or rejected by continuing to type. +- `completed`: This state indicates that the Copilot has generated a suggestion and it's ready to be inserted into the editor. You'll see this suggestion as gray text in the editor in this state. You can access the current state of the Copilot using: ```ts From 881892ec491b0e9593974113224ff84e5e5b6b44 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Fri, 27 Sep 2024 19:26:10 +0800 Subject: [PATCH 017/127] fix --- apps/www/content/docs/copilot.mdx | 61 ------------------------------- 1 file changed, 61 deletions(-) diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index d4214b976a..242c3f105e 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -176,57 +176,6 @@ All of the backend setup is available in [Potion template](https://pro.platejs.o component="copilot" /> - - -Options for the selection area. Example: - -```ts -{ - boundaries: ['#selection-demo #scroll_container'], - container: ['#selection-demo #scroll_container'], - selectables: ['#selection-demo #scroll_container .slate-selectable'], - selectionAreaClass: 'slate-selection-area', -} -``` - - - -The padding-right of the editor. - - - -Enables or disables the context menu for block selection. - -- **Default:** `false` - - - -Indicates whether block selection is currently active. - -- **Default:** `false` - - - -A function to handle the **`keydown`** event when selecting. - - - -Options for querying nodes during block selection. - -- **Default:** `{ maxLevel: 1 }` - - - -A set of IDs for the currently selected blocks. - -- **Default:** `new Set()` - - - -### BlockContextMenuPlugin - -This plugin is used by `BlockSelectionPlugin` and doesn't need to be added manually. - ## API ### editor.getApi(CopilotPlugin).copilot.abortCopilot(); @@ -240,16 +189,6 @@ Aborts the ongoing API request and removes any suggestion text currently display -### editor.api.blockSelection.getSelectedBlocks - -Gets the selected blocks in the editor. - - - - An array of selected block entries. - - - ### editor.getApi(CopilotPlugin).copilot.setCopilot Sets the suggestion text to the editor. From 23fab25e46ccb30fb9960a9f6ee75432ce2570d8 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Fri, 27 Sep 2024 19:42:24 +0800 Subject: [PATCH 018/127] fix --- apps/www/src/app/docs/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/src/app/docs/layout.tsx b/apps/www/src/app/docs/layout.tsx index 3f0ce4eb77..7126e9a836 100644 --- a/apps/www/src/app/docs/layout.tsx +++ b/apps/www/src/app/docs/layout.tsx @@ -9,7 +9,7 @@ interface DocsLayoutProps { export default function DocsLayout({ children }: DocsLayoutProps) { return (
-
+
\n );\n};\n\nexport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n};\n", + "content": "import React, {\n type HTMLAttributes,\n type ReactNode,\n type RefObject,\n createContext,\n forwardRef,\n startTransition,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport type { PointRef } from 'slate';\n\nimport {\n type ComboboxItemProps,\n Combobox,\n ComboboxItem,\n ComboboxPopover,\n ComboboxProvider,\n Portal,\n useComboboxContext,\n useComboboxStore,\n} from '@ariakit/react';\nimport { cn } from '@udecode/cn';\nimport { filterWords } from '@udecode/plate-combobox';\nimport {\n type UseComboboxInputResult,\n useComboboxInput,\n useHTMLInputCursorState,\n} from '@udecode/plate-combobox/react';\nimport {\n type TElement,\n createPointRef,\n getPointBefore,\n insertText,\n moveSelection,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useComposedRef,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { cva } from 'class-variance-authority';\n\ntype FilterFn = (\n item: { value: string; keywords?: string[] },\n search: string\n) => boolean;\n\ninterface InlineComboboxContextValue {\n filter: FilterFn | false;\n inputProps: UseComboboxInputResult['props'];\n inputRef: RefObject;\n removeInput: UseComboboxInputResult['removeInput'];\n setHasEmpty: (hasEmpty: boolean) => void;\n showTrigger: boolean;\n trigger: string;\n}\n\nconst InlineComboboxContext = createContext(\n null as any\n);\n\nexport const defaultFilter: FilterFn = ({ keywords = [], value }, search) =>\n [value, ...keywords].some((keyword) => filterWords(keyword, search));\n\ninterface InlineComboboxProps {\n children: ReactNode;\n element: TElement;\n trigger: string;\n filter?: FilterFn | false;\n hideWhenNoValue?: boolean;\n setValue?: (value: string) => void;\n showTrigger?: boolean;\n value?: string;\n}\n\nconst InlineCombobox = ({\n children,\n element,\n filter = defaultFilter,\n hideWhenNoValue = false,\n setValue: setValueProp,\n showTrigger = true,\n trigger,\n value: valueProp,\n}: InlineComboboxProps) => {\n const editor = useEditorRef();\n const inputRef = React.useRef(null);\n const cursorState = useHTMLInputCursorState(inputRef);\n\n const [valueState, setValueState] = useState('');\n const hasValueProp = valueProp !== undefined;\n const value = hasValueProp ? valueProp : valueState;\n\n const setValue = useCallback(\n (newValue: string) => {\n setValueProp?.(newValue);\n\n if (!hasValueProp) {\n setValueState(newValue);\n }\n },\n [setValueProp, hasValueProp]\n );\n\n /**\n * Track the point just before the input element so we know where to\n * insertText if the combobox closes due to a selection change.\n */\n const [insertPoint, setInsertPoint] = useState(null);\n\n useEffect(() => {\n const path = findNodePath(editor, element);\n\n if (!path) return;\n\n const point = getPointBefore(editor, path);\n\n if (!point) return;\n\n const pointRef = createPointRef(editor, point);\n setInsertPoint(pointRef);\n\n return () => {\n pointRef.unref();\n };\n }, [editor, element]);\n\n const { props: inputProps, removeInput } = useComboboxInput({\n cancelInputOnBlur: false,\n cursorState,\n ref: inputRef,\n onCancelInput: (cause) => {\n if (cause !== 'backspace') {\n insertText(editor, trigger + value, {\n at: insertPoint?.current ?? undefined,\n });\n }\n if (cause === 'arrowLeft' || cause === 'arrowRight') {\n moveSelection(editor, {\n distance: 1,\n reverse: cause === 'arrowLeft',\n });\n }\n },\n });\n\n const [hasEmpty, setHasEmpty] = useState(false);\n\n const contextValue: InlineComboboxContextValue = useMemo(\n () => ({\n filter,\n inputProps,\n inputRef,\n removeInput,\n setHasEmpty,\n showTrigger,\n trigger,\n }),\n [\n trigger,\n showTrigger,\n filter,\n inputRef,\n inputProps,\n removeInput,\n setHasEmpty,\n ]\n );\n\n const store = useComboboxStore({\n // open: ,\n setValue: (newValue) => startTransition(() => setValue(newValue)),\n });\n\n const items = store.useState('items');\n\n /**\n * If there is no active ID and the list of items changes, select the first\n * item.\n */\n useEffect(() => {\n if (!store.getState().activeId) {\n store.setActiveId(store.first());\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, store]);\n\n return (\n \n 0 || hasEmpty) &&\n (!hideWhenNoValue || value.length > 0)\n }\n store={store}\n >\n \n {children}\n \n \n \n );\n};\n\nconst InlineComboboxInput = forwardRef<\n HTMLInputElement,\n HTMLAttributes\n>(({ className, ...props }, propRef) => {\n const {\n inputProps,\n inputRef: contextRef,\n showTrigger,\n trigger,\n } = useContext(InlineComboboxContext);\n\n const store = useComboboxContext()!;\n const value = store.useState('value');\n\n const ref = useComposedRef(propRef, contextRef);\n\n /**\n * To create an auto-resizing input, we render a visually hidden span\n * containing the input value and position the input element on top of it.\n * This works well for all cases except when input exceeds the width of the\n * container.\n */\n\n return (\n <>\n {showTrigger && trigger}\n\n \n \n {value || '\\u200B'}\n \n\n \n \n \n );\n});\n\nInlineComboboxInput.displayName = 'InlineComboboxInput';\n\nconst InlineComboboxContent: typeof ComboboxPopover = ({\n className,\n ...props\n}) => {\n // Portal prevents CSS from leaking into popover\n return (\n \n \n \n );\n};\n\nconst comboboxItemVariants = cva(\n 'relative flex h-9 select-none items-center rounded-sm px-2 py-1.5 text-sm text-foreground outline-none',\n {\n defaultVariants: {\n interactive: true,\n },\n variants: {\n interactive: {\n false: '',\n true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground',\n },\n },\n }\n);\n\nexport type InlineComboboxItemProps = {\n focusEditor?: boolean;\n keywords?: string[];\n} & ComboboxItemProps &\n Required>;\n\nconst InlineComboboxItem = ({\n className,\n focusEditor = true,\n keywords,\n onClick,\n ...props\n}: InlineComboboxItemProps) => {\n const { value } = props;\n\n const { filter, removeInput } = useContext(InlineComboboxContext);\n\n const store = useComboboxContext()!;\n\n // Optimization: Do not subscribe to value if filter is false\n const search = filter && store.useState('value');\n\n const visible = useMemo(\n () => !filter || filter({ keywords, value }, search as string),\n [filter, value, keywords, search]\n );\n\n if (!visible) return null;\n\n return (\n {\n removeInput(focusEditor);\n onClick?.(event);\n }}\n {...props}\n />\n );\n};\n\nconst InlineComboboxEmpty = ({\n children,\n className,\n}: HTMLAttributes) => {\n const { setHasEmpty } = useContext(InlineComboboxContext);\n const store = useComboboxContext()!;\n const items = store.useState('items');\n\n useEffect(() => {\n setHasEmpty(true);\n\n return () => {\n setHasEmpty(false);\n };\n }, [setHasEmpty]);\n\n if (items.length > 0) return null;\n\n return (\n \n {children}\n
\n );\n};\n\nexport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n};\n", "path": "plate-ui/inline-combobox.tsx", "target": "", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/slash-input-element.json b/apps/www/public/r/styles/default/slash-input-element.json index f026267f31..5e1410c837 100644 --- a/apps/www/public/r/styles/default/slash-input-element.json +++ b/apps/www/public/r/styles/default/slash-input-element.json @@ -5,7 +5,7 @@ ], "files": [ { - "content": "import React, { type ComponentType, type SVGProps } from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { type PlateEditor, PlateElement } from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list';\n\nimport { Icons } from '@/components/icons';\n\nimport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n} from './inline-combobox';\n\ninterface SlashCommandRule {\n icon: ComponentType>;\n onSelect: (editor: PlateEditor) => void;\n value: string;\n keywords?: string[];\n}\n\nconst rules: SlashCommandRule[] = [\n {\n icon: Icons.h1,\n value: 'Heading 1',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h1 });\n },\n },\n {\n icon: Icons.h2,\n value: 'Heading 2',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h2 });\n },\n },\n {\n icon: Icons.h3,\n value: 'Heading 3',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h3 });\n },\n },\n {\n icon: Icons.ul,\n keywords: ['ul', 'unordered list'],\n value: 'Bulleted list',\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n });\n },\n },\n {\n icon: Icons.ol,\n keywords: ['ol', 'ordered list'],\n value: 'Numbered list',\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n });\n },\n },\n {\n icon: Icons.add,\n keywords: ['inline', 'date'],\n value: 'Date',\n onSelect: (editor) => {\n editor.getTransforms(DatePlugin).insert.date();\n },\n },\n];\n\nexport const SlashInputElement = withRef(\n ({ className, ...props }, ref) => {\n const { children, editor, element } = props;\n\n return (\n \n \n \n\n \n \n No matching commands found\n \n\n {rules.map(({ icon: Icon, keywords, value, onSelect }) => (\n onSelect(editor)}\n keywords={keywords}\n >\n \n {value}\n \n ))}\n \n \n\n {children}\n \n );\n }\n);\n", + "content": "import React, { type ComponentType, type SVGProps } from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { type PlateEditor, PlateElement } from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list';\n\nimport { Icons } from '@/components/icons';\n\nimport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n} from './inline-combobox';\n\ninterface SlashCommandRule {\n icon: ComponentType>;\n onSelect: (editor: PlateEditor) => void;\n value: string;\n focusEditor?: boolean;\n keywords?: string[];\n}\n\nconst rules: SlashCommandRule[] = [\n {\n icon: Icons.h1,\n value: 'Heading 1',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h1 });\n },\n },\n {\n icon: Icons.h2,\n value: 'Heading 2',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h2 });\n },\n },\n {\n icon: Icons.h3,\n value: 'Heading 3',\n onSelect: (editor) => {\n editor.tf.toggle.block({ type: HEADING_KEYS.h3 });\n },\n },\n {\n icon: Icons.ul,\n keywords: ['ul', 'unordered list'],\n value: 'Bulleted list',\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n });\n },\n },\n {\n icon: Icons.ol,\n keywords: ['ol', 'ordered list'],\n value: 'Numbered list',\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n });\n },\n },\n {\n icon: Icons.add,\n keywords: ['inline', 'date'],\n value: 'Date',\n onSelect: (editor) => {\n editor.getTransforms(DatePlugin).insert.date();\n },\n },\n];\n\nexport const SlashInputElement = withRef(\n ({ className, ...props }, ref) => {\n const { children, editor, element } = props;\n\n return (\n \n \n \n\n \n \n No matching commands found\n \n\n {rules.map(({ icon: Icon, keywords, value, onSelect }) => (\n onSelect(editor)}\n keywords={keywords}\n >\n \n {value}\n \n ))}\n \n \n\n {children}\n \n );\n }\n);\n", "path": "plate-ui/slash-input-element.tsx", "target": "", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/toc-element.json b/apps/www/public/r/styles/default/toc-element.json new file mode 100644 index 0000000000..622d92db8c --- /dev/null +++ b/apps/www/public/r/styles/default/toc-element.json @@ -0,0 +1,18 @@ +{ + "dependencies": [ + "@udecode/plate-heading" + ], + "files": [ + { + "content": "import { cn } from '@udecode/cn';\nimport { PlateElement } from '@udecode/plate-common/react';\nimport {\n useTocElement,\n useTocElementState,\n} from '@udecode/plate-heading/react';\nimport { withRef } from '@udecode/react-utils';\nimport { cva } from 'class-variance-authority';\n\nimport { Button } from './button';\n\nconst headingItemVariants = cva(\n 'block h-auto w-full cursor-pointer truncate rounded-none px-0.5 py-1.5 text-left font-medium text-muted-foreground underline decoration-[0.5px] underline-offset-4 hover:bg-accent hover:text-muted-foreground',\n {\n variants: {\n depth: {\n 1: 'pl-0.5',\n 2: 'pl-[26px]',\n 3: 'pl-[50px]',\n },\n },\n }\n);\n\nexport const TocElement = withRef(\n ({ children, className, ...props }, ref) => {\n const state = useTocElementState();\n\n const { props: btnProps } = useTocElement(state);\n\n const { headingList } = state;\n\n return (\n \n \n {children}\n \n );\n }\n);\n", + "path": "plate-ui/toc-element.tsx", + "target": "", + "type": "registry:ui" + } + ], + "name": "toc-element", + "registryDependencies": [ + "" + ], + "type": "registry:ui" +} \ No newline at end of file diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 8634e9369f..af8892fb28 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -159,6 +159,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "toc-element": { + name: "toc-element", + type: "registry:ui", + registryDependencies: [""], + files: ["registry/default/plate-ui/toc-element.tsx"], + component: React.lazy(() => import("@/registry/default/plate-ui/toc-element.tsx")), + source: "", + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "calendar": { name: "calendar", type: "registry:ui", From cdd2e0db7452cb10cb3623d7c5bea3548c062ab8 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 15:19:28 +0800 Subject: [PATCH 031/127] AI --- apps/www/package.json | 3 +- apps/www/src/components/icons.tsx | 25 + .../playground-fixed-toolbar-buttons.tsx | 5 + .../playground-floating-toolbar-buttons.tsx | 12 +- .../www/src/lib/plate/demo/values/aiValue.tsx | 47 ++ .../plate/demo/values/usePlaygroundValue.ts | 6 +- .../default/example/playground-demo.tsx | 8 + .../action-handler/defaultActionHandler.ts | 57 ++ .../defaultSuggestionActionHandler.ts | 128 ++++ .../default/plate-ui/action-handler/index.ts | 9 + .../action-handler/selectionActionHandler.ts | 82 +++ .../selectionSuggestionActionHandler.ts | 156 ++++ .../action-handler/useActionHandler.ts | 42 ++ .../registry/default/plate-ui/ai-actions.tsx | 197 +++++ .../default/plate-ui/ai-menu-items.tsx | 88 +++ .../src/registry/default/plate-ui/ai-menu.tsx | 267 +++++++ .../default/plate-ui/ai-previdew-editor.tsx | 163 +++++ .../default/plate-ui/ai-toolbar-button.tsx | 43 ++ .../default/plate-ui/floating-toolbar.tsx | 7 + .../src/registry/default/plate-ui/menu.tsx | 679 ++++++++++++++++++ .../plate-ui/stream/getSystemMessage.ts | 10 + .../registry/default/plate-ui/stream/index.ts | 3 + .../plate-ui/stream/streamInsertText.ts | 166 +++++ .../stream/streamInsertTextSelection.ts | 118 +++ .../plate-ui/stream/streamTraversal.ts | 59 ++ .../default/plate-ui/utils/getContent.ts | 59 ++ .../utils/getFirstBlockSelectedNode.ts | 9 + .../plate-ui/utils/getNextPathByNumber.ts | 11 + .../registry/default/plate-ui/utils/index.ts | 3 + yarn.lock | 57 +- 30 files changed, 2501 insertions(+), 18 deletions(-) create mode 100644 apps/www/src/lib/plate/demo/values/aiValue.tsx create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/index.ts create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts create mode 100644 apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts create mode 100644 apps/www/src/registry/default/plate-ui/ai-actions.tsx create mode 100644 apps/www/src/registry/default/plate-ui/ai-menu-items.tsx create mode 100644 apps/www/src/registry/default/plate-ui/ai-menu.tsx create mode 100644 apps/www/src/registry/default/plate-ui/ai-previdew-editor.tsx create mode 100644 apps/www/src/registry/default/plate-ui/ai-toolbar-button.tsx create mode 100644 apps/www/src/registry/default/plate-ui/menu.tsx create mode 100644 apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts create mode 100644 apps/www/src/registry/default/plate-ui/stream/index.ts create mode 100644 apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts create mode 100644 apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts create mode 100644 apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts create mode 100644 apps/www/src/registry/default/plate-ui/utils/getContent.ts create mode 100644 apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts create mode 100644 apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts create mode 100644 apps/www/src/registry/default/plate-ui/utils/index.ts diff --git a/apps/www/package.json b/apps/www/package.json index 5a26d41e4e..ade1476029 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,7 +35,7 @@ "react-dom": "^18.3.1" }, "dependencies": { - "@ariakit/react": "0.4.11", + "@ariakit/react": "0.4.7", "@radix-ui/colors": "3.0.0", "@radix-ui/react-accessible-icon": "^1.1.0", "@radix-ui/react-accordion": "^1.2.0", @@ -138,6 +138,7 @@ "framer-motion": "^11.5.4", "lodash.template": "^4.5.0", "lucide-react": "^0.441.0", + "match-sorter": "6.3.4", "next": "14.3.0-canary.43", "next-contentlayer2": "^0.4.6", "next-themes": "^0.3.0", diff --git a/apps/www/src/components/icons.tsx b/apps/www/src/components/icons.tsx index 341c547540..1c87699bf4 100644 --- a/apps/www/src/components/icons.tsx +++ b/apps/www/src/components/icons.tsx @@ -6,12 +6,14 @@ import { cva } from 'class-variance-authority'; import { type LucideIcon, type LucideProps, + Album, AlignCenter, AlignJustify, AlignLeft, AlignRight, ArrowLeft, ArrowRight, + BadgeHelp, Baseline, Bold, Check, @@ -19,10 +21,12 @@ import { ChevronLeft, ChevronRight, ChevronsUpDown, + CircleStop, ClipboardCheck, Code2, Combine, Copy, + CornerUpLeft, DownloadCloud, DownloadIcon, ExternalLink, @@ -42,12 +46,15 @@ import { Indent, Italic, Keyboard, + Languages, Laptop, Link, Link2, Link2Off, List, + ListMinus, ListOrdered, + ListPlus, Loader2, MessageSquare, MessageSquarePlus, @@ -58,6 +65,7 @@ import { Outdent, PaintBucket, Paperclip, + PartyPopper, Pen, PenLine, PenTool, @@ -68,9 +76,11 @@ import { RectangleVertical, RotateCcw, Search, + Send, Settings, Settings2, Smile, + Sparkles, Square, Strikethrough, Subscript, @@ -82,6 +92,7 @@ import { Underline, Ungroup, Unlink, + Wand, WrapText, X, } from 'lucide-react'; @@ -377,6 +388,7 @@ const LayoutIcon = (props: LucideProps) => ( export const Icons = { LayoutIcon, add: Plus, + ai: Sparkles, alignCenter: AlignCenter, alignJustify: AlignJustify, alignLeft: AlignLeft, @@ -409,11 +421,14 @@ export const Icons = { comment: MessageSquare, commentAdd: MessageSquarePlus, conflict: Unlink, + continueWrite: PenLine, + continuewrite: PenLine, copy: Copy, copyDone: ClipboardCheck, delete: Trash, dependency: Link, discord, + done: Check, doubleColumn: DoubleColumnOutlined, doubleSideDoubleColumn: DoubleSideDoubleColumnOutlined, download: DownloadIcon, @@ -424,6 +439,7 @@ export const Icons = { embed: Film, emoji: Smile, excalidraw: PenTool, + explain: BadgeHelp, externalLink: ExternalLink, gitHub, h1: Heading1, @@ -435,6 +451,7 @@ export const Icons = { highlight: Highlighter, hr: Minus, image: Image, + improve: Wand, indent: Indent, italic: Italic, kbd: Keyboard, @@ -442,6 +459,8 @@ export const Icons = { leftSideDoubleColumn: LeftSideDoubleColumnOutlined, lineHeight: WrapText, link: Link2, + makeLonger: ListPlus, + makeShorter: ListMinus, media: Image, minus: Minus, moon: Moon, @@ -461,10 +480,14 @@ export const Icons = { row: RectangleHorizontal, search: Search, settings: Settings, + simplify: PartyPopper, spinner: Loader2, + stop: CircleStop, strikethrough: Strikethrough, + submit: Send, subscript: Subscript, suggesting: PenLine, + summarize: Album, sun: SunMedium, superscript: Superscript, table: Table, @@ -472,7 +495,9 @@ export const Icons = { text: Text, threeColumn: ThreeColumnOutlined, todo: Square, + translate: Languages, trash: Trash, + tryAgain: CornerUpLeft, twitter: (props: IconProps) => ( + + + + diff --git a/apps/www/src/components/plate-ui/playground-floating-toolbar-buttons.tsx b/apps/www/src/components/plate-ui/playground-floating-toolbar-buttons.tsx index 31eb066d39..060a4e845a 100644 --- a/apps/www/src/components/plate-ui/playground-floating-toolbar-buttons.tsx +++ b/apps/www/src/components/plate-ui/playground-floating-toolbar-buttons.tsx @@ -13,10 +13,14 @@ import { LinkPlugin } from '@udecode/plate-link/react'; import { CheckPlugin } from '@/components/context/check-plugin'; import { Icons } from '@/components/icons'; +import { AIToolbarButton } from '@/registry/default/plate-ui/ai-toolbar-button'; import { CommentToolbarButton } from '@/registry/default/plate-ui/comment-toolbar-button'; import { LinkToolbarButton } from '@/registry/default/plate-ui/link-toolbar-button'; import { MarkToolbarButton } from '@/registry/default/plate-ui/mark-toolbar-button'; -import { ToolbarSeparator } from '@/registry/default/plate-ui/toolbar'; +import { + ToolbarGroup, + ToolbarSeparator, +} from '@/registry/default/plate-ui/toolbar'; import { PlaygroundMoreDropdownMenu } from './playground-more-dropdown-menu'; import { PlaygroundTurnIntoDropdownMenu } from './playground-turn-into-dropdown-menu'; @@ -28,6 +32,12 @@ export function PlaygroundFloatingToolbarButtons() { <> {!readOnly && ( <> + + + + Ask AI + + diff --git a/apps/www/src/lib/plate/demo/values/aiValue.tsx b/apps/www/src/lib/plate/demo/values/aiValue.tsx new file mode 100644 index 0000000000..4ab27f13e7 --- /dev/null +++ b/apps/www/src/lib/plate/demo/values/aiValue.tsx @@ -0,0 +1,47 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@udecode/plate-test-utils'; + +jsx; + +export const aiValue: any = ( + + ✨ AI Assistant + + To trigger the AI assistant, you can: + + + Press the Space key on a new empty block + + + Select text then click the AI icon in the floating toolbar + + + + Type your prompt or question in the input field that appears. + + + + + Press Enter to submit your prompt and generate AI-assisted content. + + + + + The AI will generate content based on your prompt. You can then: + + + + Edit the generated content as needed + + + Accept the content as is + + + Regenerate the content with a different prompt + + + + + +); diff --git a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts index 227b71ded4..cd38c20c81 100644 --- a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts +++ b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts @@ -6,6 +6,7 @@ import { settingsStore } from '@/components/context/settings-store'; import { type ValueId, customizerPlugins } from '@/config/customizer-plugins'; import { mapNodeId } from '@/plate/demo/mapNodeId'; +import { aiValue } from './aiValue'; import { alignValue } from './alignValue'; import { autoformatValue } from './autoformatValue'; import { basicElementsValue } from './basicElementsValue'; @@ -71,9 +72,12 @@ export const usePlaygroundValue = (id?: ValueId): MyValue => { return mapNodeId(newValue); } - //AI if (enabled.toc) value.unshift(...tocValue); + //AI if (enabled.copilot) value.unshift(...copilotValue); + + value.unshift(...aiValue); + // Marks if (enabled.color || enabled.backgroundColor) value.push(...fontValue); if (enabled.highlight) value.push(...highlightValue); diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 2d6dc3abc5..65ce9b852b 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -7,6 +7,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import type { ValueId } from '@/config/customizer-plugins'; import { cn } from '@udecode/cn'; +import { AIPlugin } from '@udecode/plate-ai/react'; import { AlignPlugin } from '@udecode/plate-alignment/react'; import { AutoformatPlugin } from '@udecode/plate-autoformat/react'; import { @@ -80,6 +81,7 @@ import { softBreakPlugin } from '@/plate/demo/plugins/softBreakPlugin'; import { tabbablePlugin } from '@/plate/demo/plugins/tabbablePlugin'; import { commentsData, usersData } from '@/plate/demo/values/commentsValue'; import { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue'; +import { AIMenu } from '@/registry/default/plate-ui/ai-menu'; import { CommentsPopover } from '@/registry/default/plate-ui/comments-popover'; import { CursorOverlay } from '@/registry/default/plate-ui/cursor-overlay'; import { Editor } from '@/registry/default/plate-ui/editor'; @@ -161,6 +163,12 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { enableMerging: id === 'tableMerge', }, }), + AIPlugin.configure({ + options: { + scrollContainerSelector: `#${scrollSelector}`, + }, + render: { aboveEditable: AIMenu }, + }), TodoListPlugin, TogglePlugin, ExcalidrawPlugin, diff --git a/apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts new file mode 100644 index 0000000000..cb761125eb --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts @@ -0,0 +1,57 @@ +'use client'; +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { + ACTION_CONTINUE_WRITE, + ACTION_EXPLAIN, + ACTION_SUMMARIZE, + GROUP_LANGUAGES, +} from '@/registry/default/plate-ui/ai-actions'; +import { streamInsertText } from '@/registry/default/plate-ui/stream'; +import { serializeAI } from '@/registry/default/plate-ui/utils'; + +import type { ActionHandlerOptions } from './useActionHandler'; + +export const defaultActionHandler = async ( + editor: PlateEditor, + { group, value }: ActionHandlerOptions +) => { + if (group === GROUP_LANGUAGES) { + const content = serializeAI(editor); + await streamInsertText(editor, { + prompt: `Keep the original paragraph format. Translate the following article to ${value?.slice(7)}: ${content}`, + }); + + return; + } + + switch (value) { + case ACTION_CONTINUE_WRITE: { + const content = serializeAI(editor); + + await streamInsertText(editor, { + prompt: `Continue writing the following article in 3-5 sentences: ${content}`, + }); + + break; + } + case ACTION_SUMMARIZE: { + const content = serializeAI(editor); + + await streamInsertText(editor, { + prompt: `Summarize the following article in 3-5 sentences: ${content}`, + }); + + break; + } + case ACTION_EXPLAIN: { + const content = serializeAI(editor); + + await streamInsertText(editor, { + prompt: `Explain the following article in 3-5 sentences: ${content}`, + }); + + break; + } + } +}; diff --git a/apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts new file mode 100644 index 0000000000..97ed746bb7 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts @@ -0,0 +1,128 @@ +'use client'; +import { AIPlugin } from '@udecode/plate-ai/react'; +import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; +import { serializeMdNodes } from '@udecode/plate-markdown'; +import { getEndPoint, setSelection, withMerging } from '@udecode/slate'; +import { focusEditor } from '@udecode/slate-react'; +import { insertEmptyElement } from '@udecode/slate-utils'; +import { Path } from 'slate'; + +import { + ACTION_SUGGESTION_CLOSE, + ACTION_SUGGESTION_CONTINUE_WRITE, + ACTION_SUGGESTION_DONE, + ACTION_SUGGESTION_MAKE_LONGER, + ACTION_SUGGESTION_TRY_AGAIN, +} from '@/registry/default/plate-ui/ai-actions'; +import { streamInsertText } from '@/registry/default/plate-ui/stream'; +import { + getBlockSelectedEntries, + getFirstBlockSelectedNode, +} from '@/registry/default/plate-ui/utils'; + +import type { ActionHandlerOptions } from './useActionHandler'; + +export const defaultSuggestionActionHandler = ( + editor: PlateEditor, + { group: _, value }: ActionHandlerOptions +) => { + switch (value) { + case ACTION_SUGGESTION_CONTINUE_WRITE: { + const entries = getBlockSelectedEntries(editor); + const nodes = Array.from(entries, (entry) => entry[0]); + + if (!nodes) return; + + const md = serializeMdNodes(nodes as any); + + const curPath = editor.getOptions(AIPlugin).curNodeEntry?.[1]; + + const newLine = Path.next(curPath!); + withMerging(editor, () => { + insertEmptyElement(editor, ParagraphPlugin.key, { + at: newLine, + select: true, + }); + }); + + setTimeout(() => { + void streamInsertText(editor, { + prompt: `continue write the following article in 3-5 sentences: ${md}`, + startWritingPath: newLine, + }); + }, 0); + + break; + } + case ACTION_SUGGESTION_MAKE_LONGER: { + const first = getFirstBlockSelectedNode(editor); + + editor.setOptions(AIPlugin, { openEditorId: null }); + editor.undo(); + editor.history.redos.pop(); + + setTimeout(() => { + const lastGenerate = editor.getOptions(AIPlugin).lastGenerate; + editor.setOptions(AIPlugin, { openEditorId: editor.id }); + void streamInsertText(editor, { + prompt: `make longer with the following article ${lastGenerate}`, + startWritingPath: first[1], + }); + }, 0); + + break; + } + // BUG: first block focus + case ACTION_SUGGESTION_DONE: { + editor.getApi(AIPlugin).ai.hide(); + clearBlockSelected(editor); + const curNodeEntry = editor.getOptions(AIPlugin).curNodeEntry; + + // set selection to last point + if (curNodeEntry) { + const endPoint = getEndPoint(editor, curNodeEntry[1]); + setSelection(editor, { anchor: endPoint, focus: endPoint }); + setTimeout(() => { + focusEditor(editor); + }, 0); + } + + break; + } + // TODO: + case ACTION_SUGGESTION_CLOSE: { + editor.getApi(AIPlugin).ai.hide(); + clearBlockSelected(editor); + setTimeout(() => { + // set selection to last point + focusEditor(editor); + }, 0); + + break; + } + case ACTION_SUGGESTION_TRY_AGAIN: { + const first = getFirstBlockSelectedNode(editor); + editor.undo(); + editor.history.redos.pop(); + + setTimeout(() => { + const lastPrompt = editor.getOptions(AIPlugin).lastPrompt; + + void streamInsertText(editor, { + prompt: lastPrompt!, + startWritingPath: (first as any)[1], + }); + }, 0); + + break; + } + } +}; + +export const clearBlockSelected = (editor: PlateEditor) => { + const { blockSelectionStore } = editor as any; + + if (blockSelectionStore) { + blockSelectionStore.set.resetSelectedIds(); + } +}; diff --git a/apps/www/src/registry/default/plate-ui/action-handler/index.ts b/apps/www/src/registry/default/plate-ui/action-handler/index.ts new file mode 100644 index 0000000000..45d1effb0f --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/index.ts @@ -0,0 +1,9 @@ +export * from './defaultActionHandler'; + +export * from './defaultSuggestionActionHandler'; + +export * from './selectionActionHandler'; + +export * from './selectionSuggestionActionHandler'; + +export * from './useActionHandler'; \ No newline at end of file diff --git a/apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts new file mode 100644 index 0000000000..bfce009b9b --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts @@ -0,0 +1,82 @@ +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { + ACTION_SELECTION_FIX_SPELLING, + ACTION_SELECTION_IMPROVE_WRITING, + ACTION_SELECTION_MAKE_LONGER, + ACTION_SELECTION_MAKE_SHORTER, + ACTION_SELECTION_SIMPLIFY_LANGUAGE, + GROUP_SELECTION_LANGUAGES, +} from '@/registry/default/plate-ui/ai-actions'; +import { streamInsertTextSelection } from '@/registry/default/plate-ui/stream'; +import { getContent } from '@/registry/default/plate-ui/utils'; + +import type { ActionHandlerOptions } from './useActionHandler'; + +export const selectionActionHandler = ( + editor: PlateEditor, + aiEditor: PlateEditor, + { group, value }: ActionHandlerOptions +) => { + if (group === GROUP_SELECTION_LANGUAGES) { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `translate the following article to ${value?.slice(7)}: ${content} Please keep the original paragraph format.`, + }); + + return; + } + + switch (value) { + case ACTION_SELECTION_IMPROVE_WRITING: { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `improve the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_FIX_SPELLING: { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `Correct the grammatical or spelling errors in the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_MAKE_SHORTER: { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `make shorter with the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_MAKE_LONGER: { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `make longer with the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_SIMPLIFY_LANGUAGE: { + const content = getContent(editor, aiEditor); + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `Simplify the language in the following content: ${content}`, + }); + } + } +}; diff --git a/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts new file mode 100644 index 0000000000..cb301b71c6 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts @@ -0,0 +1,156 @@ +'use client'; +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { AIPlugin } from '@udecode/plate-ai/react'; +import { nanoid } from '@udecode/plate-core'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; +import { insertNodes, isBlock, removeNodes, withMerging } from '@udecode/slate'; +import { Path, Range } from 'slate'; + +import { + ACTION_SELECTION_SUGGESTION_CONTINUE_WRITE, + ACTION_SELECTION_SUGGESTION_DONE, + ACTION_SELECTION_SUGGESTION_INSERT_BELOW, + ACTION_SELECTION_SUGGESTION_MAKE_LONGER, + ACTION_SELECTION_SUGGESTION_REPLACE, + ACTION_SELECTION_SUGGESTION_TRY_AGAIN, +} from '@/registry/default/plate-ui/ai-actions'; +import { streamInsertTextSelection } from '@/registry/default/plate-ui/stream'; +import { getContent } from '@/registry/default/plate-ui/utils'; + +import type { ActionHandlerOptions } from './useActionHandler'; + +import { clearBlockSelected } from './defaultSuggestionActionHandler'; + +export const selectionSuggestionActionHandler = ( + editor: PlateEditor, + aiEditor: PlateEditor, + { group: _, value }: ActionHandlerOptions +) => { + switch (value) { + case ACTION_SELECTION_SUGGESTION_REPLACE: { + editor.getApi(AIPlugin).ai.hide(); + const nodes = aiEditor.children; + + let path: Path = []; + + const selectedBlocks = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + + const selectedIds = editor.getOptions(BlockSelectionPlugin).selectedIds; + + if (selectedBlocks.length > 0 && selectedIds?.size) { + path = selectedBlocks[0][1]; + + removeNodes(editor, { + at: [], + match: (n) => selectedIds.has(n.id), + }); + } else { + const start = Range.start(editor.selection!); + path = [start.path[0]]; + + removeNodes(editor, { + at: editor.selection!, + match: (n) => isBlock(editor, n), + mode: 'highest', + }); + } + + const ids: string[] = []; + + nodes.forEach((node) => { + const id = nanoid(); + ids.push(id); + node.id = id; + }); + + withMerging(editor, () => { + insertNodes(editor, nodes, { at: path }); + }); + + setTimeout(() => { + clearBlockSelected(editor); + ids.forEach((id) => + editor + .getApi(BlockSelectionPlugin) + .blockSelection.addSelectedRow(id, { clear: false }) + ); + }, 0); + + break; + } + case ACTION_SELECTION_SUGGESTION_INSERT_BELOW: { + editor.getApi(AIPlugin).ai.hide(); + const nodes = aiEditor.children; + + const selectedBlocks = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + + let path: Path = []; + + if (selectedBlocks.length > 0) { + const lastBlockPath = selectedBlocks.at(-1)![1]; + path = Path.next(lastBlockPath); + } else { + const end = Range.end(editor.selection!); + path = Path.next([end.path[0]]); + } + + const ids: string[] = []; + + nodes.forEach((node) => { + const id = nanoid(); + ids.push(id); + node.id = id; + }); + + insertNodes(editor, nodes, { at: path }); + + setTimeout(() => { + clearBlockSelected(editor); + ids.forEach((id) => + editor + .getApi(BlockSelectionPlugin) + .blockSelection.addSelectedRow(id, { clear: false }) + ); + }, 0); + + break; + } + case ACTION_SELECTION_SUGGESTION_CONTINUE_WRITE: { + const content = getContent(editor, aiEditor); + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `continue write the following: ${content}`, + }); + + break; + } + case ACTION_SELECTION_SUGGESTION_MAKE_LONGER: { + const content = getContent(editor, aiEditor); + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `make longer with the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_SUGGESTION_TRY_AGAIN: { + const content = getContent(editor, aiEditor); + + void streamInsertTextSelection(editor, aiEditor, { + prompt: `rewrite the following content: ${content}`, + }); + + break; + } + case ACTION_SELECTION_SUGGESTION_DONE: { + editor.getApi(AIPlugin).ai.hide(); + + break; + } + } +}; diff --git a/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts new file mode 100644 index 0000000000..a2696baaf6 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts @@ -0,0 +1,42 @@ +'use client'; +import { useEffect } from 'react'; + +import type { actionGroup } from '@/registry/default/plate-ui/menu'; + +import { type PlateEditor, useEditorRef } from '@udecode/plate-core/react'; + +import { defaultActionHandler } from './defaultActionHandler'; +import { defaultSuggestionActionHandler } from './defaultSuggestionActionHandler'; +import { selectionActionHandler } from './selectionActionHandler'; +import { selectionSuggestionActionHandler } from './selectionSuggestionActionHandler'; + +export const useActionHandler = ( + action: actionGroup | null, + aiEditor: PlateEditor +) => { + const editor = useEditorRef(); + + useEffect(() => { + if (!action) return; + + const { group, value } = action; + + if (!value) return; + + void defaultActionHandler(editor, { group, value }); + + void defaultSuggestionActionHandler(editor, { group, value }); + + void selectionActionHandler(editor, aiEditor, { group, value }); + + void selectionSuggestionActionHandler(editor, aiEditor, { + group, + value, + }); + }, [action, aiEditor, editor]); +}; + +export interface ActionHandlerOptions { + value: string; + group?: string; +} diff --git a/apps/www/src/registry/default/plate-ui/ai-actions.tsx b/apps/www/src/registry/default/plate-ui/ai-actions.tsx new file mode 100644 index 0000000000..a0ae554c12 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-actions.tsx @@ -0,0 +1,197 @@ +import { Icons } from '@/components/icons'; + +import type { Action } from './menu'; + +/** Common */ +const ACTION_CHINESE = 'action_chinese'; +const ACTION_ENGLISH = 'action_english'; +const ACTION_KOREAN = 'action_korean'; + +const languages = [ + { label: 'English', value: ACTION_ENGLISH }, + { label: 'Chinese', value: ACTION_CHINESE }, + { label: 'Korean', value: ACTION_KOREAN }, +] satisfies Action[]; + +/** DefaultItems */ + +export const ACTION_CONTINUE_WRITE = 'action_continue_write'; + +export const ACTION_EXPLAIN = 'action_explain'; + +export const ACTION_SUMMARIZE = 'action_summarize'; + +export const GROUP_LANGUAGES = 'group_languages'; + +export const GROUP_ALIGN = 'group_align'; + +export const ACTION_CAPTION = 'action_cation'; + +export const DefaultActions = { + Summarize: { + icon: , + label: 'Summarize', + value: ACTION_SUMMARIZE, + }, + continueWrite: { + icon: , + label: 'Continue writing', + value: ACTION_CONTINUE_WRITE, + }, + explain: { + icon: , + label: 'Explain this', + value: ACTION_EXPLAIN, + }, + translate: { + group: GROUP_LANGUAGES, + icon: , + items: languages, + label: 'Translate', + }, +} satisfies Record; + +export const defaultValues = { + // [GROUP_LANGUAGES]: ACTION_ENGLISH, +}; + +/** SuggestionItems */ + +export const ACTION_SUGGESTION_CLOSE = 'action_close'; + +export const ACTION_SUGGESTION_CONTINUE_WRITE = + 'action_suggestion_continue_write'; + +export const ACTION_SUGGESTION_DONE = 'action_done'; + +export const ACTION_SUGGESTION_MAKE_LONGER = 'action_longer'; + +export const ACTION_SUGGESTION_TRY_AGAIN = 'action_try_again'; + +export const DefaultSuggestionActions = { + close: { + icon: , + label: 'Close', + value: ACTION_SUGGESTION_CLOSE, + }, + continueWrite: { + icon: , + label: 'Continue writing', + value: ACTION_SUGGESTION_CONTINUE_WRITE, + }, + done: { + icon: , + label: 'Done', + value: ACTION_SUGGESTION_DONE, + }, + makeLonger: { + icon: , + label: 'Make longer', + value: ACTION_SUGGESTION_MAKE_LONGER, + }, + tryAgain: { + icon: , + label: 'Try again', + value: ACTION_SUGGESTION_TRY_AGAIN, + }, +}; + +/** SelectionItems */ + +export const ACTION_SELECTION_IMPROVE_WRITING = 'action_improve_writing'; + +export const ACTION_SELECTION_FIX_SPELLING = 'action_fix_spelling'; + +export const ACTION_SELECTION_MAKE_LONGER = 'action_selection_make_longer'; + +export const ACTION_SELECTION_MAKE_SHORTER = 'action_selection_make_shorter'; + +export const ACTION_SELECTION_SIMPLIFY_LANGUAGE = 'action_simplify_language'; + +export const GROUP_SELECTION_LANGUAGES = 'group_selection_languages'; + +export const SelectionActions = { + fixSpell: { + icon: , + label: 'Fix spelling & grammar', + value: ACTION_SELECTION_FIX_SPELLING, + }, + improveWriting: { + icon: , + label: 'Improve writing', + value: ACTION_SELECTION_IMPROVE_WRITING, + }, + makeLonger: { + icon: , + label: 'Make longer', + value: ACTION_SELECTION_MAKE_LONGER, + }, + makeShorter: { + icon: , + label: 'Make shorter', + value: ACTION_SELECTION_MAKE_SHORTER, + }, + simplifyLanguage: { + icon: , + label: 'Simplify language', + value: ACTION_SELECTION_SIMPLIFY_LANGUAGE, + }, + translate: { + group: GROUP_SELECTION_LANGUAGES, + icon: , + items: languages, + label: 'Translate', + }, +}; + +/** SelectionSuggestionItems */ +export const ACTION_SELECTION_SUGGESTION_REPLACE = + 'action_selection_suggestion_replace'; + +export const ACTION_SELECTION_SUGGESTION_INSERT_BELOW = + 'action_selection_suggestion_below'; + +export const ACTION_SELECTION_SUGGESTION_MAKE_LONGER = + 'action_selection_suggestion_make_longer'; + +export const ACTION_SELECTION_SUGGESTION_DONE = + 'action_selection_suggestion_done'; + +export const ACTION_SELECTION_SUGGESTION_TRY_AGAIN = + 'action_selection_suggestion_try_again'; + +export const ACTION_SELECTION_SUGGESTION_CONTINUE_WRITE = + 'action_selection_suggestion_continue_write'; + +export const SelectionSuggestionActions = { + continueWrite: { + icon: , + label: 'Continue writing', + value: ACTION_SELECTION_SUGGESTION_CONTINUE_WRITE, + }, + done: { + icon: , + label: 'Done', + value: ACTION_SELECTION_SUGGESTION_DONE, + }, + insertBelow: { + icon: , + label: 'Insert below', + value: ACTION_SELECTION_SUGGESTION_INSERT_BELOW, + }, + makeLonger: { + icon: , + label: 'Make longer', + value: ACTION_SELECTION_SUGGESTION_MAKE_LONGER, + }, + replace: { + icon: , + label: 'Replace selection', + value: ACTION_SELECTION_SUGGESTION_REPLACE, + }, + tryAgain: { + icon: , + label: 'Try again', + value: ACTION_SELECTION_SUGGESTION_TRY_AGAIN, + }, +}; diff --git a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx new file mode 100644 index 0000000000..e0d5a80f6c --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx @@ -0,0 +1,88 @@ +import { + DefaultActions, + DefaultSuggestionActions, + SelectionActions, + SelectionSuggestionActions, +} from './ai-actions'; +import { + Menu, + MenuGroup, + MenuItem, + MenuSeparator, + renderMenuItems, +} from './menu'; + +export const DefaultItems = () => { + return ( + <> + + + + + + + + {renderMenuItems(DefaultActions.translate)} + + + + + ); +}; + +export const DefaultSuggestionItems = () => { + return ( + <> + + + + + + + + + + ); +}; + +export const SelectionItems = () => { + return ( + <> + + + + + + + + + + + {renderMenuItems(SelectionActions.translate)} + + + + ); +}; + +export const SelectionSuggestionItems = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx new file mode 100644 index 0000000000..f82d5a3b65 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -0,0 +1,267 @@ +/* eslint-disable tailwindcss/no-custom-classname */ +'use client'; + +import React, { + type KeyboardEvent, + memo, + startTransition, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; + +import { cn } from '@udecode/cn'; +import { AIPlugin } from '@udecode/plate-ai/react'; +import { useEditorPlugin } from '@udecode/plate-core/react'; +import { focusEditor } from '@udecode/slate-react'; +import isHotkey from 'is-hotkey'; + +import { Icons } from '@/components/icons'; +import { Button } from '@/registry/default/plate-ui/button'; + +import { useActionHandler } from './action-handler'; +import { + DefaultActions, + DefaultSuggestionActions, + SelectionActions, + SelectionSuggestionActions, + defaultValues, +} from './ai-actions'; +import { + DefaultItems, + DefaultSuggestionItems, + SelectionItems, + SelectionSuggestionItems, +} from './ai-menu-items'; +import { AIPreviewEditor } from './ai-previdew-editor'; +import { + type actionGroup, + Ariakit, + Menu, + comboboxVariants, + filterAndBuildMenuTree, + renderSearchMenuItems, +} from './menu'; +import { streamInsertText, streamInsertTextSelection } from './stream'; +import { getContent } from './utils'; + +// eslint-disable-next-line react/display-name +export const AIMenu = memo(({ children }: React.PropsWithChildren) => { + const { api, editor, setOption, setOptions, useOption } = + useEditorPlugin(AIPlugin); + + const isOpen = useOption('isOpen', editor.id); + const action = useOption('action'); + const aiState = useOption('aiState'); + const menuType = useOption('menuType'); + const setAction = (action: actionGroup) => setOption('action', action); + + const { aiEditor } = editor.useOptions(AIPlugin); + + const isModalOpenRef = React.useRef(false); + // init + const menu = Ariakit.useMenuStore(); + useEffect(() => { + setOptions({ + store: menu, + }); + // eslint-disable-next`-line react-hooks/exhaustive-deps + }, [isOpen, menu, setOptions]); + + const [values, setValues] = useState(defaultValues); + const [searchValue, setSearchValue] = useState(''); + + const streamInsert = useCallback(async () => { + if (!aiEditor) return; + if (menuType === 'selection') { + const content = getContent(editor, aiEditor); + + await streamInsertTextSelection(editor, aiEditor, { + prompt: `user prompt is ${searchValue} the content is ${content}`, + }); + } else if (menuType === 'space') { + await streamInsertText(editor, { + prompt: searchValue, + }); + } + }, [aiEditor, editor, menuType, searchValue]); + + const onInputKeyDown = async (e: KeyboardEvent) => { + if (isHotkey('backspace')(e) && searchValue.length === 0) { + e.preventDefault(); + api.ai.hide(); + focusEditor(editor); + } + if (isHotkey('enter')(e)) await streamInsert(); + }; + + const onCloseMenu = useCallback(() => { + // close menu if ai is not generating + if (aiState === 'idle' || aiState === 'done') { + api.ai.hide(); + focusEditor(editor); + } + // abort if ai is generating + if (aiState === 'generating' || aiState === 'requesting') { + api.ai.abort(); + } + }, [aiState, api.ai, editor]); + + // close on escape + useEffect(() => { + const keydown = (e: any) => { + if (!isOpen || !isHotkey('escape')(e)) return; + + onCloseMenu(); + }; + + document.addEventListener('keydown', keydown); + + return () => { + document.removeEventListener('keydown', keydown); + }; + }, [aiState, api.ai, editor, isOpen, onCloseMenu]); + + // block editor while generating + // const setReadOnly = usePlateStore().set.readOnly(); + useEffect(() => { + if (aiState === 'generating') { + // setReadOnly(true); + } + if (aiState === 'done') { + // setReadOnly(false); + setSearchValue(''); + } + }, [aiState, setSearchValue]); + + useActionHandler(action, aiEditor!); + + const [CurrentItems, CurrentActions] = React.useMemo(() => { + if (aiState === 'done') { + if (menuType === 'selection') + return [SelectionSuggestionItems, SelectionSuggestionActions]; + + return [DefaultSuggestionItems, DefaultSuggestionActions]; + } + if (menuType === 'selection') return [SelectionItems, SelectionActions]; + + return [DefaultItems, DefaultActions]; + }, [aiState, menuType]); + + /** IME */ + const [isComposing, setIsComposing] = useState(false); + + const searchItems = useMemo(() => { + return isComposing + ? [] + : filterAndBuildMenuTree(Object.values(CurrentActions), searchValue); + }, [CurrentActions, isComposing, searchValue]); + + return ( + <> + { + if (aiState === 'idle') return editor.getApi(AIPlugin).ai.hide(); + if (isModalOpenRef.current) return; + + e.preventDefault(); + isModalOpenRef.current = true; + + // pushModal('Discard', { + // onCancel: () => { + // setTimeout(() => { + // editor.getApi(AIPlugin).ai.focusMenu(); + // }, 0); + // }, + // onConfirm: () => { + // // TODO: cancel stream + // editor.getApi(AIPlugin).ai.hide(); + // }, + // onSettled: () => { + // isModalOpenRef.current = false; + // }, + // }); + }} + onValueChange={(value) => startTransition(() => setSearchValue(value))} + onValuesChange={(values: typeof defaultValues) => { + setValues(values); + }} + combobox={ + setSearchValue(e.target.value)} + onCompositionEnd={() => setIsComposing(false)} + onCompositionStart={() => setIsComposing(true)} + onKeyDown={onInputKeyDown} + placeholder={ + aiState === 'done' + ? 'Tell AI what todo next' + : 'Ask AI to write something' + } + /> + } + comboboxClassName={cn( + menuType === 'selection' && + aiState !== 'idle' && + 'rounded-t-none border-t-0 ' + )} + comboboxListClassName={cn( + searchItems && searchItems.length === 0 && 'border-0' + )} + comboboxSubmitButton={ + + } + flip={false} + injectAboveMenu={useMemo(() => { + if (menuType === 'selection' && aiState !== 'idle') + return ; + }, [aiState, menuType])} + loadingPlaceholder={ +
+
+ AI is Writing +
+
+ + onCloseMenu()} + > +
+ } + setAction={setAction} + store={menu} + values={values} + > + {renderSearchMenuItems(searchItems, { hiddenOnEmpty: true }) ?? ( + + )} +
+ {children} + + ); +}); diff --git a/apps/www/src/registry/default/plate-ui/ai-previdew-editor.tsx b/apps/www/src/registry/default/plate-ui/ai-previdew-editor.tsx new file mode 100644 index 0000000000..d4a274f2b7 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-previdew-editor.tsx @@ -0,0 +1,163 @@ +import { memo } from 'react'; + +import { cn, withProps } from '@udecode/cn'; +import { type AIPluginConfig, AIPlugin } from '@udecode/plate-ai/react'; +import { + BoldPlugin, + CodePlugin, + ItalicPlugin, + StrikethroughPlugin, + SubscriptPlugin, + SuperscriptPlugin, + UnderlinePlugin, +} from '@udecode/plate-basic-marks/react'; +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; +import { + CodeBlockPlugin, + CodeLinePlugin, + CodeSyntaxPlugin, +} from '@udecode/plate-code-block/react'; +import { + ParagraphPlugin, + Plate, + createPlateEditor, + useEditorPlugin, +} from '@udecode/plate-core/react'; +import { + FontBackgroundColorPlugin, + FontColorPlugin, + FontSizePlugin, +} from '@udecode/plate-font/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; +import { HeadingPlugin } from '@udecode/plate-heading/react'; +import { HighlightPlugin } from '@udecode/plate-highlight/react'; +import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react'; +import { IndentPlugin } from '@udecode/plate-indent/react'; +import { IndentListPlugin } from '@udecode/plate-indent-list/react'; +import { KbdPlugin } from '@udecode/plate-kbd/react'; +import { LinkPlugin } from '@udecode/plate-link/react'; +import { MarkdownPlugin } from '@udecode/plate-markdown'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; +import { PlateLeaf } from '@udecode/plate-utils/react'; +import Prism from 'prismjs'; + +import { CodeBlockElement } from './code-block-element'; +import { CodeLeaf } from './code-leaf'; +import { CodeLineElement } from './code-line-element'; +import { CodeSyntaxLeaf } from './code-syntax-leaf'; +import { Editor } from './editor'; +import { HeadingElement } from './heading-element'; +import { HrElement } from './hr-element'; +import { LinkElement } from './link-element'; +import { LinkFloatingToolbar } from './link-floating-toolbar'; +import { ParagraphElement } from './paragraph-element'; + +interface props { + className?: string; +} + +// eslint-disable-next-line react/display-name +export const AIPreviewEditor = memo(({ className }: props) => { + const { editor } = useEditorPlugin(AIPlugin); + const { aiEditor } = editor.useOptions(AIPlugin); + + return ( + + + + ); +}); + +export const createAIEditor = () => { + const editor = createPlateEditor({ + id: 'ai-minimal-editor', + override: { + components: { + [CodeBlockPlugin.key]: CodeBlockElement, + [CodeLinePlugin.key]: CodeLineElement, + [CodePlugin.key]: CodeLeaf, + [CodeSyntaxPlugin.key]: CodeSyntaxLeaf, + [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), + [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), + [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), + [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), + [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), + [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), + [HorizontalRulePlugin.key]: HrElement, + [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), + [LinkPlugin.key]: LinkElement, + [ParagraphPlugin.key]: ParagraphElement, + [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), + [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }), + [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }), + [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), + }, + }, + plugins: [ + ParagraphPlugin, + IndentPlugin.configure({ + inject: { + targetPlugins: [ + ParagraphPlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + BlockquotePlugin.key, + CodeBlockPlugin.key, + ], + }, + }), + IndentListPlugin.configure({ + inject: { + targetPlugins: [ + ParagraphPlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + BlockquotePlugin.key, + CodeBlockPlugin.key, + ], + }, + }), + HeadingPlugin.configure({ options: { levels: 3 } }), + BlockquotePlugin, + CodeBlockPlugin.configure({ options: { prism: Prism } }), + HorizontalRulePlugin, + LinkPlugin.configure({ + render: { afterEditable: () => }, + }), + MarkdownPlugin.configure({ options: { indentList: true } }), + // FIXME: Fixed the throw error: BlockSelectionPlugin is missing. readonly editor need'nt this plugin so using an empty plugin instead + BlockSelectionPlugin.configure({ + api: {}, + extendEditor: null, + options: {}, + render: {}, + useHooks: null, + handlers: {}, + }), + BoldPlugin, + ItalicPlugin, + UnderlinePlugin, + StrikethroughPlugin, + CodePlugin, + SubscriptPlugin, + SuperscriptPlugin, + FontColorPlugin, + FontBackgroundColorPlugin, + FontSizePlugin, + HighlightPlugin, + KbdPlugin, + ], + value: [{ children: [{ text: '' }], type: 'p' }], + }); + + return editor; +}; diff --git a/apps/www/src/registry/default/plate-ui/ai-toolbar-button.tsx b/apps/www/src/registry/default/plate-ui/ai-toolbar-button.tsx new file mode 100644 index 0000000000..a89a2a005c --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/ai-toolbar-button.tsx @@ -0,0 +1,43 @@ +'use client'; + +import React from 'react'; + +import { withRef } from '@udecode/cn'; +import { AIPlugin } from '@udecode/plate-ai/react'; +import { useEditorPlugin } from '@udecode/plate-core/react'; +import { getNodeEntries } from '@udecode/slate'; +import { toDOMNode } from '@udecode/slate-react'; + +import { ToolbarButton } from './toolbar'; + +export const AIToolbarButton = withRef( + ({ children, ...rest }, ref) => { + const { api, editor, setOptions } = useEditorPlugin(AIPlugin); + const onOpenAI: React.MouseEventHandler = () => { + const _nodeEntries = getNodeEntries(editor); + const nodeEntries = Array.from(_nodeEntries); + const bottomNode = nodeEntries.at(-1); + + if (bottomNode) { + const anchorElement = toDOMNode(editor, bottomNode[0])!; + api.ai.show(editor.id, anchorElement, bottomNode); + setOptions({ + aiState: 'idle', + menuType: 'selection', + }); + } + }; + + return ( + + {children} + + ); + } +); diff --git a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx index a9fda8e243..3b1aedec74 100644 --- a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx +++ b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx @@ -3,10 +3,12 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; +import { AIPlugin } from '@udecode/plate-ai/react'; import { PortalBody, useComposedRef, useEditorId, + useEditorPlugin, useEventEditorSelectors, } from '@udecode/plate-common/react'; import { @@ -28,9 +30,14 @@ export const FloatingToolbar = withRef< const editorId = useEditorId(); const focusedEditorId = useEventEditorSelectors.focus(); + // FIXME: can not using isOpen + const { editor, useOption } = useEditorPlugin(AIPlugin); + const aiOpen = useOption('openEditorId') === editor.id; + const floatingToolbarState = useFloatingToolbarState({ editorId, focusedEditorId, + hideToolbar: aiOpen, ...state, floatingOptions: { middleware: [ diff --git a/apps/www/src/registry/default/plate-ui/menu.tsx b/apps/www/src/registry/default/plate-ui/menu.tsx new file mode 100644 index 0000000000..6f25755191 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/menu.tsx @@ -0,0 +1,679 @@ +'use client'; + +import * as React from 'react'; + +import * as Ariakit from '@ariakit/react'; +import { cn } from '@udecode/cn'; +import { cva } from 'class-variance-authority'; +import { matchSorter } from 'match-sorter'; + +const menuVariants = cva( + 'z-50 overflow-auto text-popover-foreground outline-none', + { + defaultVariants: { + variant: 'default', + }, + variants: { + variant: { + ai: 'min-h-[380px] bg-inherit', + default: 'rounded-md border bg-popover p-0', + }, + }, + } +); + +const menuItemVariants = cva( + 'relative flex min-w-40 cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm text-accent-foreground outline-none hover:bg-accent focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[selected=true]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + { + defaultVariants: { + variant: 'default', + }, + variants: { + variant: { + ai: '', + default: 'rounded-sm', + }, + }, + } +); + +export const comboboxVariants = cva('overflow-hidden', { + defaultVariants: { + variant: 'default', + }, + variants: { + variant: { + ai: 'mb-2 flex min-w-[320px] rounded-sm border bg-popover p-2 shadow sm:w-[408px] md:w-[508px] lg:w-[608px] xl:w-[708px]', + default: 'mb-0 p-2', + }, + }, +}); + +const comboboxListVariants = cva('rounded-sm', { + defaultVariants: { + variant: 'default', + }, + variants: { + variant: { + ai: 'w-[320px] border bg-white p-0', + default: '', + }, + }, +}); + +export interface Action { + group?: string; + groupName?: string; + icon?: React.ReactNode; + items?: Action[]; + keywords?: string[]; + label?: string; + shortcut?: string; + value?: string; +} + +export type actionGroup = { + group?: string; + value?: string; +}; + +export interface MenuProps extends Ariakit.MenuButtonProps<'div'> { + combobox?: Ariakit.ComboboxProps['render']; + comboboxClassName?: string; + comboboxListClassName?: string; + comboboxSubmitButton?: React.ReactElement; + dragButton?: Ariakit.MenuButtonProps['render']; + flip?: boolean; + getAnchorRect?: Ariakit.MenuProps['getAnchorRect']; + icon?: React.ReactNode; + injectAboveMenu?: React.ReactElement; + label?: React.ReactNode; + loading?: boolean; + loadingPlaceholder?: React.ReactNode; + onClickOutside?: (event: MouseEvent) => void; + onOpenChange?: (open: boolean) => void; + onRootMenuClose?: () => void; + onValueChange?: (value: string) => void; + onValuesChange?: Ariakit.MenuProviderProps['setValues']; + open?: boolean; + placement?: Ariakit.MenuProviderProps['placement']; + portal?: Ariakit.MenuProps['portal']; + searchValue?: string; + setAction?: setAction; + values?: Ariakit.MenuProviderProps['values']; + variant?: variant; +} + +const SearchableContext = React.createContext(false); + +type setAction = (actionGroup: actionGroup) => void; +const ActionContext = React.createContext(null); + +type variant = 'ai' | 'default'; + +export const Menu = React.forwardRef(function Menu( + { + children, + combobox, + comboboxClassName, + comboboxListClassName, + comboboxSubmitButton, + dragButton, + flip = true, + getAnchorRect, + icon, + injectAboveMenu, + label, + loading, + loadingPlaceholder, + open, + placement, + portal, + searchValue, + setAction, + store, + values, + variant, + onClickOutside, + onOpenChange, + onRootMenuClose, + onValueChange, + onValuesChange, + ...props + }, + ref +) { + const parent = Ariakit.useMenuContext(); + const searchable = searchValue != null || !!onValueChange || !!combobox; + const ParentSetAction = React.useContext(ActionContext); + + const isRootMenu = !parent; + const isDraggleButtonMenu = !!dragButton; + const menuRef = React.useRef(null); + + useOnClickOutside(menuRef, onClickOutside); + + const menuProviderProps = { + open, + placement: isRootMenu ? placement : 'right', + setOpen: (v: boolean) => { + onOpenChange?.(v); + + if (!v && !parent && !dragButton) onRootMenuClose?.(); + }, + setValues: onValuesChange, + showTimeout: 100, + store, + values, + }; + + const menuButtonProps = { + ref, + ...props, + className: cn(isRootMenu && !isDraggleButtonMenu && 'hidden'), + render: isRootMenu ? dragButton : , + }; + + const menuProps = { + className: cn(menuVariants({ variant }), props.className, searchable && ''), + flip, + getAnchorRect, + gutter: isRootMenu ? 0 : 4, + portal, + ref: isRootMenu ? menuRef : undefined, + unmountOnHide: true, + }; + + const menuContent = ( + + + {icon} + {label} + + + + + {open && isRootMenu && injectAboveMenu} + + + {searchable ? ( + loading ? ( + + {loadingPlaceholder ??
loading...
} +
+ ) : ( + +
+ + {comboboxSubmitButton && comboboxSubmitButton} +
+ + {children} + +
+ ) + ) : ( + children + )} +
+
+
+
+ ); + + const comboboxProviderProps = { + includesBaseElement: false, + resetValueOnHide: true, + setValue: onValueChange, + value: searchValue, + }; + + return searchable ? ( + + {menuContent} + + ) : ( + menuContent + ); +}); + +export interface MenuSeparatorProps extends Ariakit.MenuSeparatorProps {} + +export const MenuSeparator = React.forwardRef< + HTMLHRElement, + MenuSeparatorProps +>(function MenuSeparator(props, ref) { + return ( + + ); +}); + +export interface MenuGroupProps extends Ariakit.MenuGroupProps { + label?: React.ReactNode; + variant?: variant; +} + +export const MenuGroup = React.forwardRef( + function MenuGroup({ label, variant, ...props }, ref) { + return ( + + {label && ( + + {label} + + )} + {props.children} + + ); + } +); + +export interface MenuShortcutProps + extends React.HTMLAttributes {} + +export const MenuShortcut = React.forwardRef< + HTMLSpanElement, + MenuShortcutProps +>(function MenuShortcut(props, ref) { + return ( + + ); +}); + +export interface MenuItemProps + extends Omit { + group?: string; + icon?: React.ReactNode; + label?: string; + name?: string; + parentGroup?: string; + preventClose?: boolean; + shortcut?: string; + value?: string; +} + +export const MenuItem = React.forwardRef( + function MenuItem( + { + className, + group, + icon, + label, + name, + parentGroup, + preventClose, + shortcut, + value, + ...props + }, + ref + ) { + const menu = Ariakit.useMenuContext(); + + if (!menu) throw new Error('MenuItem should be used inside a Menu'); + + const setAction = React.useContext(ActionContext); + + const searchable = React.useContext(SearchableContext); + + const baseOnClick = ( + event: React.MouseEvent + ) => { + props.onClick?.(event); + + if (event.isDefaultPrevented()) return; + if (setAction === null) + console.warn('Did you forget to pass the setAction prop?'); + + setAction?.({ group, value }); + }; + + const baseProps: MenuItemProps = { + blurOnHoverEnd: false, + focusOnHover: true, + label, + ref, + ...props, + className: cn( + menuItemVariants(), + shortcut && 'justify-between', + className + ), + group: parentGroup, + name: group, + value: value || label, + onClick: baseOnClick, + }; + + const isCheckable = menu.useState((state) => { + if (!group) return false; + if (value == null) return false; + + return state.values[group] != null; + }); + + const isChecked = menu.useState((state) => { + if (!group) return false; + + return state.values[group] === value; + }); + + baseProps.children = ( + <> +
+ {icon} + {baseProps.children ?? label} +
+ + {(shortcut || isCheckable) && ( +
+ {isCheckable && ( + + )} + {shortcut && {shortcut}} + {isCheckable && searchable && ( + + {isChecked ? 'checked' : 'not checked'} + + )} +
+ )} + + ); + + if (!searchable) { + if (name != null && value != null) { + const radioProps = { ...baseProps, hideOnClick: true, name, value }; + + return ; + } + + return ; + } + + const hideOnClick = (event: React.MouseEvent) => { + const expandable = event.currentTarget.hasAttribute('aria-expanded'); + + if (expandable) return false; + if (preventClose) return false; + + menu.hideAll(); + + return false; + }; + + const selectValueOnClick = () => { + if (name == null || value == null) return false; + + menu.setValue(name, value); + + return true; + }; + + return ( + + ); + } +); + +export function filterAndBuildMenuTree( + actions: Action[], + searchValue: string +): Action[] | null { + if (!searchValue) return null; + + const options = flattenMenuTree(actions); + + const matches = matchSorter(options, searchValue, { + keys: ['label', 'group', 'value', 'keywords'], + }); + + return buildMenuTree(matches.slice(0, 15)); +} + +export function flattenMenuTree(actions: Action[]): Action[] { + return actions.flatMap((item) => { + if (item.items) { + const parentGroup = item.group ?? item.label; + const groupName = item.label; + + return flattenMenuTree( + item.items.map(({ group, ...item }) => ({ + ...item, + group: group ?? parentGroup, + groupName, + })) + ); + } + + return item; + }); +} + +export function buildMenuTree(actions: Action[] | null) { + if (!actions) return null; + + return actions.reduce((actions, option) => { + if (option.groupName) { + const groupName = actions.find( + (action) => action.label === option.groupName + ); + + if (groupName) { + groupName.items!.push(option); + } else { + actions.push({ items: [option], label: option.groupName }); + } + } else { + actions.push(option); + } + + return actions; + }, []); +} + +export function renderMenuItems({ + group, + items, +}: { + items: Action[]; + group?: string; +}) { + return items.map((item, index) => { + const value = item.value || item.label; + const separator = !!items[index - 1]?.items || (item.items && index > 0); + + const element = item.items ? ( + + {renderMenuItems({ group: item.group, items: item.items })} + + ) : ( + + ); + + return ( + + {separator && } + {element} + + ); + }); +} + +interface renderMatchesOptions { + hiddenOnEmpty: boolean; +} + +export function renderSearchMenuItems( + matches: Action[] | null, + options?: renderMatchesOptions +) { + if (!matches) return null; + if (matches.length === 0) { + if (options?.hiddenOnEmpty) return <>; + + return
No results
; + } + + return renderMenuItems({ items: matches }); +} + +export * as Ariakit from '@ariakit/react'; + +// Utils --------------------------------------------------------------- + +import { useEffect, useLayoutEffect, useRef } from 'react'; + +/** + * Determines the appropriate effect hook to use based on the environment. If + * the code is running on the client-side (browser), it uses the + * `useLayoutEffect` hook, otherwise, it uses the `useEffect` hook. + */ +export const useIsomorphicLayoutEffect = + typeof window === 'undefined' ? useEffect : useLayoutEffect; + +// MediaQueryList Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Window Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: AddEventListenerOptions | boolean +): void; + +// Element Event based useEventListener interface +function useEventListener< + K extends keyof HTMLElementEventMap, + T extends HTMLElement = HTMLDivElement, +>( + eventName: K, + handler: (event: HTMLElementEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// https://usehooks-ts.com/react-hook/use-event-listener +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | MediaQueryList | void = void, +>( + eventName: KH | KM | KW, + handler: ( + event: + | Event + | HTMLElementEventMap[KH] + | MediaQueryListEventMap[KM] + | WindowEventMap[KW] + ) => void, + element?: React.RefObject, + options?: AddEventListenerOptions | boolean +) { + // Create a ref that stores handler + const savedHandler = useRef(handler); + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current ?? window; + + if (!(targetElement && targetElement.addEventListener)) return; + + // Create event listener that calls handler function stored in ref + const listener: typeof handler = (event) => savedHandler.current(event); + + targetElement.addEventListener(eventName, listener, options); + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, options); + }; + }, [eventName, element, options]); +} + +type Handler = (event: MouseEvent) => void; + +/** + * Attaches an event listener to detect clicks that occur outside a given + * element. + * + * @template T - The type of HTMLElement that the ref is referring to. + * @param {RefObject} ref - A React ref object that points to the element to + * listen for clicks outside of. + * @param {Handler} handler - The callback function to be executed when a click + * occurs outside the element. + * @param {string} [mouseEvent='mousedown'] - The type of mouse event to listen + * for (e.g., 'mousedown', 'mouseup'). Default is `'mousedown'` + */ +export const useOnClickOutside = ( + ref: React.RefObject, + handler?: Handler, + mouseEvent: 'mousedown' | 'mouseup' = 'mousedown' +): void => { + useEventListener(mouseEvent, (event) => { + if (!handler) return; + + const el = ref?.current; + + // Do nothing if clicking ref's element or descendent elements + if (!el || el.contains(event.target as Node)) { + return; + } + + handler(event); + }); +}; diff --git a/apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts b/apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts new file mode 100644 index 0000000000..a33e61b6d6 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts @@ -0,0 +1,10 @@ +export const getSelectionMenuSystem = () => `\ +You are a text-based conversational robot that helps users with tasks such as continuation and refinement. +Users will provide you with some content, and you will help them with their needs. + +CRITICAL RULE:If you want to start a new line, output a '\n'. If you want to start a new paragraph, output two '\n'. +Do not respond to the user,generate the content directly.`; + +export const getAISystem = () => `\ +'Unless the user explicitly requests otherwise, the output will be two to three sentences.' +`; diff --git a/apps/www/src/registry/default/plate-ui/stream/index.ts b/apps/www/src/registry/default/plate-ui/stream/index.ts new file mode 100644 index 0000000000..0743c4a6b2 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/stream/index.ts @@ -0,0 +1,3 @@ +export * from './streamInsertText'; + +export * from './streamInsertTextSelection'; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts b/apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts new file mode 100644 index 0000000000..61079e8513 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts @@ -0,0 +1,166 @@ +'use client'; +import { AIPlugin, updateMenuAnchorByPath } from '@udecode/plate-ai/react'; +import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; +import { deserializeMd } from '@udecode/plate-markdown'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; +import { getEndPoint, insertText, withMerging } from '@udecode/slate'; +import { + getAncestorNode, + insertEmptyElement, + replaceNode, +} from '@udecode/slate-utils'; +import { Path } from 'slate'; + +import { getNextPathByNumber } from '@/registry/default/plate-ui/utils/getNextPathByNumber'; + +import { getAISystem } from './getSystemMessage'; +import { streamTraversal } from './streamTraversal'; + +interface streamInsertTextOptions { + prompt: string; + startWritingPath?: Path; + system?: string; +} + +export const streamInsertText = async ( + editor: PlateEditor, + { prompt, system = getAISystem(), ...options }: streamInsertTextOptions +) => { + editor.setOptions(AIPlugin, { + aiState: 'requesting', + lastPrompt: prompt, + }); + + const initNodeEntry = editor.getOptions(AIPlugin).initNodeEntry; + + if (!initNodeEntry) return; + + let chuck = ''; + + let workPath = options?.startWritingPath ?? initNodeEntry[1]; + let lastWorkPath: Path | null = null; + + const effectPath: Path[] = []; + + let matchStartCodeblock = false; + let matchEndCodeblock = false; + + let isFirst = true; + + let total = ''; + + await streamTraversal( + editor, + (delta, done) => { + total += delta; + + if (typeof delta !== 'string') return; + if (delta.includes('``') && matchStartCodeblock) { + matchEndCodeblock = true; + } + if (delta.includes('```') && !matchEndCodeblock) { + matchStartCodeblock = true; + } + + const matchParagraph = !matchStartCodeblock && /\n+/.test(delta); + const matchCodeblock = matchStartCodeblock && matchEndCodeblock; + + if (matchParagraph || matchCodeblock) { + const parts = delta.split(/\n+/); + const nextChunkStart = parts[1] ?? ''; + const previousChunkEnd = parts[0] ?? ''; + + if (previousChunkEnd.length > 0) { + const insert = () => { + insertText(editor, previousChunkEnd, { + at: getEndPoint(editor, workPath), + }); + }; + + withMerging(editor, insert); + + chuck += previousChunkEnd; + } + + matchStartCodeblock = false; + matchEndCodeblock = false; + + const v = deserializeMd(editor, chuck); + + const nextWorkPath = getNextPathByNumber(workPath, v.length); + const replace = () => { + // FIX: replace make the anchor disappear + editor.setOptions(AIPlugin, { + openEditorId: null, + }); + + replaceNode(editor, { + at: workPath, + nodes: v, + }); + + if (!done) { + insertEmptyElement(editor, ParagraphPlugin.key, { + at: nextWorkPath, + }); + } + }; + + withMerging(editor, replace); + + if (!done) workPath = nextWorkPath; + + chuck = nextChunkStart; + + return; + } else { + chuck += delta; + } + if (delta) { + if (lastWorkPath === null || !Path.equals(lastWorkPath, workPath)) { + updateMenuAnchorByPath(editor, workPath); + effectPath.push(workPath); + lastWorkPath = workPath; + } + + editor.setOption(AIPlugin, 'aiState', 'generating'); + + const insert = () => { + insertText(editor, delta, { + at: getEndPoint(editor, workPath), + }); + }; + + if (isFirst) { + insert(); + isFirst = false; + } else { + withMerging(editor, insert); + } + } + }, + { + prompt, + system, + } + ); + + /** After the stream */ + updateMenuAnchorByPath(editor, workPath); + + /** Add block selection to all the ai generated blocks */ + effectPath.forEach((path) => { + setTimeout(() => { + const nodeEntry = getAncestorNode(editor, path); + + if (nodeEntry) { + editor + .getApi(BlockSelectionPlugin) + .blockSelection.addSelectedRow(nodeEntry[0].id, { clear: false }); + } + }, 0); + }); + + editor.setOptions(AIPlugin, { aiState: 'done', lastGenerate: total }); + editor.getApi(AIPlugin).ai.focusMenu(); +}; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts b/apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts new file mode 100644 index 0000000000..fddf058e93 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts @@ -0,0 +1,118 @@ +'use client'; +import { AIPlugin } from '@udecode/plate-ai/react'; +import { resetEditor } from '@udecode/plate-core'; +import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; +import { deserializeMd } from '@udecode/plate-markdown'; +import { getEndPoint, insertText, withMerging } from '@udecode/slate'; +import { insertEmptyElement, replaceNode } from '@udecode/slate-utils'; + +import { getNextPathByNumber } from '@/registry/default/plate-ui/utils/getNextPathByNumber'; + +import { getSelectionMenuSystem } from './getSystemMessage'; +import { streamTraversal } from './streamTraversal'; + +interface StreamInsertTextSelectionOptions { + prompt: string; + system?: string; +} + +export const streamInsertTextSelection = async ( + editor: PlateEditor, + aiEditor: PlateEditor, + { + prompt, + system = getSelectionMenuSystem(), + }: StreamInsertTextSelectionOptions +) => { + editor.setOptions(AIPlugin, { + aiState: 'requesting', + lastPrompt: prompt, + }); + + // const { output } = await generate(prompt, getSelectionMenuSystem()); + + let workPath = [0]; + let matchStartCodeblock = false; + let matchEndCodeblock = false; + let chuck = ''; + + aiEditor.children = [{ children: [{ text: '' }], type: 'p' }]; + resetEditor(aiEditor); + + await streamTraversal( + editor, + (delta, done) => { + if (typeof delta !== 'string') return; + // match code block + if (delta.includes('``') && matchStartCodeblock) { + matchEndCodeblock = true; + } + if (delta.includes('```') && !matchEndCodeblock) { + matchStartCodeblock = true; + } + + const matchParagraph = !matchStartCodeblock && delta.match(/\n+/g); + const matchCodeblock = matchStartCodeblock && matchEndCodeblock; + + if (matchParagraph || matchCodeblock) { + const parts = delta.split(/\n+/); + const nextChunkStart = parts[1] ?? ''; + const previousChunkEnd = parts[0] ?? ''; + + if (previousChunkEnd.length > 0) { + insertText(aiEditor, previousChunkEnd, { + at: getEndPoint(aiEditor, workPath), + }); + chuck += previousChunkEnd; + } + + matchStartCodeblock = false; + matchEndCodeblock = false; + + const v = deserializeMd(aiEditor, chuck); + + const nextWorkPath = getNextPathByNumber(workPath, v.length); + + const replace = () => { + replaceNode(aiEditor, { + at: workPath, + nodes: v, + }); + + if (!done) { + insertEmptyElement(aiEditor, ParagraphPlugin.key, { + at: nextWorkPath, + }); + } + }; + + withMerging(aiEditor, replace); + + workPath = nextWorkPath; + + chuck = nextChunkStart; + + return; + } else { + chuck += delta; + } + if (delta) { + insertText(aiEditor, delta, { + at: getEndPoint(aiEditor, workPath), + }); + } + }, + // streamInsertTextSelectionOptions + { + prompt, + system, + } + ); + + editor.setOptions(AIPlugin, { + aiState: 'done', + lastPrompt: prompt, + lastWorkPath: workPath, + }); + editor.getApi(AIPlugin).ai.focusMenu(); +}; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts b/apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts new file mode 100644 index 0000000000..195579bd49 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts @@ -0,0 +1,59 @@ +'use client'; + +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { AIPlugin } from '@udecode/plate-ai/react'; + +interface StreamTraversalOptions { + prompt: string; + system: string; +} + +export const streamTraversal = async ( + editor: PlateEditor, + fn: (delta: string, done: boolean) => void, + { prompt, system }: StreamTraversalOptions +) => { + const abortController = new AbortController(); + editor.setOptions(AIPlugin, { abortController }); + + const response = await fetch('/api/ai/command', { + body: JSON.stringify({ prompt, system }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + signal: abortController.signal, + }).catch((error) => { + console.error(error); + }); + + if (response?.status === 429) { + return fn( + 'Rate limit exceeded. You have made too many requests. Please try again later.', + true + ); + } + if (!response || !response.body) { + throw new Error('Response or response body is null or abort'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read(); + + if (done) { + fn('\n', done); + + break; + } + if (value) { + const delta = decoder.decode(value); + + fn(delta, done); + } + } +}; diff --git a/apps/www/src/registry/default/plate-ui/utils/getContent.ts b/apps/www/src/registry/default/plate-ui/utils/getContent.ts new file mode 100644 index 0000000000..5ee7a78988 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/utils/getContent.ts @@ -0,0 +1,59 @@ +import type { PlateEditor } from '@udecode/plate-common/react'; + +import { AIPlugin } from '@udecode/plate-ai/react'; +import { serializeMd, serializeMdNodes } from '@udecode/plate-markdown'; +import { + type BlockSelectionConfig, + BlockSelectionPlugin, +} from '@udecode/plate-selection/react'; +import { + type GetNodeEntriesOptions, + getNodeEntries, + isBlock, + isElement, +} from '@udecode/slate'; + +// If some content has already been generated using it to modify(improve) otherwise using the selection or block selected nodes. +export const getContent = (editor: PlateEditor, aiEditor: PlateEditor) => { + const aiState = editor.getOptions(AIPlugin).aiState; + + if (aiState === 'done') return serializeAI(aiEditor); + // Not Sure + if ( + editor.getOption( + BlockSelectionPlugin, + 'isSelecting', + editor.id + ) + ) { + const entries = getBlockSelectedEntries(editor); + const nodes = Array.from(entries, (entry) => entry[0]); + + return serializeMdNodes(nodes as any); + } + + const entries = getNodeEntries(editor, { + match: (n) => isBlock(editor, n), + mode: 'highest', + }); + const nodes = Array.from(entries, (entry) => entry[0]); + + return serializeMdNodes(nodes as any); +}; + +export const serializeAI = (editor: PlateEditor) => { + return serializeMd(editor as any).trim(); +}; + +export const getBlockSelectedEntries = ( + editor: PlateEditor, + options?: GetNodeEntriesOptions +) => { + const ids = editor.getOptions(BlockSelectionPlugin).selectedIds; + + return getNodeEntries(editor, { + at: [], + match: (n) => isElement(n) && ids?.has(n.id as string), + ...options, + }); +}; diff --git a/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts b/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts new file mode 100644 index 0000000000..d9123d4a95 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts @@ -0,0 +1,9 @@ +import type { PlateEditor } from '@udecode/plate-core/react'; + +import { getBlockSelectedEntries } from '.'; + +export const getFirstBlockSelectedNode = (editor: PlateEditor) => { + const entries = getBlockSelectedEntries(editor); + + return Array.from(entries)[0]; +}; diff --git a/apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts b/apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts new file mode 100644 index 0000000000..e994d244e6 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts @@ -0,0 +1,11 @@ +import { Path } from 'slate'; + +export const getNextPathByNumber = (startPath: Path, number: number) => { + let workPath = startPath; + + for (let i = 0; i < number; i++) { + workPath = Path.next(workPath); + } + + return workPath; +}; diff --git a/apps/www/src/registry/default/plate-ui/utils/index.ts b/apps/www/src/registry/default/plate-ui/utils/index.ts new file mode 100644 index 0000000000..c1ae3fa7d1 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/utils/index.ts @@ -0,0 +1,3 @@ +export * from './getContent'; + +export * from './getFirstBlockSelectedNode'; diff --git a/yarn.lock b/yarn.lock index 9c545d401d..86cf25530b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -51,36 +51,36 @@ __metadata: languageName: node linkType: hard -"@ariakit/core@npm:0.4.10": - version: 0.4.10 - resolution: "@ariakit/core@npm:0.4.10" - checksum: 10c0/8eb6e31218d1c9d4e0daf02b4a031c07ce48a63c907d28be545104106ad30be73c7ba8247f84d4baff3b1bf47605b31e7e78e77289287c7989216e5418994830 +"@ariakit/core@npm:0.4.7": + version: 0.4.7 + resolution: "@ariakit/core@npm:0.4.7" + checksum: 10c0/107f0cf197f2674c0e0b11660a8b5e4a966704914f49726f72ffa81c04ef6d8144c2df0a9f7ac23481b9598d1b10b73cf9e00f211c611172b4946ed024ee4460 languageName: node linkType: hard -"@ariakit/react-core@npm:0.4.11": - version: 0.4.11 - resolution: "@ariakit/react-core@npm:0.4.11" +"@ariakit/react-core@npm:0.4.7": + version: 0.4.7 + resolution: "@ariakit/react-core@npm:0.4.7" dependencies: - "@ariakit/core": "npm:0.4.10" + "@ariakit/core": "npm:0.4.7" "@floating-ui/dom": "npm:^1.0.0" use-sync-external-store: "npm:^1.2.0" peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/e55c8c7f7e3cd75c7694c1159cb35d34c27c452299d8ab77ed67e99a2b2612c00a2e9beec07238b1ac388e688f407058173f598d626ac7ed58ecd9f3d3ea0bc7 + checksum: 10c0/8cbd462bed3f328d523c05eee12424b2f70e35e585772d333d6c4513631f10bfab39cc0e36acca09afad4d3e50c44c2305fdbf917574e4e3ab084b425eb8a245 languageName: node linkType: hard -"@ariakit/react@npm:0.4.11": - version: 0.4.11 - resolution: "@ariakit/react@npm:0.4.11" +"@ariakit/react@npm:0.4.7": + version: 0.4.7 + resolution: "@ariakit/react@npm:0.4.7" dependencies: - "@ariakit/react-core": "npm:0.4.11" + "@ariakit/react-core": "npm:0.4.7" peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/b64eaee58a82a416605488b0df0c3f74598d59cbbb7cbfdd6dce3dd2be9677b9e9a65045d519dfa99590af6ef75356683ff56d92918e2dd2d21a686ad423c87f + checksum: 10c0/9f919d72d3a57cc270747d83e10e3a0ef057cdaa038f2af07e16284892802bb365cff58fb0a287f6233ae24cf2d2f38c315411b16a68a6b4c212922b921a63cc languageName: node linkType: hard @@ -469,6 +469,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.23.8": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/d6143adf5aa1ce79ed374e33fdfd74fa975055a80bc6e479672ab1eadc4e4bfd7484444e17dd063a1d180e051f3ec62b357c7a2b817e7657687b47313158c3d2 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0, @babel/template@npm:^7.3.3": version: 7.24.0 resolution: "@babel/template@npm:7.24.0" @@ -14322,6 +14331,16 @@ __metadata: languageName: node linkType: hard +"match-sorter@npm:6.3.4": + version: 6.3.4 + resolution: "match-sorter@npm:6.3.4" + dependencies: + "@babel/runtime": "npm:^7.23.8" + remove-accents: "npm:0.5.0" + checksum: 10c0/35d2a6b6df003c677d9ec87ecd4683657638f5bce856f43f9cf90b03e357ed2f09813ebbac759defa7e7438706936dd34dc2bfe1a18771f7d2541f14d639b4ad + languageName: node + linkType: hard + "mdast-util-find-and-replace@npm:^3.0.0": version: 3.0.1 resolution: "mdast-util-find-and-replace@npm:3.0.1" @@ -18140,6 +18159,13 @@ __metadata: languageName: node linkType: hard +"remove-accents@npm:0.5.0": + version: 0.5.0 + resolution: "remove-accents@npm:0.5.0" + checksum: 10c0/a75321aa1b53d9abe82637115a492770bfe42bb38ed258be748bf6795871202bc8b4badff22013494a7029f5a241057ad8d3f72adf67884dbe15a9e37e87adc4 + languageName: node + linkType: hard + "repeat-string@npm:^1.6.1": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" @@ -21583,7 +21609,7 @@ __metadata: version: 0.0.0-use.local resolution: "www@workspace:apps/www" dependencies: - "@ariakit/react": "npm:0.4.11" + "@ariakit/react": "npm:0.4.7" "@radix-ui/colors": "npm:3.0.0" "@radix-ui/react-accessible-icon": "npm:^1.1.0" "@radix-ui/react-accordion": "npm:^1.2.0" @@ -21693,6 +21719,7 @@ __metadata: glob: "npm:^11.0.0" lodash.template: "npm:^4.5.0" lucide-react: "npm:^0.441.0" + match-sorter: "npm:6.3.4" mdast-util-toc: "npm:^7.1.0" next: "npm:14.3.0-canary.43" next-contentlayer2: "npm:^0.4.6" From 8c01072f61f87f1cee8df74191c0b821d658fb2e Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 15:22:48 +0800 Subject: [PATCH 032/127] Selection overlay --- .../default/example/playground-demo.tsx | 6 ++- .../default/plate-ui/cursor-overlay.tsx | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 65ce9b852b..e903a46bb2 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -83,7 +83,10 @@ import { commentsData, usersData } from '@/plate/demo/values/commentsValue'; import { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue'; import { AIMenu } from '@/registry/default/plate-ui/ai-menu'; import { CommentsPopover } from '@/registry/default/plate-ui/comments-popover'; -import { CursorOverlay } from '@/registry/default/plate-ui/cursor-overlay'; +import { + CursorOverlay, + SelectionOverlayPlugin, +} from '@/registry/default/plate-ui/cursor-overlay'; import { Editor } from '@/registry/default/plate-ui/editor'; import { FixedToolbar } from '@/registry/default/plate-ui/fixed-toolbar'; import { FloatingToolbar } from '@/registry/default/plate-ui/floating-toolbar'; @@ -163,6 +166,7 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { enableMerging: id === 'tableMerge', }, }), + SelectionOverlayPlugin, AIPlugin.configure({ options: { scrollContainerSelector: `#${scrollSelector}`, diff --git a/apps/www/src/registry/default/plate-ui/cursor-overlay.tsx b/apps/www/src/registry/default/plate-ui/cursor-overlay.tsx index 7d67142c85..5a10c0aecc 100644 --- a/apps/www/src/registry/default/plate-ui/cursor-overlay.tsx +++ b/apps/www/src/registry/default/plate-ui/cursor-overlay.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { cn } from '@udecode/cn'; import { createPlatePlugin, findEventRange, + useEditorPlugin, useEditorRef, } from '@udecode/plate-common/react'; import { @@ -14,6 +15,7 @@ import { CursorOverlay as CursorOverlayPrimitive, } from '@udecode/plate-cursor'; import { DndPlugin } from '@udecode/plate-dnd'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; export function Cursor({ caretPosition, @@ -104,3 +106,44 @@ const DragOverCursorPlugin = createPlatePlugin({ }, }, }); + +export const SelectionOverlayPlugin = createPlatePlugin({ + key: 'selection_over_lay', + useHooks: () => { + const { editor } = useEditorPlugin(BlockSelectionPlugin); + const isSelecting = editor.useOptions(BlockSelectionPlugin).isSelecting; + + useEffect(() => { + if (isSelecting) { + setTimeout(() => { + editor.setOption(DragOverCursorPlugin, 'cursors', {}); + }, 0); + } + }, [editor, isSelecting]); + }, + handlers: { + onBlur: ({ editor, event }) => { + const isPrevented = + (event.relatedTarget as HTMLElement)?.dataset?.platePreventOverlay === + 'true'; + + if (isPrevented) return; + if (editor.selection) { + editor.setOption(DragOverCursorPlugin, 'cursors', { + drag: { + key: 'blur', + data: { + selectionStyle: { + backgroundColor: 'rgba(47, 121, 216, 0.35)', + }, + }, + selection: editor.selection, + }, + }); + } + }, + onFocus: ({ editor }) => { + editor.setOption(DragOverCursorPlugin, 'cursors', {}); + }, + }, +}); From 8edbabd0b52ebdec90e2b8ca126271b1fcaff761 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 18:05:25 +0800 Subject: [PATCH 033/127] package ai --- .../www/src/lib/plate/demo/values/aiValue.tsx | 8 ++-- ...ionHandler.ts => cursorCommandsHandler.ts} | 15 +++---- ...Handler.ts => cursorSuggestionsHandler.ts} | 40 +++++++++---------- .../default/plate-ui/action-handler/index.ts | 8 ++-- ...Handler.ts => selectionCommandsHandler.ts} | 6 +-- ...dler.ts => selectionSuggestionsHandler.ts} | 16 ++++---- .../action-handler/useActionHandler.ts | 16 ++++---- .../default/plate-ui/ai-menu-items.tsx | 8 ++-- .../src/registry/default/plate-ui/ai-menu.tsx | 27 +++++++------ .../registry/default/plate-ui/stream/index.ts | 3 -- .../utils/getFirstBlockSelectedNode.ts | 9 ----- .../registry/default/plate-ui/utils/index.ts | 3 -- packages/ai/package.json | 2 + packages/ai/src/react/ai/index.ts | 1 + .../src/react/ai}/stream/getSystemMessage.ts | 0 packages/ai/src/react/ai/stream/index.ts | 8 ++++ .../src/react/ai}/stream/streamInsertText.ts | 18 +++++---- .../ai}/stream/streamInsertTextSelection.ts | 18 +++++---- .../src/react/ai}/stream/streamTraversal.ts | 2 +- .../ai/src/react/ai}/utils/getContent.ts | 34 ++++------------ .../react/ai}/utils/getNextPathByNumber.ts | 0 packages/ai/src/react/ai/utils/index.ts | 2 + .../selection/src/react/components/index.ts | 5 --- .../selection/src/react/context-menu/index.ts | 7 ---- packages/selection/src/react/index.ts | 12 ------ packages/selection/src/react/utils/index.ts | 9 ----- yarn.lock | 2 + 27 files changed, 118 insertions(+), 161 deletions(-) rename apps/www/src/registry/default/plate-ui/action-handler/{defaultActionHandler.ts => cursorCommandsHandler.ts} (76%) rename apps/www/src/registry/default/plate-ui/action-handler/{defaultSuggestionActionHandler.ts => cursorSuggestionsHandler.ts} (79%) rename apps/www/src/registry/default/plate-ui/action-handler/{selectionActionHandler.ts => selectionCommandsHandler.ts} (93%) rename apps/www/src/registry/default/plate-ui/action-handler/{selectionSuggestionActionHandler.ts => selectionSuggestionsHandler.ts} (90%) delete mode 100644 apps/www/src/registry/default/plate-ui/stream/index.ts delete mode 100644 apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts delete mode 100644 apps/www/src/registry/default/plate-ui/utils/index.ts rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/stream/getSystemMessage.ts (100%) create mode 100644 packages/ai/src/react/ai/stream/index.ts rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/stream/streamInsertText.ts (93%) rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/stream/streamInsertTextSelection.ts (88%) rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/stream/streamTraversal.ts (96%) rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/utils/getContent.ts (59%) rename {apps/www/src/registry/default/plate-ui => packages/ai/src/react/ai}/utils/getNextPathByNumber.ts (100%) delete mode 100644 packages/selection/src/react/components/index.ts delete mode 100644 packages/selection/src/react/context-menu/index.ts delete mode 100644 packages/selection/src/react/index.ts delete mode 100644 packages/selection/src/react/utils/index.ts diff --git a/apps/www/src/lib/plate/demo/values/aiValue.tsx b/apps/www/src/lib/plate/demo/values/aiValue.tsx index 4ab27f13e7..4c0a017a6a 100644 --- a/apps/www/src/lib/plate/demo/values/aiValue.tsx +++ b/apps/www/src/lib/plate/demo/values/aiValue.tsx @@ -6,9 +6,9 @@ jsx; export const aiValue: any = ( - ✨ AI Assistant + ✨ AI - To trigger the AI assistant, you can: + To trigger the AI, you can: Press the Space key on a new empty block @@ -22,9 +22,7 @@ export const aiValue: any = ( - - Press Enter to submit your prompt and generate AI-assisted content. - + Press Enter to submit your prompt and generate AI content. diff --git a/apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts similarity index 76% rename from apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts rename to apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts index cb761125eb..bbd1bd9789 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/defaultActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts @@ -1,23 +1,24 @@ 'use client'; import type { PlateEditor } from '@udecode/plate-core/react'; +import { streamInsertText } from '@udecode/plate-ai/react'; +import { serializeMd } from '@udecode/plate-markdown'; + import { ACTION_CONTINUE_WRITE, ACTION_EXPLAIN, ACTION_SUMMARIZE, GROUP_LANGUAGES, } from '@/registry/default/plate-ui/ai-actions'; -import { streamInsertText } from '@/registry/default/plate-ui/stream'; -import { serializeAI } from '@/registry/default/plate-ui/utils'; import type { ActionHandlerOptions } from './useActionHandler'; -export const defaultActionHandler = async ( +export const cursorCommandsHandler = async ( editor: PlateEditor, { group, value }: ActionHandlerOptions ) => { if (group === GROUP_LANGUAGES) { - const content = serializeAI(editor); + const content = serializeMd(editor as any); await streamInsertText(editor, { prompt: `Keep the original paragraph format. Translate the following article to ${value?.slice(7)}: ${content}`, }); @@ -27,7 +28,7 @@ export const defaultActionHandler = async ( switch (value) { case ACTION_CONTINUE_WRITE: { - const content = serializeAI(editor); + const content = serializeMd(editor as any); await streamInsertText(editor, { prompt: `Continue writing the following article in 3-5 sentences: ${content}`, @@ -36,7 +37,7 @@ export const defaultActionHandler = async ( break; } case ACTION_SUMMARIZE: { - const content = serializeAI(editor); + const content = serializeMd(editor as any); await streamInsertText(editor, { prompt: `Summarize the following article in 3-5 sentences: ${content}`, @@ -45,7 +46,7 @@ export const defaultActionHandler = async ( break; } case ACTION_EXPLAIN: { - const content = serializeAI(editor); + const content = serializeMd(editor as any); await streamInsertText(editor, { prompt: `Explain the following article in 3-5 sentences: ${content}`, diff --git a/apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/cursorSuggestionsHandler.ts similarity index 79% rename from apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts rename to apps/www/src/registry/default/plate-ui/action-handler/cursorSuggestionsHandler.ts index 97ed746bb7..42cfa6924b 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/defaultSuggestionActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/cursorSuggestionsHandler.ts @@ -1,7 +1,8 @@ 'use client'; -import { AIPlugin } from '@udecode/plate-ai/react'; +import { AIPlugin, streamInsertText } from '@udecode/plate-ai/react'; import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; import { serializeMdNodes } from '@udecode/plate-markdown'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; import { getEndPoint, setSelection, withMerging } from '@udecode/slate'; import { focusEditor } from '@udecode/slate-react'; import { insertEmptyElement } from '@udecode/slate-utils'; @@ -14,21 +15,19 @@ import { ACTION_SUGGESTION_MAKE_LONGER, ACTION_SUGGESTION_TRY_AGAIN, } from '@/registry/default/plate-ui/ai-actions'; -import { streamInsertText } from '@/registry/default/plate-ui/stream'; -import { - getBlockSelectedEntries, - getFirstBlockSelectedNode, -} from '@/registry/default/plate-ui/utils'; import type { ActionHandlerOptions } from './useActionHandler'; -export const defaultSuggestionActionHandler = ( +export const cursorSuggestionsHandler = ( editor: PlateEditor, { group: _, value }: ActionHandlerOptions ) => { switch (value) { case ACTION_SUGGESTION_CONTINUE_WRITE: { - const entries = getBlockSelectedEntries(editor); + const entries = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + const nodes = Array.from(entries, (entry) => entry[0]); if (!nodes) return; @@ -55,7 +54,11 @@ export const defaultSuggestionActionHandler = ( break; } case ACTION_SUGGESTION_MAKE_LONGER: { - const first = getFirstBlockSelectedNode(editor); + const entries = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + + const first = Array.from(entries)[0]; editor.setOptions(AIPlugin, { openEditorId: null }); editor.undo(); @@ -75,7 +78,7 @@ export const defaultSuggestionActionHandler = ( // BUG: first block focus case ACTION_SUGGESTION_DONE: { editor.getApi(AIPlugin).ai.hide(); - clearBlockSelected(editor); + editor.getApi(BlockSelectionPlugin).blockSelection.resetSelectedIds(); const curNodeEntry = editor.getOptions(AIPlugin).curNodeEntry; // set selection to last point @@ -92,7 +95,7 @@ export const defaultSuggestionActionHandler = ( // TODO: case ACTION_SUGGESTION_CLOSE: { editor.getApi(AIPlugin).ai.hide(); - clearBlockSelected(editor); + editor.getApi(BlockSelectionPlugin).blockSelection.resetSelectedIds(); setTimeout(() => { // set selection to last point focusEditor(editor); @@ -101,7 +104,12 @@ export const defaultSuggestionActionHandler = ( break; } case ACTION_SUGGESTION_TRY_AGAIN: { - const first = getFirstBlockSelectedNode(editor); + const entries = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + + const first = Array.from(entries)[0]; + editor.undo(); editor.history.redos.pop(); @@ -118,11 +126,3 @@ export const defaultSuggestionActionHandler = ( } } }; - -export const clearBlockSelected = (editor: PlateEditor) => { - const { blockSelectionStore } = editor as any; - - if (blockSelectionStore) { - blockSelectionStore.set.resetSelectedIds(); - } -}; diff --git a/apps/www/src/registry/default/plate-ui/action-handler/index.ts b/apps/www/src/registry/default/plate-ui/action-handler/index.ts index 45d1effb0f..84a5f736ae 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/index.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/index.ts @@ -1,9 +1,9 @@ -export * from './defaultActionHandler'; +export * from './cursorCommandsHandler'; -export * from './defaultSuggestionActionHandler'; +export * from './cursorSuggestionsHandler'; -export * from './selectionActionHandler'; +export * from './selectionCommandsHandler'; -export * from './selectionSuggestionActionHandler'; +export * from './selectionSuggestionsHandler'; export * from './useActionHandler'; \ No newline at end of file diff --git a/apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/selectionCommandsHandler.ts similarity index 93% rename from apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts rename to apps/www/src/registry/default/plate-ui/action-handler/selectionCommandsHandler.ts index bfce009b9b..8bac1ff139 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/selectionActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/selectionCommandsHandler.ts @@ -1,5 +1,7 @@ import type { PlateEditor } from '@udecode/plate-core/react'; +import { getContent, streamInsertTextSelection } from '@udecode/plate-ai/react'; + import { ACTION_SELECTION_FIX_SPELLING, ACTION_SELECTION_IMPROVE_WRITING, @@ -8,12 +10,10 @@ import { ACTION_SELECTION_SIMPLIFY_LANGUAGE, GROUP_SELECTION_LANGUAGES, } from '@/registry/default/plate-ui/ai-actions'; -import { streamInsertTextSelection } from '@/registry/default/plate-ui/stream'; -import { getContent } from '@/registry/default/plate-ui/utils'; import type { ActionHandlerOptions } from './useActionHandler'; -export const selectionActionHandler = ( +export const selectionCommandsHandler = ( editor: PlateEditor, aiEditor: PlateEditor, { group, value }: ActionHandlerOptions diff --git a/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionsHandler.ts similarity index 90% rename from apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts rename to apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionsHandler.ts index cb301b71c6..c621f45784 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/selectionSuggestionsHandler.ts @@ -1,7 +1,11 @@ 'use client'; import type { PlateEditor } from '@udecode/plate-core/react'; -import { AIPlugin } from '@udecode/plate-ai/react'; +import { + AIPlugin, + getContent, + streamInsertTextSelection, +} from '@udecode/plate-ai/react'; import { nanoid } from '@udecode/plate-core'; import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; import { insertNodes, isBlock, removeNodes, withMerging } from '@udecode/slate'; @@ -15,14 +19,10 @@ import { ACTION_SELECTION_SUGGESTION_REPLACE, ACTION_SELECTION_SUGGESTION_TRY_AGAIN, } from '@/registry/default/plate-ui/ai-actions'; -import { streamInsertTextSelection } from '@/registry/default/plate-ui/stream'; -import { getContent } from '@/registry/default/plate-ui/utils'; import type { ActionHandlerOptions } from './useActionHandler'; -import { clearBlockSelected } from './defaultSuggestionActionHandler'; - -export const selectionSuggestionActionHandler = ( +export const selectionSuggestionsHandler = ( editor: PlateEditor, aiEditor: PlateEditor, { group: _, value }: ActionHandlerOptions @@ -71,7 +71,7 @@ export const selectionSuggestionActionHandler = ( }); setTimeout(() => { - clearBlockSelected(editor); + editor.getApi(BlockSelectionPlugin).blockSelection.resetSelectedIds(); ids.forEach((id) => editor .getApi(BlockSelectionPlugin) @@ -110,7 +110,7 @@ export const selectionSuggestionActionHandler = ( insertNodes(editor, nodes, { at: path }); setTimeout(() => { - clearBlockSelected(editor); + editor.getApi(BlockSelectionPlugin).blockSelection.resetSelectedIds(); ids.forEach((id) => editor .getApi(BlockSelectionPlugin) diff --git a/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts index a2696baaf6..7523785bfc 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts @@ -5,10 +5,10 @@ import type { actionGroup } from '@/registry/default/plate-ui/menu'; import { type PlateEditor, useEditorRef } from '@udecode/plate-core/react'; -import { defaultActionHandler } from './defaultActionHandler'; -import { defaultSuggestionActionHandler } from './defaultSuggestionActionHandler'; -import { selectionActionHandler } from './selectionActionHandler'; -import { selectionSuggestionActionHandler } from './selectionSuggestionActionHandler'; +import { cursorCommandsHandler } from './cursorCommandsHandler'; +import { cursorSuggestionsHandler } from './cursorSuggestionsHandler'; +import { selectionCommandsHandler } from './selectionCommandsHandler'; +import { selectionSuggestionsHandler } from './selectionSuggestionsHandler'; export const useActionHandler = ( action: actionGroup | null, @@ -23,13 +23,13 @@ export const useActionHandler = ( if (!value) return; - void defaultActionHandler(editor, { group, value }); + void cursorCommandsHandler(editor, { group, value }); - void defaultSuggestionActionHandler(editor, { group, value }); + void cursorSuggestionsHandler(editor, { group, value }); - void selectionActionHandler(editor, aiEditor, { group, value }); + void selectionCommandsHandler(editor, aiEditor, { group, value }); - void selectionSuggestionActionHandler(editor, aiEditor, { + void selectionSuggestionsHandler(editor, aiEditor, { group, value, }); diff --git a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx index e0d5a80f6c..1a3000ad7a 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx @@ -12,7 +12,7 @@ import { renderMenuItems, } from './menu'; -export const DefaultItems = () => { +export const CursorCommands = () => { return ( <> @@ -33,7 +33,7 @@ export const DefaultItems = () => { ); }; -export const DefaultSuggestionItems = () => { +export const CursorSuggestions = () => { return ( <> @@ -48,7 +48,7 @@ export const DefaultSuggestionItems = () => { ); }; -export const SelectionItems = () => { +export const SelectionCommands = () => { return ( <> @@ -71,7 +71,7 @@ export const SelectionItems = () => { ); }; -export const SelectionSuggestionItems = () => { +export const SelectionSuggestions = () => { return ( <> diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx index f82d5a3b65..aaddf77fb1 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -12,13 +12,17 @@ import React, { } from 'react'; import { cn } from '@udecode/cn'; -import { AIPlugin } from '@udecode/plate-ai/react'; +import { + AIPlugin, + getContent, + streamInsertText, + streamInsertTextSelection, +} from '@udecode/plate-ai/react'; import { useEditorPlugin } from '@udecode/plate-core/react'; import { focusEditor } from '@udecode/slate-react'; import isHotkey from 'is-hotkey'; import { Icons } from '@/components/icons'; -import { Button } from '@/registry/default/plate-ui/button'; import { useActionHandler } from './action-handler'; import { @@ -29,12 +33,13 @@ import { defaultValues, } from './ai-actions'; import { - DefaultItems, - DefaultSuggestionItems, - SelectionItems, - SelectionSuggestionItems, + CursorCommands, + CursorSuggestions, + SelectionCommands, + SelectionSuggestions, } from './ai-menu-items'; import { AIPreviewEditor } from './ai-previdew-editor'; +import { Button } from './button'; import { type actionGroup, Ariakit, @@ -43,8 +48,6 @@ import { filterAndBuildMenuTree, renderSearchMenuItems, } from './menu'; -import { streamInsertText, streamInsertTextSelection } from './stream'; -import { getContent } from './utils'; // eslint-disable-next-line react/display-name export const AIMenu = memo(({ children }: React.PropsWithChildren) => { @@ -140,13 +143,13 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { const [CurrentItems, CurrentActions] = React.useMemo(() => { if (aiState === 'done') { if (menuType === 'selection') - return [SelectionSuggestionItems, SelectionSuggestionActions]; + return [SelectionSuggestions, SelectionSuggestionActions]; - return [DefaultSuggestionItems, DefaultSuggestionActions]; + return [CursorSuggestions, DefaultSuggestionActions]; } - if (menuType === 'selection') return [SelectionItems, SelectionActions]; + if (menuType === 'selection') return [SelectionCommands, SelectionActions]; - return [DefaultItems, DefaultActions]; + return [CursorCommands, DefaultActions]; }, [aiState, menuType]); /** IME */ diff --git a/apps/www/src/registry/default/plate-ui/stream/index.ts b/apps/www/src/registry/default/plate-ui/stream/index.ts deleted file mode 100644 index 0743c4a6b2..0000000000 --- a/apps/www/src/registry/default/plate-ui/stream/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './streamInsertText'; - -export * from './streamInsertTextSelection'; diff --git a/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts b/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts deleted file mode 100644 index d9123d4a95..0000000000 --- a/apps/www/src/registry/default/plate-ui/utils/getFirstBlockSelectedNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PlateEditor } from '@udecode/plate-core/react'; - -import { getBlockSelectedEntries } from '.'; - -export const getFirstBlockSelectedNode = (editor: PlateEditor) => { - const entries = getBlockSelectedEntries(editor); - - return Array.from(entries)[0]; -}; diff --git a/apps/www/src/registry/default/plate-ui/utils/index.ts b/apps/www/src/registry/default/plate-ui/utils/index.ts deleted file mode 100644 index c1ae3fa7d1..0000000000 --- a/apps/www/src/registry/default/plate-ui/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './getContent'; - -export * from './getFirstBlockSelectedNode'; diff --git a/packages/ai/package.json b/packages/ai/package.json index a6c0ca6de6..23fee2aa90 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -51,6 +51,8 @@ }, "dependencies": { "@udecode/plate-combobox": "38.0.1", + "@udecode/plate-markdown": "38.0.1", + "@udecode/plate-selection": "38.0.11", "lodash": "^4.17.21" }, "peerDependencies": { diff --git a/packages/ai/src/react/ai/index.ts b/packages/ai/src/react/ai/index.ts index e460f9c7c1..5e38cbf6e9 100644 --- a/packages/ai/src/react/ai/index.ts +++ b/packages/ai/src/react/ai/index.ts @@ -3,4 +3,5 @@ */ export * from './AIPlugin'; +export * from './stream/index'; export * from './utils/index'; diff --git a/apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts b/packages/ai/src/react/ai/stream/getSystemMessage.ts similarity index 100% rename from apps/www/src/registry/default/plate-ui/stream/getSystemMessage.ts rename to packages/ai/src/react/ai/stream/getSystemMessage.ts diff --git a/packages/ai/src/react/ai/stream/index.ts b/packages/ai/src/react/ai/stream/index.ts new file mode 100644 index 0000000000..88a3670ff2 --- /dev/null +++ b/packages/ai/src/react/ai/stream/index.ts @@ -0,0 +1,8 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './getSystemMessage'; +export * from './streamInsertText'; +export * from './streamInsertTextSelection'; +export * from './streamTraversal'; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts b/packages/ai/src/react/ai/stream/streamInsertText.ts similarity index 93% rename from apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts rename to packages/ai/src/react/ai/stream/streamInsertText.ts index 61079e8513..b7a0f4493c 100644 --- a/apps/www/src/registry/default/plate-ui/stream/streamInsertText.ts +++ b/packages/ai/src/react/ai/stream/streamInsertText.ts @@ -1,18 +1,20 @@ 'use client'; -import { AIPlugin, updateMenuAnchorByPath } from '@udecode/plate-ai/react'; -import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; -import { deserializeMd } from '@udecode/plate-markdown'; -import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; -import { getEndPoint, insertText, withMerging } from '@udecode/slate'; import { getAncestorNode, + getEndPoint, insertEmptyElement, + insertText, replaceNode, -} from '@udecode/slate-utils'; + withMerging, +} from '@udecode/plate-common'; +import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-common/react'; +import { deserializeMd } from '@udecode/plate-markdown'; +import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; import { Path } from 'slate'; -import { getNextPathByNumber } from '@/registry/default/plate-ui/utils/getNextPathByNumber'; - +import { AIPlugin } from '../AIPlugin'; +import { updateMenuAnchorByPath } from '../utils'; +import { getNextPathByNumber } from '../utils/getNextPathByNumber'; import { getAISystem } from './getSystemMessage'; import { streamTraversal } from './streamTraversal'; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts b/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts similarity index 88% rename from apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts rename to packages/ai/src/react/ai/stream/streamInsertTextSelection.ts index fddf058e93..6d51e41497 100644 --- a/apps/www/src/registry/default/plate-ui/stream/streamInsertTextSelection.ts +++ b/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts @@ -1,13 +1,17 @@ 'use client'; -import { AIPlugin } from '@udecode/plate-ai/react'; -import { resetEditor } from '@udecode/plate-core'; -import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-core/react'; +import { + getEndPoint, + insertEmptyElement, + insertText, + replaceNode, + resetEditor, + withMerging, +} from '@udecode/plate-common'; +import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-common/react'; import { deserializeMd } from '@udecode/plate-markdown'; -import { getEndPoint, insertText, withMerging } from '@udecode/slate'; -import { insertEmptyElement, replaceNode } from '@udecode/slate-utils'; - -import { getNextPathByNumber } from '@/registry/default/plate-ui/utils/getNextPathByNumber'; +import { AIPlugin } from '../AIPlugin'; +import { getNextPathByNumber } from '../utils/getNextPathByNumber'; import { getSelectionMenuSystem } from './getSystemMessage'; import { streamTraversal } from './streamTraversal'; diff --git a/apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts b/packages/ai/src/react/ai/stream/streamTraversal.ts similarity index 96% rename from apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts rename to packages/ai/src/react/ai/stream/streamTraversal.ts index 195579bd49..6b926feaab 100644 --- a/apps/www/src/registry/default/plate-ui/stream/streamTraversal.ts +++ b/packages/ai/src/react/ai/stream/streamTraversal.ts @@ -2,7 +2,7 @@ import type { PlateEditor } from '@udecode/plate-core/react'; -import { AIPlugin } from '@udecode/plate-ai/react'; +import { AIPlugin } from '../AIPlugin'; interface StreamTraversalOptions { prompt: string; diff --git a/apps/www/src/registry/default/plate-ui/utils/getContent.ts b/packages/ai/src/react/ai/utils/getContent.ts similarity index 59% rename from apps/www/src/registry/default/plate-ui/utils/getContent.ts rename to packages/ai/src/react/ai/utils/getContent.ts index 5ee7a78988..18348e7e26 100644 --- a/apps/www/src/registry/default/plate-ui/utils/getContent.ts +++ b/packages/ai/src/react/ai/utils/getContent.ts @@ -1,23 +1,19 @@ import type { PlateEditor } from '@udecode/plate-common/react'; -import { AIPlugin } from '@udecode/plate-ai/react'; +import { getNodeEntries, isBlock } from '@udecode/plate-common'; import { serializeMd, serializeMdNodes } from '@udecode/plate-markdown'; import { type BlockSelectionConfig, BlockSelectionPlugin, } from '@udecode/plate-selection/react'; -import { - type GetNodeEntriesOptions, - getNodeEntries, - isBlock, - isElement, -} from '@udecode/slate'; + +import { AIPlugin } from '../AIPlugin'; // If some content has already been generated using it to modify(improve) otherwise using the selection or block selected nodes. export const getContent = (editor: PlateEditor, aiEditor: PlateEditor) => { const aiState = editor.getOptions(AIPlugin).aiState; - if (aiState === 'done') return serializeAI(aiEditor); + if (aiState === 'done') return serializeMd(aiEditor); // Not Sure if ( editor.getOption( @@ -26,7 +22,10 @@ export const getContent = (editor: PlateEditor, aiEditor: PlateEditor) => { editor.id ) ) { - const entries = getBlockSelectedEntries(editor); + const entries = editor + .getApi(BlockSelectionPlugin) + .blockSelection.getSelectedBlocks(); + const nodes = Array.from(entries, (entry) => entry[0]); return serializeMdNodes(nodes as any); @@ -40,20 +39,3 @@ export const getContent = (editor: PlateEditor, aiEditor: PlateEditor) => { return serializeMdNodes(nodes as any); }; - -export const serializeAI = (editor: PlateEditor) => { - return serializeMd(editor as any).trim(); -}; - -export const getBlockSelectedEntries = ( - editor: PlateEditor, - options?: GetNodeEntriesOptions -) => { - const ids = editor.getOptions(BlockSelectionPlugin).selectedIds; - - return getNodeEntries(editor, { - at: [], - match: (n) => isElement(n) && ids?.has(n.id as string), - ...options, - }); -}; diff --git a/apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts b/packages/ai/src/react/ai/utils/getNextPathByNumber.ts similarity index 100% rename from apps/www/src/registry/default/plate-ui/utils/getNextPathByNumber.ts rename to packages/ai/src/react/ai/utils/getNextPathByNumber.ts diff --git a/packages/ai/src/react/ai/utils/index.ts b/packages/ai/src/react/ai/utils/index.ts index c75e5e81cc..c243b7956b 100644 --- a/packages/ai/src/react/ai/utils/index.ts +++ b/packages/ai/src/react/ai/utils/index.ts @@ -2,4 +2,6 @@ * @file Automatically generated by barrelsby. */ +export * from './getContent'; +export * from './getNextPathByNumber'; export * from './updateMenuAnchorByPath'; diff --git a/packages/selection/src/react/components/index.ts b/packages/selection/src/react/components/index.ts deleted file mode 100644 index b34c8e115d..0000000000 --- a/packages/selection/src/react/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './BlockSelectable'; diff --git a/packages/selection/src/react/context-menu/index.ts b/packages/selection/src/react/context-menu/index.ts deleted file mode 100644 index 866fb2c896..0000000000 --- a/packages/selection/src/react/context-menu/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './types'; -export * from './useBlockContextMenu'; -export * from './useBlockMenuItems'; diff --git a/packages/selection/src/react/index.ts b/packages/selection/src/react/index.ts deleted file mode 100644 index 716cec46e5..0000000000 --- a/packages/selection/src/react/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './BlockContextMenuPlugin'; -export * from './BlockSelectionPlugin'; -export * from './onKeyDownSelection'; -export * from './useHooksBlockSelection'; -export * from './useSelectionArea'; -export * from './components/index'; -export * from './context-menu/index'; -export * from './utils/index'; diff --git a/packages/selection/src/react/utils/index.ts b/packages/selection/src/react/utils/index.ts deleted file mode 100644 index 0e0ad49409..0000000000 --- a/packages/selection/src/react/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './copySelectedBlocks'; -export * from './onChangeBlockSelection'; -export * from './openContextMenu'; -export * from './pasteSelectedBlocks'; -export * from './selectInsertedBlocks'; diff --git a/yarn.lock b/yarn.lock index 86cf25530b..2646f6516e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5897,6 +5897,8 @@ __metadata: resolution: "@udecode/plate-ai@workspace:packages/ai" dependencies: "@udecode/plate-combobox": "npm:38.0.1" + "@udecode/plate-markdown": "npm:38.0.1" + "@udecode/plate-selection": "npm:38.0.11" lodash: "npm:^4.17.21" peerDependencies: "@udecode/plate-common": ">=38.0.6" From 08dd0c60b5634f54f20bcefba1d18f682aec19d3 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 18:13:42 +0800 Subject: [PATCH 034/127] fix --- packages/selection/src/react/components/index.ts | 5 +++++ packages/selection/src/react/context-menu/index.ts | 7 +++++++ packages/selection/src/react/index.ts | 12 ++++++++++++ packages/selection/src/react/utils/index.ts | 9 +++++++++ 4 files changed, 33 insertions(+) create mode 100644 packages/selection/src/react/components/index.ts create mode 100644 packages/selection/src/react/context-menu/index.ts create mode 100644 packages/selection/src/react/index.ts create mode 100644 packages/selection/src/react/utils/index.ts diff --git a/packages/selection/src/react/components/index.ts b/packages/selection/src/react/components/index.ts new file mode 100644 index 0000000000..b34c8e115d --- /dev/null +++ b/packages/selection/src/react/components/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './BlockSelectable'; diff --git a/packages/selection/src/react/context-menu/index.ts b/packages/selection/src/react/context-menu/index.ts new file mode 100644 index 0000000000..866fb2c896 --- /dev/null +++ b/packages/selection/src/react/context-menu/index.ts @@ -0,0 +1,7 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './types'; +export * from './useBlockContextMenu'; +export * from './useBlockMenuItems'; diff --git a/packages/selection/src/react/index.ts b/packages/selection/src/react/index.ts new file mode 100644 index 0000000000..716cec46e5 --- /dev/null +++ b/packages/selection/src/react/index.ts @@ -0,0 +1,12 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './BlockContextMenuPlugin'; +export * from './BlockSelectionPlugin'; +export * from './onKeyDownSelection'; +export * from './useHooksBlockSelection'; +export * from './useSelectionArea'; +export * from './components/index'; +export * from './context-menu/index'; +export * from './utils/index'; diff --git a/packages/selection/src/react/utils/index.ts b/packages/selection/src/react/utils/index.ts new file mode 100644 index 0000000000..0e0ad49409 --- /dev/null +++ b/packages/selection/src/react/utils/index.ts @@ -0,0 +1,9 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './copySelectedBlocks'; +export * from './onChangeBlockSelection'; +export * from './openContextMenu'; +export * from './pasteSelectedBlocks'; +export * from './selectInsertedBlocks'; From 582152d7e1c75187f830318785ca04c2e6f20f90 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 18:13:59 +0800 Subject: [PATCH 035/127] docs --- apps/www/content/docs/copilot.mdx | 5 ++++- apps/www/public/r/styles/default/cursor-overlay.json | 2 +- apps/www/public/r/styles/default/floating-toolbar.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index 9ee72aa75c..fa3f75d729 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -122,7 +122,10 @@ const copilotState = editor.getOptions(CopilotPlugin).copilotState; -## Conflict with Tabbable Plugin +## Conflict with Other Plugins + + +### Conflict with Tabbable Plugin When using both the [Tabbable Plugin](https://platejs.org/docs/tabbable) and the Copilot feature in your editor, you may encounter conflicts with the `Tab` key functionality. This is because both features utilize the same key binding. diff --git a/apps/www/public/r/styles/default/cursor-overlay.json b/apps/www/public/r/styles/default/cursor-overlay.json index 353b337305..2704250a5e 100644 --- a/apps/www/public/r/styles/default/cursor-overlay.json +++ b/apps/www/public/r/styles/default/cursor-overlay.json @@ -2,7 +2,7 @@ "dependencies": [], "files": [ { - "content": "import React from 'react';\n\nimport { cn } from '@udecode/cn';\nimport {\n createPlatePlugin,\n findEventRange,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport {\n type CursorData,\n type CursorOverlayProps,\n type CursorProps,\n type CursorState,\n CursorOverlay as CursorOverlayPrimitive,\n} from '@udecode/plate-cursor';\nimport { DndPlugin } from '@udecode/plate-dnd';\n\nexport function Cursor({\n caretPosition,\n classNames,\n data,\n disableCaret,\n disableSelection,\n selectionRects,\n}: CursorProps) {\n const { style, selectionStyle = style } = data ?? ({} as CursorData);\n\n return (\n <>\n {!disableSelection &&\n selectionRects.map((position, i) => (\n \n ))}\n {!disableCaret && caretPosition && (\n \n )}\n \n );\n}\n\nexport function CursorOverlay({ cursors, ...props }: CursorOverlayProps) {\n const editor = useEditorRef();\n const dynamicCursors = editor.useOption(DragOverCursorPlugin, 'cursors');\n\n const allCursors = { ...cursors, ...dynamicCursors };\n\n return (\n \n );\n}\n\nconst DragOverCursorPlugin = createPlatePlugin({\n key: 'dragOverCursor',\n options: { cursors: {} as Record> },\n handlers: {\n onDragEnd: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n onDragLeave: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n onDragOver: ({ editor, event, plugin }) => {\n if (editor.getOptions(DndPlugin).isDragging) return;\n\n const range = findEventRange(editor, event);\n\n if (!range) return;\n\n editor.setOption(plugin, 'cursors', {\n drag: {\n key: 'drag',\n data: {\n style: {\n backgroundColor: 'hsl(222.2 47.4% 11.2%)',\n width: 3,\n },\n },\n selection: range,\n },\n });\n },\n onDrop: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n },\n});\n", + "content": "import React, { useEffect } from 'react';\n\nimport { cn } from '@udecode/cn';\nimport {\n createPlatePlugin,\n findEventRange,\n useEditorPlugin,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport {\n type CursorData,\n type CursorOverlayProps,\n type CursorProps,\n type CursorState,\n CursorOverlay as CursorOverlayPrimitive,\n} from '@udecode/plate-cursor';\nimport { DndPlugin } from '@udecode/plate-dnd';\nimport { BlockSelectionPlugin } from '@udecode/plate-selection/react';\n\nexport function Cursor({\n caretPosition,\n classNames,\n data,\n disableCaret,\n disableSelection,\n selectionRects,\n}: CursorProps) {\n const { style, selectionStyle = style } = data ?? ({} as CursorData);\n\n return (\n <>\n {!disableSelection &&\n selectionRects.map((position, i) => (\n \n ))}\n {!disableCaret && caretPosition && (\n \n )}\n \n );\n}\n\nexport function CursorOverlay({ cursors, ...props }: CursorOverlayProps) {\n const editor = useEditorRef();\n const dynamicCursors = editor.useOption(DragOverCursorPlugin, 'cursors');\n\n const allCursors = { ...cursors, ...dynamicCursors };\n\n return (\n \n );\n}\n\nconst DragOverCursorPlugin = createPlatePlugin({\n key: 'dragOverCursor',\n options: { cursors: {} as Record> },\n handlers: {\n onDragEnd: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n onDragLeave: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n onDragOver: ({ editor, event, plugin }) => {\n if (editor.getOptions(DndPlugin).isDragging) return;\n\n const range = findEventRange(editor, event);\n\n if (!range) return;\n\n editor.setOption(plugin, 'cursors', {\n drag: {\n key: 'drag',\n data: {\n style: {\n backgroundColor: 'hsl(222.2 47.4% 11.2%)',\n width: 3,\n },\n },\n selection: range,\n },\n });\n },\n onDrop: ({ editor, plugin }) => {\n editor.setOption(plugin, 'cursors', {});\n },\n },\n});\n\nexport const SelectionOverlayPlugin = createPlatePlugin({\n key: 'selection_over_lay',\n useHooks: () => {\n const { editor } = useEditorPlugin(BlockSelectionPlugin);\n const isSelecting = editor.useOptions(BlockSelectionPlugin).isSelecting;\n\n useEffect(() => {\n if (isSelecting) {\n setTimeout(() => {\n editor.setOption(DragOverCursorPlugin, 'cursors', {});\n }, 0);\n }\n }, [editor, isSelecting]);\n },\n handlers: {\n onBlur: ({ editor, event }) => {\n const isPrevented =\n (event.relatedTarget as HTMLElement)?.dataset?.platePreventOverlay ===\n 'true';\n\n if (isPrevented) return;\n if (editor.selection) {\n editor.setOption(DragOverCursorPlugin, 'cursors', {\n drag: {\n key: 'blur',\n data: {\n selectionStyle: {\n backgroundColor: 'rgba(47, 121, 216, 0.35)',\n },\n },\n selection: editor.selection,\n },\n });\n }\n },\n onFocus: ({ editor }) => {\n editor.setOption(DragOverCursorPlugin, 'cursors', {});\n },\n },\n});\n", "path": "plate-ui/cursor-overlay.tsx", "target": "", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/floating-toolbar.json b/apps/www/public/r/styles/default/floating-toolbar.json index 9de2dc03c0..52d9f50c49 100644 --- a/apps/www/public/r/styles/default/floating-toolbar.json +++ b/apps/www/public/r/styles/default/floating-toolbar.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport {\n PortalBody,\n useComposedRef,\n useEditorId,\n useEventEditorSelectors,\n} from '@udecode/plate-common/react';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const editorId = useEditorId();\n const focusedEditorId = useEventEditorSelectors.focus();\n\n const floatingToolbarState = useFloatingToolbarState({\n editorId,\n focusedEditorId,\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n PortalBody,\n useComposedRef,\n useEditorId,\n useEditorPlugin,\n useEventEditorSelectors,\n} from '@udecode/plate-common/react';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const editorId = useEditorId();\n const focusedEditorId = useEventEditorSelectors.focus();\n\n // FIXME: can not using isOpen\n const { editor, useOption } = useEditorPlugin(AIPlugin);\n const aiOpen = useOption('openEditorId') === editor.id;\n\n const floatingToolbarState = useFloatingToolbarState({\n editorId,\n focusedEditorId,\n hideToolbar: aiOpen,\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", "path": "plate-ui/floating-toolbar.tsx", "target": "", "type": "registry:ui" From 0bfbfb9df9056e5b8b23052002931a4fbeef0cd4 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 18:35:01 +0800 Subject: [PATCH 036/127] fix --- apps/www/src/registry/default/example/playground-demo.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index e903a46bb2..f99f5861b8 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -273,7 +273,6 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { // Functionality AutoformatPlugin.configure({ options: autoformatOptions }), BlockSelectionPlugin.configure({ - enabled: !!scrollSelector, options: { areaOptions: { boundaries: `#${scrollSelector}`, @@ -350,7 +349,10 @@ export default function PlaygroundDemo({ const containerRef = useRef(null); const enabled = settingsStore.use.checkedComponents(); - const editor = usePlaygroundEditor(id, scrollSelector); + const editor = usePlaygroundEditor( + id, + scrollSelector ?? `blockSelection-${id}` + ); return ( @@ -374,7 +376,7 @@ export default function PlaygroundDemo({ } >
Date: Sun, 29 Sep 2024 22:17:10 +0800 Subject: [PATCH 037/127] ci --- apps/www/public/r/styles/default/floating-toolbar.json | 2 +- apps/www/src/config/customizer-items.ts | 10 +++++++++- apps/www/src/config/customizer-list.ts | 3 ++- apps/www/src/config/customizer-plugins.ts | 10 +++++++++- .../src/lib/plate/demo/values/usePlaygroundValue.ts | 3 +-- .../src/registry/default/plate-ui/floating-toolbar.tsx | 7 ------- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/www/public/r/styles/default/floating-toolbar.json b/apps/www/public/r/styles/default/floating-toolbar.json index 52d9f50c49..9de2dc03c0 100644 --- a/apps/www/public/r/styles/default/floating-toolbar.json +++ b/apps/www/public/r/styles/default/floating-toolbar.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n PortalBody,\n useComposedRef,\n useEditorId,\n useEditorPlugin,\n useEventEditorSelectors,\n} from '@udecode/plate-common/react';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const editorId = useEditorId();\n const focusedEditorId = useEventEditorSelectors.focus();\n\n // FIXME: can not using isOpen\n const { editor, useOption } = useEditorPlugin(AIPlugin);\n const aiOpen = useOption('openEditorId') === editor.id;\n\n const floatingToolbarState = useFloatingToolbarState({\n editorId,\n focusedEditorId,\n hideToolbar: aiOpen,\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport {\n PortalBody,\n useComposedRef,\n useEditorId,\n useEventEditorSelectors,\n} from '@udecode/plate-common/react';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const editorId = useEditorId();\n const focusedEditorId = useEventEditorSelectors.focus();\n\n const floatingToolbarState = useFloatingToolbarState({\n editorId,\n focusedEditorId,\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", "path": "plate-ui/floating-toolbar.tsx", "target": "", "type": "registry:ui" diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index c90a8f9851..e222cac094 100644 --- a/apps/www/src/config/customizer-items.ts +++ b/apps/www/src/config/customizer-items.ts @@ -1,4 +1,4 @@ -import { CopilotPlugin } from '@udecode/plate-ai/react'; +import { AIPlugin, CopilotPlugin } from '@udecode/plate-ai/react'; import { AlignPlugin } from '@udecode/plate-alignment/react'; import { AutoformatPlugin } from '@udecode/plate-autoformat/react'; import { @@ -105,6 +105,14 @@ export type SettingPlugin = { }; export const customizerItems: Record = { + [AIPlugin.key]: { + id: AIPlugin.key, + badges: [customizerBadges.handler], + label: 'AI', + npmPackage: '@udecode/plate-ai', + pluginFactory: 'AIPlugin', + route: customizerPlugins.ai.route, + }, [AlignPlugin.key]: { id: AlignPlugin.key, badges: [customizerBadges.style], diff --git a/apps/www/src/config/customizer-list.ts b/apps/www/src/config/customizer-list.ts index 7d331ac04b..aab94f0c08 100644 --- a/apps/www/src/config/customizer-list.ts +++ b/apps/www/src/config/customizer-list.ts @@ -1,4 +1,4 @@ -import { CopilotPlugin } from '@udecode/plate-ai/react'; +import { AIPlugin, CopilotPlugin } from '@udecode/plate-ai/react'; import { AlignPlugin } from '@udecode/plate-alignment/react'; import { AutoformatPlugin } from '@udecode/plate-autoformat/react'; import { @@ -134,6 +134,7 @@ export const customizerList = [ customizerItems[TrailingBlockPlugin.key], customizerItems[CopilotPlugin.key], customizerItems[SlashPlugin.key], + customizerItems[AIPlugin.key], ], label: 'Functionality', }, diff --git a/apps/www/src/config/customizer-plugins.ts b/apps/www/src/config/customizer-plugins.ts index cdf3e3ab79..8dd6e473fd 100644 --- a/apps/www/src/config/customizer-plugins.ts +++ b/apps/www/src/config/customizer-plugins.ts @@ -1,4 +1,4 @@ -import { CopilotPlugin } from '@udecode/plate-ai/react'; +import { AIPlugin, CopilotPlugin } from '@udecode/plate-ai/react'; import { AlignPlugin } from '@udecode/plate-alignment/react'; import { AutoformatPlugin } from '@udecode/plate-autoformat/react'; import { @@ -41,6 +41,7 @@ import { TablePlugin } from '@udecode/plate-table/react'; import { TogglePlugin } from '@udecode/plate-toggle/react'; import { TrailingBlockPlugin } from '@udecode/plate-trailing-block'; +import { aiValue } from '@/lib/plate/demo/values/aiValue'; import { columnValue } from '@/lib/plate/demo/values/columnValue'; import { copilotValue } from '@/lib/plate/demo/values/copilotValue'; import { slashCommandValue } from '@/lib/plate/demo/values/slahMenuValue'; @@ -85,6 +86,13 @@ export type ValueId = keyof typeof customizerPlugins | 'tableMerge'; // cmdk needs lowercase export const customizerPlugins = { + ai: { + id: 'ai', + label: 'AI', + plugins: [AIPlugin.key], + route: '/docs/ai', + value: aiValue, + }, align: { id: 'align', label: 'Align', diff --git a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts index cd38c20c81..8bcdc82258 100644 --- a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts +++ b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts @@ -6,7 +6,6 @@ import { settingsStore } from '@/components/context/settings-store'; import { type ValueId, customizerPlugins } from '@/config/customizer-plugins'; import { mapNodeId } from '@/plate/demo/mapNodeId'; -import { aiValue } from './aiValue'; import { alignValue } from './alignValue'; import { autoformatValue } from './autoformatValue'; import { basicElementsValue } from './basicElementsValue'; @@ -76,7 +75,7 @@ export const usePlaygroundValue = (id?: ValueId): MyValue => { //AI if (enabled.copilot) value.unshift(...copilotValue); - value.unshift(...aiValue); + // value.unshift(...aiValue); // Marks if (enabled.color || enabled.backgroundColor) value.push(...fontValue); diff --git a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx index 3b1aedec74..a9fda8e243 100644 --- a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx +++ b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx @@ -3,12 +3,10 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; -import { AIPlugin } from '@udecode/plate-ai/react'; import { PortalBody, useComposedRef, useEditorId, - useEditorPlugin, useEventEditorSelectors, } from '@udecode/plate-common/react'; import { @@ -30,14 +28,9 @@ export const FloatingToolbar = withRef< const editorId = useEditorId(); const focusedEditorId = useEventEditorSelectors.focus(); - // FIXME: can not using isOpen - const { editor, useOption } = useEditorPlugin(AIPlugin); - const aiOpen = useOption('openEditorId') === editor.id; - const floatingToolbarState = useFloatingToolbarState({ editorId, focusedEditorId, - hideToolbar: aiOpen, ...state, floatingOptions: { middleware: [ From d2072513f3810cb61ef2ee6ec288ef36c52eb71e Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 29 Sep 2024 22:33:56 +0800 Subject: [PATCH 038/127] docs --- apps/www/content/docs/copilot.mdx | 100 +++++++++++++++--- .../plate/demo/values/usePlaygroundValue.ts | 5 +- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index fa3f75d729..0858b129a3 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -108,6 +108,7 @@ Here's a more detailed example of how you might implement this function: ## Copilot state The Copilot plugin maintains its own state to manage the suggestion process. The state can be either 'idle' or 'completed'. Here's a breakdown of what each state means: + ```ts type CopilotState = 'idle' | 'completed'; ``` @@ -116,13 +117,93 @@ type CopilotState = 'idle' | 'completed'; - `completed`: This state indicates that the Copilot has generated a suggestion and it's ready to be inserted into the editor. You'll see this suggestion as gray text in the editor in this state. You can access the current state of the Copilot using: + ```ts const copilotState = editor.getOptions(CopilotPlugin).copilotState; ``` +## Conflict with Other Plugins +The Tab key is a popular and frequently used key in text editing, which can lead to conflicts. -## Conflict with Other Plugins +### Conflict with Indent Plugin + +Ideally, we should resolve conflicts within the plugins themselves. However, there isn't a straightforward method to do this for the Indent Plugin and Copilot Plugin conflict. +As a workaround, you can place the Copilot Plugin before the Indent Plugin in your plugin configuration. + +Here's an example of how to order your plugins: + +```tsx +const editor = usePlateEditor({ + id: 'ai-demo', + override: { + components: PlateUI, + }, + plugins: [ + MarkdownPlugin.configure({ options: { indentList: true } }), + // CopilotPlugin should be before indent plugin + CopilotPlugin, + IndentPlugin.extend({ + inject: { + targetPlugins: [ + ParagraphPlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + HEADING_KEYS.h4, + HEADING_KEYS.h5, + HEADING_KEYS.h6, + BlockquotePlugin.key, + CodeBlockPlugin.key, + TogglePlugin.key, + ], + }, + }), + IndentListPlugin.extend({ + inject: { + targetPlugins: [ + ParagraphPlugin.key, + HEADING_KEYS.h1, + HEADING_KEYS.h2, + HEADING_KEYS.h3, + HEADING_KEYS.h4, + HEADING_KEYS.h5, + HEADING_KEYS.h6, + BlockquotePlugin.key, + CodeBlockPlugin.key, + TogglePlugin.key, + ], + }, + options: { + listStyleTypes: { + fire: { + liComponent: FireLiComponent, + markerComponent: FireMarker, + type: 'fire', + }, + todo: { + liComponent: TodoLi, + markerComponent: TodoMarker, + type: 'todo', + }, + }, + }, + }), + ...otherPlugins, + ], + value: copilotValue, +}); +``` + +It's important to note that when using `IndentListPlugin` instead of `ListPlugin`, you should configure the `MarkdownPlugin` with the `indentList: true` option. + +This is necessary because the LLM generates Markdown, which needs to be converted to Plate nodes.If your LLM just generate the plain text, you can ignore this. + +Here's how you can set it up: + +```tsx +MarkdownPlugin.configure({ options: { indentList: true } }), +``` ### Conflict with Tabbable Plugin @@ -162,17 +243,14 @@ TabbablePlugin.configure(({ editor }) => ({ })); ``` - - ## Plate Plus + In Plate Plus, We using debounce mode by default. That's mean you will see the suggestion automatically without pressing `Control+Space`. We also provide a new style of hover card that is more user-friendly.You can hover on the suggestion to see the hover card. - All of the backend setup is available in [Potion template](https://pro.platejs.org/docs/templates/potion). - - - This function does not return a value. - + This function does not return a value. - ### editor.getApi(CopilotPlugin).copilot.setCopilot Sets the suggestion text to the editor. @@ -205,10 +280,10 @@ Sets the suggestion text to the editor. - ## Utils functions ### withoutAbort + Temporarily disables the abort functionality of the Copilot plugin, by default any apply will cause the Copilot to remove the suggestion text and abort the request. @@ -222,12 +297,11 @@ by default any apply will cause the Copilot to remove the suggestion text and ab - - This function does not return a value. - + This function does not return a value. Usage example: + ```ts import { withoutAbort } from '@udecode/plate-ai/react'; diff --git a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts index 8bcdc82258..0dbc496516 100644 --- a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts +++ b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts @@ -6,6 +6,7 @@ import { settingsStore } from '@/components/context/settings-store'; import { type ValueId, customizerPlugins } from '@/config/customizer-plugins'; import { mapNodeId } from '@/plate/demo/mapNodeId'; +import { aiValue } from './aiValue'; import { alignValue } from './alignValue'; import { autoformatValue } from './autoformatValue'; import { basicElementsValue } from './basicElementsValue'; @@ -74,9 +75,7 @@ export const usePlaygroundValue = (id?: ValueId): MyValue => { if (enabled.toc) value.unshift(...tocValue); //AI if (enabled.copilot) value.unshift(...copilotValue); - - // value.unshift(...aiValue); - + if (enabled.ai) value.unshift(...aiValue); // Marks if (enabled.color || enabled.backgroundColor) value.push(...fontValue); if (enabled.highlight) value.push(...highlightValue); From 0e5bfb91dc07cf6aeab38e3be1d1b5b835a6b82f Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Mon, 30 Sep 2024 12:49:07 +0800 Subject: [PATCH 039/127] temp --- apps/www/package.json | 2 +- .../plate-ui/playground-floating-toolbar.tsx | 83 +++++ .../default/example/playground-demo.tsx | 14 +- .../registry/default/plate-ui/ai-actions.tsx | 6 +- .../default/plate-ui/ai-menu-items.tsx | 44 +-- .../src/registry/default/plate-ui/ai-menu.tsx | 42 +-- .../src/registry/default/plate-ui/menu.tsx | 318 ++++++++---------- packages/ai/src/react/ai/AIPlugin.ts | 12 +- packages/ai/src/react/ai/hook/useAI.ts | 151 +++++++++ packages/ai/src/react/ai/useAIHook.ts | 15 + packages/menu/.npmignore | 3 + packages/menu/README.md | 11 + packages/menu/package.json | 72 ++++ packages/menu/src/hooks/index.ts | 1 + packages/menu/src/hooks/useMenu.ts | 208 ++++++++++++ packages/menu/src/index.ts | 2 + packages/menu/src/types.ts | 56 +++ packages/menu/tsconfig.build.json | 8 + packages/menu/tsconfig.json | 5 + yarn.lock | 21 +- 20 files changed, 833 insertions(+), 241 deletions(-) create mode 100644 apps/www/src/components/plate-ui/playground-floating-toolbar.tsx create mode 100644 packages/ai/src/react/ai/hook/useAI.ts create mode 100644 packages/ai/src/react/ai/useAIHook.ts create mode 100644 packages/menu/.npmignore create mode 100644 packages/menu/README.md create mode 100644 packages/menu/package.json create mode 100644 packages/menu/src/hooks/index.ts create mode 100644 packages/menu/src/hooks/useMenu.ts create mode 100644 packages/menu/src/index.ts create mode 100644 packages/menu/src/types.ts create mode 100644 packages/menu/tsconfig.build.json create mode 100644 packages/menu/tsconfig.json diff --git a/apps/www/package.json b/apps/www/package.json index ade1476029..b16625d194 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,7 +35,6 @@ "react-dom": "^18.3.1" }, "dependencies": { - "@ariakit/react": "0.4.7", "@radix-ui/colors": "3.0.0", "@radix-ui/react-accessible-icon": "^1.1.0", "@radix-ui/react-accordion": "^1.2.0", @@ -109,6 +108,7 @@ "@udecode/plate-markdown": "workspace:^", "@udecode/plate-media": "workspace:^", "@udecode/plate-mention": "workspace:^", + "@udecode/plate-menu": "workspace:^", "@udecode/plate-node-id": "workspace:^", "@udecode/plate-normalizers": "workspace:^", "@udecode/plate-playwright": "workspace:^", diff --git a/apps/www/src/components/plate-ui/playground-floating-toolbar.tsx b/apps/www/src/components/plate-ui/playground-floating-toolbar.tsx new file mode 100644 index 0000000000..dbb71134a1 --- /dev/null +++ b/apps/www/src/components/plate-ui/playground-floating-toolbar.tsx @@ -0,0 +1,83 @@ +'use client'; + +import React from 'react'; + +import { cn, withRef } from '@udecode/cn'; +import { AIPlugin } from '@udecode/plate-ai/react'; +import { + PortalBody, + useComposedRef, + useEditorId, + useEditorRef, + useEventEditorSelectors, +} from '@udecode/plate-common/react'; +import { + type FloatingToolbarState, + flip, + offset, + useFloatingToolbar, + useFloatingToolbarState, +} from '@udecode/plate-floating'; + +import { Toolbar } from '@/registry/default/plate-ui/toolbar'; + +export const PlaygroundFloatingToolbar = withRef< + typeof Toolbar, + { + state?: FloatingToolbarState; + } +>(({ children, state, ...props }, componentRef) => { + const editor = useEditorRef(); + const editorId = useEditorId(); + const focusedEditorId = useEventEditorSelectors.focus(); + + const aiOpen = editor.useOptions(AIPlugin).openEditorId === editor.id; + + const floatingToolbarState = useFloatingToolbarState({ + editorId, + focusedEditorId, + hideToolbar: aiOpen, + ...state, + floatingOptions: { + middleware: [ + offset(12), + flip({ + fallbackPlacements: [ + 'top-start', + 'top-end', + 'bottom-start', + 'bottom-end', + ], + padding: 12, + }), + ], + placement: 'top', + ...state?.floatingOptions, + }, + }); + + const { + hidden, + props: rootProps, + ref: floatingRef, + } = useFloatingToolbar(floatingToolbarState); + + const ref = useComposedRef(componentRef, floatingRef); + + if (hidden) return null; + + return ( + + + {children} + + + ); +}); diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index f99f5861b8..e42c8baaf2 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -68,6 +68,7 @@ import Prism from 'prismjs'; import { CheckPlugin } from '@/components/context/check-plugin'; import { settingsStore } from '@/components/context/settings-store'; import { PlaygroundFixedToolbarButtons } from '@/components/plate-ui/playground-fixed-toolbar-buttons'; +import { PlaygroundFloatingToolbar } from '@/components/plate-ui/playground-floating-toolbar'; import { PlaygroundFloatingToolbarButtons } from '@/components/plate-ui/playground-floating-toolbar-buttons'; import { getAutoformatOptions } from '@/lib/plate/demo/plugins/autoformatOptions'; import { copilotPlugin } from '@/lib/plate/demo/plugins/copilotPlugin'; @@ -82,6 +83,7 @@ import { tabbablePlugin } from '@/plate/demo/plugins/tabbablePlugin'; import { commentsData, usersData } from '@/plate/demo/values/commentsValue'; import { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue'; import { AIMenu } from '@/registry/default/plate-ui/ai-menu'; +import { createAIEditor } from '@/registry/default/plate-ui/ai-previdew-editor'; import { CommentsPopover } from '@/registry/default/plate-ui/comments-popover'; import { CursorOverlay, @@ -89,7 +91,6 @@ import { } from '@/registry/default/plate-ui/cursor-overlay'; import { Editor } from '@/registry/default/plate-ui/editor'; import { FixedToolbar } from '@/registry/default/plate-ui/fixed-toolbar'; -import { FloatingToolbar } from '@/registry/default/plate-ui/floating-toolbar'; import { ImagePreview } from '@/registry/default/plate-ui/image-preview'; import { FireLiComponent, @@ -169,6 +170,7 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { SelectionOverlayPlugin, AIPlugin.configure({ options: { + createAIEditor: createAIEditor, scrollContainerSelector: `#${scrollSelector}`, }, render: { aboveEditable: AIMenu }, @@ -275,6 +277,9 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { BlockSelectionPlugin.configure({ options: { areaOptions: { + behaviour: { + startThreshold: 20, + }, boundaries: `#${scrollSelector}`, container: `#${scrollSelector}`, selectables: [`#${scrollSelector} .slate-selectable`], @@ -357,7 +362,7 @@ export default function PlaygroundDemo({ return ( - console.log(value)} editor={editor}> + @@ -401,8 +406,9 @@ export default function PlaygroundDemo({ /> - - + diff --git a/apps/www/src/registry/default/plate-ui/ai-actions.tsx b/apps/www/src/registry/default/plate-ui/ai-actions.tsx index a0ae554c12..8e195b956d 100644 --- a/apps/www/src/registry/default/plate-ui/ai-actions.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-actions.tsx @@ -27,7 +27,7 @@ export const GROUP_ALIGN = 'group_align'; export const ACTION_CAPTION = 'action_cation'; -export const DefaultActions = { +export const CursorCommandsActions = { Summarize: { icon: , label: 'Summarize', @@ -68,7 +68,7 @@ export const ACTION_SUGGESTION_MAKE_LONGER = 'action_longer'; export const ACTION_SUGGESTION_TRY_AGAIN = 'action_try_again'; -export const DefaultSuggestionActions = { +export const CursorSuggestionActions = { close: { icon: , label: 'Close', @@ -110,7 +110,7 @@ export const ACTION_SELECTION_SIMPLIFY_LANGUAGE = 'action_simplify_language'; export const GROUP_SELECTION_LANGUAGES = 'group_selection_languages'; -export const SelectionActions = { +export const SelectionCommandsActions = { fixSpell: { icon: , label: 'Fix spelling & grammar', diff --git a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx index 1a3000ad7a..7af6d9605c 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx @@ -1,7 +1,7 @@ import { - DefaultActions, - DefaultSuggestionActions, - SelectionActions, + CursorCommandsActions, + CursorSuggestionActions, + SelectionCommandsActions, SelectionSuggestionActions, } from './ai-actions'; import { @@ -16,18 +16,18 @@ export const CursorCommands = () => { return ( <> - + - + - {renderMenuItems(DefaultActions.translate)} + {renderMenuItems(CursorCommandsActions.translate)} - + ); @@ -36,14 +36,14 @@ export const CursorCommands = () => { export const CursorSuggestions = () => { return ( <> - - - + + + - - + + ); }; @@ -51,20 +51,20 @@ export const CursorSuggestions = () => { export const SelectionCommands = () => { return ( <> - - - - - + + + + + - {renderMenuItems(SelectionActions.translate)} + {renderMenuItems(SelectionCommandsActions.translate)} diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx index aaddf77fb1..95757758fa 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -11,6 +11,8 @@ import React, { useState, } from 'react'; +import type { actionGroup } from '@udecode/plate-menu'; + import { cn } from '@udecode/cn'; import { AIPlugin, @@ -19,6 +21,7 @@ import { streamInsertTextSelection, } from '@udecode/plate-ai/react'; import { useEditorPlugin } from '@udecode/plate-core/react'; +import { Ariakit } from '@udecode/plate-menu'; import { focusEditor } from '@udecode/slate-react'; import isHotkey from 'is-hotkey'; @@ -26,9 +29,9 @@ import { Icons } from '@/components/icons'; import { useActionHandler } from './action-handler'; import { - DefaultActions, - DefaultSuggestionActions, - SelectionActions, + CursorCommandsActions, + CursorSuggestionActions, + SelectionCommandsActions, SelectionSuggestionActions, defaultValues, } from './ai-actions'; @@ -41,8 +44,6 @@ import { import { AIPreviewEditor } from './ai-previdew-editor'; import { Button } from './button'; import { - type actionGroup, - Ariakit, Menu, comboboxVariants, filterAndBuildMenuTree, @@ -62,7 +63,6 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { const { aiEditor } = editor.useOptions(AIPlugin); - const isModalOpenRef = React.useRef(false); // init const menu = Ariakit.useMenuStore(); useEffect(() => { @@ -145,11 +145,12 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { if (menuType === 'selection') return [SelectionSuggestions, SelectionSuggestionActions]; - return [CursorSuggestions, DefaultSuggestionActions]; + return [CursorSuggestions, CursorSuggestionActions]; } - if (menuType === 'selection') return [SelectionCommands, SelectionActions]; + if (menuType === 'selection') + return [SelectionCommands, SelectionCommandsActions]; - return [CursorCommands, DefaultActions]; + return [CursorCommands, CursorCommandsActions]; }, [aiState, menuType]); /** IME */ @@ -167,27 +168,8 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { variant="ai" loading={aiState === 'generating' || aiState === 'requesting'} open={isOpen} - onClickOutside={(e) => { - if (aiState === 'idle') return editor.getApi(AIPlugin).ai.hide(); - if (isModalOpenRef.current) return; - - e.preventDefault(); - isModalOpenRef.current = true; - - // pushModal('Discard', { - // onCancel: () => { - // setTimeout(() => { - // editor.getApi(AIPlugin).ai.focusMenu(); - // }, 0); - // }, - // onConfirm: () => { - // // TODO: cancel stream - // editor.getApi(AIPlugin).ai.hide(); - // }, - // onSettled: () => { - // isModalOpenRef.current = false; - // }, - // }); + onClickOutside={() => { + return editor.getApi(AIPlugin).ai.hide(); }} onValueChange={(value) => startTransition(() => setSearchValue(value))} onValuesChange={(values: typeof defaultValues) => { diff --git a/apps/www/src/registry/default/plate-ui/menu.tsx b/apps/www/src/registry/default/plate-ui/menu.tsx index 6f25755191..1f7b2f35f5 100644 --- a/apps/www/src/registry/default/plate-ui/menu.tsx +++ b/apps/www/src/registry/default/plate-ui/menu.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; -import * as Ariakit from '@ariakit/react'; +import type { Action, MenuProps, setAction } from '@udecode/plate-menu'; + import { cn } from '@udecode/cn'; +import { Ariakit } from '@udecode/plate-menu'; import { cva } from 'class-variance-authority'; import { matchSorter } from 'match-sorter'; @@ -61,191 +63,157 @@ const comboboxListVariants = cva('rounded-sm', { }, }); -export interface Action { - group?: string; - groupName?: string; - icon?: React.ReactNode; - items?: Action[]; - keywords?: string[]; - label?: string; - shortcut?: string; - value?: string; -} - -export type actionGroup = { - group?: string; - value?: string; -}; - -export interface MenuProps extends Ariakit.MenuButtonProps<'div'> { - combobox?: Ariakit.ComboboxProps['render']; - comboboxClassName?: string; - comboboxListClassName?: string; - comboboxSubmitButton?: React.ReactElement; - dragButton?: Ariakit.MenuButtonProps['render']; - flip?: boolean; - getAnchorRect?: Ariakit.MenuProps['getAnchorRect']; - icon?: React.ReactNode; - injectAboveMenu?: React.ReactElement; - label?: React.ReactNode; - loading?: boolean; - loadingPlaceholder?: React.ReactNode; - onClickOutside?: (event: MouseEvent) => void; - onOpenChange?: (open: boolean) => void; - onRootMenuClose?: () => void; - onValueChange?: (value: string) => void; - onValuesChange?: Ariakit.MenuProviderProps['setValues']; - open?: boolean; - placement?: Ariakit.MenuProviderProps['placement']; - portal?: Ariakit.MenuProps['portal']; - searchValue?: string; - setAction?: setAction; - values?: Ariakit.MenuProviderProps['values']; - variant?: variant; -} - const SearchableContext = React.createContext(false); -type setAction = (actionGroup: actionGroup) => void; const ActionContext = React.createContext(null); type variant = 'ai' | 'default'; -export const Menu = React.forwardRef(function Menu( - { - children, - combobox, - comboboxClassName, - comboboxListClassName, - comboboxSubmitButton, - dragButton, - flip = true, - getAnchorRect, - icon, - injectAboveMenu, - label, - loading, - loadingPlaceholder, - open, - placement, - portal, - searchValue, - setAction, - store, - values, - variant, - onClickOutside, - onOpenChange, - onRootMenuClose, - onValueChange, - onValuesChange, - ...props - }, - ref -) { - const parent = Ariakit.useMenuContext(); - const searchable = searchValue != null || !!onValueChange || !!combobox; - const ParentSetAction = React.useContext(ActionContext); +type StyledMenuProps = MenuProps & { + variant: variant; +}; + +export const Menu = React.forwardRef( + function Menu( + { + children, + combobox, + comboboxClassName, + comboboxListClassName, + comboboxSubmitButton, + dragButton, + flip = true, + getAnchorRect, + icon, + injectAboveMenu, + label, + loading, + loadingPlaceholder, + open, + placement, + portal, + searchValue, + setAction, + store, + values, + variant, + onClickOutside, + onOpenChange, + onRootMenuClose, + onValueChange, + onValuesChange, + ...props + }, + ref + ) { + const parent = Ariakit.useMenuContext(); + const searchable = searchValue != null || !!onValueChange || !!combobox; + const ParentSetAction = React.useContext(ActionContext); - const isRootMenu = !parent; - const isDraggleButtonMenu = !!dragButton; - const menuRef = React.useRef(null); + const isRootMenu = !parent; + const isDraggleButtonMenu = !!dragButton; + const menuRef = React.useRef(null); - useOnClickOutside(menuRef, onClickOutside); + useOnClickOutside(menuRef, onClickOutside); - const menuProviderProps = { - open, - placement: isRootMenu ? placement : 'right', - setOpen: (v: boolean) => { - onOpenChange?.(v); + const menuProviderProps = { + open, + placement: isRootMenu ? placement : 'right', + setOpen: (v: boolean) => { + onOpenChange?.(v); - if (!v && !parent && !dragButton) onRootMenuClose?.(); - }, - setValues: onValuesChange, - showTimeout: 100, - store, - values, - }; - - const menuButtonProps = { - ref, - ...props, - className: cn(isRootMenu && !isDraggleButtonMenu && 'hidden'), - render: isRootMenu ? dragButton : , - }; - - const menuProps = { - className: cn(menuVariants({ variant }), props.className, searchable && ''), - flip, - getAnchorRect, - gutter: isRootMenu ? 0 : 4, - portal, - ref: isRootMenu ? menuRef : undefined, - unmountOnHide: true, - }; - - const menuContent = ( - - - {icon} - {label} - - - - - {open && isRootMenu && injectAboveMenu} - - - {searchable ? ( - loading ? ( - - {loadingPlaceholder ??
loading...
} -
+ if (!v && !parent && !dragButton) onRootMenuClose?.(); + }, + setValues: onValuesChange, + showTimeout: 100, + store, + values, + }; + + const menuButtonProps = { + ref, + ...props, + className: cn(isRootMenu && !isDraggleButtonMenu && 'hidden'), + render: isRootMenu ? dragButton : , + }; + + const menuProps = { + className: cn( + menuVariants({ variant }), + props.className, + searchable && '' + ), + flip, + getAnchorRect, + gutter: isRootMenu ? 0 : 4, + portal, + ref: isRootMenu ? menuRef : undefined, + unmountOnHide: true, + }; + + const menuContent = ( + + + {icon} + {label} + + + + + {open && isRootMenu && injectAboveMenu} + + + {searchable ? ( + loading ? ( + + {loadingPlaceholder ??
loading...
} +
+ ) : ( + +
+ + {comboboxSubmitButton && comboboxSubmitButton} +
+ + {children} + +
+ ) ) : ( - -
- - {comboboxSubmitButton && comboboxSubmitButton} -
- - {children} - -
- ) - ) : ( - children - )} -
-
-
-
- ); + children + )} +
+
+
+
+ ); - const comboboxProviderProps = { - includesBaseElement: false, - resetValueOnHide: true, - setValue: onValueChange, - value: searchValue, - }; - - return searchable ? ( - - {menuContent} - - ) : ( - menuContent - ); -}); + const comboboxProviderProps = { + includesBaseElement: false, + resetValueOnHide: true, + setValue: onValueChange, + value: searchValue, + }; + + return searchable ? ( + + {menuContent} + + ) : ( + menuContent + ); + } +); export interface MenuSeparatorProps extends Ariakit.MenuSeparatorProps {} @@ -553,8 +521,6 @@ export function renderSearchMenuItems( return renderMenuItems({ items: matches }); } -export * as Ariakit from '@ariakit/react'; - // Utils --------------------------------------------------------------- import { useEffect, useLayoutEffect, useRef } from 'react'; diff --git a/packages/ai/src/react/ai/AIPlugin.ts b/packages/ai/src/react/ai/AIPlugin.ts index 08f95f3fba..aaf45a58c2 100644 --- a/packages/ai/src/react/ai/AIPlugin.ts +++ b/packages/ai/src/react/ai/AIPlugin.ts @@ -8,12 +8,15 @@ import { } from '@udecode/plate-common/react'; import { type BaseAIPluginConfig, BaseAIPlugin } from '../../lib'; +import { useAIHooks } from './useAIHook'; export const KEY_AI = 'ai'; -export interface AIPlugin { - scrollContainerSelector?: string; +interface ExposeOptions { + createAIEditor: () => PlateEditor; + scrollContainerSelector: string; trigger?: RegExp | string[] | string; + triggerPreviousCharPattern?: RegExp; } @@ -50,9 +53,9 @@ export type AIPluginConfig = ExtendConfig< lastWorkPath: Path | null; menuType: 'selection' | 'space' | null; openEditorId: string | null; - scrollContainerSelector: string; store: any | null; - } & AIApi & + } & ExposeOptions & + AIApi & AISelectors, { ai: AIApi; @@ -182,4 +185,5 @@ export const AIPlugin = toTPlatePlugin(BaseAIPlugin, { }); }, }, + useHooks: useAIHooks, })); diff --git a/packages/ai/src/react/ai/hook/useAI.ts b/packages/ai/src/react/ai/hook/useAI.ts new file mode 100644 index 0000000000..8c91b04555 --- /dev/null +++ b/packages/ai/src/react/ai/hook/useAI.ts @@ -0,0 +1,151 @@ +import React, { + type KeyboardEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import { isHotkey } from '@udecode/plate-common'; +import { focusEditor, useEditorPlugin } from '@udecode/plate-common/react'; + +import { type AIActionGroup, AIPlugin } from '../AIPlugin'; +import { streamInsertText, streamInsertTextSelection } from '../stream'; +import { getContent } from '../utils'; + +interface UseAIStateProps { + CursorCommands: () => JSX.Element; + CursorCommandsActions: any; + CursorSuggestionActions: any; + CursorSuggestions: () => JSX.Element; + SelectionCommands: () => JSX.Element; + SelectionCommandsActions: any; + SelectionSuggestionActions: any; + SelectionSuggestions: () => JSX.Element; + defaultValues: Record; + menu: any; +} + +export const useAIState = ({ + CursorCommands, + CursorCommandsActions, + CursorSuggestionActions, + CursorSuggestions, + SelectionCommands, + SelectionCommandsActions, + SelectionSuggestionActions, + SelectionSuggestions, + defaultValues, + menu, +}: UseAIStateProps) => { + const { api, editor, setOption, setOptions, useOption } = + useEditorPlugin(AIPlugin); + + const isOpen = useOption('isOpen', editor.id); + const action = useOption('action'); + const aiState = useOption('aiState'); + const menuType = useOption('menuType'); + const setAction = (action: AIActionGroup) => setOption('action', action); + + const { aiEditor } = editor.useOptions(AIPlugin); + + useEffect(() => { + setOptions({ + store: menu, + }); + // eslint-disable-next`-line react-hooks/exhaustive-deps + }, [isOpen, menu, setOptions]); + + const [values, setValues] = useState(defaultValues); + const [searchValue, setSearchValue] = useState(''); + + const streamInsert = useCallback(async () => { + if (!aiEditor) return; + if (menuType === 'selection') { + const content = getContent(editor, aiEditor); + + await streamInsertTextSelection(editor, aiEditor, { + prompt: `user prompt is ${searchValue} the content is ${content}`, + }); + } else if (menuType === 'space') { + await streamInsertText(editor, { + prompt: searchValue, + }); + } + }, [aiEditor, editor, menuType, searchValue]); + + return { + CursorCommands, + CursorCommandsActions, + CursorSuggestionActions, + CursorSuggestions, + SelectionCommands, + SelectionCommandsActions, + SelectionSuggestionActions, + SelectionSuggestions, + action, + aiState, + api, + editor, + menuType, + searchValue, + setAction, + setSearchValue, + setValues, + streamInsert, + values, + }; +}; + +export const useAI = (props: ReturnType) => { + const { + CursorCommands, + CursorCommandsActions, + CursorSuggestionActions, + CursorSuggestions, + SelectionCommands, + SelectionCommandsActions, + SelectionSuggestionActions, + SelectionSuggestions, + action, + aiState, + api, + editor, + menuType, + searchValue, + setAction, + setSearchValue, + setValues, + streamInsert, + values, + } = props; + + const onInputKeyDown = async (e: KeyboardEvent) => { + if (isHotkey('backspace')(e) && searchValue.length === 0) { + e.preventDefault(); + api.ai.hide(); + focusEditor(editor); + } + if (isHotkey('enter')(e)) await streamInsert(); + }; + const [CurrentItems, CurrentActions] = React.useMemo(() => { + if (aiState === 'done') { + if (menuType === 'selection') + return [SelectionSuggestions, SelectionSuggestionActions]; + + return [CursorSuggestions, CursorSuggestionActions]; + } + if (menuType === 'selection') + return [SelectionCommands, SelectionCommandsActions]; + + return [CursorCommands, CursorCommandsActions]; + }, [aiState, menuType]); + + /** IME */ + const [isComposing, setIsComposing] = useState(false); + + // const searchItems = useMemo(() => { + // return isComposing + // ? [] + // : filterAndBuildMenuTree(Object.values(CurrentActions), searchValue); + // }, [CurrentActions, isComposing, searchValue]); +}; diff --git a/packages/ai/src/react/ai/useAIHook.ts b/packages/ai/src/react/ai/useAIHook.ts new file mode 100644 index 0000000000..93e46544e8 --- /dev/null +++ b/packages/ai/src/react/ai/useAIHook.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +import { useEditorPlugin } from '@udecode/plate-common/react'; + +import { type AIPluginConfig, AIPlugin } from './AIPlugin'; + +export const useAIHooks = () => { + const { getOptions, setOptions } = useEditorPlugin(AIPlugin); + useEffect(() => { + setTimeout(() => { + const editor = getOptions().createAIEditor(); + setOptions({ aiEditor: editor }); + }, 0); + }, [setOptions, getOptions]); +}; diff --git a/packages/menu/.npmignore b/packages/menu/.npmignore new file mode 100644 index 0000000000..7d3b305b17 --- /dev/null +++ b/packages/menu/.npmignore @@ -0,0 +1,3 @@ +__tests__ +__test-utils__ +__mocks__ diff --git a/packages/menu/README.md b/packages/menu/README.md new file mode 100644 index 0000000000..06fb7e9d0e --- /dev/null +++ b/packages/menu/README.md @@ -0,0 +1,11 @@ +# Plate list plugin + +This package implements the list plugin for Plate. + +## Documentation + +Check out [List](https://platejs.org/docs/list). + +## License + +[MIT](../../LICENSE) diff --git a/packages/menu/package.json b/packages/menu/package.json new file mode 100644 index 0000000000..196d57ceeb --- /dev/null +++ b/packages/menu/package.json @@ -0,0 +1,72 @@ +{ + "name": "@udecode/plate-menu", + "version": "38.0.1", + "description": "Menu for Plate", + "keywords": [ + "plate", + "menu", + "slate" + ], + "homepage": "https://platejs.org", + "bugs": { + "url": "https://github.com/udecode/plate/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/udecode/plate.git", + "directory": "packages/menu" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "module": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./react": { + "types": "./dist/react/index.d.ts", + "import": "./dist/react/index.mjs", + "module": "./dist/react/index.mjs", + "require": "./dist/react/index.js" + } + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "brl": "yarn p:brl", + "build": "yarn p:build", + "build:watch": "yarn p:build:watch", + "clean": "yarn p:clean", + "lint": "yarn p:lint", + "lint:fix": "yarn p:lint:fix", + "test": "yarn p:test", + "test:watch": "yarn p:test:watch", + "typecheck": "yarn p:typecheck" + }, + "dependencies": { + "@ariakit/react": "0.4.7", + "@udecode/plate-reset-node": "38.0.1", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@udecode/plate-common": "workspace:^" + }, + "peerDependencies": { + "@udecode/plate-common": ">=38.0.6", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.103.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.108.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/menu/src/hooks/index.ts b/packages/menu/src/hooks/index.ts new file mode 100644 index 0000000000..e35ccebae9 --- /dev/null +++ b/packages/menu/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useMenu'; \ No newline at end of file diff --git a/packages/menu/src/hooks/useMenu.ts b/packages/menu/src/hooks/useMenu.ts new file mode 100644 index 0000000000..7b8113c542 --- /dev/null +++ b/packages/menu/src/hooks/useMenu.ts @@ -0,0 +1,208 @@ +import React, { useEffect, useLayoutEffect, useRef } from 'react'; + +import * as Ariakit from '@ariakit/react'; + +import type { MenuProps, setAction } from '../types'; + +export const SearchableContext = React.createContext(false); + +export const ActionContext = React.createContext(null); + +export const useMenu = ({ + children, + combobox, + comboboxClassName, + comboboxListClassName, + comboboxSubmitButton, + dragButton, + flip = true, + getAnchorRect, + icon, + injectAboveMenu, + label, + loading, + loadingPlaceholder, + open, + placement, + portal, + ref, + searchValue, + setAction, + store, + values, + onClickOutside, + onOpenChange, + onRootMenuClose, + onValueChange, + onValuesChange, + ...props +}: MenuProps & { ref: React.ForwardedRef }) => { + const parent = Ariakit.useMenuContext(); + const searchable = searchValue != null || !!onValueChange || !!combobox; + const ParentSetAction = React.useContext(ActionContext); + + const isRootMenu = !parent; + const isDraggleButtonMenu = !!dragButton; + const menuRef = React.useRef(null); + + useOnClickOutside(menuRef, onClickOutside); + + const menuProviderProps = { + open, + placement: isRootMenu ? placement : 'right', + setOpen: (v: boolean) => { + onOpenChange?.(v); + + if (!v && !parent && !dragButton) onRootMenuClose?.(); + }, + setValues: onValuesChange, + showTimeout: 100, + store, + values, + }; + + const menuButtonProps = { + ref, + ...props, + }; + + const menuProps = { + flip, + getAnchorRect, + gutter: isRootMenu ? 0 : 4, + portal, + ref: isRootMenu ? menuRef : undefined, + unmountOnHide: true, + }; + + return { + ParentSetAction, + isDraggleButtonMenu, + isRootMenu, + menuButtonProps, + menuProps, + menuProviderProps, + searchable, + }; +}; + +/** + * Determines the appropriate effect hook to use based on the environment. If + * the code is running on the client-side (browser), it uses the + * `useLayoutEffect` hook, otherwise, it uses the `useEffect` hook. + */ +export const useIsomorphicLayoutEffect = + typeof window === 'undefined' ? useEffect : useLayoutEffect; + +// MediaQueryList Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Window Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: AddEventListenerOptions | boolean +): void; + +// Element Event based useEventListener interface +function useEventListener< + K extends keyof HTMLElementEventMap, + T extends HTMLElement = HTMLDivElement, +>( + eventName: K, + handler: (event: HTMLElementEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: React.RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// https://usehooks-ts.com/react-hook/use-event-listener +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | MediaQueryList | void = void, +>( + eventName: KH | KM | KW, + handler: ( + event: + | Event + | HTMLElementEventMap[KH] + | MediaQueryListEventMap[KM] + | WindowEventMap[KW] + ) => void, + element?: React.RefObject, + options?: AddEventListenerOptions | boolean +) { + // Create a ref that stores handler + const savedHandler = useRef(handler); + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current ?? window; + + if (!(targetElement && targetElement.addEventListener)) return; + + // Create event listener that calls handler function stored in ref + const listener: typeof handler = (event) => savedHandler.current(event); + + targetElement.addEventListener(eventName, listener, options); + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, options); + }; + }, [eventName, element, options]); +} + +type Handler = (event: MouseEvent) => void; + +/** + * Attaches an event listener to detect clicks that occur outside a given + * element. + * + * @template T - The type of HTMLElement that the ref is referring to. + * @param {RefObject} ref - A React ref object that points to the element to + * listen for clicks outside of. + * @param {Handler} handler - The callback function to be executed when a click + * occurs outside the element. + * @param {string} [mouseEvent='mousedown'] - The type of mouse event to listen + * for (e.g., 'mousedown', 'mouseup'). Default is `'mousedown'` + */ +export const useOnClickOutside = ( + ref: React.RefObject, + handler?: Handler, + mouseEvent: 'mousedown' | 'mouseup' = 'mousedown' +): void => { + useEventListener(mouseEvent, (event) => { + if (!handler) return; + + const el = ref?.current; + + // Do nothing if clicking ref's element or descendent elements + if (!el || el.contains(event.target as Node)) { + return; + } + + handler(event); + }); +}; + +export * as Ariakit from '@ariakit/react'; diff --git a/packages/menu/src/index.ts b/packages/menu/src/index.ts new file mode 100644 index 0000000000..230da62e45 --- /dev/null +++ b/packages/menu/src/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './hooks'; \ No newline at end of file diff --git a/packages/menu/src/types.ts b/packages/menu/src/types.ts new file mode 100644 index 0000000000..67ac3b13ec --- /dev/null +++ b/packages/menu/src/types.ts @@ -0,0 +1,56 @@ +import type React from 'react'; + +import type * as Ariakit from '@ariakit/react'; + +export interface Action { + group?: string; + groupName?: string; + icon?: React.ReactNode; + items?: Action[]; + keywords?: string[]; + label?: string; + shortcut?: string; + value?: string; +} + +export type actionGroup = { + group?: string; + value?: string; +}; + +export type setAction = (actionGroup: actionGroup) => void; + +export interface MenuProps extends Ariakit.MenuButtonProps<'div'> { + combobox?: Ariakit.ComboboxProps['render']; + comboboxClassName?: string; + comboboxListClassName?: string; + comboboxSubmitButton?: React.ReactElement; + dragButton?: Ariakit.MenuButtonProps['render']; + flip?: boolean; + getAnchorRect?: (anchor: HTMLElement | null) => AnchorRect | null; + icon?: React.ReactNode; + injectAboveMenu?: React.ReactElement; + label?: React.ReactNode; + loading?: boolean; + loadingPlaceholder?: React.ReactNode; + onClickOutside?: (event: MouseEvent) => void; + onOpenChange?: (open: boolean) => void; + onRootMenuClose?: () => void; + onValueChange?: (value: string) => void; + onValuesChange?: Ariakit.MenuProviderProps['setValues']; + open?: boolean; + placement?: Ariakit.MenuProviderProps['placement']; + portal?: Ariakit.MenuProps['portal']; + searchValue?: string; + setAction?: setAction; + values?: Ariakit.MenuProviderProps['values']; +} + +export interface AnchorRect { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; +} diff --git a/packages/menu/tsconfig.build.json b/packages/menu/tsconfig.build.json new file mode 100644 index 0000000000..425481e027 --- /dev/null +++ b/packages/menu/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../config/tsconfig.build.json", + "compilerOptions": { + "declarationDir": "./dist", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/packages/menu/tsconfig.json b/packages/menu/tsconfig.json new file mode 100644 index 0000000000..ad83d092a5 --- /dev/null +++ b/packages/menu/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../config/tsconfig.base.json", + "include": ["src"], + "exclude": [] +} diff --git a/yarn.lock b/yarn.lock index 2646f6516e..3750be5ca1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6636,6 +6636,25 @@ __metadata: languageName: unknown linkType: soft +"@udecode/plate-menu@workspace:^, @udecode/plate-menu@workspace:packages/menu": + version: 0.0.0-use.local + resolution: "@udecode/plate-menu@workspace:packages/menu" + dependencies: + "@ariakit/react": "npm:0.4.7" + "@udecode/plate-common": "workspace:^" + "@udecode/plate-reset-node": "npm:38.0.1" + lodash: "npm:^4.17.21" + peerDependencies: + "@udecode/plate-common": ">=38.0.6" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + languageName: unknown + linkType: soft + "@udecode/plate-node-id@npm:38.0.1, @udecode/plate-node-id@workspace:^, @udecode/plate-node-id@workspace:packages/node-id": version: 0.0.0-use.local resolution: "@udecode/plate-node-id@workspace:packages/node-id" @@ -21611,7 +21630,6 @@ __metadata: version: 0.0.0-use.local resolution: "www@workspace:apps/www" dependencies: - "@ariakit/react": "npm:0.4.7" "@radix-ui/colors": "npm:3.0.0" "@radix-ui/react-accessible-icon": "npm:^1.1.0" "@radix-ui/react-accordion": "npm:^1.2.0" @@ -21689,6 +21707,7 @@ __metadata: "@udecode/plate-markdown": "workspace:^" "@udecode/plate-media": "workspace:^" "@udecode/plate-mention": "workspace:^" + "@udecode/plate-menu": "workspace:^" "@udecode/plate-node-id": "workspace:^" "@udecode/plate-normalizers": "workspace:^" "@udecode/plate-playwright": "workspace:^" From 3e8cd2afdd9283ed12503f0da332626d7159a80e Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Mon, 30 Sep 2024 16:57:14 +0800 Subject: [PATCH 040/127] menu --- .../action-handler/useActionHandler.ts | 2 +- .../registry/default/plate-ui/ai-actions.tsx | 4 +- .../src/registry/default/plate-ui/ai-menu.tsx | 9 +- .../src/registry/default/plate-ui/menu.tsx | 466 +++--------------- packages/menu/package.json | 3 +- packages/menu/src/hooks/index.ts | 7 +- packages/menu/src/hooks/useMenu.tsx | 96 ++++ packages/menu/src/hooks/useMenuItem.tsx | 113 +++++ packages/menu/src/index.ts | 10 +- packages/menu/src/lib/Ariakit.ts | 1 + packages/menu/src/lib/index.ts | 5 + packages/menu/src/types/action.ts | 17 + packages/menu/src/types/index.ts | 7 + packages/menu/src/{types.ts => types/menu.ts} | 23 +- packages/menu/src/types/menuItem.ts | 13 + packages/menu/src/utils/buildMenuTree.ts | 23 + .../menu/src/utils/filterAndBuildMenuTree.ts | 21 + packages/menu/src/utils/flattenMenuTree.ts | 20 + packages/menu/src/utils/index.ts | 8 + .../useMenu.ts => utils/useOnClickOutside.ts} | 90 +--- yarn.lock | 1 + 21 files changed, 427 insertions(+), 512 deletions(-) create mode 100644 packages/menu/src/hooks/useMenu.tsx create mode 100644 packages/menu/src/hooks/useMenuItem.tsx create mode 100644 packages/menu/src/lib/Ariakit.ts create mode 100644 packages/menu/src/lib/index.ts create mode 100644 packages/menu/src/types/action.ts create mode 100644 packages/menu/src/types/index.ts rename packages/menu/src/{types.ts => types/menu.ts} (72%) create mode 100644 packages/menu/src/types/menuItem.ts create mode 100644 packages/menu/src/utils/buildMenuTree.ts create mode 100644 packages/menu/src/utils/filterAndBuildMenuTree.ts create mode 100644 packages/menu/src/utils/flattenMenuTree.ts create mode 100644 packages/menu/src/utils/index.ts rename packages/menu/src/{hooks/useMenu.ts => utils/useOnClickOutside.ts} (66%) diff --git a/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts index 7523785bfc..6a56ce6aec 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/useActionHandler.ts @@ -1,7 +1,7 @@ 'use client'; import { useEffect } from 'react'; -import type { actionGroup } from '@/registry/default/plate-ui/menu'; +import type { actionGroup } from '@udecode/plate-menu'; import { type PlateEditor, useEditorRef } from '@udecode/plate-core/react'; diff --git a/apps/www/src/registry/default/plate-ui/ai-actions.tsx b/apps/www/src/registry/default/plate-ui/ai-actions.tsx index 8e195b956d..fdfe0274d4 100644 --- a/apps/www/src/registry/default/plate-ui/ai-actions.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-actions.tsx @@ -1,6 +1,6 @@ -import { Icons } from '@/components/icons'; +import type { Action } from '@udecode/plate-menu'; -import type { Action } from './menu'; +import { Icons } from '@/components/icons'; /** Common */ const ACTION_CHINESE = 'action_chinese'; diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx index 95757758fa..249e2f6848 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -21,7 +21,7 @@ import { streamInsertTextSelection, } from '@udecode/plate-ai/react'; import { useEditorPlugin } from '@udecode/plate-core/react'; -import { Ariakit } from '@udecode/plate-menu'; +import { Ariakit, filterAndBuildMenuTree } from '@udecode/plate-menu'; import { focusEditor } from '@udecode/slate-react'; import isHotkey from 'is-hotkey'; @@ -43,12 +43,7 @@ import { } from './ai-menu-items'; import { AIPreviewEditor } from './ai-previdew-editor'; import { Button } from './button'; -import { - Menu, - comboboxVariants, - filterAndBuildMenuTree, - renderSearchMenuItems, -} from './menu'; +import { Menu, comboboxVariants, renderSearchMenuItems } from './menu'; // eslint-disable-next-line react/display-name export const AIMenu = memo(({ children }: React.PropsWithChildren) => { diff --git a/apps/www/src/registry/default/plate-ui/menu.tsx b/apps/www/src/registry/default/plate-ui/menu.tsx index 1f7b2f35f5..583b69bf22 100644 --- a/apps/www/src/registry/default/plate-ui/menu.tsx +++ b/apps/www/src/registry/default/plate-ui/menu.tsx @@ -2,12 +2,17 @@ import * as React from 'react'; -import type { Action, MenuProps, setAction } from '@udecode/plate-menu'; +import type { Action, MenuItemProps, MenuProps } from '@udecode/plate-menu'; import { cn } from '@udecode/cn'; -import { Ariakit } from '@udecode/plate-menu'; +import { + ActionContext, + Ariakit, + SearchableContext, + useMenu, + useMenuItem, +} from '@udecode/plate-menu'; import { cva } from 'class-variance-authority'; -import { matchSorter } from 'match-sorter'; const menuVariants = cva( 'z-50 overflow-auto text-popover-foreground outline-none', @@ -63,10 +68,6 @@ const comboboxListVariants = cva('rounded-sm', { }, }); -const SearchableContext = React.createContext(false); - -const ActionContext = React.createContext(null); - type variant = 'ai' | 'default'; type StyledMenuProps = MenuProps & { @@ -74,123 +75,78 @@ type StyledMenuProps = MenuProps & { }; export const Menu = React.forwardRef( - function Menu( - { - children, - combobox, - comboboxClassName, - comboboxListClassName, - comboboxSubmitButton, - dragButton, - flip = true, - getAnchorRect, - icon, - injectAboveMenu, - label, - loading, - loadingPlaceholder, - open, - placement, - portal, - searchValue, - setAction, - store, - values, - variant, - onClickOutside, - onOpenChange, - onRootMenuClose, - onValueChange, - onValuesChange, - ...props - }, - ref - ) { - const parent = Ariakit.useMenuContext(); - const searchable = searchValue != null || !!onValueChange || !!combobox; - const ParentSetAction = React.useContext(ActionContext); - - const isRootMenu = !parent; - const isDraggleButtonMenu = !!dragButton; - const menuRef = React.useRef(null); - - useOnClickOutside(menuRef, onClickOutside); - - const menuProviderProps = { - open, - placement: isRootMenu ? placement : 'right', - setOpen: (v: boolean) => { - onOpenChange?.(v); - - if (!v && !parent && !dragButton) onRootMenuClose?.(); - }, - setValues: onValuesChange, - showTimeout: 100, - store, - values, - }; - - const menuButtonProps = { + function BaseMenu({ variant, ...props }, ref) { + const { + ParentSetAction, + comboboxProviderProps, + isDraggleButtonMenu, + isRootMenu, + menuButtonProps, + menuProps, + menuProviderProps, + searchable, + } = useMenu({ ref, ...props, - className: cn(isRootMenu && !isDraggleButtonMenu && 'hidden'), - render: isRootMenu ? dragButton : , - }; - - const menuProps = { - className: cn( - menuVariants({ variant }), - props.className, - searchable && '' - ), - flip, - getAnchorRect, - gutter: isRootMenu ? 0 : 4, - portal, - ref: isRootMenu ? menuRef : undefined, - unmountOnHide: true, - }; + }); const menuContent = ( - - {icon} - {label} + + ) + } + {...menuButtonProps} + > + {props.icon} + {props.label} - - {open && isRootMenu && injectAboveMenu} - + + {props.open && isRootMenu && props.injectAboveMenu} + {searchable ? ( - loading ? ( + props.loading ? ( - {loadingPlaceholder ??
loading...
} + {props.loadingPlaceholder ??
loading...
}
) : (
- - {comboboxSubmitButton && comboboxSubmitButton} + + {props.comboboxSubmitButton && props.comboboxSubmitButton}
- {children} + {props.children}
) ) : ( - children + props.children )}
@@ -198,13 +154,6 @@ export const Menu = React.forwardRef(
); - const comboboxProviderProps = { - includesBaseElement: false, - resetValueOnHide: true, - setValue: onValueChange, - value: searchValue, - }; - return searchable ? ( {menuContent} @@ -270,97 +219,40 @@ export const MenuShortcut = React.forwardRef< ); }); -export interface MenuItemProps - extends Omit { - group?: string; - icon?: React.ReactNode; - label?: string; - name?: string; - parentGroup?: string; - preventClose?: boolean; - shortcut?: string; - value?: string; -} - export const MenuItem = React.forwardRef( - function MenuItem( - { - className, - group, - icon, - label, - name, - parentGroup, - preventClose, - shortcut, - value, - ...props - }, - ref - ) { - const menu = Ariakit.useMenuContext(); - - if (!menu) throw new Error('MenuItem should be used inside a Menu'); - - const setAction = React.useContext(ActionContext); - - const searchable = React.useContext(SearchableContext); - - const baseOnClick = ( - event: React.MouseEvent - ) => { - props.onClick?.(event); - - if (event.isDefaultPrevented()) return; - if (setAction === null) - console.warn('Did you forget to pass the setAction prop?'); - - setAction?.({ group, value }); - }; - - const baseProps: MenuItemProps = { - blurOnHoverEnd: false, - focusOnHover: true, - label, - ref, + function MenuItem({ className, ...props }, ref) { + const { + baseProps, + comboboxProps, + isCheckable, + isChecked, + isRadio, + radioProps, + searchable, + } = useMenuItem({ ...props, - className: cn( - menuItemVariants(), - shortcut && 'justify-between', - className - ), - group: parentGroup, - name: group, - value: value || label, - onClick: baseOnClick, - }; - - const isCheckable = menu.useState((state) => { - if (!group) return false; - if (value == null) return false; - - return state.values[group] != null; + ref, }); - const isChecked = menu.useState((state) => { - if (!group) return false; - - return state.values[group] === value; - }); + const baseClassName = cn( + menuItemVariants(), + props.shortcut && 'justify-between', + className + ); baseProps.children = ( <>
- {icon} - {baseProps.children ?? label} + {props.icon} + {baseProps.children ?? props.label}
- {(shortcut || isCheckable) && ( + {(props.shortcut || isCheckable) && (
{isCheckable && ( )} - {shortcut && {shortcut}} + {props.shortcut && {props.shortcut}} {isCheckable && searchable && ( {isChecked ? 'checked' : 'not checked'} @@ -372,102 +264,23 @@ export const MenuItem = React.forwardRef( ); if (!searchable) { - if (name != null && value != null) { - const radioProps = { ...baseProps, hideOnClick: true, name, value }; - - return ; - } - - return ; + return isRadio ? ( + + ) : ( + + ); } - const hideOnClick = (event: React.MouseEvent) => { - const expandable = event.currentTarget.hasAttribute('aria-expanded'); - - if (expandable) return false; - if (preventClose) return false; - - menu.hideAll(); - - return false; - }; - - const selectValueOnClick = () => { - if (name == null || value == null) return false; - - menu.setValue(name, value); - - return true; - }; - return ( ); } ); -export function filterAndBuildMenuTree( - actions: Action[], - searchValue: string -): Action[] | null { - if (!searchValue) return null; - - const options = flattenMenuTree(actions); - - const matches = matchSorter(options, searchValue, { - keys: ['label', 'group', 'value', 'keywords'], - }); - - return buildMenuTree(matches.slice(0, 15)); -} - -export function flattenMenuTree(actions: Action[]): Action[] { - return actions.flatMap((item) => { - if (item.items) { - const parentGroup = item.group ?? item.label; - const groupName = item.label; - - return flattenMenuTree( - item.items.map(({ group, ...item }) => ({ - ...item, - group: group ?? parentGroup, - groupName, - })) - ); - } - - return item; - }); -} - -export function buildMenuTree(actions: Action[] | null) { - if (!actions) return null; - - return actions.reduce((actions, option) => { - if (option.groupName) { - const groupName = actions.find( - (action) => action.label === option.groupName - ); - - if (groupName) { - groupName.items!.push(option); - } else { - actions.push({ items: [option], label: option.groupName }); - } - } else { - actions.push(option); - } - - return actions; - }, []); -} - export function renderMenuItems({ group, items, @@ -520,126 +333,3 @@ export function renderSearchMenuItems( return renderMenuItems({ items: matches }); } - -// Utils --------------------------------------------------------------- - -import { useEffect, useLayoutEffect, useRef } from 'react'; - -/** - * Determines the appropriate effect hook to use based on the environment. If - * the code is running on the client-side (browser), it uses the - * `useLayoutEffect` hook, otherwise, it uses the `useEffect` hook. - */ -export const useIsomorphicLayoutEffect = - typeof window === 'undefined' ? useEffect : useLayoutEffect; - -// MediaQueryList Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: MediaQueryListEventMap[K]) => void, - element: React.RefObject, - options?: AddEventListenerOptions | boolean -): void; - -// Window Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: WindowEventMap[K]) => void, - element?: undefined, - options?: AddEventListenerOptions | boolean -): void; - -// Element Event based useEventListener interface -function useEventListener< - K extends keyof HTMLElementEventMap, - T extends HTMLElement = HTMLDivElement, ->( - eventName: K, - handler: (event: HTMLElementEventMap[K]) => void, - element: React.RefObject, - options?: AddEventListenerOptions | boolean -): void; - -// Document Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: DocumentEventMap[K]) => void, - element: React.RefObject, - options?: AddEventListenerOptions | boolean -): void; - -// https://usehooks-ts.com/react-hook/use-event-listener -function useEventListener< - KW extends keyof WindowEventMap, - KH extends keyof HTMLElementEventMap, - KM extends keyof MediaQueryListEventMap, - T extends HTMLElement | MediaQueryList | void = void, ->( - eventName: KH | KM | KW, - handler: ( - event: - | Event - | HTMLElementEventMap[KH] - | MediaQueryListEventMap[KM] - | WindowEventMap[KW] - ) => void, - element?: React.RefObject, - options?: AddEventListenerOptions | boolean -) { - // Create a ref that stores handler - const savedHandler = useRef(handler); - - useIsomorphicLayoutEffect(() => { - savedHandler.current = handler; - }, [handler]); - - useEffect(() => { - // Define the listening target - const targetElement: T | Window = element?.current ?? window; - - if (!(targetElement && targetElement.addEventListener)) return; - - // Create event listener that calls handler function stored in ref - const listener: typeof handler = (event) => savedHandler.current(event); - - targetElement.addEventListener(eventName, listener, options); - - // Remove event listener on cleanup - return () => { - targetElement.removeEventListener(eventName, listener, options); - }; - }, [eventName, element, options]); -} - -type Handler = (event: MouseEvent) => void; - -/** - * Attaches an event listener to detect clicks that occur outside a given - * element. - * - * @template T - The type of HTMLElement that the ref is referring to. - * @param {RefObject} ref - A React ref object that points to the element to - * listen for clicks outside of. - * @param {Handler} handler - The callback function to be executed when a click - * occurs outside the element. - * @param {string} [mouseEvent='mousedown'] - The type of mouse event to listen - * for (e.g., 'mousedown', 'mouseup'). Default is `'mousedown'` - */ -export const useOnClickOutside = ( - ref: React.RefObject, - handler?: Handler, - mouseEvent: 'mousedown' | 'mouseup' = 'mousedown' -): void => { - useEventListener(mouseEvent, (event) => { - if (!handler) return; - - const el = ref?.current; - - // Do nothing if clicking ref's element or descendent elements - if (!el || el.contains(event.target as Node)) { - return; - } - - handler(event); - }); -}; diff --git a/packages/menu/package.json b/packages/menu/package.json index 196d57ceeb..58139925f1 100644 --- a/packages/menu/package.json +++ b/packages/menu/package.json @@ -52,7 +52,8 @@ "dependencies": { "@ariakit/react": "0.4.7", "@udecode/plate-reset-node": "38.0.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "match-sorter": "6.3.4" }, "devDependencies": { "@udecode/plate-common": "workspace:^" diff --git a/packages/menu/src/hooks/index.ts b/packages/menu/src/hooks/index.ts index e35ccebae9..c78921426b 100644 --- a/packages/menu/src/hooks/index.ts +++ b/packages/menu/src/hooks/index.ts @@ -1 +1,6 @@ -export * from './useMenu'; \ No newline at end of file +/** + * @file Automatically generated by barrelsby. + */ + +export * from './useMenu'; +export * from './useMenuItem'; \ No newline at end of file diff --git a/packages/menu/src/hooks/useMenu.tsx b/packages/menu/src/hooks/useMenu.tsx new file mode 100644 index 0000000000..9f52266d0a --- /dev/null +++ b/packages/menu/src/hooks/useMenu.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import type { MenuProps, setAction } from '../types'; + +import { Ariakit } from '../lib'; +import { useOnClickOutside } from '../utils/useOnClickOutside'; + +export const SearchableContext = React.createContext(false); + +export const ActionContext = React.createContext(null); + +export const useMenu = ({ + children, + combobox, + comboboxClassName, + comboboxListClassName, + comboboxSubmitButton, + dragButton, + flip = true, + getAnchorRect, + icon, + injectAboveMenu, + label, + loading, + loadingPlaceholder, + open, + placement, + portal, + ref, + searchValue, + setAction, + store, + values, + onClickOutside, + onOpenChange, + onRootMenuClose, + onValueChange, + onValuesChange, + ...props +}: MenuProps & { ref: React.ForwardedRef }) => { + const parent = Ariakit.useMenuContext(); + const searchable = searchValue != null || !!onValueChange || !!combobox; + const ParentSetAction = React.useContext(ActionContext); + + const isRootMenu = !parent; + const isDraggleButtonMenu = !!dragButton; + const menuRef = React.useRef(null); + + useOnClickOutside(menuRef, onClickOutside); + + const menuProviderProps = { + open, + placement: isRootMenu ? placement : 'right', + setOpen: (v: boolean) => { + onOpenChange?.(v); + + if (!v && !parent && !dragButton) onRootMenuClose?.(); + }, + setValues: onValuesChange, + showTimeout: 100, + store, + values, + }; + + const menuButtonProps = { + ref, + ...props, + }; + + const menuProps = { + flip, + getAnchorRect, + gutter: isRootMenu ? 0 : 4, + portal, + ref: isRootMenu ? menuRef : undefined, + unmountOnHide: true, + }; + + const comboboxProviderProps = { + includesBaseElement: false, + resetValueOnHide: true, + setValue: onValueChange, + value: searchValue, + }; + + return { + ParentSetAction, + comboboxProviderProps, + isDraggleButtonMenu, + isRootMenu, + menuButtonProps, + menuProps, + menuProviderProps, + searchable, + }; +}; diff --git a/packages/menu/src/hooks/useMenuItem.tsx b/packages/menu/src/hooks/useMenuItem.tsx new file mode 100644 index 0000000000..1368fc0483 --- /dev/null +++ b/packages/menu/src/hooks/useMenuItem.tsx @@ -0,0 +1,113 @@ +import React from 'react'; + +import type { MenuItemProps } from '../types'; + +import { Ariakit } from '../lib'; +import { ActionContext, SearchableContext } from './useMenu'; + +type UseMenuItemProps = MenuItemProps & { + ref: React.ForwardedRef; +}; + +export const useMenuItem = ({ + className, + group, + icon, + label, + name, + parentGroup, + preventClose, + ref, + shortcut, + value, + ...props +}: UseMenuItemProps) => { + const menu = Ariakit.useMenuContext(); + + if (!menu) throw new Error('MenuItem should be used inside a Menu'); + + const setAction = React.useContext(ActionContext); + + const searchable = React.useContext(SearchableContext); + + const baseOnClick = (event: React.MouseEvent) => { + props.onClick?.(event); + + if (event.isDefaultPrevented()) return; + if (setAction === null) + console.warn('Did you forget to pass the setAction prop?'); + + setAction?.({ group, value }); + }; + + const baseProps: MenuItemProps = { + blurOnHoverEnd: false, + focusOnHover: true, + label, + ref, + ...props, + group: parentGroup, + name: group, + value: value || label, + onClick: baseOnClick, + }; + + const isCheckable = menu.useState((state) => { + if (!group) return false; + if (value == null) return false; + + return state.values[group] != null; + }); + + const isChecked = menu.useState((state) => { + if (!group) return false; + + return state.values[group] === value; + }); + + const isRadio = name != null && value != null; + + const radioProps = { + ...baseProps, + hideOnClick: true, + name: name as string, + value: value as string, + }; + + const hideOnClick = (event: React.MouseEvent) => { + const expandable = event.currentTarget.hasAttribute('aria-expanded'); + + if (expandable) return false; + if (preventClose) return false; + + menu.hideAll(); + + return false; + }; + + const selectValueOnClick = () => { + if (name == null || value == null) return false; + + menu.setValue(name, value); + + return true; + }; + + const comboboxProps = { + ...baseProps, + hideOnClick: hideOnClick, + selectValueOnClick: selectValueOnClick, + setValueOnClick: selectValueOnClick, + value: isCheckable ? value : undefined, + }; + + return { + baseProps, + comboboxProps, + isCheckable, + isChecked, + isRadio, + radioProps, + searchable, + }; +}; diff --git a/packages/menu/src/index.ts b/packages/menu/src/index.ts index 230da62e45..cad68a5708 100644 --- a/packages/menu/src/index.ts +++ b/packages/menu/src/index.ts @@ -1,2 +1,8 @@ -export * from './types'; -export * from './hooks'; \ No newline at end of file +/** + * @file Automatically generated by barrelsby. + */ + +export * from './hooks/index'; +export * from './lib/index'; +export * from './types/index'; +export * from './utils/index'; diff --git a/packages/menu/src/lib/Ariakit.ts b/packages/menu/src/lib/Ariakit.ts new file mode 100644 index 0000000000..f50e6d8880 --- /dev/null +++ b/packages/menu/src/lib/Ariakit.ts @@ -0,0 +1 @@ +export * as Ariakit from '@ariakit/react'; diff --git a/packages/menu/src/lib/index.ts b/packages/menu/src/lib/index.ts new file mode 100644 index 0000000000..dc5cc8d196 --- /dev/null +++ b/packages/menu/src/lib/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './Ariakit'; diff --git a/packages/menu/src/types/action.ts b/packages/menu/src/types/action.ts new file mode 100644 index 0000000000..1cb6173db2 --- /dev/null +++ b/packages/menu/src/types/action.ts @@ -0,0 +1,17 @@ +export interface Action { + group?: string; + groupName?: string; + icon?: React.ReactNode; + items?: Action[]; + keywords?: string[]; + label?: string; + shortcut?: string; + value?: string; +} + +export type actionGroup = { + group?: string; + value?: string; +}; + +export type setAction = (actionGroup: actionGroup) => void; diff --git a/packages/menu/src/types/index.ts b/packages/menu/src/types/index.ts new file mode 100644 index 0000000000..cfc956a561 --- /dev/null +++ b/packages/menu/src/types/index.ts @@ -0,0 +1,7 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './action'; +export * from './menu'; +export * from './menuItem'; diff --git a/packages/menu/src/types.ts b/packages/menu/src/types/menu.ts similarity index 72% rename from packages/menu/src/types.ts rename to packages/menu/src/types/menu.ts index 67ac3b13ec..3b33a8a8c8 100644 --- a/packages/menu/src/types.ts +++ b/packages/menu/src/types/menu.ts @@ -1,24 +1,5 @@ -import type React from 'react'; - -import type * as Ariakit from '@ariakit/react'; - -export interface Action { - group?: string; - groupName?: string; - icon?: React.ReactNode; - items?: Action[]; - keywords?: string[]; - label?: string; - shortcut?: string; - value?: string; -} - -export type actionGroup = { - group?: string; - value?: string; -}; - -export type setAction = (actionGroup: actionGroup) => void; +import type { Ariakit } from '../lib'; +import type { setAction } from './action'; export interface MenuProps extends Ariakit.MenuButtonProps<'div'> { combobox?: Ariakit.ComboboxProps['render']; diff --git a/packages/menu/src/types/menuItem.ts b/packages/menu/src/types/menuItem.ts new file mode 100644 index 0000000000..d078457492 --- /dev/null +++ b/packages/menu/src/types/menuItem.ts @@ -0,0 +1,13 @@ +import type { Ariakit } from '../lib'; + +export interface MenuItemProps + extends Omit { + group?: string; + icon?: React.ReactNode; + label?: string; + name?: string; + parentGroup?: string; + preventClose?: boolean; + shortcut?: string; + value?: string; +} diff --git a/packages/menu/src/utils/buildMenuTree.ts b/packages/menu/src/utils/buildMenuTree.ts new file mode 100644 index 0000000000..d456a17c49 --- /dev/null +++ b/packages/menu/src/utils/buildMenuTree.ts @@ -0,0 +1,23 @@ +import type { Action } from '../types'; + +export function buildMenuTree(actions: Action[] | null) { + if (!actions) return null; + + return actions.reduce((actions, option) => { + if (option.groupName) { + const groupName = actions.find( + (action) => action.label === option.groupName + ); + + if (groupName) { + groupName.items!.push(option); + } else { + actions.push({ items: [option], label: option.groupName }); + } + } else { + actions.push(option); + } + + return actions; + }, []); +} diff --git a/packages/menu/src/utils/filterAndBuildMenuTree.ts b/packages/menu/src/utils/filterAndBuildMenuTree.ts new file mode 100644 index 0000000000..21c477132f --- /dev/null +++ b/packages/menu/src/utils/filterAndBuildMenuTree.ts @@ -0,0 +1,21 @@ +import { matchSorter } from 'match-sorter'; + +import type { Action } from '../types'; + +import { buildMenuTree } from './buildMenuTree'; +import { flattenMenuTree } from './flattenMenuTree'; + +export function filterAndBuildMenuTree( + actions: Action[], + searchValue: string +): Action[] | null { + if (!searchValue) return null; + + const options = flattenMenuTree(actions); + + const matches = matchSorter(options, searchValue, { + keys: ['label', 'group', 'value', 'keywords'], + }); + + return buildMenuTree(matches.slice(0, 15)); +} diff --git a/packages/menu/src/utils/flattenMenuTree.ts b/packages/menu/src/utils/flattenMenuTree.ts new file mode 100644 index 0000000000..fcb39ee254 --- /dev/null +++ b/packages/menu/src/utils/flattenMenuTree.ts @@ -0,0 +1,20 @@ +import type { Action } from '../types'; + +export function flattenMenuTree(actions: Action[]): Action[] { + return actions.flatMap((item) => { + if (item.items) { + const parentGroup = item.group ?? item.label; + const groupName = item.label; + + return flattenMenuTree( + item.items.map(({ group, ...item }) => ({ + ...item, + group: group ?? parentGroup, + groupName, + })) + ); + } + + return item; + }); +} diff --git a/packages/menu/src/utils/index.ts b/packages/menu/src/utils/index.ts new file mode 100644 index 0000000000..b37821fcc7 --- /dev/null +++ b/packages/menu/src/utils/index.ts @@ -0,0 +1,8 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './buildMenuTree'; +export * from './filterAndBuildMenuTree'; +export * from './flattenMenuTree'; +export * from './useOnClickOutside'; diff --git a/packages/menu/src/hooks/useMenu.ts b/packages/menu/src/utils/useOnClickOutside.ts similarity index 66% rename from packages/menu/src/hooks/useMenu.ts rename to packages/menu/src/utils/useOnClickOutside.ts index 7b8113c542..ea829c96aa 100644 --- a/packages/menu/src/hooks/useMenu.ts +++ b/packages/menu/src/utils/useOnClickOutside.ts @@ -1,90 +1,4 @@ -import React, { useEffect, useLayoutEffect, useRef } from 'react'; - -import * as Ariakit from '@ariakit/react'; - -import type { MenuProps, setAction } from '../types'; - -export const SearchableContext = React.createContext(false); - -export const ActionContext = React.createContext(null); - -export const useMenu = ({ - children, - combobox, - comboboxClassName, - comboboxListClassName, - comboboxSubmitButton, - dragButton, - flip = true, - getAnchorRect, - icon, - injectAboveMenu, - label, - loading, - loadingPlaceholder, - open, - placement, - portal, - ref, - searchValue, - setAction, - store, - values, - onClickOutside, - onOpenChange, - onRootMenuClose, - onValueChange, - onValuesChange, - ...props -}: MenuProps & { ref: React.ForwardedRef }) => { - const parent = Ariakit.useMenuContext(); - const searchable = searchValue != null || !!onValueChange || !!combobox; - const ParentSetAction = React.useContext(ActionContext); - - const isRootMenu = !parent; - const isDraggleButtonMenu = !!dragButton; - const menuRef = React.useRef(null); - - useOnClickOutside(menuRef, onClickOutside); - - const menuProviderProps = { - open, - placement: isRootMenu ? placement : 'right', - setOpen: (v: boolean) => { - onOpenChange?.(v); - - if (!v && !parent && !dragButton) onRootMenuClose?.(); - }, - setValues: onValuesChange, - showTimeout: 100, - store, - values, - }; - - const menuButtonProps = { - ref, - ...props, - }; - - const menuProps = { - flip, - getAnchorRect, - gutter: isRootMenu ? 0 : 4, - portal, - ref: isRootMenu ? menuRef : undefined, - unmountOnHide: true, - }; - - return { - ParentSetAction, - isDraggleButtonMenu, - isRootMenu, - menuButtonProps, - menuProps, - menuProviderProps, - searchable, - }; -}; +import { useEffect, useLayoutEffect, useRef } from 'react'; /** * Determines the appropriate effect hook to use based on the environment. If @@ -204,5 +118,3 @@ export const useOnClickOutside = ( handler(event); }); }; - -export * as Ariakit from '@ariakit/react'; diff --git a/yarn.lock b/yarn.lock index 3750be5ca1..8b85411ff9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6644,6 +6644,7 @@ __metadata: "@udecode/plate-common": "workspace:^" "@udecode/plate-reset-node": "npm:38.0.1" lodash: "npm:^4.17.21" + match-sorter: "npm:6.3.4" peerDependencies: "@udecode/plate-common": ">=38.0.6" react: ">=16.8.0" From a0b6e25a89a70580077ddbc28be15c48c62d2531 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Mon, 30 Sep 2024 21:26:57 +0800 Subject: [PATCH 041/127] useAI --- .../default/example/playground-demo.tsx | 2 +- .../src/registry/default/plate-ui/ai-menu.tsx | 179 +++------------- .../src/registry/default/plate-ui/menu.tsx | 3 +- packages/ai/package.json | 1 + packages/ai/src/react/ai/AIPlugin.ts | 3 +- packages/ai/src/react/ai/hook/index.ts | 5 + packages/ai/src/react/ai/hook/useAI.ts | 194 +++++++++++------- packages/ai/src/react/ai/index.ts | 3 + packages/ai/src/react/ai/types.ts | 15 ++ packages/menu/src/lib/Ariakit.ts | 2 + yarn.lock | 3 +- 11 files changed, 188 insertions(+), 222 deletions(-) create mode 100644 packages/ai/src/react/ai/hook/index.ts create mode 100644 packages/ai/src/react/ai/types.ts diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index e42c8baaf2..4e12491422 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -278,7 +278,7 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { options: { areaOptions: { behaviour: { - startThreshold: 20, + startThreshold: 10, }, boundaries: `#${scrollSelector}`, container: `#${scrollSelector}`, diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx index 249e2f6848..9df70d7515 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -1,29 +1,10 @@ /* eslint-disable tailwindcss/no-custom-classname */ 'use client'; -import React, { - type KeyboardEvent, - memo, - startTransition, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; - -import type { actionGroup } from '@udecode/plate-menu'; +import React, { memo, useMemo } from 'react'; import { cn } from '@udecode/cn'; -import { - AIPlugin, - getContent, - streamInsertText, - streamInsertTextSelection, -} from '@udecode/plate-ai/react'; -import { useEditorPlugin } from '@udecode/plate-core/react'; -import { Ariakit, filterAndBuildMenuTree } from '@udecode/plate-menu'; -import { focusEditor } from '@udecode/slate-react'; -import isHotkey from 'is-hotkey'; +import { useAI } from '@udecode/plate-ai/react'; import { Icons } from '@/components/icons'; @@ -47,138 +28,46 @@ import { Menu, comboboxVariants, renderSearchMenuItems } from './menu'; // eslint-disable-next-line react/display-name export const AIMenu = memo(({ children }: React.PropsWithChildren) => { - const { api, editor, setOption, setOptions, useOption } = - useEditorPlugin(AIPlugin); - - const isOpen = useOption('isOpen', editor.id); - const action = useOption('action'); - const aiState = useOption('aiState'); - const menuType = useOption('menuType'); - const setAction = (action: actionGroup) => setOption('action', action); - - const { aiEditor } = editor.useOptions(AIPlugin); - - // init - const menu = Ariakit.useMenuStore(); - useEffect(() => { - setOptions({ - store: menu, - }); - // eslint-disable-next`-line react-hooks/exhaustive-deps - }, [isOpen, menu, setOptions]); - - const [values, setValues] = useState(defaultValues); - const [searchValue, setSearchValue] = useState(''); - - const streamInsert = useCallback(async () => { - if (!aiEditor) return; - if (menuType === 'selection') { - const content = getContent(editor, aiEditor); - - await streamInsertTextSelection(editor, aiEditor, { - prompt: `user prompt is ${searchValue} the content is ${content}`, - }); - } else if (menuType === 'space') { - await streamInsertText(editor, { - prompt: searchValue, - }); - } - }, [aiEditor, editor, menuType, searchValue]); - - const onInputKeyDown = async (e: KeyboardEvent) => { - if (isHotkey('backspace')(e) && searchValue.length === 0) { - e.preventDefault(); - api.ai.hide(); - focusEditor(editor); - } - if (isHotkey('enter')(e)) await streamInsert(); - }; - - const onCloseMenu = useCallback(() => { - // close menu if ai is not generating - if (aiState === 'idle' || aiState === 'done') { - api.ai.hide(); - focusEditor(editor); - } - // abort if ai is generating - if (aiState === 'generating' || aiState === 'requesting') { - api.ai.abort(); - } - }, [aiState, api.ai, editor]); - - // close on escape - useEffect(() => { - const keydown = (e: any) => { - if (!isOpen || !isHotkey('escape')(e)) return; - - onCloseMenu(); - }; - - document.addEventListener('keydown', keydown); - - return () => { - document.removeEventListener('keydown', keydown); - }; - }, [aiState, api.ai, editor, isOpen, onCloseMenu]); - - // block editor while generating - // const setReadOnly = usePlateStore().set.readOnly(); - useEffect(() => { - if (aiState === 'generating') { - // setReadOnly(true); - } - if (aiState === 'done') { - // setReadOnly(false); - setSearchValue(''); - } - }, [aiState, setSearchValue]); + const { + CurrentItems, + action, + aiEditor, + aiState, + comboboxProps, + menuProps, + menuType, + searchItems, + submitButtonProps, + onCloseMenu, + } = useAI({ + aiActions: { + CursorCommandsActions, + CursorSuggestionActions, + SelectionCommandsActions, + SelectionSuggestionActions, + }, + aiCommands: { + CursorCommands, + CursorSuggestions, + SelectionCommands, + SelectionSuggestions, + }, + defaultValues, + }); useActionHandler(action, aiEditor!); - const [CurrentItems, CurrentActions] = React.useMemo(() => { - if (aiState === 'done') { - if (menuType === 'selection') - return [SelectionSuggestions, SelectionSuggestionActions]; - - return [CursorSuggestions, CursorSuggestionActions]; - } - if (menuType === 'selection') - return [SelectionCommands, SelectionCommandsActions]; - - return [CursorCommands, CursorCommandsActions]; - }, [aiState, menuType]); - /** IME */ - const [isComposing, setIsComposing] = useState(false); - - const searchItems = useMemo(() => { - return isComposing - ? [] - : filterAndBuildMenuTree(Object.values(CurrentActions), searchValue); - }, [CurrentActions, isComposing, searchValue]); return ( <> { - return editor.getApi(AIPlugin).ai.hide(); - }} - onValueChange={(value) => startTransition(() => setSearchValue(value))} - onValuesChange={(values: typeof defaultValues) => { - setValues(values); - }} + {...menuProps} combobox={ setSearchValue(e.target.value)} - onCompositionEnd={() => setIsComposing(false)} - onCompositionStart={() => setIsComposing(true)} - onKeyDown={onInputKeyDown} + {...comboboxProps} placeholder={ aiState === 'done' ? 'Tell AI what todo next' @@ -199,10 +88,7 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { size="icon" variant="ghost" className="ml-2" - disabled={searchValue.trim().length === 0} - onClick={async () => { - await streamInsert(); - }} + {...submitButtonProps} > @@ -233,9 +119,6 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { >
} - setAction={setAction} - store={menu} - values={values} > {renderSearchMenuItems(searchItems, { hiddenOnEmpty: true }) ?? ( diff --git a/apps/www/src/registry/default/plate-ui/menu.tsx b/apps/www/src/registry/default/plate-ui/menu.tsx index 583b69bf22..0cd42c8ee7 100644 --- a/apps/www/src/registry/default/plate-ui/menu.tsx +++ b/apps/www/src/registry/default/plate-ui/menu.tsx @@ -71,7 +71,7 @@ const comboboxListVariants = cva('rounded-sm', { type variant = 'ai' | 'default'; type StyledMenuProps = MenuProps & { - variant: variant; + variant?: variant; }; export const Menu = React.forwardRef( @@ -326,6 +326,7 @@ export function renderSearchMenuItems( ) { if (!matches) return null; if (matches.length === 0) { + // eslint-disable-next-line react/jsx-no-useless-fragment if (options?.hiddenOnEmpty) return <>; return
No results
; diff --git a/packages/ai/package.json b/packages/ai/package.json index 23fee2aa90..862c2962f0 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -52,6 +52,7 @@ "dependencies": { "@udecode/plate-combobox": "38.0.1", "@udecode/plate-markdown": "38.0.1", + "@udecode/plate-menu": "38.0.1", "@udecode/plate-selection": "38.0.11", "lodash": "^4.17.21" }, diff --git a/packages/ai/src/react/ai/AIPlugin.ts b/packages/ai/src/react/ai/AIPlugin.ts index aaf45a58c2..5c3674f10d 100644 --- a/packages/ai/src/react/ai/AIPlugin.ts +++ b/packages/ai/src/react/ai/AIPlugin.ts @@ -1,4 +1,5 @@ import type { ExtendConfig } from '@udecode/plate-core'; +import type { AriakitTypes } from '@udecode/plate-menu'; import type { NodeEntry, Path } from 'slate'; import { @@ -53,7 +54,7 @@ export type AIPluginConfig = ExtendConfig< lastWorkPath: Path | null; menuType: 'selection' | 'space' | null; openEditorId: string | null; - store: any | null; + store: AriakitTypes.MenuStore | null; } & ExposeOptions & AIApi & AISelectors, diff --git a/packages/ai/src/react/ai/hook/index.ts b/packages/ai/src/react/ai/hook/index.ts new file mode 100644 index 0000000000..c2ddddf1da --- /dev/null +++ b/packages/ai/src/react/ai/hook/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './useAI'; diff --git a/packages/ai/src/react/ai/hook/useAI.ts b/packages/ai/src/react/ai/hook/useAI.ts index 8c91b04555..fc30728da7 100644 --- a/packages/ai/src/react/ai/hook/useAI.ts +++ b/packages/ai/src/react/ai/hook/useAI.ts @@ -1,42 +1,54 @@ import React, { type KeyboardEvent, + startTransition, useCallback, useEffect, + useMemo, useState, } from 'react'; import { isHotkey } from '@udecode/plate-common'; import { focusEditor, useEditorPlugin } from '@udecode/plate-common/react'; +import { + type Action, + Ariakit, + filterAndBuildMenuTree, +} from '@udecode/plate-menu'; + +import type { AIActions, AICommands } from '../types'; import { type AIActionGroup, AIPlugin } from '../AIPlugin'; import { streamInsertText, streamInsertTextSelection } from '../stream'; import { getContent } from '../utils'; interface UseAIStateProps { - CursorCommands: () => JSX.Element; - CursorCommandsActions: any; - CursorSuggestionActions: any; - CursorSuggestions: () => JSX.Element; - SelectionCommands: () => JSX.Element; - SelectionCommandsActions: any; - SelectionSuggestionActions: any; - SelectionSuggestions: () => JSX.Element; + aiActions: AIActions; + aiCommands: AICommands; + defaultValues: Record; - menu: any; } -export const useAIState = ({ - CursorCommands, - CursorCommandsActions, - CursorSuggestionActions, - CursorSuggestions, - SelectionCommands, - SelectionCommandsActions, - SelectionSuggestionActions, - SelectionSuggestions, +export type AICommandsAction = Record; + +export const useAI = ({ + aiActions, + aiCommands, defaultValues, - menu, }: UseAIStateProps) => { + const { + CursorCommandsActions, + CursorSuggestionActions, + SelectionCommandsActions, + SelectionSuggestionActions, + } = aiActions; + + const { + CursorCommands, + CursorSuggestions, + SelectionCommands, + SelectionSuggestions, + } = aiCommands; + const { api, editor, setOption, setOptions, useOption } = useEditorPlugin(AIPlugin); @@ -44,10 +56,12 @@ export const useAIState = ({ const action = useOption('action'); const aiState = useOption('aiState'); const menuType = useOption('menuType'); + // eslint-disable-next-line react-hooks/exhaustive-deps const setAction = (action: AIActionGroup) => setOption('action', action); const { aiEditor } = editor.useOptions(AIPlugin); + const menu = Ariakit.useMenuStore(); useEffect(() => { setOptions({ store: menu, @@ -73,52 +87,7 @@ export const useAIState = ({ } }, [aiEditor, editor, menuType, searchValue]); - return { - CursorCommands, - CursorCommandsActions, - CursorSuggestionActions, - CursorSuggestions, - SelectionCommands, - SelectionCommandsActions, - SelectionSuggestionActions, - SelectionSuggestions, - action, - aiState, - api, - editor, - menuType, - searchValue, - setAction, - setSearchValue, - setValues, - streamInsert, - values, - }; -}; - -export const useAI = (props: ReturnType) => { - const { - CursorCommands, - CursorCommandsActions, - CursorSuggestionActions, - CursorSuggestions, - SelectionCommands, - SelectionCommandsActions, - SelectionSuggestionActions, - SelectionSuggestions, - action, - aiState, - api, - editor, - menuType, - searchValue, - setAction, - setSearchValue, - setValues, - streamInsert, - values, - } = props; - + // eslint-disable-next-line react-hooks/exhaustive-deps const onInputKeyDown = async (e: KeyboardEvent) => { if (isHotkey('backspace')(e) && searchValue.length === 0) { e.preventDefault(); @@ -127,6 +96,35 @@ export const useAI = (props: ReturnType) => { } if (isHotkey('enter')(e)) await streamInsert(); }; + + // TODO: move to API + const onCloseMenu = useCallback(() => { + // close menu if ai is not generating + if (aiState === 'idle' || aiState === 'done') { + api.ai.hide(); + focusEditor(editor); + } + // abort if ai is generating + if (aiState === 'generating' || aiState === 'requesting') { + api.ai.abort(); + } + }, [aiState, api.ai, editor]); + + // close on escape + useEffect(() => { + const keydown = (e: any) => { + if (!isOpen || !isHotkey('escape')(e)) return; + + onCloseMenu(); + }; + + document.addEventListener('keydown', keydown); + + return () => { + document.removeEventListener('keydown', keydown); + }; + }, [aiState, api.ai, editor, isOpen, onCloseMenu]); + const [CurrentItems, CurrentActions] = React.useMemo(() => { if (aiState === 'done') { if (menuType === 'selection') @@ -138,14 +136,70 @@ export const useAI = (props: ReturnType) => { return [SelectionCommands, SelectionCommandsActions]; return [CursorCommands, CursorCommandsActions]; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [aiState, menuType]); /** IME */ const [isComposing, setIsComposing] = useState(false); - // const searchItems = useMemo(() => { - // return isComposing - // ? [] - // : filterAndBuildMenuTree(Object.values(CurrentActions), searchValue); - // }, [CurrentActions, isComposing, searchValue]); + const searchItems = useMemo(() => { + return isComposing + ? [] + : filterAndBuildMenuTree(Object.values(CurrentActions), searchValue); + }, [CurrentActions, isComposing, searchValue]); + + /** Props */ + + const menuProps = useMemo(() => { + return { + flip: false, + loading: aiState === 'generating' || aiState === 'requesting', + open: isOpen, + setAction: setAction, + store: menu, + values: values, + onClickOutside: () => { + return editor.getApi(AIPlugin).ai.hide(); + }, + onValueChange: (value: string) => + startTransition(() => setSearchValue(value)), + onValuesChange: (values: typeof defaultValues) => { + setValues(values); + }, + }; + }, [aiState, editor, isOpen, menu, setAction, values]); + + const comboboxProps = useMemo(() => { + return { + id: '__potion_ai_menu_searchRef', + value: searchValue, + onChange: (e: React.ChangeEvent) => + setSearchValue(e.target.value), + onCompositionEnd: () => setIsComposing(false), + onCompositionStart: () => setIsComposing(true), + onKeyDown: onInputKeyDown, + }; + }, [onInputKeyDown, searchValue]); + + const submitButtonProps = useMemo(() => { + return { + disabled: searchValue.trim().length === 0, + onClick: async () => { + await streamInsert(); + }, + }; + }, [searchValue, streamInsert]); + + return { + CurrentItems, + action, + aiEditor, + aiState, + comboboxProps, + menuProps, + menuType, + searchItems, + submitButtonProps, + onCloseMenu, + }; }; diff --git a/packages/ai/src/react/ai/index.ts b/packages/ai/src/react/ai/index.ts index 5e38cbf6e9..b5a3f40ad6 100644 --- a/packages/ai/src/react/ai/index.ts +++ b/packages/ai/src/react/ai/index.ts @@ -3,5 +3,8 @@ */ export * from './AIPlugin'; +export * from './types'; +export * from './useAIHook'; +export * from './hook/index'; export * from './stream/index'; export * from './utils/index'; diff --git a/packages/ai/src/react/ai/types.ts b/packages/ai/src/react/ai/types.ts new file mode 100644 index 0000000000..7a7e087bfd --- /dev/null +++ b/packages/ai/src/react/ai/types.ts @@ -0,0 +1,15 @@ +import type { Action } from '@udecode/plate-menu'; + +export interface AIActions { + CursorCommandsActions: Record; + CursorSuggestionActions: Record; + SelectionCommandsActions: Record; + SelectionSuggestionActions: Record; +} + +export interface AICommands { + CursorCommands: React.FC; + CursorSuggestions: React.FC; + SelectionCommands: React.FC; + SelectionSuggestions: React.FC; +} diff --git a/packages/menu/src/lib/Ariakit.ts b/packages/menu/src/lib/Ariakit.ts index f50e6d8880..20cabed108 100644 --- a/packages/menu/src/lib/Ariakit.ts +++ b/packages/menu/src/lib/Ariakit.ts @@ -1 +1,3 @@ export * as Ariakit from '@ariakit/react'; + +export type * as AriakitTypes from '@ariakit/react'; diff --git a/yarn.lock b/yarn.lock index 8b85411ff9..01bee3eaf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5898,6 +5898,7 @@ __metadata: dependencies: "@udecode/plate-combobox": "npm:38.0.1" "@udecode/plate-markdown": "npm:38.0.1" + "@udecode/plate-menu": "npm:38.0.1" "@udecode/plate-selection": "npm:38.0.11" lodash: "npm:^4.17.21" peerDependencies: @@ -6636,7 +6637,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-menu@workspace:^, @udecode/plate-menu@workspace:packages/menu": +"@udecode/plate-menu@npm:38.0.1, @udecode/plate-menu@workspace:^, @udecode/plate-menu@workspace:packages/menu": version: 0.0.0-use.local resolution: "@udecode/plate-menu@workspace:packages/menu" dependencies: From 0ae094ebd123af425ea79dea229891667cd7bc9f Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 1 Oct 2024 10:18:11 +0800 Subject: [PATCH 042/127] ai --- apps/www/src/app/api/stream/route.ts | 19 ++++++++++++ .../default/example/playground-demo.tsx | 19 ++++++++++++ .../registry/default/plate-ui/ai-actions.tsx | 7 +++++ .../default/plate-ui/ai-menu-items.tsx | 7 +++++ .../src/registry/default/plate-ui/ai-menu.tsx | 31 +++---------------- packages/ai/src/react/ai/AIPlugin.ts | 7 +++++ packages/ai/src/react/ai/hook/useAI.ts | 1 - .../ai/src/react/ai/stream/streamTraversal.ts | 27 +++++----------- 8 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 apps/www/src/app/api/stream/route.ts diff --git a/apps/www/src/app/api/stream/route.ts b/apps/www/src/app/api/stream/route.ts new file mode 100644 index 0000000000..e5f8129805 --- /dev/null +++ b/apps/www/src/app/api/stream/route.ts @@ -0,0 +1,19 @@ +export function POST() { + const streams = [ + { delay: 100, texts: 'Hello' }, + { delay: 200, texts: 'World' }, + ]; + + const stream = new ReadableStream({ + async start(controller) { + for (const stream of streams) { + await new Promise((resolve) => setTimeout(resolve, stream.delay)); // Increasing delay + controller.enqueue(stream.texts); + } + + controller.close(); + }, + }); + + return new Response(stream); +} diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 4e12491422..fa7f5c3af2 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -171,6 +171,25 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { AIPlugin.configure({ options: { createAIEditor: createAIEditor, + // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars + fetchSuggestion: async ({ abortSignal, prompt, system }) => { + const response = await fetch('/api/stream', { + body: JSON.stringify({ prompt, system }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + signal: abortSignal.signal, + }).catch((error) => { + console.error(error); + }); + + if (!response || !response.body) { + throw new Error('Response or response body is null or abort'); + } + + return response.body; + }, scrollContainerSelector: `#${scrollSelector}`, }, render: { aboveEditable: AIMenu }, diff --git a/apps/www/src/registry/default/plate-ui/ai-actions.tsx b/apps/www/src/registry/default/plate-ui/ai-actions.tsx index fdfe0274d4..6cd6d830e0 100644 --- a/apps/www/src/registry/default/plate-ui/ai-actions.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-actions.tsx @@ -195,3 +195,10 @@ export const SelectionSuggestionActions = { value: ACTION_SELECTION_SUGGESTION_TRY_AGAIN, }, }; + +export const aiActions = { + CursorCommandsActions, + CursorSuggestionActions, + SelectionCommandsActions, + SelectionSuggestionActions, +}; diff --git a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx index 7af6d9605c..028a3d66b7 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu-items.tsx @@ -86,3 +86,10 @@ export const SelectionSuggestions = () => { ); }; + +export const aiCommands = { + CursorCommands, + CursorSuggestions, + SelectionCommands, + SelectionSuggestions, +}; diff --git a/apps/www/src/registry/default/plate-ui/ai-menu.tsx b/apps/www/src/registry/default/plate-ui/ai-menu.tsx index 9df70d7515..12d70c3b9b 100644 --- a/apps/www/src/registry/default/plate-ui/ai-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/ai-menu.tsx @@ -9,19 +9,8 @@ import { useAI } from '@udecode/plate-ai/react'; import { Icons } from '@/components/icons'; import { useActionHandler } from './action-handler'; -import { - CursorCommandsActions, - CursorSuggestionActions, - SelectionCommandsActions, - SelectionSuggestionActions, - defaultValues, -} from './ai-actions'; -import { - CursorCommands, - CursorSuggestions, - SelectionCommands, - SelectionSuggestions, -} from './ai-menu-items'; +import { aiActions, defaultValues } from './ai-actions'; +import { aiCommands } from './ai-menu-items'; import { AIPreviewEditor } from './ai-previdew-editor'; import { Button } from './button'; import { Menu, comboboxVariants, renderSearchMenuItems } from './menu'; @@ -40,25 +29,13 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => { submitButtonProps, onCloseMenu, } = useAI({ - aiActions: { - CursorCommandsActions, - CursorSuggestionActions, - SelectionCommandsActions, - SelectionSuggestionActions, - }, - aiCommands: { - CursorCommands, - CursorSuggestions, - SelectionCommands, - SelectionSuggestions, - }, + aiActions: aiActions, + aiCommands: aiCommands, defaultValues, }); useActionHandler(action, aiEditor!); - /** IME */ - return ( <> PlateEditor; scrollContainerSelector: string; + fetchSuggestion?: (props: FetchAISuggestionProps) => Promise; trigger?: RegExp | string[] | string; triggerPreviousCharPattern?: RegExp; diff --git a/packages/ai/src/react/ai/hook/useAI.ts b/packages/ai/src/react/ai/hook/useAI.ts index fc30728da7..c6d2b99bc1 100644 --- a/packages/ai/src/react/ai/hook/useAI.ts +++ b/packages/ai/src/react/ai/hook/useAI.ts @@ -97,7 +97,6 @@ export const useAI = ({ if (isHotkey('enter')(e)) await streamInsert(); }; - // TODO: move to API const onCloseMenu = useCallback(() => { // close menu if ai is not generating if (aiState === 'idle' || aiState === 'done') { diff --git a/packages/ai/src/react/ai/stream/streamTraversal.ts b/packages/ai/src/react/ai/stream/streamTraversal.ts index 6b926feaab..1be7b22d11 100644 --- a/packages/ai/src/react/ai/stream/streamTraversal.ts +++ b/packages/ai/src/react/ai/stream/streamTraversal.ts @@ -17,28 +17,15 @@ export const streamTraversal = async ( const abortController = new AbortController(); editor.setOptions(AIPlugin, { abortController }); - const response = await fetch('/api/ai/command', { - body: JSON.stringify({ prompt, system }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - signal: abortController.signal, - }).catch((error) => { - console.error(error); - }); + const fetchSuggestion = editor.getOptions(AIPlugin).fetchSuggestion!; - if (response?.status === 429) { - return fn( - 'Rate limit exceeded. You have made too many requests. Please try again later.', - true - ); - } - if (!response || !response.body) { - throw new Error('Response or response body is null or abort'); - } + const response = await fetchSuggestion({ + abortSignal: abortController, + prompt, + system, + }); - const reader = response.body.getReader(); + const reader = response.getReader(); const decoder = new TextDecoder(); // eslint-disable-next-line no-constant-condition From fcc4e5f5f3aedf3302f8705b7a425e6b29233757 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 1 Oct 2024 20:32:38 +0800 Subject: [PATCH 043/127] docs --- apps/www/content/docs/copilot.mdx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/www/content/docs/copilot.mdx b/apps/www/content/docs/copilot.mdx index 0858b129a3..8fc34e22f7 100644 --- a/apps/www/content/docs/copilot.mdx +++ b/apps/www/content/docs/copilot.mdx @@ -128,8 +128,12 @@ The Tab key is a popular and frequently used key in text editing, which can lead ### Conflict with Indent Plugin -Ideally, we should resolve conflicts within the plugins themselves. However, there isn't a straightforward method to do this for the Indent Plugin and Copilot Plugin conflict. -As a workaround, you can place the Copilot Plugin before the Indent Plugin in your plugin configuration. +The **IndentPlugin** and **IndentListPlugin** have a similar conflict with **Copilot Plugin**. + +As a workaround, you can place the Copilot Plugin before the these two plugins in your plugin configuration. + +Or set the priority of Copilot Plugin to a higher value than Indent Plugin see [priority](https://platejs.org/docs/plugin#plugin-priority). + Here's an example of how to order your plugins: From ed6fad055caf81d9435efe3d102d7acad330aa96 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 1 Oct 2024 20:52:49 +0800 Subject: [PATCH 044/127] version --- packages/ai/package.json | 12 +- packages/menu/package.json | 4 +- yarn.lock | 415 +++++++++++++++++++++---------------- 3 files changed, 248 insertions(+), 183 deletions(-) diff --git a/packages/ai/package.json b/packages/ai/package.json index 862c2962f0..edc7addcef 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@udecode/plate-ai", - "version": "38.0.1", + "version": "39.0.0", "description": "Text AI plugin for Plate", "keywords": [ "plate", @@ -50,14 +50,14 @@ "typecheck": "yarn p:typecheck" }, "dependencies": { - "@udecode/plate-combobox": "38.0.1", - "@udecode/plate-markdown": "38.0.1", - "@udecode/plate-menu": "38.0.1", - "@udecode/plate-selection": "38.0.11", + "@udecode/plate-combobox": "39.0.0", + "@udecode/plate-markdown": "39.0.0", + "@udecode/plate-menu": "39.0.0", + "@udecode/plate-selection": "39.0.0", "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=38.0.6", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.103.0", diff --git a/packages/menu/package.json b/packages/menu/package.json index 58139925f1..b9242b4d4e 100644 --- a/packages/menu/package.json +++ b/packages/menu/package.json @@ -1,6 +1,6 @@ { "name": "@udecode/plate-menu", - "version": "38.0.1", + "version": "39.0.0", "description": "Menu for Plate", "keywords": [ "plate", @@ -59,7 +59,7 @@ "@udecode/plate-common": "workspace:^" }, "peerDependencies": { - "@udecode/plate-common": ">=38.0.6", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.103.0", diff --git a/yarn.lock b/yarn.lock index 3db0fea3fa..2b9a6497c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3166,7 +3166,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-compose-refs@npm:1.1.0, @radix-ui/react-compose-refs@npm:^1.1.0": +"@radix-ui/react-compose-refs@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-compose-refs@npm:1.1.0" peerDependencies: @@ -5883,7 +5883,7 @@ __metadata: version: 0.0.0-use.local resolution: "@udecode/cn@workspace:packages/cn" dependencies: - "@udecode/react-utils": "npm:38.0.1" + "@udecode/react-utils": "npm:39.0.0" peerDependencies: class-variance-authority: ">=0.7.0" react: ">=16.8.0" @@ -5912,13 +5912,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-alignment@npm:38.0.1, @udecode/plate-alignment@workspace:^, @udecode/plate-alignment@workspace:packages/alignment": +"@udecode/plate-alignment@npm:39.0.0, @udecode/plate-alignment@workspace:^, @udecode/plate-alignment@workspace:packages/alignment": version: 0.0.0-use.local resolution: "@udecode/plate-alignment@workspace:packages/alignment" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -5928,14 +5928,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-autoformat@npm:38.0.1, @udecode/plate-autoformat@workspace:^, @udecode/plate-autoformat@workspace:packages/autoformat": +"@udecode/plate-autoformat@npm:39.0.0, @udecode/plate-autoformat@workspace:^, @udecode/plate-autoformat@workspace:packages/autoformat": version: 0.0.0-use.local resolution: "@udecode/plate-autoformat@workspace:packages/autoformat" dependencies: "@udecode/plate-common": "workspace:^" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -5945,16 +5945,16 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-basic-elements@npm:38.0.12, @udecode/plate-basic-elements@workspace:^, @udecode/plate-basic-elements@workspace:packages/basic-elements": +"@udecode/plate-basic-elements@npm:39.0.0, @udecode/plate-basic-elements@workspace:^, @udecode/plate-basic-elements@workspace:packages/basic-elements": version: 0.0.0-use.local resolution: "@udecode/plate-basic-elements@workspace:packages/basic-elements" dependencies: - "@udecode/plate-block-quote": "npm:38.0.1" - "@udecode/plate-code-block": "npm:38.0.1" + "@udecode/plate-block-quote": "npm:39.0.0" + "@udecode/plate-code-block": "npm:39.0.0" "@udecode/plate-common": "workspace:^" - "@udecode/plate-heading": "npm:38.0.12" + "@udecode/plate-heading": "npm:39.0.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -5964,13 +5964,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-basic-marks@npm:38.0.1, @udecode/plate-basic-marks@workspace:^, @udecode/plate-basic-marks@workspace:packages/basic-marks": +"@udecode/plate-basic-marks@npm:39.0.0, @udecode/plate-basic-marks@workspace:^, @udecode/plate-basic-marks@workspace:packages/basic-marks": version: 0.0.0-use.local resolution: "@udecode/plate-basic-marks@workspace:packages/basic-marks" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -5980,13 +5980,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-block-quote@npm:38.0.1, @udecode/plate-block-quote@workspace:^, @udecode/plate-block-quote@workspace:packages/block-quote": +"@udecode/plate-block-quote@npm:39.0.0, @udecode/plate-block-quote@workspace:^, @udecode/plate-block-quote@workspace:packages/block-quote": version: 0.0.0-use.local resolution: "@udecode/plate-block-quote@workspace:packages/block-quote" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -5996,13 +5996,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-break@npm:38.0.1, @udecode/plate-break@workspace:^, @udecode/plate-break@workspace:packages/break": +"@udecode/plate-break@npm:39.0.0, @udecode/plate-break@workspace:^, @udecode/plate-break@workspace:packages/break": version: 0.0.0-use.local resolution: "@udecode/plate-break@workspace:packages/break" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6018,7 +6018,7 @@ __metadata: dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6035,7 +6035,7 @@ __metadata: "@udecode/plate-common": "workspace:^" react-textarea-autosize: "npm:^8.5.3" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6054,7 +6054,7 @@ __metadata: delay: "npm:5.0.0" p-defer: "npm:^4.0.1" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6064,14 +6064,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-code-block@npm:38.0.1, @udecode/plate-code-block@workspace:^, @udecode/plate-code-block@workspace:packages/code-block": +"@udecode/plate-code-block@npm:39.0.0, @udecode/plate-code-block@workspace:^, @udecode/plate-code-block@workspace:packages/code-block": version: 0.0.0-use.local resolution: "@udecode/plate-code-block@workspace:packages/code-block" dependencies: "@udecode/plate-common": "workspace:^" prismjs: "npm:^1.29.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6081,13 +6081,28 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-combobox@npm:38.0.1, @udecode/plate-combobox@workspace:^, @udecode/plate-combobox@workspace:packages/combobox": +"@udecode/plate-combobox@npm:38.0.1": + version: 38.0.1 + resolution: "@udecode/plate-combobox@npm:38.0.1" + peerDependencies: + "@udecode/plate-common": ">=38.0.1" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + checksum: 10c0/3fbbce2bd454ce5777a8dcada8cfc6b46c141b9ebf3a8b431f3c11e5fb08b30cabb2752bc90cb1db49cedcd67f33e4cc4cd615f68c91983ad221c80e77a6f625 + languageName: node + linkType: hard + +"@udecode/plate-combobox@npm:39.0.0, @udecode/plate-combobox@workspace:^, @udecode/plate-combobox@workspace:packages/combobox": version: 0.0.0-use.local resolution: "@udecode/plate-combobox@workspace:packages/combobox" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6097,14 +6112,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-comments@npm:38.0.1, @udecode/plate-comments@workspace:^, @udecode/plate-comments@workspace:packages/comments": +"@udecode/plate-comments@npm:39.0.0, @udecode/plate-comments@workspace:^, @udecode/plate-comments@workspace:packages/comments": version: 0.0.0-use.local resolution: "@udecode/plate-comments@workspace:packages/comments" dependencies: "@udecode/plate-common": "workspace:^" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6114,16 +6129,16 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-common@npm:38.0.6, @udecode/plate-common@workspace:^, @udecode/plate-common@workspace:packages/common": +"@udecode/plate-common@npm:39.0.0, @udecode/plate-common@workspace:^, @udecode/plate-common@workspace:packages/common": version: 0.0.0-use.local resolution: "@udecode/plate-common@workspace:packages/common" dependencies: - "@udecode/plate-core": "npm:38.0.6" - "@udecode/plate-utils": "npm:38.0.6" + "@udecode/plate-core": "npm:39.0.0" + "@udecode/plate-utils": "npm:39.0.0" "@udecode/react-hotkeys": "npm:37.0.0" - "@udecode/react-utils": "npm:38.0.1" + "@udecode/react-utils": "npm:39.0.0" "@udecode/slate": "npm:38.0.4" - "@udecode/slate-react": "npm:38.0.4" + "@udecode/slate-react": "npm:39.0.0" "@udecode/slate-utils": "npm:38.0.4" "@udecode/utils": "npm:37.0.0" peerDependencies: @@ -6136,14 +6151,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-core@npm:38.0.6, @udecode/plate-core@workspace:^, @udecode/plate-core@workspace:packages/core": +"@udecode/plate-core@npm:39.0.0, @udecode/plate-core@workspace:^, @udecode/plate-core@workspace:packages/core": version: 0.0.0-use.local resolution: "@udecode/plate-core@workspace:packages/core" dependencies: "@udecode/react-hotkeys": "npm:37.0.0" - "@udecode/react-utils": "npm:38.0.1" + "@udecode/react-utils": "npm:39.0.0" "@udecode/slate": "npm:38.0.4" - "@udecode/slate-react": "npm:38.0.4" + "@udecode/slate-react": "npm:39.0.0" "@udecode/slate-utils": "npm:38.0.4" "@udecode/utils": "npm:37.0.0" clsx: "npm:^2.1.1" @@ -6167,16 +6182,16 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-csv@npm:38.0.8, @udecode/plate-csv@workspace:^, @udecode/plate-csv@workspace:packages/csv": +"@udecode/plate-csv@npm:39.0.0, @udecode/plate-csv@workspace:^, @udecode/plate-csv@workspace:packages/csv": version: 0.0.0-use.local resolution: "@udecode/plate-csv@workspace:packages/csv" dependencies: "@types/papaparse": "npm:^5.3.14" "@udecode/plate-common": "workspace:^" - "@udecode/plate-table": "npm:38.0.8" + "@udecode/plate-table": "npm:39.0.0" papaparse: "npm:^5.4.1" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6192,7 +6207,7 @@ __metadata: dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6208,7 +6223,7 @@ __metadata: dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.94.0" @@ -6218,7 +6233,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-diff@npm:38.0.0, @udecode/plate-diff@workspace:^, @udecode/plate-diff@workspace:packages/diff": +"@udecode/plate-diff@npm:39.0.0, @udecode/plate-diff@workspace:^, @udecode/plate-diff@workspace:packages/diff": version: 0.0.0-use.local resolution: "@udecode/plate-diff@workspace:packages/diff" dependencies: @@ -6226,7 +6241,7 @@ __metadata: diff-match-patch-ts: "npm:^0.6.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6244,7 +6259,7 @@ __metadata: lodash: "npm:^4.17.21" raf: "npm:^3.4.1" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dnd: ">=14.0.0" react-dnd-html5-backend: ">=14.0.0" @@ -6256,19 +6271,19 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-docx@npm:38.0.12, @udecode/plate-docx@workspace:^, @udecode/plate-docx@workspace:packages/docx": +"@udecode/plate-docx@npm:39.0.0, @udecode/plate-docx@workspace:^, @udecode/plate-docx@workspace:packages/docx": version: 0.0.0-use.local resolution: "@udecode/plate-docx@workspace:packages/docx" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-heading": "npm:38.0.12" - "@udecode/plate-indent": "npm:38.0.1" - "@udecode/plate-indent-list": "npm:38.0.10" - "@udecode/plate-media": "npm:38.0.6" - "@udecode/plate-table": "npm:38.0.8" + "@udecode/plate-heading": "npm:39.0.0" + "@udecode/plate-indent": "npm:39.0.0" + "@udecode/plate-indent-list": "npm:39.0.0" + "@udecode/plate-media": "npm:39.0.0" + "@udecode/plate-table": "npm:39.0.0" validator: "npm:^13.12.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6283,10 +6298,10 @@ __metadata: resolution: "@udecode/plate-emoji@workspace:packages/emoji" dependencies: "@emoji-mart/data": "npm:^1.2.1" - "@udecode/plate-combobox": "npm:38.0.1" + "@udecode/plate-combobox": "npm:39.0.0" "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6303,7 +6318,7 @@ __metadata: "@excalidraw/excalidraw": "npm:0.16.4" "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6313,13 +6328,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-find-replace@npm:38.0.0, @udecode/plate-find-replace@workspace:^, @udecode/plate-find-replace@workspace:packages/find-replace": +"@udecode/plate-find-replace@npm:39.0.0, @udecode/plate-find-replace@workspace:^, @udecode/plate-find-replace@workspace:packages/find-replace": version: 0.0.0-use.local resolution: "@udecode/plate-find-replace@workspace:packages/find-replace" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6329,7 +6344,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-floating@npm:38.0.1, @udecode/plate-floating@workspace:^, @udecode/plate-floating@workspace:packages/floating": +"@udecode/plate-floating@npm:39.0.0, @udecode/plate-floating@workspace:^, @udecode/plate-floating@workspace:packages/floating": version: 0.0.0-use.local resolution: "@udecode/plate-floating@workspace:packages/floating" dependencies: @@ -6337,7 +6352,7 @@ __metadata: "@floating-ui/react": "npm:^0.26.23" "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6347,14 +6362,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-font@npm:38.0.1, @udecode/plate-font@workspace:^, @udecode/plate-font@workspace:packages/font": +"@udecode/plate-font@npm:39.0.0, @udecode/plate-font@workspace:^, @udecode/plate-font@workspace:packages/font": version: 0.0.0-use.local resolution: "@udecode/plate-font@workspace:packages/font" dependencies: "@udecode/plate-common": "workspace:^" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6364,13 +6379,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-heading@npm:38.0.12, @udecode/plate-heading@workspace:^, @udecode/plate-heading@workspace:packages/heading": +"@udecode/plate-heading@npm:39.0.0, @udecode/plate-heading@workspace:^, @udecode/plate-heading@workspace:packages/heading": version: 0.0.0-use.local resolution: "@udecode/plate-heading@workspace:packages/heading" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6380,13 +6395,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-highlight@npm:38.0.1, @udecode/plate-highlight@workspace:^, @udecode/plate-highlight@workspace:packages/highlight": +"@udecode/plate-highlight@npm:39.0.0, @udecode/plate-highlight@workspace:^, @udecode/plate-highlight@workspace:packages/highlight": version: 0.0.0-use.local resolution: "@udecode/plate-highlight@workspace:packages/highlight" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6396,13 +6411,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-horizontal-rule@npm:38.0.1, @udecode/plate-horizontal-rule@workspace:^, @udecode/plate-horizontal-rule@workspace:packages/horizontal-rule": +"@udecode/plate-horizontal-rule@npm:39.0.0, @udecode/plate-horizontal-rule@workspace:^, @udecode/plate-horizontal-rule@workspace:packages/horizontal-rule": version: 0.0.0-use.local resolution: "@udecode/plate-horizontal-rule@workspace:packages/horizontal-rule" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6412,7 +6427,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-html@npm:38.0.1, @udecode/plate-html@workspace:^, @udecode/plate-html@workspace:packages/html": +"@udecode/plate-html@npm:39.0.0, @udecode/plate-html@workspace:^, @udecode/plate-html@workspace:packages/html": version: 0.0.0-use.local resolution: "@udecode/plate-html@workspace:packages/html" dependencies: @@ -6420,7 +6435,7 @@ __metadata: "@udecode/plate-common": "workspace:^" html-entities: "npm:^2.5.2" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6430,16 +6445,16 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-indent-list@npm:38.0.10, @udecode/plate-indent-list@workspace:^, @udecode/plate-indent-list@workspace:packages/indent-list": +"@udecode/plate-indent-list@npm:39.0.0, @udecode/plate-indent-list@workspace:^, @udecode/plate-indent-list@workspace:packages/indent-list": version: 0.0.0-use.local resolution: "@udecode/plate-indent-list@workspace:packages/indent-list" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-indent": "npm:38.0.1" - "@udecode/plate-list": "npm:38.0.1" + "@udecode/plate-indent": "npm:39.0.0" + "@udecode/plate-list": "npm:39.0.0" clsx: "npm:^2.1.1" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6449,13 +6464,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-indent@npm:38.0.1, @udecode/plate-indent@workspace:^, @udecode/plate-indent@workspace:packages/indent": +"@udecode/plate-indent@npm:39.0.0, @udecode/plate-indent@workspace:^, @udecode/plate-indent@workspace:packages/indent": version: 0.0.0-use.local resolution: "@udecode/plate-indent@workspace:packages/indent" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6472,7 +6487,7 @@ __metadata: "@udecode/plate-common": "workspace:^" juice: "npm:^8.1.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6482,13 +6497,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-kbd@npm:38.0.1, @udecode/plate-kbd@workspace:^, @udecode/plate-kbd@workspace:packages/kbd": +"@udecode/plate-kbd@npm:39.0.0, @udecode/plate-kbd@workspace:^, @udecode/plate-kbd@workspace:packages/kbd": version: 0.0.0-use.local resolution: "@udecode/plate-kbd@workspace:packages/kbd" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6498,13 +6513,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-layout@npm:38.0.1, @udecode/plate-layout@workspace:^, @udecode/plate-layout@workspace:packages/layout": +"@udecode/plate-layout@npm:39.0.0, @udecode/plate-layout@workspace:^, @udecode/plate-layout@workspace:packages/layout": version: 0.0.0-use.local resolution: "@udecode/plate-layout@workspace:packages/layout" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6514,13 +6529,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-line-height@npm:38.0.1, @udecode/plate-line-height@workspace:^, @udecode/plate-line-height@workspace:packages/line-height": +"@udecode/plate-line-height@npm:39.0.0, @udecode/plate-line-height@workspace:^, @udecode/plate-line-height@workspace:packages/line-height": version: 0.0.0-use.local resolution: "@udecode/plate-line-height@workspace:packages/line-height" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6530,15 +6545,15 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-link@npm:38.0.6, @udecode/plate-link@workspace:^, @udecode/plate-link@workspace:packages/link": +"@udecode/plate-link@npm:39.0.0, @udecode/plate-link@workspace:^, @udecode/plate-link@workspace:packages/link": version: 0.0.0-use.local resolution: "@udecode/plate-link@workspace:packages/link" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-floating": "npm:38.0.1" - "@udecode/plate-normalizers": "npm:38.0.1" + "@udecode/plate-floating": "npm:39.0.0" + "@udecode/plate-normalizers": "npm:39.0.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6548,15 +6563,15 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-list@npm:38.0.1, @udecode/plate-list@workspace:^, @udecode/plate-list@workspace:packages/list": +"@udecode/plate-list@npm:39.0.0, @udecode/plate-list@workspace:^, @udecode/plate-list@workspace:packages/list": version: 0.0.0-use.local resolution: "@udecode/plate-list@workspace:packages/list" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-reset-node": "npm:38.0.1" + "@udecode/plate-reset-node": "npm:39.0.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6566,7 +6581,26 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-markdown@npm:38.0.13, @udecode/plate-markdown@workspace:^, @udecode/plate-markdown@workspace:packages/markdown": +"@udecode/plate-markdown@npm:38.0.1": + version: 38.0.1 + resolution: "@udecode/plate-markdown@npm:38.0.1" + dependencies: + lodash: "npm:^4.17.21" + remark-parse: "npm:^9.0.0" + unified: "npm:^11.0.5" + peerDependencies: + "@udecode/plate-common": ">=38.0.1" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + checksum: 10c0/0198a4cce86408e7389c0878668d79b6b78db8ab98b0d93aa9fe9601298b7bc2ae5cd1b41037691738bcb482ad10871163a4fb94dde2df03bdd09f7b4539a9b0 + languageName: node + linkType: hard + +"@udecode/plate-markdown@npm:39.0.0, @udecode/plate-markdown@workspace:^, @udecode/plate-markdown@workspace:packages/markdown": version: 0.0.0-use.local resolution: "@udecode/plate-markdown@workspace:packages/markdown" dependencies: @@ -6575,7 +6609,7 @@ __metadata: remark-parse: "npm:^9.0.0" unified: "npm:^11.0.5" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6593,7 +6627,7 @@ __metadata: "@udecode/plate-common": "workspace:^" katex: "npm:0.16.11" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6603,14 +6637,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-media@npm:38.0.6, @udecode/plate-media@workspace:^, @udecode/plate-media@workspace:packages/media": +"@udecode/plate-media@npm:39.0.0, @udecode/plate-media@workspace:^, @udecode/plate-media@workspace:packages/media": version: 0.0.0-use.local resolution: "@udecode/plate-media@workspace:packages/media" dependencies: "@udecode/plate-common": "workspace:^" js-video-url-parser: "npm:^0.5.1" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6620,14 +6654,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-mention@npm:38.0.1, @udecode/plate-mention@workspace:^, @udecode/plate-mention@workspace:packages/mention": +"@udecode/plate-mention@npm:39.0.0, @udecode/plate-mention@workspace:^, @udecode/plate-mention@workspace:packages/mention": version: 0.0.0-use.local resolution: "@udecode/plate-mention@workspace:packages/mention" dependencies: - "@udecode/plate-combobox": "npm:38.0.1" + "@udecode/plate-combobox": "npm:39.0.0" "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6657,14 +6691,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-node-id@npm:38.0.1, @udecode/plate-node-id@workspace:^, @udecode/plate-node-id@workspace:packages/node-id": +"@udecode/plate-node-id@npm:39.0.0, @udecode/plate-node-id@workspace:^, @udecode/plate-node-id@workspace:packages/node-id": version: 0.0.0-use.local resolution: "@udecode/plate-node-id@workspace:packages/node-id" dependencies: "@udecode/plate-common": "workspace:^" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6674,14 +6708,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-normalizers@npm:38.0.1, @udecode/plate-normalizers@workspace:^, @udecode/plate-normalizers@workspace:packages/normalizers": +"@udecode/plate-normalizers@npm:39.0.0, @udecode/plate-normalizers@workspace:^, @udecode/plate-normalizers@workspace:packages/normalizers": version: 0.0.0-use.local resolution: "@udecode/plate-normalizers@workspace:packages/normalizers" dependencies: "@udecode/plate-common": "workspace:^" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6698,7 +6732,7 @@ __metadata: "@udecode/plate-common": "workspace:^" peerDependencies: "@playwright/test": ">=1.42.1" - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6708,13 +6742,28 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-reset-node@npm:38.0.1, @udecode/plate-reset-node@workspace:^, @udecode/plate-reset-node@workspace:packages/reset-node": +"@udecode/plate-reset-node@npm:38.0.1": + version: 38.0.1 + resolution: "@udecode/plate-reset-node@npm:38.0.1" + peerDependencies: + "@udecode/plate-common": ">=38.0.1" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + checksum: 10c0/13db73f040d82388b270318f9cfecc60e28e9383e33c1a1cd771bec21348553ceccbe4fd1ddaf58529fe5b6b75cb0b1e11e27cddc3c80cb0bcd187d1bb71fd72 + languageName: node + linkType: hard + +"@udecode/plate-reset-node@npm:39.0.0, @udecode/plate-reset-node@workspace:^, @udecode/plate-reset-node@workspace:packages/reset-node": version: 0.0.0-use.local resolution: "@udecode/plate-reset-node@workspace:packages/reset-node" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6724,13 +6773,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-resizable@npm:38.0.0, @udecode/plate-resizable@workspace:^, @udecode/plate-resizable@workspace:packages/resizable": +"@udecode/plate-resizable@npm:39.0.0, @udecode/plate-resizable@workspace:^, @udecode/plate-resizable@workspace:packages/resizable": version: 0.0.0-use.local resolution: "@udecode/plate-resizable@workspace:packages/resizable" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6740,13 +6789,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-select@npm:38.0.1, @udecode/plate-select@workspace:^, @udecode/plate-select@workspace:packages/select": +"@udecode/plate-select@npm:39.0.0, @udecode/plate-select@workspace:^, @udecode/plate-select@workspace:packages/select": version: 0.0.0-use.local resolution: "@udecode/plate-select@workspace:packages/select" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6756,14 +6805,31 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-selection@npm:38.0.11, @udecode/plate-selection@workspace:^, @udecode/plate-selection@workspace:packages/selection": +"@udecode/plate-selection@npm:38.0.11": + version: 38.0.11 + resolution: "@udecode/plate-selection@npm:38.0.11" + dependencies: + copy-to-clipboard: "npm:^3.3.3" + peerDependencies: + "@udecode/plate-common": ">=38.0.6" + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.103.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.108.0" + checksum: 10c0/7343d4179590c8af60a3e7333756d79660c0a7d32fb92fa16f2d377800f22f43dc5babebd3aa565b09fa8375f5713c89339b93d7f7eb37e30c0bb9f1ea7cd790 + languageName: node + linkType: hard + +"@udecode/plate-selection@npm:39.0.0, @udecode/plate-selection@workspace:^, @udecode/plate-selection@workspace:packages/selection": version: 0.0.0-use.local resolution: "@udecode/plate-selection@workspace:packages/selection" dependencies: "@udecode/plate-common": "workspace:^" copy-to-clipboard: "npm:^3.3.3" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6773,14 +6839,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-slash-command@npm:38.0.1, @udecode/plate-slash-command@workspace:^, @udecode/plate-slash-command@workspace:packages/slash-command": +"@udecode/plate-slash-command@npm:39.0.0, @udecode/plate-slash-command@workspace:^, @udecode/plate-slash-command@workspace:packages/slash-command": version: 0.0.0-use.local resolution: "@udecode/plate-slash-command@workspace:packages/slash-command" dependencies: - "@udecode/plate-combobox": "npm:38.0.1" + "@udecode/plate-combobox": "npm:39.0.0" "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6790,15 +6856,15 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-suggestion@npm:38.0.1, @udecode/plate-suggestion@workspace:^, @udecode/plate-suggestion@workspace:packages/suggestion": +"@udecode/plate-suggestion@npm:39.0.0, @udecode/plate-suggestion@workspace:^, @udecode/plate-suggestion@workspace:packages/suggestion": version: 0.0.0-use.local resolution: "@udecode/plate-suggestion@workspace:packages/suggestion" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-diff": "npm:38.0.0" + "@udecode/plate-diff": "npm:39.0.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6808,14 +6874,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-tabbable@npm:38.0.1, @udecode/plate-tabbable@workspace:^, @udecode/plate-tabbable@workspace:packages/tabbable": +"@udecode/plate-tabbable@npm:39.0.0, @udecode/plate-tabbable@workspace:^, @udecode/plate-tabbable@workspace:packages/tabbable": version: 0.0.0-use.local resolution: "@udecode/plate-tabbable@workspace:packages/tabbable" dependencies: "@udecode/plate-common": "workspace:^" tabbable: "npm:^6.2.0" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6825,15 +6891,15 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-table@npm:38.0.8, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": +"@udecode/plate-table@npm:39.0.0, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": version: 0.0.0-use.local resolution: "@udecode/plate-table@workspace:packages/table" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-resizable": "npm:38.0.0" + "@udecode/plate-resizable": "npm:39.0.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6851,16 +6917,16 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-toggle@npm:38.0.1, @udecode/plate-toggle@workspace:^, @udecode/plate-toggle@workspace:packages/toggle": +"@udecode/plate-toggle@npm:39.0.0, @udecode/plate-toggle@workspace:^, @udecode/plate-toggle@workspace:packages/toggle": version: 0.0.0-use.local resolution: "@udecode/plate-toggle@workspace:packages/toggle" dependencies: "@udecode/plate-common": "workspace:^" - "@udecode/plate-indent": "npm:38.0.1" - "@udecode/plate-node-id": "npm:38.0.1" + "@udecode/plate-indent": "npm:39.0.0" + "@udecode/plate-node-id": "npm:39.0.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6870,13 +6936,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-trailing-block@npm:38.0.1, @udecode/plate-trailing-block@workspace:^, @udecode/plate-trailing-block@workspace:packages/trailing-block": +"@udecode/plate-trailing-block@npm:39.0.0, @udecode/plate-trailing-block@workspace:^, @udecode/plate-trailing-block@workspace:packages/trailing-block": version: 0.0.0-use.local resolution: "@udecode/plate-trailing-block@workspace:packages/trailing-block" dependencies: "@udecode/plate-common": "workspace:^" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6917,14 +6983,14 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-utils@npm:38.0.6, @udecode/plate-utils@workspace:^, @udecode/plate-utils@workspace:packages/plate-utils": +"@udecode/plate-utils@npm:39.0.0, @udecode/plate-utils@workspace:^, @udecode/plate-utils@workspace:packages/plate-utils": version: 0.0.0-use.local resolution: "@udecode/plate-utils@workspace:packages/plate-utils" dependencies: - "@udecode/plate-core": "npm:38.0.6" - "@udecode/react-utils": "npm:38.0.1" + "@udecode/plate-core": "npm:39.0.0" + "@udecode/react-utils": "npm:39.0.0" "@udecode/slate": "npm:38.0.4" - "@udecode/slate-react": "npm:38.0.4" + "@udecode/slate-react": "npm:39.0.0" "@udecode/slate-utils": "npm:38.0.4" "@udecode/utils": "npm:37.0.0" clsx: "npm:^2.1.1" @@ -6948,7 +7014,7 @@ __metadata: "@udecode/plate-common": "workspace:^" yjs: "npm:^13.6.19" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6962,48 +7028,48 @@ __metadata: version: 0.0.0-use.local resolution: "@udecode/plate@workspace:packages/plate" dependencies: - "@udecode/plate-alignment": "npm:38.0.1" - "@udecode/plate-autoformat": "npm:38.0.1" - "@udecode/plate-basic-elements": "npm:38.0.12" - "@udecode/plate-basic-marks": "npm:38.0.1" - "@udecode/plate-block-quote": "npm:38.0.1" - "@udecode/plate-break": "npm:38.0.1" - "@udecode/plate-code-block": "npm:38.0.1" - "@udecode/plate-combobox": "npm:38.0.1" - "@udecode/plate-comments": "npm:38.0.1" - "@udecode/plate-common": "npm:38.0.6" - "@udecode/plate-csv": "npm:38.0.8" - "@udecode/plate-diff": "npm:38.0.0" - "@udecode/plate-docx": "npm:38.0.12" - "@udecode/plate-find-replace": "npm:38.0.0" - "@udecode/plate-floating": "npm:38.0.1" - "@udecode/plate-font": "npm:38.0.1" - "@udecode/plate-heading": "npm:38.0.12" - "@udecode/plate-highlight": "npm:38.0.1" - "@udecode/plate-horizontal-rule": "npm:38.0.1" - "@udecode/plate-html": "npm:38.0.1" - "@udecode/plate-indent": "npm:38.0.1" - "@udecode/plate-indent-list": "npm:38.0.10" - "@udecode/plate-kbd": "npm:38.0.1" - "@udecode/plate-layout": "npm:38.0.1" - "@udecode/plate-line-height": "npm:38.0.1" - "@udecode/plate-link": "npm:38.0.6" - "@udecode/plate-list": "npm:38.0.1" - "@udecode/plate-markdown": "npm:38.0.13" - "@udecode/plate-media": "npm:38.0.6" - "@udecode/plate-mention": "npm:38.0.1" - "@udecode/plate-node-id": "npm:38.0.1" - "@udecode/plate-normalizers": "npm:38.0.1" - "@udecode/plate-reset-node": "npm:38.0.1" - "@udecode/plate-resizable": "npm:38.0.0" - "@udecode/plate-select": "npm:38.0.1" - "@udecode/plate-selection": "npm:38.0.11" - "@udecode/plate-slash-command": "npm:38.0.1" - "@udecode/plate-suggestion": "npm:38.0.1" - "@udecode/plate-tabbable": "npm:38.0.1" - "@udecode/plate-table": "npm:38.0.8" - "@udecode/plate-toggle": "npm:38.0.1" - "@udecode/plate-trailing-block": "npm:38.0.1" + "@udecode/plate-alignment": "npm:39.0.0" + "@udecode/plate-autoformat": "npm:39.0.0" + "@udecode/plate-basic-elements": "npm:39.0.0" + "@udecode/plate-basic-marks": "npm:39.0.0" + "@udecode/plate-block-quote": "npm:39.0.0" + "@udecode/plate-break": "npm:39.0.0" + "@udecode/plate-code-block": "npm:39.0.0" + "@udecode/plate-combobox": "npm:39.0.0" + "@udecode/plate-comments": "npm:39.0.0" + "@udecode/plate-common": "npm:39.0.0" + "@udecode/plate-csv": "npm:39.0.0" + "@udecode/plate-diff": "npm:39.0.0" + "@udecode/plate-docx": "npm:39.0.0" + "@udecode/plate-find-replace": "npm:39.0.0" + "@udecode/plate-floating": "npm:39.0.0" + "@udecode/plate-font": "npm:39.0.0" + "@udecode/plate-heading": "npm:39.0.0" + "@udecode/plate-highlight": "npm:39.0.0" + "@udecode/plate-horizontal-rule": "npm:39.0.0" + "@udecode/plate-html": "npm:39.0.0" + "@udecode/plate-indent": "npm:39.0.0" + "@udecode/plate-indent-list": "npm:39.0.0" + "@udecode/plate-kbd": "npm:39.0.0" + "@udecode/plate-layout": "npm:39.0.0" + "@udecode/plate-line-height": "npm:39.0.0" + "@udecode/plate-link": "npm:39.0.0" + "@udecode/plate-list": "npm:39.0.0" + "@udecode/plate-markdown": "npm:39.0.0" + "@udecode/plate-media": "npm:39.0.0" + "@udecode/plate-mention": "npm:39.0.0" + "@udecode/plate-node-id": "npm:39.0.0" + "@udecode/plate-normalizers": "npm:39.0.0" + "@udecode/plate-reset-node": "npm:39.0.0" + "@udecode/plate-resizable": "npm:39.0.0" + "@udecode/plate-select": "npm:39.0.0" + "@udecode/plate-selection": "npm:39.0.0" + "@udecode/plate-slash-command": "npm:39.0.0" + "@udecode/plate-suggestion": "npm:39.0.0" + "@udecode/plate-tabbable": "npm:39.0.0" + "@udecode/plate-table": "npm:39.0.0" + "@udecode/plate-toggle": "npm:39.0.0" + "@udecode/plate-trailing-block": "npm:39.0.0" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" @@ -7023,11 +7089,10 @@ __metadata: languageName: unknown linkType: soft -"@udecode/react-utils@npm:38.0.1, @udecode/react-utils@workspace:^, @udecode/react-utils@workspace:packages/react-utils": +"@udecode/react-utils@npm:39.0.0, @udecode/react-utils@workspace:^, @udecode/react-utils@workspace:packages/react-utils": version: 0.0.0-use.local resolution: "@udecode/react-utils@workspace:packages/react-utils" dependencies: - "@radix-ui/react-compose-refs": "npm:^1.1.0" "@radix-ui/react-slot": "npm:^1.1.0" "@udecode/utils": "npm:37.0.0" clsx: "npm:^2.1.1" @@ -7037,11 +7102,11 @@ __metadata: languageName: unknown linkType: soft -"@udecode/slate-react@npm:38.0.4, @udecode/slate-react@workspace:^, @udecode/slate-react@workspace:packages/slate-react": +"@udecode/slate-react@npm:39.0.0, @udecode/slate-react@workspace:^, @udecode/slate-react@workspace:packages/slate-react": version: 0.0.0-use.local resolution: "@udecode/slate-react@workspace:packages/slate-react" dependencies: - "@udecode/react-utils": "npm:38.0.1" + "@udecode/react-utils": "npm:39.0.0" "@udecode/slate": "npm:38.0.4" "@udecode/utils": "npm:37.0.0" peerDependencies: From 88c3c0d8482c37b41b7abd4cd7eaf3b07856dc6e Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 1 Oct 2024 21:55:27 +0800 Subject: [PATCH 045/127] api --- .../default/example/playground-demo.tsx | 20 +++--- yarn.lock | 65 ++----------------- 2 files changed, 19 insertions(+), 66 deletions(-) diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 47b5223ba5..e315524248 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -173,16 +173,20 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { createAIEditor: createAIEditor, // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars fetchSuggestion: async ({ abortSignal, prompt, system }) => { - const response = await fetch('/api/stream', { - body: JSON.stringify({ prompt, system }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - signal: abortSignal.signal, - }).catch((error) => { + const response = await fetch( + 'https://pro.platejs.org/api/ai/command', + { + body: JSON.stringify({ prompt, system }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + signal: abortSignal.signal, + } + ).catch((error) => { console.error(error); }); + console.log(response, 'fj'); if (!response || !response.body) { throw new Error('Response or response body is null or abort'); diff --git a/yarn.lock b/yarn.lock index 2b9a6497c2..f56befd9cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5896,13 +5896,13 @@ __metadata: version: 0.0.0-use.local resolution: "@udecode/plate-ai@workspace:packages/ai" dependencies: - "@udecode/plate-combobox": "npm:38.0.1" - "@udecode/plate-markdown": "npm:38.0.1" - "@udecode/plate-menu": "npm:38.0.1" - "@udecode/plate-selection": "npm:38.0.11" + "@udecode/plate-combobox": "npm:39.0.0" + "@udecode/plate-markdown": "npm:39.0.0" + "@udecode/plate-menu": "npm:39.0.0" + "@udecode/plate-selection": "npm:39.0.0" lodash: "npm:^4.17.21" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6081,21 +6081,6 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-combobox@npm:38.0.1": - version: 38.0.1 - resolution: "@udecode/plate-combobox@npm:38.0.1" - peerDependencies: - "@udecode/plate-common": ">=38.0.1" - react: ">=16.8.0" - react-dom: ">=16.8.0" - slate: ">=0.103.0" - slate-history: ">=0.93.0" - slate-hyperscript: ">=0.66.0" - slate-react: ">=0.108.0" - checksum: 10c0/3fbbce2bd454ce5777a8dcada8cfc6b46c141b9ebf3a8b431f3c11e5fb08b30cabb2752bc90cb1db49cedcd67f33e4cc4cd615f68c91983ad221c80e77a6f625 - languageName: node - linkType: hard - "@udecode/plate-combobox@npm:39.0.0, @udecode/plate-combobox@workspace:^, @udecode/plate-combobox@workspace:packages/combobox": version: 0.0.0-use.local resolution: "@udecode/plate-combobox@workspace:packages/combobox" @@ -6581,25 +6566,6 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-markdown@npm:38.0.1": - version: 38.0.1 - resolution: "@udecode/plate-markdown@npm:38.0.1" - dependencies: - lodash: "npm:^4.17.21" - remark-parse: "npm:^9.0.0" - unified: "npm:^11.0.5" - peerDependencies: - "@udecode/plate-common": ">=38.0.1" - react: ">=16.8.0" - react-dom: ">=16.8.0" - slate: ">=0.103.0" - slate-history: ">=0.93.0" - slate-hyperscript: ">=0.66.0" - slate-react: ">=0.108.0" - checksum: 10c0/0198a4cce86408e7389c0878668d79b6b78db8ab98b0d93aa9fe9601298b7bc2ae5cd1b41037691738bcb482ad10871163a4fb94dde2df03bdd09f7b4539a9b0 - languageName: node - linkType: hard - "@udecode/plate-markdown@npm:39.0.0, @udecode/plate-markdown@workspace:^, @udecode/plate-markdown@workspace:packages/markdown": version: 0.0.0-use.local resolution: "@udecode/plate-markdown@workspace:packages/markdown" @@ -6671,7 +6637,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-menu@npm:38.0.1, @udecode/plate-menu@workspace:^, @udecode/plate-menu@workspace:packages/menu": +"@udecode/plate-menu@npm:39.0.0, @udecode/plate-menu@workspace:^, @udecode/plate-menu@workspace:packages/menu": version: 0.0.0-use.local resolution: "@udecode/plate-menu@workspace:packages/menu" dependencies: @@ -6681,7 +6647,7 @@ __metadata: lodash: "npm:^4.17.21" match-sorter: "npm:6.3.4" peerDependencies: - "@udecode/plate-common": ">=38.0.6" + "@udecode/plate-common": ">=39.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" slate: ">=0.103.0" @@ -6805,23 +6771,6 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-selection@npm:38.0.11": - version: 38.0.11 - resolution: "@udecode/plate-selection@npm:38.0.11" - dependencies: - copy-to-clipboard: "npm:^3.3.3" - peerDependencies: - "@udecode/plate-common": ">=38.0.6" - react: ">=16.8.0" - react-dom: ">=16.8.0" - slate: ">=0.103.0" - slate-history: ">=0.93.0" - slate-hyperscript: ">=0.66.0" - slate-react: ">=0.108.0" - checksum: 10c0/7343d4179590c8af60a3e7333756d79660c0a7d32fb92fa16f2d377800f22f43dc5babebd3aa565b09fa8375f5713c89339b93d7f7eb37e30c0bb9f1ea7cd790 - languageName: node - linkType: hard - "@udecode/plate-selection@npm:39.0.0, @udecode/plate-selection@workspace:^, @udecode/plate-selection@workspace:packages/selection": version: 0.0.0-use.local resolution: "@udecode/plate-selection@workspace:packages/selection" From 54c85f0c255adfdfef206afba3dc6239326decf5 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Wed, 2 Oct 2024 16:50:02 +0800 Subject: [PATCH 046/127] docs --- apps/www/content/docs/ai.mdx | 365 ++++++++++++++++++++++++++++++++++- 1 file changed, 360 insertions(+), 5 deletions(-) diff --git a/apps/www/content/docs/ai.mdx b/apps/www/content/docs/ai.mdx index 7d6a639fb6..2352c2ab5a 100644 --- a/apps/www/content/docs/ai.mdx +++ b/apps/www/content/docs/ai.mdx @@ -6,8 +6,6 @@ docs: title: AIMenu --- - - ## Features @@ -21,7 +19,7 @@ docs: ## Installation ```bash -npm install @udecode/plate-ai +npm install @udecode/plate-ai @udecode/plate-menu @udecode/plate-selection @udecode/plate-markdown ``` ## Usage @@ -37,11 +35,34 @@ const editor = usePlateEditor({ }, plugins: [ ...commonPlugins, - SelectionOverlayPlugin, MarkdownPlugin.configure({ options: { indentList: true } }), + SelectionOverlayPlugin, AIPlugin.configure({ options: { - scrollContainerSelector: '#scroll_container', + createAIEditor: createAIEditor, + // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars + fetchStream: async ({ abortSignal, prompt, system }) => { + const response = await fetch( + 'https://pro.platejs.org/api/ai/command', + { + body: JSON.stringify({ prompt, system }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + signal: abortSignal.signal, + } + ).catch((error) => { + console.error(error); + }); + + if (!response || !response.body) { + throw new Error('Response or response body is null or abort'); + } + + return response.body; + }, + scrollContainerSelector: `#your_scroll_container_id`, }, render: { aboveEditable: AIMenu }, }), @@ -50,5 +71,339 @@ const editor = usePlateEditor({ }); ``` +## Install dependencies + +Before integrating the AI plugin, you need to install the dependencies and it's **components** if required. + +* [@udecode/plate-menu](docs/menu) + - The AI menu component for user interaction. + - We separated this menu from ai package mainly to reuse it for the [context menu](/docs/context-menu). + - Make sure to check the menu [documentation](docs/menu) and install the menu **component** sucessfully. + +* [@udecode/plate-selection](docs/selection) + - To add a selected highlight to the newly generated paragraph after executing the AI command. + +* [@udecode/plate-markdown](docs/markdown) + - To convert the ai generated markdown to slate nodes. + - If you are using indent list plugin, make sure to set the `options.indentList` to `true` like the [usage](#usage) code above. + +## Integrate with your backend + +### fetchStream +`options.fetchStream` is an asynchronous function that you need to implement to fetch suggestions from your backend. This function is crucial for integrating the Copilot feature with your own AI model or service. + +The function receives an object with three properties: + +* `abortSignal`: An `AbortSignal` object that allows you to cancel the request when pressing the `esc` key or clicking the `cancel` button. + +* `prompt`: provider by the `streamInsertText` function you will see in the [custom commands](#create-new-commands) section below. + +* `system`: provider by the `streamInsertText` function you will see in the [custom commands](#create-new-commands) section below. + + +The function should return a Promise that resolves to a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) containing the AI-generated text. +This stream allows for real-time processing of the AI response, enabling features like streaming the text as it's generated. + +Here's an next.js api route example of how you mock the return: + +```ts +export function POST() { + const streams = [ + { delay: 100, texts: 'Hello' }, + { delay: 200, texts: 'World' }, + ]; + + const stream = new ReadableStream({ + async start(controller) { + for (const stream of streams) { + await new Promise((resolve) => setTimeout(resolve, stream.delay)); + controller.enqueue(stream.texts); + } + + controller.close(); + }, + }); + + return new Response(stream); +} +``` + +### Vercel AI SDK example: + +We recommend using [vercel ai sdk](https://sdk.vercel.ai/examples/node/generating-text/stream-text#without-reader) to generate the stream. + +It's return `ReadableStream`, which is compatible with our requirement. + +Note: **Without reader** means you don't need to use `getReader` we will parse the stream to text in our package. + +So here is the example code to generate the stream: + +api/ai/command +```ts +import { openai } from '@ai-sdk/openai'; +import { streamText } from 'ai'; +import { NextResponse } from 'next/server'; + +export async function POST(req: Request) { + const { prompt, system, userId } = await req.json(); + + const result = await streamText({ + model: openai('gpt-4o-mini'), + prompt: prompt, + system: system, + }); + + return new NextResponse(result.textStream); +} +``` + +Plugin usage: +```ts + AIPlugin.configure({ + options: { + fetchStream: async ({ abortSignal, prompt, system }) => { + const response = await fetch( + 'https://pro.platejs.org/api/ai/command', + { + body: JSON.stringify({ prompt, system }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + signal: abortSignal.signal, + } + ) + + if(!response.body) { + throw new Error('Failed to fetch stream'); + } + + // return the `ReadableStream` directly + return response.body; + } + }, + }), +``` + + +## Customization commands + +### Customize existing commands. +Before learning how to create custom commands, you need to know that the ai plugin will provide three model for opening the ai menu: + +1. Open by **space** in the new block.We call it **Cursor** model. +2. Open by **floating toolbar button** or **context menu**.We call it **Selection** model. +3. After the AI completes the first generation, we don't close the AI menu but instead modify the commands. We call it **Suggestion** model. + +Due to the special nature of Suggestion mode: whether Cursor mode or Selection mode ends, it will switch to Suggestion mode. +To distinguish between these two sets of commands, we need to maintain four different menus in total. + +The following is all the commands you can see in the `ai-menu-items.tsx` file. +* CursorCommands + - Show when you open the ai menu by `space` in the new block. +* CursorSuggestions + - Show when you open the ai menu by `space` and then complete the first generation. +* SelectionCommands + - Show when you open the ai menu by selectd some text. +* SelectionSuggestions + - Show when you open the ai menu by selectd some text and then complete the first generation. + +If you understand the above modes, you can easily find the corresponding command to modify its text or icons. + +If you want to modify the AI Menu style, you should check the [menu](/docs/components/menu) component docs. + + +### Create new commands. + +Understand the above modes then you can create new commands in the menu you want. + +Next, I will show you how to add a custom command to **Cursor** mode (opened by space). + +Find `CursorCommands` in the `ai-menu-items.tsx` file. then add a new `MenuGroup` with *Generate Jokes* label. +Create a `MenuItem` inside the `MenuGroup` with `jokes` action. +```tsx + + + +``` +Full code: +```tsx +export const CursorCommands = () => { + return ( + <> + + + + + + + + {renderMenuItems(CursorCommandsActions.translate)} + + + + // new command group + + + + + ); +}; +``` +Now we need to create a new action for the `jokes` command. + +Find `CursorCommandsActions` in the `ai-actions.ts` file. + +Then add a new `jokes` action and the const `ACTION_JOKES` it will be used in the `cursorCommandsHandler` file later. +```ts +export const ACTION_JOKES = 'action_jokes'; + +export const CursorCommandsActions = { + ... + jokes: { + icon: , + label: 'Generate a dry joke', + value: ACTION_JOKES, + }, +} satisfies Record; +``` + +Now we can already see the new command in the ai menu.But when we click it nothing will happen. + +Because we need to create a new case in the `cursorCommandsHandler` to handle the `ACTION_JOKES` action. + +Find `cursorCommandsHandler` in the `action-handler` folder. + +Then add `ACTION_JOKES` to the switch case. + +```ts +case ACTION_JOKES: { + await streamInsertText(editor, { + prompt: `Generate a dry joke`, + system: `You are a comedian. Generate a joke.`, + }); + break; +} +``` + +When we start calling the `streamInsertText` function, the ai menu will show the loading state. + +NOTE: If you are creating a new selection mode command, you need to use the `streamInsertTextSelection` function instead of `streamInsertText`. + +And then it will call the `fetchStream` function we implemented before. + +In the same time you can get the **same** `prompt` and `system` you passed in the `streamInsertText` function. + +```ts + AIPlugin.configure({ + options: { + fetchStream: async ({ abortSignal, prompt, system }) => { + console.log(prompt); // "Generate a dry joke" + console.log(system); // "You are a comedian. Generate a joke." + } + }, + }), +``` + +If you not pass the `system` in the `streamInsertText` function. We will use the default [system](https://github.com/udecode/plate/blob/main/packages/ai/src/react/ai/stream/getSystemMessage.ts) maintained in our package. + +When you writing the `prompt` see our existing commands to get inspiration.In general, +You need to provide the LLM with text in Markdown format instead of slate nodes. +Tests have shown that LLMs on the market are easier to understand text in MD format. + +Here are two useful functions you can use to convert slate nodesto MD format. +```ts +// convert slate nodes to md +const md = serializeMdNodes(nodes); + +// convert all editor content to md +const content = serializeMd(editor); +``` + + +At this point, you have successfully created an AI command. + +If you want to create a nested menu, like the `Translate` example we provided, it's essentially the same steps, +but you'll need an additional constant for the `Group`. + + + +## Selection overlay + +**SelectionOverlayPlugin** is part of the [CursorOverlayPlugin](/docs/components/cursor-overlay) Make sure you have installed it. + +The `SelectionOverlayPlugin` is used to display a selection overlay on the editor. It enhances the visual feedback for users: + +When the editor loses focus (blur event): It maintains a visual representation of the last selection. + +So this plugin is userful when we open the ai menu. for example: + +We selected two paragraphs of text. and then using floating toolbar open the ai menu +Normally, these two paragraphs wouldn’t display a blue background. +because our focus has already moved to the input box editor is blurred. + +AI selection overlay + +Sometimes we want to keep lose the selection highlight.In this case, You can use `data-plate-prevent-overlay` attribute add to the button which trigger the blur event. + +For example, in the turn into dropdown menu: +We’ve already prevented the default event so that clicking the ‘Turn Into’ menu doesn’t cause the editor to lose focus. +Therefore, there’s no need to enable the SelectionOverlayPlugin functionality; otherwise, two layers of selection would appear, +which would look visually unappealing. + +```tsx + + + ... + +``` + + + +Therefore, just remember one rule: only use this plugin when you need to move the focus to an external input element, +such as the AI menu or math equation plugins. + + +## Options + +* `scrollContainerSelector` + - The selector of the scroll container.When we trigger the menu in the very bottom of the page, + we need this option to make the scroll container scroll up to show the menu completely. +* `createAIEditor` + - When we use selection mode, we render a read-only editor on the AI menu, and the plugins used by this editor are provided by this option +* `fetchStream` + - The function to fetch the stream from your backend.Introdue in the [Integrate with your backend](#integrate-with-your-backend) section. + + ## API + + + + + + + + + + + + + + + + + + + + + + From d43e600f8939234b7f6849c4247cc33a8aa4da89 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Wed, 2 Oct 2024 16:50:08 +0800 Subject: [PATCH 047/127] AI --- apps/www/public/ai-selection.png | Bin 0 -> 109790 bytes .../playground-turn-into-dropdown-menu.tsx | 1 + .../default/example/playground-demo.tsx | 3 +-- .../action-handler/cursorCommandsHandler.ts | 8 ++++---- packages/ai/src/react/ai/AIPlugin.ts | 2 +- .../ai/src/react/ai/stream/getSystemMessage.ts | 6 +----- .../ai/stream/streamInsertTextSelection.ts | 7 ++----- .../ai/src/react/ai/stream/streamTraversal.ts | 4 ++-- 8 files changed, 12 insertions(+), 19 deletions(-) create mode 100644 apps/www/public/ai-selection.png diff --git a/apps/www/public/ai-selection.png b/apps/www/public/ai-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..a512f46326065a6885c97c46ea232d10c6a8eee6 GIT binary patch literal 109790 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π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 Turn into diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index e315524248..51b814cfb6 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -171,8 +171,7 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => { AIPlugin.configure({ options: { createAIEditor: createAIEditor, - // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars - fetchSuggestion: async ({ abortSignal, prompt, system }) => { + fetchStream: async ({ abortSignal, prompt, system }) => { const response = await fetch( 'https://pro.platejs.org/api/ai/command', { diff --git a/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts b/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts index bbd1bd9789..2f602c4645 100644 --- a/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts +++ b/apps/www/src/registry/default/plate-ui/action-handler/cursorCommandsHandler.ts @@ -18,7 +18,7 @@ export const cursorCommandsHandler = async ( { group, value }: ActionHandlerOptions ) => { if (group === GROUP_LANGUAGES) { - const content = serializeMd(editor as any); + const content = serializeMd(editor); await streamInsertText(editor, { prompt: `Keep the original paragraph format. Translate the following article to ${value?.slice(7)}: ${content}`, }); @@ -28,7 +28,7 @@ export const cursorCommandsHandler = async ( switch (value) { case ACTION_CONTINUE_WRITE: { - const content = serializeMd(editor as any); + const content = serializeMd(editor); await streamInsertText(editor, { prompt: `Continue writing the following article in 3-5 sentences: ${content}`, @@ -37,7 +37,7 @@ export const cursorCommandsHandler = async ( break; } case ACTION_SUMMARIZE: { - const content = serializeMd(editor as any); + const content = serializeMd(editor); await streamInsertText(editor, { prompt: `Summarize the following article in 3-5 sentences: ${content}`, @@ -46,7 +46,7 @@ export const cursorCommandsHandler = async ( break; } case ACTION_EXPLAIN: { - const content = serializeMd(editor as any); + const content = serializeMd(editor); await streamInsertText(editor, { prompt: `Explain the following article in 3-5 sentences: ${content}`, diff --git a/packages/ai/src/react/ai/AIPlugin.ts b/packages/ai/src/react/ai/AIPlugin.ts index 081ee4f31d..8e06b185a6 100644 --- a/packages/ai/src/react/ai/AIPlugin.ts +++ b/packages/ai/src/react/ai/AIPlugin.ts @@ -22,7 +22,7 @@ export interface FetchAISuggestionProps { interface ExposeOptions { createAIEditor: () => PlateEditor; scrollContainerSelector: string; - fetchSuggestion?: (props: FetchAISuggestionProps) => Promise; + fetchStream?: (props: FetchAISuggestionProps) => Promise; trigger?: RegExp | string[] | string; triggerPreviousCharPattern?: RegExp; diff --git a/packages/ai/src/react/ai/stream/getSystemMessage.ts b/packages/ai/src/react/ai/stream/getSystemMessage.ts index a33e61b6d6..abea050e19 100644 --- a/packages/ai/src/react/ai/stream/getSystemMessage.ts +++ b/packages/ai/src/react/ai/stream/getSystemMessage.ts @@ -1,10 +1,6 @@ -export const getSelectionMenuSystem = () => `\ +export const getAISystem = () => `\ You are a text-based conversational robot that helps users with tasks such as continuation and refinement. Users will provide you with some content, and you will help them with their needs. CRITICAL RULE:If you want to start a new line, output a '\n'. If you want to start a new paragraph, output two '\n'. Do not respond to the user,generate the content directly.`; - -export const getAISystem = () => `\ -'Unless the user explicitly requests otherwise, the output will be two to three sentences.' -`; diff --git a/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts b/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts index 6d51e41497..0ca90c36f6 100644 --- a/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts +++ b/packages/ai/src/react/ai/stream/streamInsertTextSelection.ts @@ -12,7 +12,7 @@ import { deserializeMd } from '@udecode/plate-markdown'; import { AIPlugin } from '../AIPlugin'; import { getNextPathByNumber } from '../utils/getNextPathByNumber'; -import { getSelectionMenuSystem } from './getSystemMessage'; +import { getAISystem } from './getSystemMessage'; import { streamTraversal } from './streamTraversal'; interface StreamInsertTextSelectionOptions { @@ -23,10 +23,7 @@ interface StreamInsertTextSelectionOptions { export const streamInsertTextSelection = async ( editor: PlateEditor, aiEditor: PlateEditor, - { - prompt, - system = getSelectionMenuSystem(), - }: StreamInsertTextSelectionOptions + { prompt, system = getAISystem() }: StreamInsertTextSelectionOptions ) => { editor.setOptions(AIPlugin, { aiState: 'requesting', diff --git a/packages/ai/src/react/ai/stream/streamTraversal.ts b/packages/ai/src/react/ai/stream/streamTraversal.ts index 1be7b22d11..63ff20c8fb 100644 --- a/packages/ai/src/react/ai/stream/streamTraversal.ts +++ b/packages/ai/src/react/ai/stream/streamTraversal.ts @@ -17,9 +17,9 @@ export const streamTraversal = async ( const abortController = new AbortController(); editor.setOptions(AIPlugin, { abortController }); - const fetchSuggestion = editor.getOptions(AIPlugin).fetchSuggestion!; + const fetchStream = editor.getOptions(AIPlugin).fetchStream!; - const response = await fetchSuggestion({ + const response = await fetchStream({ abortSignal: abortController, prompt, system, From 627fa2d73070664b2b26696af6cddc4e8e8c0fc9 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Wed, 2 Oct 2024 17:11:19 +0800 Subject: [PATCH 048/127] docs --- packages/ai/src/react/ai/AIPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/react/ai/AIPlugin.ts b/packages/ai/src/react/ai/AIPlugin.ts index 8e06b185a6..ed56e328f7 100644 --- a/packages/ai/src/react/ai/AIPlugin.ts +++ b/packages/ai/src/react/ai/AIPlugin.ts @@ -59,7 +59,7 @@ export type AIPluginConfig = ExtendConfig< lastGenerate: string | null; lastPrompt: string | null; lastWorkPath: Path | null; - menuType: 'selection' | 'space' | null; + menuType: 'cursor' | 'selection' | null; openEditorId: string | null; store: AriakitTypes.MenuStore | null; } & ExposeOptions & @@ -189,7 +189,7 @@ export const AIPlugin = toTPlatePlugin(BaseAIPlugin, { api.ai.show(editor.id, dom, [node, path]); setOptions({ aiState: 'idle', - menuType: 'space', + menuType: 'cursor', }); }, }, From 9bd5cb9a4c9f439a05c5b47ceb79b9800ed00228 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Wed, 2 Oct 2024 17:12:27 +0800 Subject: [PATCH 049/127] docs --- apps/www/content/docs/ai.mdx | 74 +++++++++++++++++++ .../default/example/playground-demo.tsx | 1 - 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/apps/www/content/docs/ai.mdx b/apps/www/content/docs/ai.mdx index 2352c2ab5a..494c445d85 100644 --- a/apps/www/content/docs/ai.mdx +++ b/apps/www/content/docs/ai.mdx @@ -382,17 +382,91 @@ such as the AI menu or math equation plugins. * `fetchStream` - The function to fetch the stream from your backend.Introdue in the [Integrate with your backend](#integrate-with-your-backend) section. +## Plus +In the [Potion template](https://potion.platejs.org/), we have meticulously configured all settings for the AI plugin, providing you with: + +- A comprehensive, full-stack AI integration +- Seamless handling of editor focus issues mentioned earlier +- Optimized configuration for peak performance +- Context menu to open the ai menu +- Slash command to open the ai menu +- debounce mode copilot + +This template serves as an excellent starting point for your AI-enhanced editing projects, +allowing you to leverage advanced features without the need for extensive setup. + +

+ ); diff --git a/apps/www/src/registry/default/example/pro-iframe-demo.tsx b/apps/www/src/registry/default/example/pro-iframe-demo.tsx index 128a59f274..16379929c1 100644 --- a/apps/www/src/registry/default/example/pro-iframe-demo.tsx +++ b/apps/www/src/registry/default/example/pro-iframe-demo.tsx @@ -11,7 +11,7 @@ export default function ProIframeDemo({ ); } diff --git a/apps/www/src/registry/default/example/mode-toggle.tsx b/apps/www/src/registry/default/example/mode-toggle.tsx index 894f734da9..fbdd5b9f1e 100644 --- a/apps/www/src/registry/default/example/mode-toggle.tsx +++ b/apps/www/src/registry/default/example/mode-toggle.tsx @@ -5,11 +5,14 @@ import * as React from 'react'; import { useTheme } from 'next-themes'; import { Icons } from '@/components/icons'; +import { useMounted } from '@/registry/default/hooks/use-mounted'; import { Button } from '@/registry/default/plate-ui/button'; export default function ModeToggle() { const { setTheme, theme } = useTheme(); + const mounted = useMounted(); + return (