mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
feat(chat): add brain selection through mention input (#969)
* feat(chat): add brain selection through mention input * feat: detect mention deletion from editor * feat: improve ux * chore: improve dx * feat: update translations * feat: improve mention popover ui * fix: update failing tests * feat: add mentions suggestion popover * feat: update translations * feat: remove add new brain button
This commit is contained in:
parent
20d5294795
commit
8e94f22782
@ -1,4 +1,4 @@
|
||||
import { ChatInput } from "../ChatInput";
|
||||
import { ChatInput } from "./components";
|
||||
|
||||
export const ActionsBar = (): JSX.Element => {
|
||||
return (
|
||||
|
@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { MentionInput } from "./components";
|
||||
|
||||
type ChatBarProps = {
|
||||
onSubmit: () => void;
|
||||
setMessage: (text: string) => void;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const ChatBar = ({
|
||||
onSubmit,
|
||||
setMessage,
|
||||
message,
|
||||
}: ChatBarProps): JSX.Element => {
|
||||
return (
|
||||
<MentionInput
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
@ -0,0 +1,64 @@
|
||||
import Editor from "@draft-js-plugins/editor";
|
||||
import { ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import "@draft-js-plugins/mention/lib/plugin.css";
|
||||
import "draft-js/dist/Draft.css";
|
||||
|
||||
import { SuggestionRow } from "./components/SuggestionRow";
|
||||
import { SuggestionsContainer } from "./components/SuggestionsContainer";
|
||||
import { useMentionInput } from "./hooks/useMentionInput";
|
||||
|
||||
type MentionInputProps = {
|
||||
onSubmit: () => void;
|
||||
setMessage: (text: string) => void;
|
||||
message: string;
|
||||
};
|
||||
export const MentionInput = ({
|
||||
onSubmit,
|
||||
setMessage,
|
||||
message,
|
||||
}: MentionInputProps): ReactElement => {
|
||||
const {
|
||||
mentionInputRef,
|
||||
MentionSuggestions,
|
||||
keyBindingFn,
|
||||
editorState,
|
||||
onOpenChange,
|
||||
onSearchChange,
|
||||
open,
|
||||
plugins,
|
||||
suggestions,
|
||||
onAddMention,
|
||||
handleEditorChange,
|
||||
} = useMentionInput({
|
||||
message,
|
||||
onSubmit,
|
||||
setMessage,
|
||||
});
|
||||
|
||||
const { t } = useTranslation(["chat"]);
|
||||
|
||||
return (
|
||||
<div className="w-full" data-testid="chat-input">
|
||||
<Editor
|
||||
editorKey={"editor"}
|
||||
editorState={editorState}
|
||||
onChange={handleEditorChange}
|
||||
plugins={plugins}
|
||||
ref={mentionInputRef}
|
||||
placeholder={t("actions_bar_placeholder")}
|
||||
keyBindingFn={keyBindingFn}
|
||||
/>
|
||||
<MentionSuggestions
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
suggestions={suggestions}
|
||||
onSearchChange={onSearchChange}
|
||||
popoverContainer={SuggestionsContainer}
|
||||
onAddMention={onAddMention}
|
||||
entryComponent={SuggestionRow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { EntryComponentProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry";
|
||||
|
||||
import { BrainSuggestion } from "./BrainSuggestion";
|
||||
|
||||
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>
|
||||
);
|
@ -0,0 +1,18 @@
|
||||
import { Popover } from "@draft-js-plugins/mention";
|
||||
import { PopoverProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Popover";
|
||||
|
||||
export const SuggestionsContainer = ({
|
||||
children,
|
||||
...popoverProps
|
||||
}: PopoverProps): JSX.Element => (
|
||||
<Popover {...popoverProps}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "max-content",
|
||||
}}
|
||||
className="bg-white dark:bg-black border border-black/10 dark:border-white/25 rounded-md shadow-md overflow-y-auto"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
@ -0,0 +1 @@
|
||||
export * from "./AddNewBrainButton";
|
@ -0,0 +1,59 @@
|
||||
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();
|
||||
|
||||
const { MentionSuggestions, plugins } = useMemo(() => {
|
||||
const mentionPlugin = createMentionPlugin({
|
||||
mentionComponent: ({ entityKey, mention: { name } }) => (
|
||||
<BrainMentionItem
|
||||
text={name}
|
||||
onRemove={() => {
|
||||
setCurrentBrainId(null);
|
||||
removeMention(entityKey);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
||||
popperOptions: {
|
||||
placement: "top-end",
|
||||
modifiers: [
|
||||
{
|
||||
name: "customStyle", // Custom modifier for applying styles
|
||||
enabled: true,
|
||||
phase: "beforeWrite",
|
||||
fn: ({ state }) => {
|
||||
state.styles.popper = {
|
||||
...state.styles.popper,
|
||||
minWidth: "auto",
|
||||
backgroundColor: "transparent",
|
||||
padding: "0",
|
||||
marginBottom: "5",
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const { MentionSuggestions: LegacyMentionSuggestions } = mentionPlugin;
|
||||
const legacyPlugins = [mentionPlugin];
|
||||
|
||||
return {
|
||||
plugins: legacyPlugins,
|
||||
MentionSuggestions: LegacyMentionSuggestions,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { MentionSuggestions, plugins };
|
||||
};
|
@ -0,0 +1,98 @@
|
||||
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 { mapMinimalBrainToMentionData } from "../../utils/mapMinimalBrainToMentionData";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useMentionState = () => {
|
||||
const { allBrains } = useBrainContext();
|
||||
const [editorState, legacySetEditorState] = useState(() =>
|
||||
EditorState.createEmpty()
|
||||
);
|
||||
|
||||
const [mentionItems, setMentionItems] = useState<MentionInputMentionsType>({
|
||||
"@": allBrains.map((brain) => ({ ...brain, value: brain.name })),
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMentionItems({
|
||||
...mentionItems,
|
||||
"@": [
|
||||
...allBrains.map((brain) => ({
|
||||
...brain,
|
||||
value: brain.name,
|
||||
})),
|
||||
],
|
||||
});
|
||||
}, [allBrains]);
|
||||
|
||||
return {
|
||||
editorState,
|
||||
setEditorState,
|
||||
mentionItems,
|
||||
setSuggestions,
|
||||
setMentionItems,
|
||||
suggestions,
|
||||
getEditorCurrentMentions,
|
||||
getEditorTextWithoutMentions,
|
||||
};
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
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";
|
||||
|
||||
type MentionUtilsProps = {
|
||||
editorState: EditorState;
|
||||
setEditorState: (editorState: EditorState) => void;
|
||||
};
|
||||
|
||||
// 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"
|
||||
);
|
||||
|
||||
setEditorState(editorStateWithMention);
|
||||
|
||||
return editorStateWithMention;
|
||||
};
|
||||
|
||||
return {
|
||||
removeMention,
|
||||
insertMention,
|
||||
};
|
||||
};
|
@ -0,0 +1,200 @@
|
||||
/* eslint-disable max-lines */
|
||||
import Editor from "@draft-js-plugins/editor";
|
||||
import {
|
||||
defaultSuggestionsFilter,
|
||||
MentionData,
|
||||
} from "@draft-js-plugins/mention";
|
||||
import { UUID } from "crypto";
|
||||
import { EditorState, getDefaultKeyBinding } from "draft-js";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
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";
|
||||
|
||||
type UseMentionInputProps = {
|
||||
message: string;
|
||||
onSubmit: () => void;
|
||||
setMessage: (text: string) => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useMentionInput = ({
|
||||
message,
|
||||
onSubmit,
|
||||
setMessage,
|
||||
}: UseMentionInputProps) => {
|
||||
const { allBrains, currentBrainId, setCurrentBrainId } = useBrainContext();
|
||||
|
||||
const {
|
||||
editorState,
|
||||
setEditorState,
|
||||
setMentionItems,
|
||||
mentionItems,
|
||||
setSuggestions,
|
||||
suggestions,
|
||||
getEditorCurrentMentions,
|
||||
getEditorTextWithoutMentions,
|
||||
} = useMentionState();
|
||||
|
||||
const { removeMention, insertMention } = useMentionUtils({
|
||||
editorState,
|
||||
setEditorState,
|
||||
});
|
||||
|
||||
const { MentionSuggestions, plugins } = useMentionPlugin({
|
||||
removeMention,
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const onSearchChange = ({
|
||||
trigger,
|
||||
value,
|
||||
}: {
|
||||
trigger: string;
|
||||
value: string;
|
||||
}) => {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
setEditorState(newEditorState);
|
||||
};
|
||||
|
||||
const keyBindingFn = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
onSubmit();
|
||||
|
||||
return "submit";
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
};
|
||||
|
||||
const handleEditorChange = (newEditorState: EditorState) => {
|
||||
setEditorState(newEditorState);
|
||||
const currentMessage = getEditorTextWithoutMentions(newEditorState);
|
||||
setMessage(currentMessage);
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return {
|
||||
mentionInputRef,
|
||||
plugins,
|
||||
MentionSuggestions,
|
||||
onOpenChange,
|
||||
onSearchChange,
|
||||
open,
|
||||
suggestions,
|
||||
onAddMention,
|
||||
editorState,
|
||||
insertCurrentBrainAsMention,
|
||||
handleEditorChange,
|
||||
keyBindingFn,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./MentionInput";
|
@ -0,0 +1,11 @@
|
||||
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,
|
||||
}));
|
@ -0,0 +1 @@
|
||||
export * from "./MentionInput";
|
@ -0,0 +1,10 @@
|
||||
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,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./ChatBar";
|
@ -0,0 +1,11 @@
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
import { MentionTriggerType } from "../../../../types";
|
||||
|
||||
export type MentionInputMentionsType = {
|
||||
"@": MinimalBrainForUser[];
|
||||
};
|
||||
export type TriggerMap = {
|
||||
trigger: MentionTriggerType;
|
||||
content: string;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./ChatBar";
|
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useChat } from "../../../hooks/useChat";
|
||||
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useChatInput = () => {
|
||||
@ -8,9 +8,6 @@ export const useChatInput = () => {
|
||||
const { addQuestion, generatingAnswer, chatId } = useChat();
|
||||
|
||||
const submitQuestion = () => {
|
||||
if (message.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!generatingAnswer) {
|
||||
void addQuestion(message, () => setMessage(""));
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
|
||||
import { ChatBar } from "./components/ChatBar/ChatBar";
|
||||
import { ConfigModal } from "./components/ConfigModal";
|
||||
import { MicButton } from "./components/MicButton/MicButton";
|
||||
import { useChatInput } from "./hooks/useChatInput";
|
||||
|
||||
export const ChatInput = (): JSX.Element => {
|
||||
const { setMessage, submitQuestion, chatId, generatingAnswer, message } =
|
||||
useChatInput();
|
||||
const { t } = useTranslation(["chat"]);
|
||||
|
||||
return (
|
||||
<form
|
||||
data-testid="chat-input-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitQuestion();
|
||||
}}
|
||||
className="sticky flex items-star bottom-0 bg-white dark:bg-black w-full flex justify-center gap-2 z-20"
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-center">
|
||||
<ChatBar
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onSubmit={submitQuestion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-end">
|
||||
<Button
|
||||
className="px-3 py-2 sm:px-4 sm:py-2"
|
||||
type="submit"
|
||||
isLoading={generatingAnswer}
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{generatingAnswer
|
||||
? t("thinking", { ns: "chat" })
|
||||
: t("chat", { ns: "chat" })}
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
<MicButton setMessage={setMessage} />
|
||||
<ConfigModal chatId={chatId} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
type StyleMentionsInputProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export const MentionsInput = ({
|
||||
onChange,
|
||||
placeholder,
|
||||
value,
|
||||
}: StyleMentionsInputProps): JSX.Element => {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={value}
|
||||
className="focus:outline-none focus:border-none"
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,2 +1 @@
|
||||
export * from "./MentionItem";
|
||||
export * from "./MentionsInput";
|
||||
export * from "./ChatInput";
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useActionsBar = () => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const handleChange = (newPlainTextValue: string) => {
|
||||
setValue(newPlainTextValue);
|
||||
};
|
||||
|
||||
return {
|
||||
handleChange,
|
||||
value,
|
||||
};
|
||||
};
|
@ -1 +1,3 @@
|
||||
export type MentionType = { id: string; display: string };
|
||||
|
||||
export type MentionTriggerType = "@" | "#";
|
||||
|
@ -1,148 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BrainProvider } from "@/lib/context";
|
||||
import { BrainConfigProvider } from "@/lib/context/BrainConfigProvider";
|
||||
|
||||
import { ChatInput } from "../index";
|
||||
|
||||
const addQuestionMock = vi.fn((...params: unknown[]) => ({ params }));
|
||||
|
||||
vi.mock("@/lib/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
|
||||
"@/lib/hooks"
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
get: vi.fn(() => ({
|
||||
data: {},
|
||||
})),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
|
||||
useChat: () => ({
|
||||
addQuestion: (...params: unknown[]) => addQuestionMock(...params),
|
||||
generatingAnswer: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
session: {},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
describe("ChatInput", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
// Rendering the ChatInput component
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
expect(chatInputForm).toBeDefined();
|
||||
|
||||
const chatInput = getByTestId("chat-input");
|
||||
expect(chatInput).toBeDefined();
|
||||
|
||||
const submitButton = getByTestId("submit-button");
|
||||
expect(submitButton).toBeDefined();
|
||||
|
||||
const micButton = getByTestId("mic-button");
|
||||
expect(micButton).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not call addQuestion on form submit when message is empty", () => {
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
fireEvent.submit(chatInputForm);
|
||||
|
||||
// Asserting that the addQuestion function was called with the expected arguments
|
||||
expect(addQuestionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call addQuestion once on form submit when message is not empty", () => {
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInput = getByTestId("chat-input");
|
||||
fireEvent.change(chatInput, { target: { value: "Test question" } });
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
fireEvent.submit(chatInputForm);
|
||||
|
||||
// Asserting that the addQuestion function was called with the expected arguments
|
||||
expect(addQuestionMock).toHaveBeenCalledTimes(1);
|
||||
expect(addQuestionMock).toHaveBeenCalledWith(
|
||||
"Test question",
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should submit a question when "Enter" key is pressed without shift', () => {
|
||||
// Mocking the addQuestion function
|
||||
|
||||
// Rendering the ChatInput component with the mock function
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInput = getByTestId("chat-input");
|
||||
|
||||
fireEvent.change(chatInput, { target: { value: "Another test question" } });
|
||||
fireEvent.keyDown(chatInput, { key: "Enter", shiftKey: false });
|
||||
|
||||
// Asserting that the addQuestion function was called with the expected arguments
|
||||
expect(addQuestionMock).toHaveBeenCalledTimes(1);
|
||||
expect(addQuestionMock).toHaveBeenCalledWith(
|
||||
"Another test question",
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not submit a question when "Enter" key is pressed with shift', () => {
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
const inputElement = getByTestId("chat-input");
|
||||
|
||||
fireEvent.change(inputElement, { target: { value: "Test question" } });
|
||||
fireEvent.keyDown(inputElement, { key: "Enter", shiftKey: true });
|
||||
|
||||
expect(addQuestionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
import { useFeature } from "@growthbook/growthbook-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { ConfigModal } from "./components/ConfigModal";
|
||||
import { MicButton } from "./components/MicButton/MicButton";
|
||||
import { useChatInput } from "./hooks/useChatInput";
|
||||
import { MentionItem } from "../ActionsBar/components";
|
||||
|
||||
export const ChatInput = (): JSX.Element => {
|
||||
const { message, setMessage, submitQuestion, chatId, generatingAnswer } =
|
||||
useChatInput();
|
||||
const { t } = useTranslation(["chat"]);
|
||||
const { currentBrain, setCurrentBrainId } = useBrainContext();
|
||||
const shouldUseNewUX = useFeature("new-ux").on;
|
||||
|
||||
return (
|
||||
<form
|
||||
data-testid="chat-input-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitQuestion();
|
||||
}}
|
||||
className="sticky flex items-star bottom-0 bg-white dark:bg-black w-full flex justify-center gap-2 z-20"
|
||||
>
|
||||
{currentBrain !== undefined && (
|
||||
<MentionItem
|
||||
text={currentBrain.name}
|
||||
onRemove={() => setCurrentBrainId(null)}
|
||||
prefix="@"
|
||||
/>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
autoFocus
|
||||
value={message}
|
||||
required
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault(); // Prevents the newline from being entered in the textarea
|
||||
submitQuestion();
|
||||
}
|
||||
}}
|
||||
className="w-full p-2 pt-0 dark:border-gray-500 outline-none rounded dark:bg-gray-800 focus:outline-none focus:border-none"
|
||||
placeholder={
|
||||
shouldUseNewUX
|
||||
? t("actions_bar_placeholder")
|
||||
: t("begin_conversation_placeholder")
|
||||
}
|
||||
data-testid="chat-input"
|
||||
rows={1}
|
||||
/>
|
||||
<Button
|
||||
className="px-3 py-2 sm:px-4 sm:py-2"
|
||||
type="submit"
|
||||
isLoading={generatingAnswer}
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{generatingAnswer
|
||||
? t("thinking", { ns: "chat" })
|
||||
: t("chat", { ns: "chat" })}
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
<MicButton setMessage={setMessage} />
|
||||
<ConfigModal chatId={chatId} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -33,10 +33,7 @@ export const ChatMessage = React.forwardRef(
|
||||
isUserSpeaker ? "items-end" : "items-start"
|
||||
);
|
||||
|
||||
const markdownClasses = cn(
|
||||
"prose",
|
||||
"dark:prose-invert"
|
||||
);
|
||||
const markdownClasses = cn("prose", "dark:prose-invert");
|
||||
|
||||
return (
|
||||
<div className={containerWrapperClasses}>
|
||||
|
@ -1,2 +1 @@
|
||||
export * from "./ChatInput";
|
||||
export * from "./ChatMessages";
|
||||
|
@ -1,18 +1,18 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { AxiosError } from 'axios';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AxiosError } from "axios";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getChatConfigFromLocalStorage } from '@/lib/api/chat/chat.local';
|
||||
import { useChatApi } from '@/lib/api/chat/useChatApi';
|
||||
import { useBrainContext } from '@/lib/context/BrainProvider/hooks/useBrainContext';
|
||||
import { useChatContext } from '@/lib/context/ChatProvider/hooks/useChatContext';
|
||||
import { useToast } from '@/lib/hooks';
|
||||
import { useEventTracking } from '@/services/analytics/useEventTracking';
|
||||
import { getChatConfigFromLocalStorage } from "@/lib/api/chat/chat.local";
|
||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
|
||||
import { useQuestion } from './useQuestion';
|
||||
import { ChatQuestion } from '../types';
|
||||
import { useQuestion } from "./useQuestion";
|
||||
import { ChatQuestion } from "../types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useChat = () => {
|
||||
@ -32,6 +32,15 @@ export const useChat = () => {
|
||||
const { t } = useTranslation(["chat"]);
|
||||
|
||||
const addQuestion = async (question: string, callback?: () => void) => {
|
||||
if (question === "") {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("ask"),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setGeneratingAnswer(true);
|
||||
|
||||
@ -39,14 +48,14 @@ export const useChat = () => {
|
||||
|
||||
//if chatId is not set, create a new chat. Chat name is from the first question
|
||||
if (currentChatId === undefined) {
|
||||
const chatName = question.split(' ').slice(0, 3).join(' ');
|
||||
const chatName = question.split(" ").slice(0, 3).join(" ");
|
||||
const chat = await createChat(chatName);
|
||||
currentChatId = chat.chat_id;
|
||||
setChatId(currentChatId);
|
||||
//TODO: update chat list here
|
||||
}
|
||||
|
||||
void track('QUESTION_ASKED');
|
||||
void track("QUESTION_ASKED");
|
||||
const chatConfig = getChatConfigFromLocalStorage(currentChatId);
|
||||
|
||||
const chatQuestion: ChatQuestion = {
|
||||
@ -65,16 +74,16 @@ export const useChat = () => {
|
||||
|
||||
if ((error as AxiosError).response?.status === 429) {
|
||||
publish({
|
||||
variant: 'danger',
|
||||
text: t('limit_reached', { ns: 'chat' }),
|
||||
variant: "danger",
|
||||
text: t("limit_reached", { ns: "chat" }),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
publish({
|
||||
variant: 'danger',
|
||||
text: t('error_occurred', { ns: 'chat' }),
|
||||
variant: "danger",
|
||||
text: t("error_occurred", { ns: "chat" }),
|
||||
});
|
||||
} finally {
|
||||
setGeneratingAnswer(false);
|
||||
|
@ -1,12 +1,9 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
||||
import { useFetch } from "@/lib/hooks";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { useFetch, useToast } from "@/lib/hooks";
|
||||
|
||||
import { ChatHistory, ChatQuestion } from "../types";
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { availableRoles } from "@/lib/components/ShareBrain/types";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { Select } from "@/lib/components/ui/Select";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { RemoveAccessIcon } from "./components/RemoveAccessIcon";
|
||||
import { useBrainUser } from "./hooks/useBrainUser";
|
||||
import { availableRoles } from "../../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/components/ShareBrain/types";
|
||||
import { BrainRoleType } from "../../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||
|
||||
type BrainUserProps = {
|
||||
email: string;
|
||||
role: BrainRoleType;
|
||||
@ -54,7 +52,7 @@ export const BrainUser = ({
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
placeholder= {t('email')}
|
||||
placeholder={t("email")}
|
||||
value={email}
|
||||
data-testid="role-assignation-email-input"
|
||||
readOnly
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Subscription } from "@/lib/api/brain/brain";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
@ -15,7 +14,7 @@ export const useBrainUsers = (brainId: string) => {
|
||||
const { publish } = useToast();
|
||||
const { getBrainUsers } = useBrainApi();
|
||||
const { session } = useSupabase();
|
||||
const { t } = useTranslation(['brain']);
|
||||
const { t } = useTranslation(["brain"]);
|
||||
|
||||
const fetchBrainUsers = async () => {
|
||||
// Optimistic update
|
||||
@ -26,7 +25,7 @@ export const useBrainUsers = (brainId: string) => {
|
||||
} catch {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t('errorFetchingBrainUsers',{ns:'brain'})
|
||||
text: t("errorFetchingBrainUsers", { ns: "brain" }),
|
||||
});
|
||||
} finally {
|
||||
setFetchingBrainUsers(false);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ShareBrain } from "@/lib/components/ShareBrain";
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
import { DeleteBrain, ShareBrain } from "./components";
|
||||
import { DeleteBrain } from "./components";
|
||||
import { BrainRoleType } from "./types";
|
||||
|
||||
type BrainActionsProps = {
|
||||
|
@ -1,2 +1 @@
|
||||
export * from "./DeleteBrain";
|
||||
export * from "./ShareBrain";
|
||||
|
@ -1,5 +1,6 @@
|
||||
export const roles = ["Viewer", "Editor", "Owner"] as const;
|
||||
|
||||
//TODO: move these types to a shared place
|
||||
export type BrainRoleType = (typeof roles)[number];
|
||||
|
||||
export type BrainRoleAssignation = {
|
||||
|
@ -49,7 +49,7 @@ export const ShareBrain = ({
|
||||
</Button>
|
||||
}
|
||||
CloseTrigger={<div />}
|
||||
title={t("shareBrain", { brain: name, ns: "brain" })}
|
||||
title={t("shareBrain", { name, ns: "brain" })}
|
||||
isOpen={isShareModalOpen}
|
||||
setOpen={setIsShareModalOpen}
|
||||
>
|
@ -1,4 +1,4 @@
|
||||
import { BrainRoleType } from "../../../types";
|
||||
import { BrainRoleType } from "../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||
|
||||
export type SelectOptionsProps = {
|
||||
label: string;
|
@ -1,4 +1,4 @@
|
||||
import { BrainRoleAssignation } from "../../../types";
|
||||
import { BrainRoleAssignation } from "../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||
|
||||
export const generateBrainAssignation = (): BrainRoleAssignation => {
|
||||
return {
|
@ -6,11 +6,11 @@ import Field from "@/lib/components/ui/Field";
|
||||
import { Select } from "@/lib/components/ui/Select";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { userRoleToAssignableRoles } from "./NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/components/ShareBrain/types";
|
||||
import {
|
||||
BrainRoleAssignation,
|
||||
BrainRoleType,
|
||||
} from "./NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||
import { userRoleToAssignableRoles } from "./ShareBrain/types";
|
||||
|
||||
type UserToInviteProps = {
|
||||
onChange: (newRole: BrainRoleAssignation) => void;
|
||||
@ -43,10 +43,10 @@ export const UserToInvite = ({
|
||||
}
|
||||
|
||||
const assignableRoles = userRoleToAssignableRoles[currentBrain.role];
|
||||
const translatedOptions = assignableRoles.map(role => ({
|
||||
const translatedOptions = assignableRoles.map((role) => ({
|
||||
value: role.value,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
label: t(role.value)
|
||||
label: t(role.value),
|
||||
}));
|
||||
|
||||
return (
|
||||
|
@ -1,17 +1,17 @@
|
||||
/* eslint-disable max-lines */
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Subscription } from "@/lib/api/brain/brain";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
import { generateBrainAssignation } from "../components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/components/ShareBrain/utils/generateBrainAssignation";
|
||||
import {
|
||||
BrainRoleAssignation,
|
||||
BrainRoleType,
|
||||
} from "../components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||
import { generateBrainAssignation } from "../components/ShareBrain/utils/generateBrainAssignation";
|
||||
import { useBrainContext } from "../context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
|
||||
@ -23,7 +23,7 @@ export const useShareBrain = (brainId: string) => {
|
||||
const [roleAssignations, setRoleAssignation] = useState<
|
||||
BrainRoleAssignation[]
|
||||
>([generateBrainAssignation()]);
|
||||
const { t } = useTranslation(['brain']);
|
||||
const { t } = useTranslation(["brain"]);
|
||||
|
||||
const baseUrl = window.location.origin;
|
||||
const brainShareLink = `${baseUrl}/invitation/${brainId}`;
|
||||
@ -40,7 +40,7 @@ export const useShareBrain = (brainId: string) => {
|
||||
await navigator.clipboard.writeText(brainShareLink);
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t('copiedToClipboard',{ns: 'brain'}),
|
||||
text: t("copiedToClipboard", { ns: "brain" }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -104,7 +104,7 @@ export const useShareBrain = (brainId: string) => {
|
||||
} else {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("errorSendingInvitation")
|
||||
text: t("errorSendingInvitation"),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
|
@ -16,6 +16,8 @@
|
||||
"format-fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@draft-js-plugins/editor": "^4.1.4",
|
||||
"@draft-js-plugins/mention": "^5.2.2",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@growthbook/growthbook-react": "^0.17.0",
|
||||
@ -33,6 +35,7 @@
|
||||
"@supabase/auth-ui-shared": "^0.1.6",
|
||||
"@supabase/supabase-js": "^2.22.0",
|
||||
"@types/dom-speech-recognition": "^0.0.1",
|
||||
"@types/draft-js": "^0.11.12",
|
||||
"@types/node": "20.1.7",
|
||||
"@types/react": "18",
|
||||
"@types/react-dom": "18",
|
||||
@ -44,6 +47,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"draft-js": "^0.11.7",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-next": "13.4.2",
|
||||
|
@ -8,16 +8,15 @@
|
||||
"error_occurred": "Error occurred while getting answer",
|
||||
"noCurrentBrain": "No current brain",
|
||||
"errorParsingData": "Error parsing data",
|
||||
"resposeBodyNull": "Responde body is null",
|
||||
"resposeBodyNull": "Response body is null",
|
||||
"tooManyRequests": "You have exceeded the number of requests per day. To continue chatting, please enter an OpenAI API key in your profile or in used brain.",
|
||||
"receivedResponse": "Received response. Starting to handle stream...",
|
||||
"errorCallingAPI": "Error calling the API",
|
||||
"ask": "Ask a question, or describe a task.",
|
||||
"begin_conversation_placeholder": "Begin conversation here...",
|
||||
"thinking": "Thinking...",
|
||||
"chat": "Chat",
|
||||
"errorFetching": "Error occurred while fetching your chats",
|
||||
"chatDeleted": "Chat sucessfully deleted. Id: {{id}}",
|
||||
"chatDeleted": "Chat successfully deleted. Id: {{id}}",
|
||||
"errorDeleting": "Error deleting chat: {{error}}",
|
||||
"chatNameUpdated": "Chat name updated",
|
||||
"shortcut_select_brain": "@: Select a brain to talk",
|
||||
@ -33,5 +32,6 @@
|
||||
"empty_brain_title_prefix": "Upload files in a",
|
||||
"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"
|
||||
"missing_brain": "Please select a brain to chat with",
|
||||
"new_brain": "Create new brain"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions_bar_placeholder": "Haz una pregunta a @brains o /files y elige tu #indicación",
|
||||
"actions_bar_placeholder": "Haz una pregunta a @brains",
|
||||
"ask": "Has una pregunta o describe un tarea.",
|
||||
"begin_conversation_placeholder": "Inicia la conversación aquí...",
|
||||
"brain": "cerebro",
|
||||
"brains": "cerebros",
|
||||
"chat": "Conversar",
|
||||
@ -34,5 +33,6 @@
|
||||
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
|
||||
"thinking": "Pensando...",
|
||||
"title": "Conversa con {{brain}}",
|
||||
"missing_brain": "No hay cerebro seleccionado"
|
||||
"missing_brain": "No hay cerebro seleccionado",
|
||||
"new_brain": "Crear nuevo cerebro"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions_bar_placeholder": "Posez une question à @brains ou /files et choisissez votre #invite",
|
||||
"actions_bar_placeholder": "Posez une question à @brains",
|
||||
"ask": "Posez une question ou décrivez une tâche.",
|
||||
"begin_conversation_placeholder": "Commencez la conversation ici...",
|
||||
"brain": "cerveau",
|
||||
"brains": "cerveaux",
|
||||
"chat": "Chat",
|
||||
@ -34,5 +33,6 @@
|
||||
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
||||
"thinking": "Réflexion...",
|
||||
"title": "Discuter avec {{brain}}",
|
||||
"missing_brain": "Veuillez selectionner un cerveau pour discuter"
|
||||
"missing_brain": "Veuillez selectionner un cerveau pour discuter",
|
||||
"new_brain": "Créer un nouveau cerveau"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions_bar_placeholder": "Faça uma pergunta para @cérebros ou /arquivos e escolha o seu #prompt",
|
||||
"ask": "Faça uma pergunta ou descreva uma tarefa.",
|
||||
"begin_conversation_placeholder": "Inicie a conversa aqui...",
|
||||
"brain": "cérebro",
|
||||
"brains": "cérebros",
|
||||
"chat": "Conversa",
|
||||
@ -34,5 +33,6 @@
|
||||
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
||||
"thinking": "Pensando...",
|
||||
"title": "Converse com {{brain}}",
|
||||
"missing_brain": "Cérebro não encontrado"
|
||||
"missing_brain": "Cérebro não encontrado",
|
||||
"new_brain": "Criar novo cérebro"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions_bar_placeholder": "Задайте вопрос @brains или /files и выберите ваш #prompt",
|
||||
"ask": "Задайте вопрос или опишите задачу.",
|
||||
"begin_conversation_placeholder": "Начните беседу здесь...",
|
||||
"brain": "мозг",
|
||||
"brains": "мозги",
|
||||
"chat": "Чат",
|
||||
@ -34,5 +33,6 @@
|
||||
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
||||
"thinking": "Думаю...",
|
||||
"title": "Чат с {{brain}}",
|
||||
"missing_brain": "Мозг не найден"
|
||||
"missing_brain": "Мозг не найден",
|
||||
"new_brain": "Создать новый мозг"
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user