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"