feat: add prompt trigger through # (#1023)

* feat: add prompt trigger to mention input

* feat: update chat shortcuts

* test: update BrainProviderMock

* feat: improve ux

* feat: update message header position

* feat: improve mention input dx

* fix(MentionInput): fix minor bugs

* feat: refactor <ShareBrain/>

* feat: add brain sharing button

* fix: make popover buttons click working

* feat: update backspace handle logic

* feat: update add new brain button ui
This commit is contained in:
Mamadou DICKO 2023-08-29 10:50:36 +02:00 committed by GitHub
parent 619f5bc007
commit 072d97adb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 426 additions and 393 deletions

View File

@ -7,17 +7,19 @@ import { NavBar } from "@/lib/components/NavBar";
import { TrackingWrapper } from "@/lib/components/TrackingWrapper";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import '../lib/config/LocaleConfig/i18n'
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
import "../lib/config/LocaleConfig/i18n";
// This wrapper is used to make effect calls at a high level in app rendering.
export const App = ({ children }: PropsWithChildren): JSX.Element => {
const { fetchAllBrains, fetchAndSetActiveBrain } = useBrainContext();
const { fetchAllBrains, fetchAndSetActiveBrain, fetchPublicPrompts } =
useBrainContext();
const { session } = useSupabase();
useEffect(() => {
void fetchAllBrains();
void fetchAndSetActiveBrain();
void fetchPublicPrompts();
}, [session?.user]);
return (

View File

@ -1,6 +1,9 @@
"use client";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { MentionInput } from "./components";
import { MentionItem } from "./components/MentionItem";
type ChatBarProps = {
onSubmit: () => void;
@ -13,11 +16,36 @@ export const ChatBar = ({
setMessage,
message,
}: ChatBarProps): JSX.Element => {
const { currentBrain, setCurrentBrainId, currentPrompt, setCurrentPromptId } =
useBrainContext();
return (
<MentionInput
message={message}
setMessage={setMessage}
onSubmit={onSubmit}
/>
<div className="flex flex-row flex-1 w-full item-start">
{currentBrain !== undefined && (
<MentionItem
text={currentBrain.name}
onRemove={() => {
setCurrentBrainId(null);
}}
trigger={"@"}
/>
)}
{currentPrompt !== undefined && (
<MentionItem
text={currentPrompt.title}
onRemove={() => {
setCurrentPromptId(null);
}}
trigger={"#"}
/>
)}
<div className="flex-1">
<MentionInput
message={message}
setMessage={setMessage}
onSubmit={onSubmit}
/>
</div>
</div>
);
};

View File

@ -1,12 +1,16 @@
import Editor from "@draft-js-plugins/editor";
import { ReactElement } from "react";
import { PopoverProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Popover";
import { ComponentType, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import "@draft-js-plugins/mention/lib/plugin.css";
import "draft-js/dist/Draft.css";
import { MentionTriggerType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { BrainSuggestionsContainer } from "./components/BrainSuggestionsContainer";
import { PromptSuggestionsContainer } from "./components/PromptSuggestionsContainer";
import { SuggestionRow } from "./components/SuggestionRow";
import { SuggestionsContainer } from "./components/SuggestionsContainer";
import { useMentionInput } from "./hooks/useMentionInput";
type MentionInputProps = {
@ -14,6 +18,15 @@ type MentionInputProps = {
setMessage: (text: string) => void;
message: string;
};
const triggerToSuggestionsContainer: Record<
MentionTriggerType,
ComponentType<PopoverProps>
> = {
"@": BrainSuggestionsContainer,
"#": PromptSuggestionsContainer,
};
export const MentionInput = ({
onSubmit,
setMessage,
@ -24,13 +37,14 @@ export const MentionInput = ({
MentionSuggestions,
keyBindingFn,
editorState,
onOpenChange,
setOpen,
onSearchChange,
open,
plugins,
suggestions,
onAddMention,
handleEditorChange,
currentTrigger,
} = useMentionInput({
message,
onSubmit,
@ -50,15 +64,24 @@ export const MentionInput = ({
placeholder={t("actions_bar_placeholder")}
keyBindingFn={keyBindingFn}
/>
<MentionSuggestions
open={open}
onOpenChange={onOpenChange}
suggestions={suggestions}
onSearchChange={onSearchChange}
popoverContainer={SuggestionsContainer}
onAddMention={onAddMention}
entryComponent={SuggestionRow}
/>
<div
style={{
// `open` should be directly passed to the MentionSuggestions component.
// However, it is not working as expected since we are not able to click on button in custom suggestion renderer.
// So, we are using this hack to make it work.
visibility: open ? "visible" : "hidden",
}}
>
<MentionSuggestions
open
onOpenChange={setOpen}
suggestions={suggestions}
onSearchChange={onSearchChange}
popoverContainer={triggerToSuggestionsContainer[currentTrigger]}
onAddMention={onAddMention}
entryComponent={SuggestionRow}
/>
</div>
</div>
);
};

View File

@ -1,22 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
export const AddNewBrainButton = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
return (
<Link
href={"/brains-management"}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
>
<Button className="px-5 py-2 text-sm" variant={"tertiary"}>
{t("new_brain")}
</Button>
</Link>
);
};

View File

@ -0,0 +1,17 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { MdAdd } from "react-icons/md";
export const AddNewPromptButton = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
return (
<Link
href={"/brains-management"}
className="flex px-5 py-3 text-sm decoration-none text-center w-full justify-between items-center"
>
{t("new_prompt")}
<MdAdd />
</Link>
);
};

View File

@ -1,23 +1,8 @@
type BrainSuggestionProps = {
content: string;
id: string;
};
export const BrainSuggestion = ({
content,
id,
}: BrainSuggestionProps): JSX.Element => {
//TODO: use this id for ShareBrain component
console.log({ id });
return (
<div className="relative flex group items-center">
<div
className={
"flex flex-1 items-center gap-2 w-full text-left px-5 py-2 text-sm leading-5 text-gray-900 dark:text-gray-300 group-hover:bg-gray-100 dark:group-hover:bg-gray-700 group-focus:bg-gray-100 dark:group-focus:bg-gray-700 group-focus:outline-none transition-colors"
}
>
<span className="flex-1">{content}</span>
</div>
</div>
);
return <span>{content}</span>;
};

View File

@ -1,18 +1,21 @@
import { Popover } from "@draft-js-plugins/mention";
import { PopoverProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Popover";
export const SuggestionsContainer = ({
import { AddBrainModal } from "@/lib/components/AddBrainModal";
export const BrainSuggestionsContainer = ({
children,
...popoverProps
}: PopoverProps): JSX.Element => (
<Popover {...popoverProps}>
<div
style={{
maxWidth: "max-content",
width: "max-content",
}}
className="bg-white dark:bg-black border border-black/10 dark:border-white/25 rounded-md shadow-md overflow-y-auto"
>
{children}
<AddBrainModal />
</div>
</Popover>
);

View File

@ -0,0 +1,14 @@
type PromptSuggestionProps = {
content: string;
};
export const PromptSuggestion = ({
content,
}: PromptSuggestionProps): JSX.Element => {
return (
<div className="flex flex-1 flex-row gap-2 w-full text-left px-5 py-2 text-sm text-gray-900 dark:text-gray-300">
<div className="flex flex-1">
<span>{content}</span>
</div>
</div>
);
};

View File

@ -0,0 +1,21 @@
import { Popover } from "@draft-js-plugins/mention";
import { PopoverProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Popover";
import { AddNewPromptButton } from "./AddNewPromptButton";
export const PromptSuggestionsContainer = ({
children,
...popoverProps
}: PopoverProps): JSX.Element => (
<Popover {...popoverProps}>
<div
style={{
width: "max-content",
}}
className="bg-white dark:bg-black border border-black/10 dark:border-white/25 rounded-md shadow-md overflow-y-auto"
>
{children}
<AddNewPromptButton />
</div>
</Popover>
);

View File

@ -1,14 +1,32 @@
import { EntryComponentProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry";
import { UUID } from "crypto";
import { MentionTriggerType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { ShareBrain } from "@/lib/components/ShareBrain";
import { BrainSuggestion } from "./BrainSuggestion";
import { PromptSuggestion } from "./PromptSuggestion";
export const SuggestionRow = ({
mention,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
className,
...otherProps
}: EntryComponentProps): JSX.Element => (
<div {...otherProps}>
<BrainSuggestion id={mention.id as string} content={mention.name} />
</div>
);
}: EntryComponentProps): JSX.Element => {
if ((mention.trigger as MentionTriggerType) === "@") {
return (
<div {...otherProps}>
<div className="relative flex group px-4">
<BrainSuggestion content={mention.name} />
<div className="absolute right-0 flex flex-row">
<ShareBrain brainId={mention.id as UUID} />
</div>
</div>
</div>
);
}
return (
<div {...otherProps}>
<PromptSuggestion content={mention.name} />
</div>
);
};

View File

@ -1 +1,5 @@
export * from "./AddNewBrainButton";
export * from "./AddNewPromptButton";
export * from "./BrainSuggestion";
export * from "./BrainSuggestionsContainer";
export * from "./PromptSuggestion";
export * from "./SuggestionRow";

View File

@ -1,31 +1,13 @@
import createMentionPlugin from "@draft-js-plugins/mention";
import { useMemo } from "react";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { BrainMentionItem } from "../../../BrainMentionItem";
interface MentionPluginProps {
removeMention: (entityKeyToRemove: string) => void;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMentionPlugin = (props: MentionPluginProps) => {
const { removeMention } = props;
const { setCurrentBrainId } = useBrainContext();
export const useMentionPlugin = () => {
const { MentionSuggestions, plugins } = useMemo(() => {
const mentionPlugin = createMentionPlugin({
mentionComponent: ({ entityKey, mention: { name } }) => (
<BrainMentionItem
text={name}
onRemove={() => {
setCurrentBrainId(null);
removeMention(entityKey);
}}
/>
),
mentionComponent: () => <span />,
mentionTrigger: ["@", "#"],
popperOptions: {
placement: "top-end",
modifiers: [

View File

@ -1,90 +1,41 @@
/* eslint-disable max-lines */
import { MentionData } from "@draft-js-plugins/mention";
import { EditorState } from "draft-js";
import { useEffect, useState } from "react";
import { MentionTriggerType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { MentionInputMentionsType, TriggerMap } from "../../../../types";
import { MentionInputMentionsType } from "../../../../types";
import { mapMinimalBrainToMentionData } from "../../utils/mapMinimalBrainToMentionData";
import { mapPromptToMentionData } from "../../utils/mapPromptToMentionData";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMentionState = () => {
const { allBrains } = useBrainContext();
const [editorState, legacySetEditorState] = useState(() =>
EditorState.createEmpty()
);
const { allBrains, publicPrompts } = useBrainContext();
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const [mentionItems, setMentionItems] = useState<MentionInputMentionsType>({
"@": allBrains.map((brain) => ({ ...brain, value: brain.name })),
"@": allBrains.map(mapMinimalBrainToMentionData),
"#": publicPrompts.map(mapPromptToMentionData),
});
const [suggestions, setSuggestions] = useState(
mapMinimalBrainToMentionData(mentionItems["@"])
);
const setEditorState = (newState: EditorState) => {
const currentSelection = newState.getSelection();
const stateWithContentAndSelection = EditorState.forceSelection(
newState,
currentSelection
);
legacySetEditorState(stateWithContentAndSelection);
};
const getEditorCurrentMentions = (): TriggerMap[] => {
const contentState = editorState.getCurrentContent();
const plainText = contentState.getPlainText();
const mentionTriggers = Object.keys(mentionItems);
const mentionTexts: TriggerMap[] = [];
mentionTriggers.forEach((trigger) => {
if (trigger === "@") {
mentionItems["@"].forEach((item) => {
const mentionText = `${trigger}${item.name}`;
if (plainText.includes(mentionText)) {
mentionTexts.push({
trigger: trigger as MentionTriggerType,
content: item.name,
});
}
});
}
});
return mentionTexts;
};
const getEditorTextWithoutMentions = (
editorCurrentState: EditorState
): string => {
const contentState = editorCurrentState.getCurrentContent();
let plainText = contentState.getPlainText();
Object.keys(mentionItems).forEach((trigger) => {
if (trigger === "@") {
mentionItems[trigger].forEach((item) => {
const regex = new RegExp(`${trigger}${item.name}`, "g");
plainText = plainText.replace(regex, "");
});
}
});
return plainText;
};
const [suggestions, setSuggestions] = useState<MentionData[]>([]);
useEffect(() => {
setMentionItems({
...mentionItems,
"@": [
...allBrains.map((brain) => ({
...brain,
value: brain.name,
})),
],
"@": allBrains.map(mapMinimalBrainToMentionData),
});
}, [allBrains]);
useEffect(() => {
setMentionItems({
...mentionItems,
"#": publicPrompts.map(mapPromptToMentionData),
});
}, [publicPrompts]);
return {
editorState,
setEditorState,
@ -92,7 +43,6 @@ export const useMentionState = () => {
setSuggestions,
setMentionItems,
suggestions,
getEditorCurrentMentions,
getEditorTextWithoutMentions,
publicPrompts,
};
};

View File

@ -1,8 +1,4 @@
import { addMention, MentionData } from "@draft-js-plugins/mention";
import { EditorState } from "draft-js";
import { MentionTriggerType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { EditorState, Modifier } from "draft-js";
type MentionUtilsProps = {
editorState: EditorState;
@ -12,49 +8,24 @@ type MentionUtilsProps = {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMentionUtils = (props: MentionUtilsProps) => {
const { editorState, setEditorState } = props;
const { setCurrentBrainId } = useBrainContext();
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 insertMention = (
mention: MentionData,
trigger: MentionTriggerType,
customEditorState?: EditorState
): EditorState => {
const editorStateWithMention = addMention(
customEditorState ?? editorState,
mention,
trigger,
trigger,
"MUTABLE"
const removeEntity = (entityKeyToRemove: string): void => {
const contentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
"",
editorState.getCurrentInlineStyle(),
entityKeyToRemove
);
setEditorState(editorStateWithMention);
const newEditorState = EditorState.set(editorState, {
currentContent: contentState,
});
return editorStateWithMention;
setEditorState(newEditorState);
};
return {
removeMention,
insertMention,
removeEntity,
};
};

View File

@ -0,0 +1,21 @@
import { MentionData } from "@draft-js-plugins/mention";
import { EditorState } from "draft-js";
import { isMention } from "../../utils/isMention";
export const getEditorMentions = (editorState: EditorState): MentionData[] => {
const contentState = editorState.getCurrentContent();
const entities = contentState.getAllEntities();
const mentions: MentionData[] = [];
entities.forEach((contentBlock) => {
if (isMention(contentBlock?.getType())) {
mentions.push(
(contentBlock?.getData() as { mention: MentionData }).mention
);
}
});
return mentions;
};

View File

@ -0,0 +1,4 @@
import { EditorState } from "draft-js";
export const getEditorText = (editorState: EditorState): string =>
editorState.getCurrentContent().getPlainText();

View File

@ -8,15 +8,19 @@ import { UUID } from "crypto";
import { EditorState, getDefaultKeyBinding } from "draft-js";
import { useCallback, useEffect, useRef, useState } from "react";
import {
mentionTriggers,
MentionTriggerType,
} from "@/app/chat/[chatId]/components/ActionsBar/types";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import "@draft-js-plugins/mention/lib/plugin.css";
import "draft-js/dist/Draft.css";
import { useMentionPlugin } from "./helpers/MentionPlugin";
import { useMentionState } from "./helpers/MentionState";
import { useMentionUtils } from "./helpers/MentionUtils";
import { mapMinimalBrainToMentionData } from "../utils/mapMinimalBrainToMentionData";
import "@draft-js-plugins/mention/lib/plugin.css";
import "draft-js/dist/Draft.css";
import { getEditorText } from "./helpers/getEditorText";
type UseMentionInputProps = {
message: string;
@ -30,95 +34,106 @@ export const useMentionInput = ({
onSubmit,
setMessage,
}: UseMentionInputProps) => {
const { allBrains, currentBrainId, setCurrentBrainId } = useBrainContext();
const {
currentBrainId,
currentPromptId,
setCurrentBrainId,
setCurrentPromptId,
} = useBrainContext();
const {
editorState,
setEditorState,
setMentionItems,
mentionItems,
setSuggestions,
suggestions,
getEditorCurrentMentions,
getEditorTextWithoutMentions,
publicPrompts,
} = useMentionState();
const { removeMention, insertMention } = useMentionUtils({
const { removeEntity } = useMentionUtils({
editorState,
setEditorState,
});
const { MentionSuggestions, plugins } = useMentionPlugin({
removeMention,
});
const [currentTrigger, setCurrentTrigger] = useState<MentionTriggerType>("@");
const { MentionSuggestions, plugins } = useMentionPlugin();
const mentionInputRef = useRef<Editor>(null);
const [selectedBrainAddedOnload, setSelectedBrainAddedOnload] =
useState(false);
const [open, setOpen] = useState(false);
const onOpenChange = useCallback((_open: boolean) => {
setOpen(_open);
}, []);
const onAddMention = (mention: MentionData) => {
setCurrentBrainId(mention.id as UUID);
if (mention.trigger === "#") {
setCurrentPromptId(mention.id as UUID);
}
if (mention.trigger === "@") {
setCurrentBrainId(mention.id as UUID);
}
const lastEntityKey = editorState
.getCurrentContent()
.getLastCreatedEntityKey();
removeEntity(lastEntityKey);
};
const onSearchChange = ({
trigger,
value,
}: {
trigger: string;
trigger: MentionTriggerType;
value: string;
}) => {
if (currentBrainId !== null) {
setSuggestions([]);
setCurrentTrigger(trigger);
if (trigger === "@") {
if (currentBrainId !== null) {
setSuggestions([]);
return;
}
setSuggestions(
defaultSuggestionsFilter(
value,
mapMinimalBrainToMentionData(mentionItems["@"]),
trigger
)
);
};
const insertCurrentBrainAsMention = (): void => {
const mention = mentionItems["@"].find(
(item) => item.id === currentBrainId
);
if (mention !== undefined) {
insertMention(mention, "@");
}
};
const resetEditorContent = () => {
const currentMentions = getEditorCurrentMentions();
let newEditorState = EditorState.createEmpty();
currentMentions.forEach((mention) => {
if (mention.trigger === "@") {
const correspondingMention = mentionItems["@"].find(
(item) => item.name === mention.content
);
if (correspondingMention !== undefined) {
newEditorState = insertMention(
correspondingMention,
mention.trigger,
newEditorState
);
}
return;
}
});
setEditorState(newEditorState);
if (value === "") {
setSuggestions(mentionItems["@"]);
return;
}
}
if (trigger === "#") {
if (currentPromptId !== null) {
setSuggestions([]);
return;
}
if (value === "") {
setSuggestions(mentionItems["#"]);
return;
}
}
setSuggestions(defaultSuggestionsFilter(value, mentionItems, trigger));
};
const resetEditorContent = useCallback(() => {
setEditorState(EditorState.createEmpty());
}, [setEditorState]);
// eslint-disable-next-line complexity
const keyBindingFn = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (mentionTriggers.includes(e.key as MentionTriggerType)) {
setOpen(true);
return getDefaultKeyBinding(e);
}
if (
(e.key === "Backspace" || e.key === "Delete") &&
getEditorText(editorState) === ""
) {
return "backspace";
}
if (e.key === "Enter" && !e.shiftKey) {
onSubmit();
@ -134,67 +149,35 @@ export const useMentionInput = ({
const handleEditorChange = (newEditorState: EditorState) => {
setEditorState(newEditorState);
const currentMessage = getEditorTextWithoutMentions(newEditorState);
setMessage(currentMessage);
};
useEffect(() => {
const currentMessage = getEditorText(editorState);
if (currentMessage !== "") {
setMessage(currentMessage);
}
}, [editorState, setMessage]);
useEffect(() => {
if (message === "") {
resetEditorContent();
}
}, [message]);
useEffect(() => {
setSuggestions(mapMinimalBrainToMentionData(mentionItems["@"]));
}, [mentionItems]);
useEffect(() => {
setMentionItems({
...mentionItems,
"@": [
...allBrains.map((brain) => ({
...brain,
value: brain.name,
})),
],
});
}, [allBrains]);
useEffect(() => {
if (
selectedBrainAddedOnload ||
mentionItems["@"].length === 0 ||
currentBrainId === null
) {
return;
}
insertCurrentBrainAsMention();
setSelectedBrainAddedOnload(true);
}, [currentBrainId, mentionItems]);
useEffect(() => {
const contentState = editorState.getCurrentContent();
const plainText = contentState.getPlainText();
if (plainText === "" && currentBrainId !== null) {
insertCurrentBrainAsMention();
}
}, [editorState]);
}, [message, resetEditorContent]);
return {
mentionInputRef,
plugins,
MentionSuggestions,
onOpenChange,
onSearchChange,
open,
suggestions,
onAddMention,
editorState,
insertCurrentBrainAsMention,
handleEditorChange,
keyBindingFn,
publicPrompts,
currentTrigger,
setOpen,
};
};

View File

@ -0,0 +1,9 @@
import { mentionTriggers } from "@/app/chat/[chatId]/components/ActionsBar/types";
const mentionsTags = [
"mention",
...mentionTriggers.map((trigger) => `${trigger}mention`),
];
export const isMention = (type?: string): boolean =>
type !== undefined && mentionsTags.includes(type);

View File

@ -3,9 +3,9 @@ import { MentionData } from "@draft-js-plugins/mention";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
export const mapMinimalBrainToMentionData = (
brains: MinimalBrainForUser[]
): MentionData[] =>
brains.map((brain) => ({
name: brain.name,
id: brain.id as string,
}));
brain: MinimalBrainForUser
): MentionData => ({
name: brain.name,
id: brain.id as string,
trigger: "@",
});

View File

@ -0,0 +1,9 @@
import { MentionData } from "@draft-js-plugins/mention";
import { Prompt } from "@/lib/types/Prompt";
export const mapPromptToMentionData = (prompt: Prompt): MentionData => ({
name: prompt.title,
id: prompt.id,
trigger: "#",
});

View File

@ -1,18 +1,22 @@
import { MdRemoveCircleOutline } from "react-icons/md";
import { MentionTriggerType } from "../../../../../types";
type MentionItemProps = {
text: string;
onRemove: () => void;
trigger?: MentionTriggerType;
};
export const BrainMentionItem = ({
export const MentionItem = ({
text,
onRemove,
trigger,
}: MentionItemProps): JSX.Element => {
return (
<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">@{text}</span>
<span className="flex-grow">{`${trigger ?? ""}${text}`}</span>
<MdRemoveCircleOutline
className="cursor-pointer absolute top-[-10px] right-[5px]"
onClick={onRemove}

View File

@ -1,10 +0,0 @@
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,
};
};

View File

@ -1,11 +1,8 @@
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { MentionData } from "@draft-js-plugins/mention";
import { MentionTriggerType } from "../../../../types";
export type MentionInputMentionsType = {
"@": MinimalBrainForUser[];
};
export type TriggerMap = {
trigger: MentionTriggerType;
content: string;
};
export type MentionInputMentionsType = Record<
MentionTriggerType,
MentionData[]
>;

View File

@ -1,3 +1,3 @@
export type MentionType = { id: string; display: string };
export const mentionTriggers = ["@", "#"] as const;
export type MentionTriggerType = "@" | "#";
export type MentionTriggerType = (typeof mentionTriggers)[number];

View File

@ -1,4 +1,3 @@
import { useFeature } from "@growthbook/growthbook-react";
import React from "react";
import ReactMarkdown from "react-markdown";
@ -16,8 +15,6 @@ export const ChatMessage = React.forwardRef(
{ speaker, text, brainName, promptName }: ChatMessageProps,
ref: React.Ref<HTMLDivElement>
) => {
const isNewUxOn = useFeature("new-ux").on;
const isUserSpeaker = speaker === "user";
const containerClasses = cn(
"py-3 px-5 w-fit ",
@ -39,14 +36,14 @@ export const ChatMessage = React.forwardRef(
<div className={containerWrapperClasses}>
{" "}
<div ref={ref} className={containerClasses}>
{isNewUxOn && (
<div className="w-full">
<span
data-testid="brain-prompt-tags"
className="text-gray-400 mb-1"
className="text-gray-400 mb-1 text-xs"
>
@{brainName ?? "-"} #{promptName ?? "-"}
</span>
)}
</div>
<div data-testid="chat-message-text">
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
</div>

View File

@ -1,5 +1,3 @@
import { useFeature } from "@growthbook/growthbook-react";
import { useChatContext } from "@/lib/context";
import { ChatMessages } from "./ChatMessages";
@ -8,8 +6,7 @@ import { ShortCuts } from "./ShortCuts";
export const ChatDialog = (): JSX.Element => {
const { history } = useChatContext();
const shouldDisplayShortcuts =
useFeature("new-ux").on && history.length === 0;
const shouldDisplayShortcuts = history.length === 0;
if (!shouldDisplayShortcuts) {
return <ChatMessages />;

View File

@ -8,26 +8,23 @@ export const ShortCuts = (): JSX.Element => {
const shortcuts = [
{
content: [
t("shortcut_select_brain"),
t("shortcut_select_file"),
t("shortcut_choose_prompt"),
],
},
{
content: [
t("shortcut_create_brain"),
t("shortcut_feed_brain"),
t("shortcut_create_prompt"),
],
},
{
content: [
t("shortcut_manage_brains"),
t("shortcut_go_to_user_page"),
t("shortcut_go_to_shortcuts"),
],
content: [t("shortcut_select_brain"), t("shortcut_choose_prompt")],
},
// {
// content: [
// t("shortcut_select_file"),
// t("shortcut_create_brain"),
// t("shortcut_feed_brain"),
// t("shortcut_create_prompt"),
// ],
// },
// {
// content: [
// t("shortcut_manage_brains"),
// t("shortcut_go_to_user_page"),
// t("shortcut_go_to_shortcuts"),
// ],
// },
];
return (

View File

@ -24,7 +24,7 @@ export const useChat = () => {
const [generatingAnswer, setGeneratingAnswer] = useState(false);
const { history } = useChatContext();
const { currentBrain } = useBrainContext();
const { currentBrain, currentPromptId } = useBrainContext();
const { publish } = useToast();
const { createChat } = useChatApi();
@ -64,6 +64,7 @@ export const useChat = () => {
temperature: chatConfig?.temperature,
max_tokens: chatConfig?.maxTokens,
brain_id: currentBrain?.id,
prompt_id: currentPromptId ?? undefined,
};
await addStreamQuestion(currentChatId, chatQuestion);

View File

@ -35,7 +35,11 @@ export const AddBrainModal = (): JSX.Element => {
return (
<Modal
Trigger={
<Button variant={"secondary"}>
<Button
onClick={() => void 0}
variant={"tertiary"}
className="border-0"
>
{t("newBrain", { ns: "brain" })}
<MdAdd className="text-xl" />
</Button>

View File

@ -2,20 +2,15 @@ import { ShareBrain } from "@/lib/components/ShareBrain";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { DeleteBrain } from "./components";
import { BrainRoleType } from "./types";
type BrainActionsProps = {
brain: MinimalBrainForUser;
};
const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
export const BrainActions = ({ brain }: BrainActionsProps): JSX.Element => {
return (
<div className="absolute right-0 flex flex-row">
{requiredAccessToShareBrain.includes(brain.role) && (
<ShareBrain brainId={brain.id} name={brain.name} />
)}
<ShareBrain brainId={brain.id} />
<DeleteBrain brainId={brain.id} />
</div>
);

View File

@ -2,6 +2,7 @@
"use client";
import { UUID } from "crypto";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { ImUserPlus } from "react-icons/im";
import { MdContentPaste, MdShare } from "react-icons/md";
@ -10,17 +11,17 @@ import { BrainUsers } from "@/lib/components/BrainUsers/BrainUsers";
import { UserToInvite } from "@/lib/components/UserToInvite";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useShareBrain } from "@/lib/hooks/useShareBrain";
import { BrainRoleType } from "../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
type ShareBrainModalProps = {
brainId: UUID;
name: string;
};
const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
export const ShareBrain = ({
brainId,
name,
}: ShareBrainModalProps): JSX.Element => {
export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
const {
roleAssignations,
brainShareLink,
@ -34,8 +35,19 @@ export const ShareBrain = ({
isShareModalOpen,
canAddNewRow,
} = useShareBrain(brainId);
const { t } = useTranslation(["translation", "brain"]);
const { allBrains } = useBrainContext();
const correspondingBrain = allBrains.find((brain) => brain.id === brainId);
if (
correspondingBrain === undefined ||
!requiredAccessToShareBrain.includes(correspondingBrain.role)
) {
return <Fragment />;
}
return (
<Modal
Trigger={
@ -49,7 +61,7 @@ export const ShareBrain = ({
</Button>
}
CloseTrigger={<div />}
title={t("shareBrain", { name, ns: "brain" })}
title={t("shareBrain", { name: correspondingBrain.name, ns: "brain" })}
isOpen={isShareModalOpen}
setOpen={setIsShareModalOpen}
>

View File

@ -38,6 +38,13 @@ vi.mock("@/lib/context/BrainProvider/hooks/useBrainContext", async () => {
...actual,
useBrainContext: () => ({
...actual.useBrainContext(),
allBrains: [
{
id: "cf9bb422-b1b6-4fd7-abc1-01bd395d2318",
name: "test",
role: "Owner",
},
],
currentBrain: {
role: "Editor",
},
@ -69,10 +76,7 @@ describe("ShareBrain", () => {
<SupabaseProviderMock>
<BrainConfigProviderMock>
<BrainProviderMock>
<ShareBrain
name="test"
brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318"
/>
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
</BrainProviderMock>
</BrainConfigProviderMock>
</SupabaseProviderMock>
@ -87,10 +91,7 @@ describe("ShareBrain", () => {
<SupabaseProviderMock>
<BrainProviderMock>
<BrainConfigProviderMock>
<ShareBrain
name="test"
brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318"
/>
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
</BrainConfigProviderMock>
</BrainProviderMock>
</SupabaseProviderMock>
@ -105,10 +106,7 @@ describe("ShareBrain", () => {
<SupabaseProviderMock>
<BrainConfigProviderMock>
<BrainProviderMock>
<ShareBrain
name="test"
brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318"
/>
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
</BrainProviderMock>
</BrainConfigProviderMock>
</SupabaseProviderMock>

View File

@ -4,7 +4,9 @@ import { useCallback, useEffect, useState } from "react";
import { CreateBrainInput } from "@/lib/api/brain/types";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
import { useToast } from "@/lib/hooks";
import { Prompt } from "@/lib/types/Prompt";
import { useEventTracking } from "@/services/analytics/useEventTracking";
import {
@ -21,12 +23,18 @@ export const useBrainProvider = () => {
const { track } = useEventTracking();
const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
useBrainApi();
const { getPublicPrompts } = usePromptApi();
const [allBrains, setAllBrains] = useState<MinimalBrainForUser[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
const [defaultBrainId, setDefaultBrainId] = useState<UUID>();
const [isFetchingBrains, setIsFetchingBrains] = useState(false);
const [publicPrompts, setPublicPrompts] = useState<Prompt[]>([]);
const [currentPromptId, setCurrentPromptId] = useState<null | string>(null);
const currentPrompt = publicPrompts.find(
(prompt) => prompt.id === currentPromptId
);
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
const createBrainHandler = async (
brain: CreateBrainInput
@ -100,6 +108,11 @@ export const useBrainProvider = () => {
const fetchDefaultBrain = async () => {
setDefaultBrainId((await getDefaultBrain())?.id);
};
const fetchPublicPrompts = async () => {
setPublicPrompts(await getPublicPrompts());
};
useEffect(() => {
void fetchDefaultBrain();
}, []);
@ -118,5 +131,10 @@ export const useBrainProvider = () => {
isFetchingBrains,
defaultBrainId,
fetchDefaultBrain,
fetchPublicPrompts,
publicPrompts,
currentPrompt,
setCurrentPromptId,
currentPromptId,
};
};

View File

@ -13,6 +13,7 @@ export const BrainProviderMock = ({
<BrainContextMock.Provider
value={{
allBrains: [],
publicPrompts:[],
currentBrain: undefined,
//@ts-ignore we are not using the functions in tests
createBrain: () => void 0,

View File

@ -33,5 +33,5 @@
"empty_brain_title_suffix": "and chat with them",
"actions_bar_placeholder": "Ask a question to @brains or /files and choose your #prompt",
"missing_brain": "Please select a brain to chat with",
"new_brain": "Create new brain"
"new_prompt": "Create new prompt"
}

View File

@ -34,5 +34,5 @@
"thinking": "Pensando...",
"title": "Conversa con {{brain}}",
"missing_brain": "No hay cerebro seleccionado",
"new_brain": "Crear nuevo cerebro"
"new_prompt": "Crear nueva instrucción"
}

View File

@ -34,5 +34,5 @@
"thinking": "Réflexion...",
"title": "Discuter avec {{brain}}",
"missing_brain": "Veuillez selectionner un cerveau pour discuter",
"new_brain": "Créer un nouveau cerveau"
"new_prompt": "Créer un nouveau prompt"
}

View File

@ -34,5 +34,5 @@
"thinking": "Pensando...",
"title": "Converse com {{brain}}",
"missing_brain": "Cérebro não encontrado",
"new_brain": "Criar novo cérebro"
"new_prompt": "Criar novo prompt"
}

View File

@ -34,5 +34,5 @@
"thinking": "Думаю...",
"title": "Чат с {{brain}}",
"missing_brain": "Мозг не найден",
"new_brain": "Создать новый мозг"
"new_prompt": "Создать новый запрос"
}

View File

@ -34,5 +34,5 @@
"empty_brain_title_suffix": "和他们聊天",
"actions_bar_placeholder": "向 @brains 或 /files 提问,并选择您的 #prompt",
"missing_brain": "请选择一个大脑进行聊天",
"new_brain": "新大脑"
"new_prompt": "新提示"
}