Skip to content

Commit

Permalink
feat(chat): add brain selection through mention input
Browse files Browse the repository at this point in the history
  • Loading branch information
mamadoudicko committed Aug 17, 2023
1 parent 85ae06c commit 18305ad
Show file tree
Hide file tree
Showing 29 changed files with 791 additions and 340 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatInput } from "../ChatInput";
import { ChatInput } from "./components";

export const ActionsBar = (): JSX.Element => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { CustomComponentMentionEditor } from "./components/MentionInput/MentionInput";

export const ChatBar = (): JSX.Element => {
return <CustomComponentMentionEditor />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ import { MdRemoveCircleOutline } from "react-icons/md";
type MentionItemProps = {
text: string;
onRemove: () => void;
prefix?: string;
};

export const MentionItem = ({
export const BrainMentionItem = ({
text,
prefix = "",
onRemove,
}: MentionItemProps): JSX.Element => {
return (
<div className="relative">
<div className="relative inline-block w-fit-content">
<div className="flex items-center bg-gray-300 mr-2 text-gray-600 rounded-2xl py-1 px-2">
<span className="flex-grow">{`${prefix}${text}`}</span>
<span className="flex-grow">{text}</span>
<MdRemoveCircleOutline
className="cursor-pointer absolute top-[-10px] right-[5px]"
onClick={onRemove}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Editor from "@draft-js-plugins/editor";
import { Popover } from "@draft-js-plugins/mention";
import { ReactElement } from "react";
import { useTranslation } from "react-i18next";

import "draft-js/dist/Draft.css";
import { AddNewBrainButton } from "./components/AddNewBrainButton";
import { useMentionInput } from "./hooks/useMentionInput";

export const CustomComponentMentionEditor = (): ReactElement => {
const {
mentionInputRef,
MentionSuggestions,
editorState,
onOpenChange,
onSearchChange,
open,
plugins,
setEditorState,
suggestions,
onAddMention,
} = useMentionInput();
const { t } = useTranslation(["chat"]);

return (
<div
className="w-full"
onClick={() => {
mentionInputRef.current?.focus();
}}
>
<Editor
editorKey={"editor"}
editorState={editorState}
onChange={setEditorState}
plugins={plugins}
ref={mentionInputRef}
placeholder={t("actions_bar_placeholder")}
/>
<MentionSuggestions
open={open}
onOpenChange={onOpenChange}
suggestions={suggestions}
onSearchChange={onSearchChange}
popoverContainer={({ children, ...otherProps }) => (
<Popover {...otherProps}>
<div className="z-50 bg-white dark:bg-black border border-black/10 dark:border-white/25 rounded-md shadow-md overflow-y-auto">
{children}
<AddNewBrainButton />
</div>
</Popover>
)}
onAddMention={onAddMention}
entryComponent={({ mention, ...otherProps }) => (
<p
{...otherProps}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
>
{mention.name}
</p>
)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Link from "next/link";

import Button from "@/lib/components/ui/Button";

export const AddNewBrainButton = (): JSX.Element => (
<Link href={"/brains-management"}>
<Button variant={"tertiary"}>Add new brain</Button>
</Link>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./AddNewBrainButton";
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable max-lines */
import Editor from "@draft-js-plugins/editor";
import createMentionPlugin, {
defaultSuggestionsFilter,
MentionData,
} from "@draft-js-plugins/mention";
import { UUID } from "crypto";
import { EditorState, Modifier } from "draft-js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";

import { BrainMentionsList } from "../../../types";
import { BrainMentionItem } from "../../BrainMentionItem";
import { mapToMentionData } from "../utils/mapToMentionData";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMentionInput = () => {
const { allBrains } = useBrainContext();

const [selectedBrainAddedOnload, setSelectedBrainAddedOnload] =
useState(false);

const { setCurrentBrainId, currentBrainId } = useBrainContext();
const [editorState, setEditorState] = useState(() =>
EditorState.createEmpty()
);

const [open, setOpen] = useState(false);

const [mentionItems, setMentionItems] = useState<BrainMentionsList>({
"@": allBrains.map((brain) => ({ ...brain, value: brain.name })),
});

const [suggestions, setSuggestions] = useState(
mapToMentionData(mentionItems["@"])
);

const mentionInputRef = useRef<Editor>(null);

const onAddMention = (mention: MentionData) => {
setCurrentBrainId(mention.id as UUID);
};

const removeMention = (entityKeyToRemove: string): void => {
const contentState = editorState.getCurrentContent();
const entity = contentState.getEntity(entityKeyToRemove);

if (entity.getType() === "mention") {
const newContentState = contentState.replaceEntityData(
entityKeyToRemove,
{}
);

const newEditorState = EditorState.push(
editorState,
newContentState,
"apply-entity"
);

setEditorState(newEditorState);
setCurrentBrainId(null);
}
};

const { MentionSuggestions, plugins } = useMemo(() => {
const mentionPlugin = createMentionPlugin({
mentionComponent: ({ entityKey, mention: { name } }) => (
<BrainMentionItem
text={name + "" + entityKey}
onRemove={() => removeMention(entityKey)}
/>
),

popperOptions: {
placement: "top-end",
},
});
const { MentionSuggestions: coreMentionSuggestions } = mentionPlugin;
const corePlugins = [mentionPlugin];

return { plugins: corePlugins, MentionSuggestions: coreMentionSuggestions };
}, []);

const onOpenChange = useCallback((_open: boolean) => {
setOpen(_open);
}, []);

const onSearchChange = useCallback(
({ trigger, value }: { trigger: string; value: string }) => {
setSuggestions(
defaultSuggestionsFilter(
value,
currentBrainId !== null ? [] : mapToMentionData(mentionItems["@"]),
trigger
)
);
},
[mentionItems, currentBrainId]
);

useEffect(() => {
setSuggestions(mapToMentionData(mentionItems["@"]));
}, [mentionItems]);

useEffect(() => {
setMentionItems({
...mentionItems,
"@": [
...allBrains.map((brain) => ({
...brain,
value: brain.name,
})),
],
});
}, [allBrains]);
useEffect(() => {
if (selectedBrainAddedOnload) {
return;
}

if (currentBrainId === null || mentionItems["@"].length === 0) {
return;
}

const mention = mentionItems["@"].find(
(item) => item.id === currentBrainId
);

if (mention !== undefined) {
const mentionText = `@${mention.name}`;
const mentionWithSpace = `${mentionText} `; // Add white space after the mention

// Check if the mention with white space is already in the editor's content
const contentState = editorState.getCurrentContent();
const plainText = contentState.getPlainText();

if (plainText.includes(mentionWithSpace)) {
return; // Mention with white space already in content, no need to add it again
}

const stateWithEntity = contentState.createEntity(
"mention",
"IMMUTABLE",
{
mention,
}
);
const entityKey = stateWithEntity.getLastCreatedEntityKey();

const selectionState = editorState.getSelection();
const updatedContentState = Modifier.insertText(
contentState,
selectionState,
mentionWithSpace,
undefined,
entityKey
);

// Calculate the new selection position after inserting the mention with white space
const newSelection = selectionState.merge({
anchorOffset: selectionState.getStartOffset() + mentionWithSpace.length,
focusOffset: selectionState.getStartOffset() + mentionWithSpace.length,
});

const newEditorState = EditorState.forceSelection(
EditorState.push(editorState, updatedContentState, "insert-characters"),
newSelection
);

setEditorState(newEditorState);
}
setSelectedBrainAddedOnload(true);
}, [currentBrainId, mentionItems]);

return {
mentionInputRef,
editorState,
setEditorState,
plugins,
MentionSuggestions,
onOpenChange,
onSearchChange,
open,
suggestions,
onAddMention,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./MentionInput";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MentionData } from "@draft-js-plugins/mention";

import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";

export const mapToMentionData = (
brains: MinimalBrainForUser[]
): MentionData[] =>
brains.map((brain) => ({
name: brain.name,
id: brain.id as string,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./MentionInput";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
import { useFeature } from "@growthbook/growthbook-react";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useChatBar = () => {
const shouldUseNewUX = useFeature("new-ux").on;

return {
shouldUseNewUX,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ChatBar";
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";

export type BrainMentionType = MinimalBrainForUser & { value: string };

export type Trigger = "@" | "#";

export type BrainMentionsList = {
"@": MinimalBrainForUser[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ChatBar";
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";

import { useChat } from "../../../hooks/useChat";
import { useChat } from "../../../../../hooks/useChat";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useChatInput = () => {
Expand Down
Loading

0 comments on commit 18305ad

Please sign in to comment.