diff --git a/frontend/lib/components/TextEditor/TextEditor.module.scss b/frontend/lib/components/TextEditor/TextEditor.module.scss index 8c87ecbcaa5b..c329e80f00e6 100644 --- a/frontend/lib/components/TextEditor/TextEditor.module.scss +++ b/frontend/lib/components/TextEditor/TextEditor.module.scss @@ -18,20 +18,41 @@ transition: transform 0.2s ease-out, opacity 0.2s ease-out; &:focus-within, - &:hover { + &:hover, + &.active { transform: translateY(0%); opacity: 1; } } +.bubble_menu { + display: flex; + align-items: center; + justify-content: center; + gap: Spacings.$spacing03; + padding: Spacings.$spacing03; +} + .editor_wrapper { min-height: 100vh; + .bubble_menu { + background-color: var(--background-3); + border: solid 1px var(--border-0); + border-radius: Radius.$normal; + } + .content_wrapper { color: var(--text-3); width: 100%; position: relative; + .ai_highlight { + color: white; + background-color: var(--primary-0); + padding: 2px 0 3px 0; + } + .has_focus { position: relative; diff --git a/frontend/lib/components/TextEditor/TextEditor.tsx b/frontend/lib/components/TextEditor/TextEditor.tsx index 0cc9e235b1b1..779381d42c9d 100644 --- a/frontend/lib/components/TextEditor/TextEditor.tsx +++ b/frontend/lib/components/TextEditor/TextEditor.tsx @@ -2,15 +2,18 @@ import { Editor, Extension } from "@tiptap/core"; import Focus from "@tiptap/extension-focus"; import { Link } from "@tiptap/extension-link"; -import { EditorContent, useEditor } from "@tiptap/react"; +import { BubbleMenu, EditorContent, useEditor } from "@tiptap/react"; import { StarterKit } from "@tiptap/starter-kit"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useBrainMention } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/Editor/hooks/useBrainMention"; import styles from "./TextEditor.module.scss"; import { TextEditorSearchBar } from "./components/TextEditorSearchBar/TextEditorSearchBar"; import { Toolbar } from "./components/Toolbar/Toolbar"; +import { AIHighlight } from "./extensions/AIHighlight"; + +import { QuivrButton } from "../ui/QuivrButton/QuivrButton"; const defaultContent = `

My Note

@@ -25,13 +28,28 @@ const defaultContent = ` export const TextEditor = (): JSX.Element => { const { BrainMention, items } = useBrainMention(); + const [searchBarOpen, setSearchBarOpen] = useState(true); const searchEditorRef = useRef(null); const FocusSearchBar = Extension.create().extend({ addKeyboardShortcuts: () => { return { - "Mod-f": () => { - searchEditorRef.current?.commands.focus(); + "Mod-f": ({ editor }) => { + const selection = editor.state.doc.textBetween( + editor.state.selection.from, + editor.state.selection.to + ); + + if (selection) { + editor.commands.setSelectionHighlight(); + } + + setSearchBarOpen(true); + searchEditorRef.current + ?.chain() + .focus() + .setContent(selection, undefined, { preserveWhitespace: true }) + .run(); return true; }, @@ -52,6 +70,11 @@ export const TextEditor = (): JSX.Element => { }), BrainMention, FocusSearchBar, + AIHighlight.configure({ + HTMLAttributes: { + class: styles.ai_highlight, + }, + }), ], content: defaultContent, immediatelyRender: false, @@ -59,6 +82,12 @@ export const TextEditor = (): JSX.Element => { }, [items.length] ); + + const toggleSearchBar = () => { + setSearchBarOpen(!searchBarOpen); + (searchBarOpen ? editor : searchEditorRef.current)?.commands.focus(); + }; + if (!editor) { return <>; } @@ -66,10 +95,49 @@ export const TextEditor = (): JSX.Element => { return (
- + +
+ { + return editor.isActive(AIHighlight.name, { type: "ai" }); + }} + tippyOptions={{ + moveTransition: "transform 0.1s", + placement: "bottom-start", + }} + className={styles.bubble_menu} + editor={editor} + > + { + editor + .chain() + .extendMarkRange(AIHighlight.name) + .unsetHighlight() + .run(); + }} + label="Accept" + color="primary" + iconName="check" + /> + { + editor.commands.undo(); + }} + label="Decline" + color="dangerous" + iconName="close" + /> + +
+
-
+
diff --git a/frontend/lib/components/TextEditor/components/TextEditorSearchBar/TextEditorSearchBar.tsx b/frontend/lib/components/TextEditor/components/TextEditorSearchBar/TextEditorSearchBar.tsx index 5a5392674880..9632ae757de2 100644 --- a/frontend/lib/components/TextEditor/components/TextEditorSearchBar/TextEditorSearchBar.tsx +++ b/frontend/lib/components/TextEditor/components/TextEditorSearchBar/TextEditorSearchBar.tsx @@ -25,7 +25,13 @@ const TextEditorSearchBar = forwardRef( if (chatInput.generatingAnswer || messages.length <= 0) { return; } - editor.commands.insertContent(messages[0].assistant); + + editor + .chain() + .setAiHighlight() + .insertContent(messages[0].assistant) + .focus() + .run(); }, [messages.length, router, editor, chatInput.generatingAnswer]); useEffect(() => { diff --git a/frontend/lib/components/TextEditor/components/Toolbar/Toolbar.tsx b/frontend/lib/components/TextEditor/components/Toolbar/Toolbar.tsx index 83b9d3840cf3..b91d1b7a061e 100644 --- a/frontend/lib/components/TextEditor/components/Toolbar/Toolbar.tsx +++ b/frontend/lib/components/TextEditor/components/Toolbar/Toolbar.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import Button from "@/lib/components/ui/Button"; import { Modal } from "@/lib/components/ui/Modal/Modal"; +import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton"; import { TextInput } from "@/lib/components/ui/TextInput/TextInput"; import styles from "./Toolbar.module.scss"; @@ -22,16 +23,15 @@ export const ToolbarSectionSeparator = (): JSX.Element => { type ToolbarProps = { editor: Editor; - searchBarEditor: Editor | null; + toggleSearchBar: () => void; }; export const Toolbar = ({ editor, - searchBarEditor, + toggleSearchBar, }: ToolbarProps): JSX.Element => { const [linkModalOpen, setLinkModalOpen] = useState(false); const [urlInp, setUrlInp] = useState(""); - const [searchBarEditorOpen, setSearchBarEditorOpen] = useState(false); const setLink = () => { const editorChain = editor.chain().extendMarkRange("link").focus(); @@ -49,16 +49,6 @@ export const Toolbar = ({ setLinkModalOpen(true); }; - const toggleSearchBarEditor = () => { - if (searchBarEditorOpen) { - setSearchBarEditorOpen(false); - editor.commands.focus(); - } else { - setSearchBarEditorOpen(true); - searchBarEditor?.commands.focus(); - } - }; - return (
- +
+ +
); }; diff --git a/frontend/lib/components/TextEditor/extensions/AIHighlight.ts b/frontend/lib/components/TextEditor/extensions/AIHighlight.ts new file mode 100644 index 000000000000..2899539289a4 --- /dev/null +++ b/frontend/lib/components/TextEditor/extensions/AIHighlight.ts @@ -0,0 +1,60 @@ +import { Command } from "@tiptap/core"; +import { Highlight } from "@tiptap/extension-highlight"; + +import { AIHighlightOptions } from "./types"; + +const unsetSelectionsInDocument: Command = ({ dispatch, state }) => { + const { tr } = state; + if (!dispatch) { + return false; + } + + state.doc.descendants((node, pos) => { + if (node.isText) { + node.marks.map((mark) => { + if ( + mark.type.name === AIHighlight.name && + mark.attrs["type"] === "selection" + ) { + tr.removeMark(pos, pos + node.nodeSize, mark.type); + } + }); + } + }); + + dispatch(tr); + + return true; +}; + +export const AIHighlight = Highlight.extend({ + name: "aiHighlight", + addAttributes() { + return { + ...this.parent?.(), + type: "selection", + }; + }, + addCommands() { + return { + ...this.parent?.(), + unsetSelectionsInDocument: () => unsetSelectionsInDocument, + setSelectionHighlight: () => { + return ({ commands }) => { + return commands.setHighlight({ type: "selection" }); + }; + }, + setAiHighlight: () => { + return ({ commands }) => { + return commands.setHighlight({ type: "ai" }); + }; + }, + }; + }, + onSelectionUpdate() { + this.editor.commands.unsetSelectionsInDocument(); + }, +}).configure({ + multicolor: true, + type: "selection", +}); diff --git a/frontend/lib/components/TextEditor/extensions/types.d.ts b/frontend/lib/components/TextEditor/extensions/types.d.ts new file mode 100644 index 000000000000..ebd23c6822fb --- /dev/null +++ b/frontend/lib/components/TextEditor/extensions/types.d.ts @@ -0,0 +1,35 @@ +import { HighlightOptions } from "@tiptap/extension-highlight"; + +export type AIHighlightType = "ai" | "selection"; + +export type AIHighlightOptions = HighlightOptions & { + type: AIHighlightType; +}; + +declare module "@tiptap/core" { + interface Commands { + aiHighlight: { + /** + * Highlights selected text to be sent to context of ai + */ + setSelectionHighlight: () => ReturnType; + /** + * Highlights newly generated text by ai + */ + setAiHighlight: () => ReturnType; + /** + * Remove highlights in selection + */ + unsetSelectionsInDocument: () => ReturnType; + /** + * Set a highlight mark + * @param attributes The highlight attributes + * @example editor.commands.setHighlight({ color: 'red' }) + */ + setHighlight: (attributes?: { + color?: string; + type?: AIHighlightType; + }) => ReturnType; + }; + } +} diff --git a/frontend/package.json b/frontend/package.json index 46af4409926a..71098faca4c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "@tiptap/extension-focus": "^2.6.6", "@tiptap/extension-gapcursor": "^2.6.6", "@tiptap/extension-hard-break": "2.1.12", + "@tiptap/extension-highlight": "^2.7.2", "@tiptap/extension-link": "^2.6.6", "@tiptap/extension-mention": "2.1.12", "@tiptap/extension-paragraph": "2.1.12", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bf1ae5b02618..566c03ce10af 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2530,6 +2530,11 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.6.6.tgz#d68964d1b935457fe3168faf7ee690b1e5384e67" integrity sha512-bgx9vptVFi5yFkIw1OI53J7+xJ71Or3SOe/Q8eSpZv53DlaKpL/TzKw8Z54t1PrI2rJ6H9vrLtkvixJvBZH1Ug== +"@tiptap/extension-highlight@^2.7.2": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.7.2.tgz#e21c2043793b04e93e8c8d3a533c7684477e8703" + integrity sha512-mWlwvhv9kQ9JiGpTS29MXX9UQ90gZ3QgdcZlRANOjwTlh9GOcxCzJ7VW1fLfPgqNvswpbUTwJnlCAP2owKKMFA== + "@tiptap/extension-history@^2.6.6": version "2.6.6" resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.6.6.tgz#8067d7f50ef529c84179eeb946e6f81af8853b90"