Skip to content

Commit

Permalink
feat(notetaker): accept decline notetaker ai generation (#3389)
Browse files Browse the repository at this point in the history
# Description

- Ability to accept or decline the ai generated content in the
notetaker.
- Notetaker Search bar is now visible by default and can be toggled via
`Ctrl+F` or the `Ask Brain` Button.
- Ability to select content and ask questions with respect to the
selection.

## Checklist before requesting a review

Please delete options that are not relevant.

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):

![image](https://github.com/user-attachments/assets/7353f27f-52ed-45f8-95e3-db628f7627c5)

![image](https://github.com/user-attachments/assets/49e6ecf0-a2f8-4614-98ad-e6960d2fc391)

---------

Co-authored-by: Antoine Dewez <[email protected]>
Co-authored-by: Stan Girard <[email protected]>
Co-authored-by: chloedia <[email protected]>
Co-authored-by: aminediro <[email protected]>
Co-authored-by: Chloé Daems <[email protected]>
Co-authored-by: Zewed <[email protected]>
Co-authored-by: Stan Girard <[email protected]>
Co-authored-by: aminediro <[email protected]>
Co-authored-by: porter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
Co-authored-by: Jacopo Chevallard <[email protected]>
  • Loading branch information
11 people authored Oct 21, 2024
1 parent cec7a0d commit 08b6bcf
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 28 deletions.
23 changes: 22 additions & 1 deletion frontend/lib/components/TextEditor/TextEditor.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
80 changes: 74 additions & 6 deletions frontend/lib/components/TextEditor/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<h1>My Note</h1>
Expand All @@ -25,13 +28,28 @@ const defaultContent = `

export const TextEditor = (): JSX.Element => {
const { BrainMention, items } = useBrainMention();
const [searchBarOpen, setSearchBarOpen] = useState(true);
const searchEditorRef = useRef<Editor>(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;
},
Expand All @@ -52,24 +70,74 @@ export const TextEditor = (): JSX.Element => {
}),
BrainMention,
FocusSearchBar,
AIHighlight.configure({
HTMLAttributes: {
class: styles.ai_highlight,
},
}),
],
content: defaultContent,
immediatelyRender: false,
autofocus: true,
},
[items.length]
);

const toggleSearchBar = () => {
setSearchBarOpen(!searchBarOpen);
(searchBarOpen ? editor : searchEditorRef.current)?.commands.focus();
};

if (!editor) {
return <></>;
}

return (
<div className={styles.main_container}>
<div className={styles.editor_wrapper}>
<Toolbar searchBarEditor={searchEditorRef.current} editor={editor} />
<Toolbar toggleSearchBar={toggleSearchBar} editor={editor} />
<div>
<BubbleMenu
shouldShow={() => {
return editor.isActive(AIHighlight.name, { type: "ai" });
}}
tippyOptions={{
moveTransition: "transform 0.1s",
placement: "bottom-start",
}}
className={styles.bubble_menu}
editor={editor}
>
<QuivrButton
onClick={() => {
editor
.chain()
.extendMarkRange(AIHighlight.name)
.unsetHighlight()
.run();
}}
label="Accept"
color="primary"
iconName="check"
/>
<QuivrButton
onClick={() => {
editor.commands.undo();
}}
label="Decline"
color="dangerous"
iconName="close"
/>
</BubbleMenu>
</div>

<EditorContent className={styles.content_wrapper} editor={editor} />
</div>
<div className={styles.search_bar_wrapper}>
<div
className={`${styles.search_bar_wrapper} ${
searchBarOpen ? styles.active : ""
}`}
>
<TextEditorSearchBar ref={searchEditorRef} editor={editor} />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ const TextEditorSearchBar = forwardRef<Editor, TextEditorSearchBarProps>(
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(() => {
Expand Down
31 changes: 11 additions & 20 deletions frontend/lib/components/TextEditor/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -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 (
<div className={styles.toolbar}>
<ToolbarButton
Expand Down Expand Up @@ -187,13 +177,14 @@ export const Toolbar = ({
}
/>

<Button
className={styles.focusSearchBarBtn}
onClick={toggleSearchBarEditor}
variant={"primary"}
>
Ask Brain
</Button>
<div className={styles.focusSearchBarBtn}>
<QuivrButton
onClick={toggleSearchBar}
label="Ask Brain"
color="primary"
iconName={"chat"}
/>
</div>
</div>
);
};
60 changes: 60 additions & 0 deletions frontend/lib/components/TextEditor/extensions/AIHighlight.ts
Original file line number Diff line number Diff line change
@@ -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<AIHighlightOptions>({
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",
});
35 changes: 35 additions & 0 deletions frontend/lib/components/TextEditor/extensions/types.d.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType> {
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;
};
}
}
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 08b6bcf

Please sign in to comment.