diff --git a/packages/editor/src/ce/extensions/ai-features/handle.ts b/packages/editor/src/ce/extensions/ai-features/handle.ts deleted file mode 100644 index d477d228ac9..00000000000 --- a/packages/editor/src/ce/extensions/ai-features/handle.ts +++ /dev/null @@ -1,13 +0,0 @@ -// extensions -import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => { - const view = () => {}; - const domEvents = {}; - - return { - view, - domEvents, - }; -}; diff --git a/packages/editor/src/ce/extensions/ai-features/index.ts b/packages/editor/src/ce/extensions/ai-features/index.ts deleted file mode 100644 index af0faafca65..00000000000 --- a/packages/editor/src/ce/extensions/ai-features/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./handle"; diff --git a/packages/editor/src/ce/extensions/index.ts b/packages/editor/src/ce/extensions/index.ts index 172d9ee1a76..4a975b8c5a1 100644 --- a/packages/editor/src/ce/extensions/index.ts +++ b/packages/editor/src/ce/extensions/index.ts @@ -1,2 +1 @@ -export * from "./ai-features"; export * from "./document-extensions"; diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx index 968bc4572ab..fcb5d477b98 100644 --- a/packages/editor/src/core/components/editors/document/editor.tsx +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -14,12 +14,14 @@ import { EditorRefApi, IMentionHighlight, IMentionSuggestion, + TAIHandler, TDisplayConfig, TExtensions, TFileHandler, } from "@/types"; interface IDocumentEditor { + aiHandler?: TAIHandler; containerClassName?: string; disabledExtensions?: TExtensions[]; displayConfig?: TDisplayConfig; @@ -41,6 +43,7 @@ interface IDocumentEditor { const DocumentEditor = (props: IDocumentEditor) => { const { + aiHandler, containerClassName, disabledExtensions, displayConfig = DEFAULT_DISPLAY_CONFIG, @@ -84,6 +87,7 @@ const DocumentEditor = (props: IDocumentEditor) => { return ( { - const { displayConfig, editor, editorContainerClassName, id, tabIndex } = props; + const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props; // states const [linkViewProps, setLinkViewProps] = useState(); const [isOpen, setIsOpen] = useState(false); @@ -138,7 +139,12 @@ export const PageRenderer = (props: IPageRenderer) => { id={id} > - {editor.isEditable && } + {editor.isEditable && ( + <> + + + + )} {isOpen && linkViewProps && coordinates && ( diff --git a/packages/editor/src/core/components/menus/ai-menu.tsx b/packages/editor/src/core/components/menus/ai-menu.tsx new file mode 100644 index 00000000000..8a714a6552d --- /dev/null +++ b/packages/editor/src/core/components/menus/ai-menu.tsx @@ -0,0 +1,95 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import tippy, { Instance } from "tippy.js"; +// helpers +import { cn } from "@/helpers/common"; +// types +import { TAIHandler } from "@/types"; + +type Props = { + menu: TAIHandler["menu"]; +}; + +export const AIFeaturesMenu: React.FC = (props) => { + const { menu } = props; + // states + const [isPopupVisible, setIsPopupVisible] = useState(false); + // refs + const menuRef = useRef(null); + const popup = useRef(null); + + useEffect(() => { + if (!menuRef.current) return; + + menuRef.current.remove(); + menuRef.current.style.visibility = "visible"; + + // @ts-expect-error - tippy types are incorrect + popup.current = tippy(document.body, { + getReferenceClientRect: null, + content: menuRef.current, + appendTo: () => document.querySelector(".frame-renderer"), + trigger: "manual", + interactive: true, + arrow: false, + placement: "bottom-start", + animation: "shift-away", + hideOnClick: true, + onShown: () => menuRef.current?.focus(), + }); + + return () => { + popup.current?.destroy(); + popup.current = null; + }; + }, []); + + const hidePopup = useCallback(() => { + popup.current?.hide(); + setIsPopupVisible(false); + }, []); + + useEffect(() => { + const handleClickAIHandle = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target.matches("#ai-handle") || menuRef.current?.contains(e.target as Node)) { + e.preventDefault(); + + if (!isPopupVisible) { + popup.current?.setProps({ + getReferenceClientRect: () => target.getBoundingClientRect(), + }); + popup.current?.show(); + setIsPopupVisible(true); + } + return; + } + + hidePopup(); + return; + }; + + document.addEventListener("click", handleClickAIHandle); + document.addEventListener("contextmenu", handleClickAIHandle); + document.addEventListener("keydown", hidePopup); + + return () => { + document.removeEventListener("click", handleClickAIHandle); + document.removeEventListener("contextmenu", handleClickAIHandle); + document.removeEventListener("keydown", hidePopup); + }; + }, [hidePopup, isPopupVisible]); + + return ( +
+
+ {menu?.({ + onClose: hidePopup, + })} +
+
+ ); +}; diff --git a/packages/editor/src/core/components/menus/index.ts b/packages/editor/src/core/components/menus/index.ts index 0c6964ea673..da050b6831e 100644 --- a/packages/editor/src/core/components/menus/index.ts +++ b/packages/editor/src/core/components/menus/index.ts @@ -1,3 +1,4 @@ export * from "./bubble-menu"; +export * from "./ai-menu"; export * from "./block-menu"; export * from "./menu-items"; diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index 5a26222cd6f..ccc13ed5eb0 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -1,8 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; -// plane editor extensions -import { AIHandlePlugin } from "@/plane-editor/extensions"; +// plugins +import { AIHandlePlugin } from "@/plugins/ai-handle"; import { DragHandlePlugin } from "@/plugins/drag-handle"; type Props = { @@ -105,7 +105,7 @@ const SideMenu = (options: SideMenuPluginProps) => { const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden"); // side menu elements const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options); - const { view: aiHandleView } = AIHandlePlugin(options); + const { view: aiHandleView, domEvents: aiHandleDOMEvents } = AIHandlePlugin(options); return new Plugin({ key: new PluginKey("sideMenu"), @@ -113,12 +113,12 @@ const SideMenu = (options: SideMenuPluginProps) => { hideSideMenu(); view?.dom.parentElement?.appendChild(editorSideMenu); // side menu elements' initialization - if (handlesConfig.dragDrop) { - dragHandleView(view, editorSideMenu); - } if (handlesConfig.ai) { aiHandleView(view, editorSideMenu); } + if (handlesConfig.dragDrop) { + dragHandleView(view, editorSideMenu); + } return { destroy: () => hideSideMenu(), @@ -175,7 +175,12 @@ const SideMenu = (options: SideMenuPluginProps) => { editorSideMenu.style.left = `${rect.left - rect.width}px`; editorSideMenu.style.top = `${rect.top}px`; showSideMenu(); - dragHandleDOMEvents?.mousemove(); + if (handlesConfig.dragDrop) { + dragHandleDOMEvents?.mousemove(); + } + if (handlesConfig.ai) { + aiHandleDOMEvents?.mousemove?.(); + } }, keydown: () => hideSideMenu(), mousewheel: () => hideSideMenu(), diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 01718fbe73a..f9e8fdd609f 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -1,4 +1,5 @@ import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react"; +import { DOMSerializer } from "@tiptap/pm/model"; import { Selection } from "@tiptap/pm/state"; import { EditorProps } from "@tiptap/pm/view"; import { useEditor as useTiptapEditor, Editor } from "@tiptap/react"; @@ -213,6 +214,41 @@ export const useEditor = (props: CustomEditorProps) => { console.error("An error occurred while setting focus at position:", error); } }, + getSelectedText: () => { + if (!editorRef.current) return null; + + const { state } = editorRef.current; + const { from, to, empty } = state.selection; + + if (empty) return null; + + const nodesArray: string[] = []; + state.doc.nodesBetween(from, to, (node, pos, parent) => { + if (parent === state.doc && editorRef.current) { + const serializer = DOMSerializer.fromSchema(editorRef.current?.schema); + const dom = serializer.serializeNode(node); + const tempDiv = document.createElement("div"); + tempDiv.appendChild(dom); + nodesArray.push(tempDiv.innerHTML); + } + }); + const selection = nodesArray.join(""); + console.log(selection); + return selection; + }, + insertText: (contentHTML, insertOnNextLine) => { + if (!editor) return; + // get selection + const { from, to, empty } = editor.state.selection; + if (empty) return; + if (insertOnNextLine) { + // move cursor to the end of the selection and insert a new line + editor.chain().focus().setTextSelection(to).insertContent("
").insertContent(contentHTML).run(); + } else { + // replace selected text with the content provided + editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); + } + }, }), [editorRef, savedSelection, fileHandler.upload] ); diff --git a/packages/editor/src/core/plugins/ai-handle.ts b/packages/editor/src/core/plugins/ai-handle.ts new file mode 100644 index 00000000000..8af93ed1dd1 --- /dev/null +++ b/packages/editor/src/core/plugins/ai-handle.ts @@ -0,0 +1,153 @@ +import { NodeSelection } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; +// extensions +import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; + +const sparklesIcon = + ''; + +const nodeDOMAtCoords = (coords: { x: number; y: number }) => { + const elements = document.elementsFromPoint(coords.x, coords.y); + const generalSelectors = [ + "li", + "p:not(:first-child)", + ".code-block", + "blockquote", + "img", + "h1, h2, h3, h4, h5, h6", + "[data-type=horizontalRule]", + ".table-wrapper", + ".issue-embed", + ].join(", "); + + for (const elem of elements) { + if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { + return elem; + } + + // if the element is a

tag that is the first child of a td or th + if ( + (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && + elem?.textContent?.trim() !== "" + ) { + return elem; // Return only if p tag is not empty in td or th + } + + // apply general selector + if (elem.matches(generalSelectors)) { + return elem; + } + } + return null; +}; + +const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => { + const boundingRect = node.getBoundingClientRect(); + + return view.posAtCoords({ + left: boundingRect.left + 50 + options.dragHandleWidth, + top: boundingRect.top + 1, + })?.inside; +}; + +const nodePosAtDOMForBlockQuotes = (node: Element, view: EditorView) => { + const boundingRect = node.getBoundingClientRect(); + + return view.posAtCoords({ + left: boundingRect.left + 1, + top: boundingRect.top + 1, + })?.inside; +}; + +const calcNodePos = (pos: number, view: EditorView, node: Element) => { + const maxPos = view.state.doc.content.size; + const safePos = Math.max(0, Math.min(pos, maxPos)); + const $pos = view.state.doc.resolve(safePos); + + if ($pos.depth > 1) { + if (node.matches("ul li, ol li")) { + // only for nested lists + const newPos = $pos.before($pos.depth); + return Math.max(0, Math.min(newPos, maxPos)); + } + } + + return safePos; +}; + +export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => { + let aiHandleElement: HTMLButtonElement | null = null; + + const handleClick = (event: MouseEvent, view: EditorView) => { + view.focus(); + + const node = nodeDOMAtCoords({ + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY, + }); + + if (!(node instanceof Element)) return; + + if (node.matches("blockquote")) { + let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view); + if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return; + + const docSize = view.state.doc.content.size; + nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize)); + + if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) { + // TODO FIX ERROR + const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes); + view.dispatch(view.state.tr.setSelection(nodeSelection)); + } + return; + } + + let nodePos = nodePosAtDOM(node, view, options); + + if (nodePos === null || nodePos === undefined) return; + + // Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied + nodePos = calcNodePos(nodePos, view, node); + + // TODO FIX ERROR + // Use NodeSelection to select the node at the calculated position + const nodeSelection = NodeSelection.create(view.state.doc, nodePos); + + // Dispatch the transaction to update the selection + view.dispatch(view.state.tr.setSelection(nodeSelection)); + }; + + const view = (view: EditorView, sideMenu: HTMLDivElement | null) => { + // create handle element + const className = + "grid place-items-center font-medium size-5 aspect-square text-xs text-custom-text-300 hover:bg-custom-background-80 rounded-sm opacity-100 !outline-none z-[5] transition-[background-color,_opacity] duration-200 ease-linear"; + aiHandleElement = document.createElement("button"); + aiHandleElement.type = "button"; + aiHandleElement.id = "ai-handle"; + aiHandleElement.classList.value = className; + const iconElement = document.createElement("span"); + iconElement.classList.value = "pointer-events-none"; + iconElement.innerHTML = sparklesIcon; + aiHandleElement.appendChild(iconElement); + // bind events + aiHandleElement.addEventListener("click", (e) => handleClick(e, view)); + + sideMenu?.appendChild(aiHandleElement); + + return { + // destroy the handle element on un-initialize + destroy: () => { + aiHandleElement?.remove(); + aiHandleElement = null; + }, + }; + }; + + const domEvents = {}; + + return { + view, + domEvents, + }; +}; diff --git a/packages/editor/src/core/types/ai.ts b/packages/editor/src/core/types/ai.ts new file mode 100644 index 00000000000..f5470f51c0a --- /dev/null +++ b/packages/editor/src/core/types/ai.ts @@ -0,0 +1,7 @@ +type TMenuProps = { + onClose: () => void; +}; + +export type TAIHandler = { + menu?: (props: TMenuProps) => React.ReactNode; +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 25a37f0ca3f..ac804b9b14f 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -20,6 +20,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { isEditorReadyToDiscard: () => boolean; setSynced: () => void; hasUnsyncedChanges: () => boolean; + getSelectedText: () => string | null; + insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; } export interface IEditorProps { diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 7190cfb51e3..d126858be6b 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -1,3 +1,4 @@ +export * from "./ai"; export * from "./config"; export * from "./editor"; export * from "./embed"; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index c29aaac139d..3bea5dcf256 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -3,7 +3,7 @@ position: fixed; display: flex; align-items: center; - opacity: 100; + opacity: 1; transition: opacity 0.2s ease 0.2s, top 0.2s ease, @@ -19,7 +19,7 @@ /* drag handle */ #drag-handle { - opacity: 100; + opacity: 1; &.drag-handle-hidden { opacity: 0; @@ -28,6 +28,17 @@ } /* end drag handle */ +/* ai handle */ +#ai-handle { + opacity: 1; + + &.handle-hidden { + opacity: 0; + pointer-events: none; + } +} +/* end ai handle */ + .ProseMirror:not(.dragging) .ProseMirror-selectednode { position: relative; cursor: grab; diff --git a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx new file mode 100644 index 00000000000..d3440ea4798 --- /dev/null +++ b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx @@ -0,0 +1,103 @@ +import { useState } from "react"; +import { CircleArrowUp, CornerDownRight, RefreshCcw, Sparkles } from "lucide-react"; +// ui +import { Tooltip } from "@plane/ui"; +// components +import { RichTextReadOnlyEditor } from "@/components/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + handleInsertText: (insertOnNextLine: boolean) => void; + handleRegenerate: () => Promise; + isRegenerating: boolean; + response: string | undefined; +}; + +export const AskPiMenu: React.FC = (props) => { + const { handleInsertText, handleRegenerate, isRegenerating, response } = props; + // states + const [query, setQuery] = useState(""); + + return ( + <> +

+ + + + {response ? ( +
+ +
+ + + + + + + +
+
+ ) : ( +

Pi is answering...

+ )} +
+
+
+ + + + setQuery(e.target.value)} + placeholder="Tell Pi what to do..." + /> + + + +
+
+ + ); +}; diff --git a/web/ce/components/pages/editor/ai/index.ts b/web/ce/components/pages/editor/ai/index.ts new file mode 100644 index 00000000000..d21eb63d70b --- /dev/null +++ b/web/ce/components/pages/editor/ai/index.ts @@ -0,0 +1,2 @@ +export * from "./ask-pi-menu"; +export * from "./menu"; diff --git a/web/ce/components/pages/editor/ai/menu.tsx b/web/ce/components/pages/editor/ai/menu.tsx new file mode 100644 index 00000000000..7610595f73f --- /dev/null +++ b/web/ce/components/pages/editor/ai/menu.tsx @@ -0,0 +1,290 @@ +"use client"; + +import React, { RefObject, useRef, useState } from "react"; +import { useParams } from "next/navigation"; +import { ChevronRight, CornerDownRight, LucideIcon, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react"; +// plane editor +import { EditorRefApi } from "@plane/editor"; +// plane ui +import { Tooltip } from "@plane/ui"; +// components +import { RichTextReadOnlyEditor } from "@/components/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; +// plane web constants +import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/plane-web/constants/ai"; +// plane web services +import { AIService, TTaskPayload } from "@/services/ai.service"; +import { AskPiMenu } from "./ask-pi-menu"; +const aiService = new AIService(); + +type Props = { + editorRef: RefObject; + onClose: () => void; +}; + +const MENU_ITEMS: { + icon: LucideIcon; + key: AI_EDITOR_TASKS; + label: string; +}[] = [ + { + key: AI_EDITOR_TASKS.ASK_ANYTHING, + icon: Sparkles, + label: "Ask Pi", + }, +]; + +const TONES_LIST = [ + { + key: "default", + label: "Default", + casual_score: 5, + formal_score: 5, + }, + { + key: "professional", + label: "💼 Professional", + casual_score: 0, + formal_score: 10, + }, + { + key: "casual", + label: "😃 Casual", + casual_score: 10, + formal_score: 0, + }, +]; + +export const EditorAIMenu: React.FC = (props) => { + const { editorRef, onClose } = props; + // states + const [activeTask, setActiveTask] = useState(null); + const [response, setResponse] = useState(undefined); + const [isRegenerating, setIsRegenerating] = useState(false); + // refs + const responseContainerRef = useRef(null); + // params + const { workspaceSlug } = useParams(); + const handleGenerateResponse = async (payload: TTaskPayload) => { + if (!workspaceSlug) return; + await aiService.performEditorTask(workspaceSlug.toString(), payload).then((res) => setResponse(res.response)); + }; + // handle task click + const handleClick = async (key: AI_EDITOR_TASKS) => { + const selection = editorRef.current?.getSelectedText(); + if (!selection || activeTask === key) return; + setActiveTask(key); + if (key === AI_EDITOR_TASKS.ASK_ANYTHING) return; + setResponse(undefined); + setIsRegenerating(false); + await handleGenerateResponse({ + task: key, + text_input: selection, + }); + }; + // handle re-generate response + const handleRegenerate = async () => { + const selection = editorRef.current?.getSelectedText(); + if (!selection || !activeTask) return; + setIsRegenerating(true); + await handleGenerateResponse({ + task: activeTask, + text_input: selection, + }) + .then(() => + responseContainerRef.current?.scrollTo({ + top: 0, + behavior: "smooth", + }) + ) + .finally(() => setIsRegenerating(false)); + }; + // handle re-generate response + const handleToneChange = async (key: string) => { + const selectedTone = TONES_LIST.find((t) => t.key === key); + const selection = editorRef.current?.getSelectedText(); + if (!selectedTone || !selection || !activeTask) return; + setResponse(undefined); + setIsRegenerating(false); + await handleGenerateResponse({ + casual_score: selectedTone.casual_score, + formal_score: selectedTone.formal_score, + task: activeTask, + text_input: selection, + }).then(() => + responseContainerRef.current?.scrollTo({ + top: 0, + behavior: "smooth", + }) + ); + }; + // handle replace selected text with the response + const handleInsertText = (insertOnNextLine: boolean) => { + if (!response) return; + editorRef.current?.insertText(response, insertOnNextLine); + onClose(); + }; + + return ( +
+
+
+ {MENU_ITEMS.map((item) => { + const isActiveTask = activeTask === item.key; + + return ( + + ); + })} +
+
+ {activeTask === AI_EDITOR_TASKS.ASK_ANYTHING ? ( + + ) : ( + <> +
+ + + + {response ? ( +
+ +
+ + + + + + + +
+
+ ) : ( +

+ {activeTask ? LOADING_TEXTS[activeTask] : "Pi is writing"}... +

+ )} +
+
+ {TONES_LIST.map((tone) => ( + + ))} +
+ + )} +
+
+ {activeTask && ( +
+ + + +

+ By using this feature, you consent to sharing the message with a 3rd party service. +

+
+ )} +
+ ); +}; diff --git a/web/ce/components/pages/editor/index.ts b/web/ce/components/pages/editor/index.ts index 12b3c5295ba..88b26fa277f 100644 --- a/web/ce/components/pages/editor/index.ts +++ b/web/ce/components/pages/editor/index.ts @@ -1 +1,2 @@ +export * from "./ai"; export * from "./embed"; diff --git a/web/ce/constants/ai.ts b/web/ce/constants/ai.ts new file mode 100644 index 00000000000..c5c1b04fa9a --- /dev/null +++ b/web/ce/constants/ai.ts @@ -0,0 +1,9 @@ +export enum AI_EDITOR_TASKS { + ASK_ANYTHING = "ASK_ANYTHING", +} + +export const LOADING_TEXTS: { + [key in AI_EDITOR_TASKS]: string; +} = { + [AI_EDITOR_TASKS.ASK_ANYTHING]: "Pi is generating response", +}; diff --git a/web/ce/hooks/use-editor-flagging.ts b/web/ce/hooks/use-editor-flagging.ts index 9019db94d95..9077d216e6f 100644 --- a/web/ce/hooks/use-editor-flagging.ts +++ b/web/ce/hooks/use-editor-flagging.ts @@ -3,18 +3,11 @@ import { TExtensions } from "@plane/editor"; /** * @description extensions disabled in various editors - * @returns - * ```ts - * { - * documentEditor: TExtensions[] - * richTextEditor: TExtensions[] - * } - * ``` */ export const useEditorFlagging = (): { documentEditor: TExtensions[]; richTextEditor: TExtensions[]; } => ({ - documentEditor: [], - richTextEditor: [], + documentEditor: ["ai"], + richTextEditor: ["ai"], }); diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 64628622df6..b9b0f232077 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -19,6 +19,8 @@ import { cn } from "@/helpers/common.helper"; // hooks import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; +// plane web components +import { EditorAIMenu } from "@/plane-web/components/pages"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; @@ -155,6 +157,9 @@ export const PageEditorBody: React.FC = observer((props) => { issue: issueEmbedProps, }} disabledExtensions={documentEditor} + aiHandler={{ + menu: ({ onClose }) => , + }} /> ) : ( { + return this.post(`/api/workspaces/${workspaceSlug}/rephrase-grammar/`, data) + .then((res) => res?.data) + .catch((error) => { + throw error?.response?.data; + }); + } }