mirror of
https://github.com/QuivrHQ/quivr.git
synced 2025-01-05 23:03:53 +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 => {
|
export const ActionsBar = (): JSX.Element => {
|
||||||
return (
|
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 = {
|
type MentionItemProps = {
|
||||||
text: string;
|
text: string;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
prefix?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MentionItem = ({
|
export const BrainMentionItem = ({
|
||||||
text,
|
text,
|
||||||
prefix = "",
|
|
||||||
onRemove,
|
onRemove,
|
||||||
}: MentionItemProps): JSX.Element => {
|
}: MentionItemProps): JSX.Element => {
|
||||||
return (
|
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">
|
<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
|
<MdRemoveCircleOutline
|
||||||
className="cursor-pointer absolute top-[-10px] right-[5px]"
|
className="cursor-pointer absolute top-[-10px] right-[5px]"
|
||||||
onClick={onRemove}
|
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 { 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
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const useChatInput = () => {
|
export const useChatInput = () => {
|
||||||
@ -8,9 +8,6 @@ export const useChatInput = () => {
|
|||||||
const { addQuestion, generatingAnswer, chatId } = useChat();
|
const { addQuestion, generatingAnswer, chatId } = useChat();
|
||||||
|
|
||||||
const submitQuestion = () => {
|
const submitQuestion = () => {
|
||||||
if (message.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!generatingAnswer) {
|
if (!generatingAnswer) {
|
||||||
void addQuestion(message, () => setMessage(""));
|
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 "./ChatInput";
|
||||||
export * from "./MentionsInput";
|
|
||||||
|
@ -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 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"
|
isUserSpeaker ? "items-end" : "items-start"
|
||||||
);
|
);
|
||||||
|
|
||||||
const markdownClasses = cn(
|
const markdownClasses = cn("prose", "dark:prose-invert");
|
||||||
"prose",
|
|
||||||
"dark:prose-invert"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerWrapperClasses}>
|
<div className={containerWrapperClasses}>
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export * from "./ChatInput";
|
|
||||||
export * from "./ChatMessages";
|
export * from "./ChatMessages";
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from "axios";
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from "next/navigation";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { getChatConfigFromLocalStorage } from '@/lib/api/chat/chat.local';
|
import { getChatConfigFromLocalStorage } from "@/lib/api/chat/chat.local";
|
||||||
import { useChatApi } from '@/lib/api/chat/useChatApi';
|
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||||
import { useBrainContext } from '@/lib/context/BrainProvider/hooks/useBrainContext';
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useChatContext } from '@/lib/context/ChatProvider/hooks/useChatContext';
|
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
||||||
import { useToast } from '@/lib/hooks';
|
import { useToast } from "@/lib/hooks";
|
||||||
import { useEventTracking } from '@/services/analytics/useEventTracking';
|
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||||
|
|
||||||
import { useQuestion } from './useQuestion';
|
import { useQuestion } from "./useQuestion";
|
||||||
import { ChatQuestion } from '../types';
|
import { ChatQuestion } from "../types";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const useChat = () => {
|
export const useChat = () => {
|
||||||
@ -32,6 +32,15 @@ export const useChat = () => {
|
|||||||
const { t } = useTranslation(["chat"]);
|
const { t } = useTranslation(["chat"]);
|
||||||
|
|
||||||
const addQuestion = async (question: string, callback?: () => void) => {
|
const addQuestion = async (question: string, callback?: () => void) => {
|
||||||
|
if (question === "") {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: t("ask"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setGeneratingAnswer(true);
|
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 chatId is not set, create a new chat. Chat name is from the first question
|
||||||
if (currentChatId === undefined) {
|
if (currentChatId === undefined) {
|
||||||
const chatName = question.split(' ').slice(0, 3).join(' ');
|
const chatName = question.split(" ").slice(0, 3).join(" ");
|
||||||
const chat = await createChat(chatName);
|
const chat = await createChat(chatName);
|
||||||
currentChatId = chat.chat_id;
|
currentChatId = chat.chat_id;
|
||||||
setChatId(currentChatId);
|
setChatId(currentChatId);
|
||||||
//TODO: update chat list here
|
//TODO: update chat list here
|
||||||
}
|
}
|
||||||
|
|
||||||
void track('QUESTION_ASKED');
|
void track("QUESTION_ASKED");
|
||||||
const chatConfig = getChatConfigFromLocalStorage(currentChatId);
|
const chatConfig = getChatConfigFromLocalStorage(currentChatId);
|
||||||
|
|
||||||
const chatQuestion: ChatQuestion = {
|
const chatQuestion: ChatQuestion = {
|
||||||
@ -65,16 +74,16 @@ export const useChat = () => {
|
|||||||
|
|
||||||
if ((error as AxiosError).response?.status === 429) {
|
if ((error as AxiosError).response?.status === 429) {
|
||||||
publish({
|
publish({
|
||||||
variant: 'danger',
|
variant: "danger",
|
||||||
text: t('limit_reached', { ns: 'chat' }),
|
text: t("limit_reached", { ns: "chat" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
publish({
|
publish({
|
||||||
variant: 'danger',
|
variant: "danger",
|
||||||
text: t('error_occurred', { ns: 'chat' }),
|
text: t("error_occurred", { ns: "chat" }),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setGeneratingAnswer(false);
|
setGeneratingAnswer(false);
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
/* eslint-disable max-lines */
|
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
||||||
import { useFetch } from "@/lib/hooks";
|
import { useFetch, useToast } from "@/lib/hooks";
|
||||||
import { useToast } from "@/lib/hooks/useToast";
|
|
||||||
|
|
||||||
import { ChatHistory, ChatQuestion } from "../types";
|
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 Field from "@/lib/components/ui/Field";
|
||||||
import { Select } from "@/lib/components/ui/Select";
|
import { Select } from "@/lib/components/ui/Select";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
|
|
||||||
import { RemoveAccessIcon } from "./components/RemoveAccessIcon";
|
import { RemoveAccessIcon } from "./components/RemoveAccessIcon";
|
||||||
import { useBrainUser } from "./hooks/useBrainUser";
|
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";
|
import { BrainRoleType } from "../../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
|
|
||||||
type BrainUserProps = {
|
type BrainUserProps = {
|
||||||
email: string;
|
email: string;
|
||||||
role: BrainRoleType;
|
role: BrainRoleType;
|
||||||
@ -54,7 +52,7 @@ export const BrainUser = ({
|
|||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
type="email"
|
type="email"
|
||||||
placeholder= {t('email')}
|
placeholder={t("email")}
|
||||||
value={email}
|
value={email}
|
||||||
data-testid="role-assignation-email-input"
|
data-testid="role-assignation-email-input"
|
||||||
readOnly
|
readOnly
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable max-lines */
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Subscription } from "@/lib/api/brain/brain";
|
import { Subscription } from "@/lib/api/brain/brain";
|
||||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||||
@ -15,7 +14,7 @@ export const useBrainUsers = (brainId: string) => {
|
|||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
const { getBrainUsers } = useBrainApi();
|
const { getBrainUsers } = useBrainApi();
|
||||||
const { session } = useSupabase();
|
const { session } = useSupabase();
|
||||||
const { t } = useTranslation(['brain']);
|
const { t } = useTranslation(["brain"]);
|
||||||
|
|
||||||
const fetchBrainUsers = async () => {
|
const fetchBrainUsers = async () => {
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
@ -26,7 +25,7 @@ export const useBrainUsers = (brainId: string) => {
|
|||||||
} catch {
|
} catch {
|
||||||
publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: t('errorFetchingBrainUsers',{ns:'brain'})
|
text: t("errorFetchingBrainUsers", { ns: "brain" }),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setFetchingBrainUsers(false);
|
setFetchingBrainUsers(false);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { ShareBrain } from "@/lib/components/ShareBrain";
|
||||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||||
|
|
||||||
import { DeleteBrain, ShareBrain } from "./components";
|
import { DeleteBrain } from "./components";
|
||||||
import { BrainRoleType } from "./types";
|
import { BrainRoleType } from "./types";
|
||||||
|
|
||||||
type BrainActionsProps = {
|
type BrainActionsProps = {
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export * from "./DeleteBrain";
|
export * from "./DeleteBrain";
|
||||||
export * from "./ShareBrain";
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const roles = ["Viewer", "Editor", "Owner"] as const;
|
export const roles = ["Viewer", "Editor", "Owner"] as const;
|
||||||
|
|
||||||
|
//TODO: move these types to a shared place
|
||||||
export type BrainRoleType = (typeof roles)[number];
|
export type BrainRoleType = (typeof roles)[number];
|
||||||
|
|
||||||
export type BrainRoleAssignation = {
|
export type BrainRoleAssignation = {
|
||||||
|
@ -49,7 +49,7 @@ export const ShareBrain = ({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
CloseTrigger={<div />}
|
CloseTrigger={<div />}
|
||||||
title={t("shareBrain", { brain: name, ns: "brain" })}
|
title={t("shareBrain", { name, ns: "brain" })}
|
||||||
isOpen={isShareModalOpen}
|
isOpen={isShareModalOpen}
|
||||||
setOpen={setIsShareModalOpen}
|
setOpen={setIsShareModalOpen}
|
||||||
>
|
>
|
@ -1,4 +1,4 @@
|
|||||||
import { BrainRoleType } from "../../../types";
|
import { BrainRoleType } from "../../NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
|
|
||||||
export type SelectOptionsProps = {
|
export type SelectOptionsProps = {
|
||||||
label: string;
|
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 => {
|
export const generateBrainAssignation = (): BrainRoleAssignation => {
|
||||||
return {
|
return {
|
@ -6,11 +6,11 @@ import Field from "@/lib/components/ui/Field";
|
|||||||
import { Select } from "@/lib/components/ui/Select";
|
import { Select } from "@/lib/components/ui/Select";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
|
|
||||||
import { userRoleToAssignableRoles } from "./NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/components/ShareBrain/types";
|
|
||||||
import {
|
import {
|
||||||
BrainRoleAssignation,
|
BrainRoleAssignation,
|
||||||
BrainRoleType,
|
BrainRoleType,
|
||||||
} from "./NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
} from "./NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
|
import { userRoleToAssignableRoles } from "./ShareBrain/types";
|
||||||
|
|
||||||
type UserToInviteProps = {
|
type UserToInviteProps = {
|
||||||
onChange: (newRole: BrainRoleAssignation) => void;
|
onChange: (newRole: BrainRoleAssignation) => void;
|
||||||
@ -43,10 +43,10 @@ export const UserToInvite = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assignableRoles = userRoleToAssignableRoles[currentBrain.role];
|
const assignableRoles = userRoleToAssignableRoles[currentBrain.role];
|
||||||
const translatedOptions = assignableRoles.map(role => ({
|
const translatedOptions = assignableRoles.map((role) => ({
|
||||||
value: role.value,
|
value: role.value,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
label: t(role.value)
|
label: t(role.value),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Subscription } from "@/lib/api/brain/brain";
|
import { Subscription } from "@/lib/api/brain/brain";
|
||||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
import { generateBrainAssignation } from "../components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/components/ShareBrain/utils/generateBrainAssignation";
|
|
||||||
import {
|
import {
|
||||||
BrainRoleAssignation,
|
BrainRoleAssignation,
|
||||||
BrainRoleType,
|
BrainRoleType,
|
||||||
} from "../components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
} from "../components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
|
import { generateBrainAssignation } from "../components/ShareBrain/utils/generateBrainAssignation";
|
||||||
import { useBrainContext } from "../context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "../context/BrainProvider/hooks/useBrainContext";
|
||||||
|
|
||||||
const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
|
const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
|
||||||
@ -23,7 +23,7 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
const [roleAssignations, setRoleAssignation] = useState<
|
const [roleAssignations, setRoleAssignation] = useState<
|
||||||
BrainRoleAssignation[]
|
BrainRoleAssignation[]
|
||||||
>([generateBrainAssignation()]);
|
>([generateBrainAssignation()]);
|
||||||
const { t } = useTranslation(['brain']);
|
const { t } = useTranslation(["brain"]);
|
||||||
|
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
const brainShareLink = `${baseUrl}/invitation/${brainId}`;
|
const brainShareLink = `${baseUrl}/invitation/${brainId}`;
|
||||||
@ -40,7 +40,7 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
await navigator.clipboard.writeText(brainShareLink);
|
await navigator.clipboard.writeText(brainShareLink);
|
||||||
publish({
|
publish({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
text: t('copiedToClipboard',{ns: 'brain'}),
|
text: t("copiedToClipboard", { ns: "brain" }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
} else {
|
} else {
|
||||||
publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: t("errorSendingInvitation")
|
text: t("errorSendingInvitation"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
"format-fix": "prettier --write ."
|
"format-fix": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@draft-js-plugins/editor": "^4.1.4",
|
||||||
|
"@draft-js-plugins/mention": "^5.2.2",
|
||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@growthbook/growthbook-react": "^0.17.0",
|
"@growthbook/growthbook-react": "^0.17.0",
|
||||||
@ -33,6 +35,7 @@
|
|||||||
"@supabase/auth-ui-shared": "^0.1.6",
|
"@supabase/auth-ui-shared": "^0.1.6",
|
||||||
"@supabase/supabase-js": "^2.22.0",
|
"@supabase/supabase-js": "^2.22.0",
|
||||||
"@types/dom-speech-recognition": "^0.0.1",
|
"@types/dom-speech-recognition": "^0.0.1",
|
||||||
|
"@types/draft-js": "^0.11.12",
|
||||||
"@types/node": "20.1.7",
|
"@types/node": "20.1.7",
|
||||||
"@types/react": "18",
|
"@types/react": "18",
|
||||||
"@types/react-dom": "18",
|
"@types/react-dom": "18",
|
||||||
@ -44,6 +47,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"draft-js": "^0.11.7",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-config-next": "13.4.2",
|
"eslint-config-next": "13.4.2",
|
||||||
|
@ -8,16 +8,15 @@
|
|||||||
"error_occurred": "Error occurred while getting answer",
|
"error_occurred": "Error occurred while getting answer",
|
||||||
"noCurrentBrain": "No current brain",
|
"noCurrentBrain": "No current brain",
|
||||||
"errorParsingData": "Error parsing data",
|
"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.",
|
"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...",
|
"receivedResponse": "Received response. Starting to handle stream...",
|
||||||
"errorCallingAPI": "Error calling the API",
|
"errorCallingAPI": "Error calling the API",
|
||||||
"ask": "Ask a question, or describe a task.",
|
"ask": "Ask a question, or describe a task.",
|
||||||
"begin_conversation_placeholder": "Begin conversation here...",
|
|
||||||
"thinking": "Thinking...",
|
"thinking": "Thinking...",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"errorFetching": "Error occurred while fetching your chats",
|
"errorFetching": "Error occurred while fetching your chats",
|
||||||
"chatDeleted": "Chat sucessfully deleted. Id: {{id}}",
|
"chatDeleted": "Chat successfully deleted. Id: {{id}}",
|
||||||
"errorDeleting": "Error deleting chat: {{error}}",
|
"errorDeleting": "Error deleting chat: {{error}}",
|
||||||
"chatNameUpdated": "Chat name updated",
|
"chatNameUpdated": "Chat name updated",
|
||||||
"shortcut_select_brain": "@: Select a brain to talk",
|
"shortcut_select_brain": "@: Select a brain to talk",
|
||||||
@ -33,5 +32,6 @@
|
|||||||
"empty_brain_title_prefix": "Upload files in a",
|
"empty_brain_title_prefix": "Upload files in a",
|
||||||
"empty_brain_title_suffix": "and chat with them",
|
"empty_brain_title_suffix": "and chat with them",
|
||||||
"actions_bar_placeholder": "Ask a question to @brains or /files and choose your #prompt",
|
"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.",
|
"ask": "Has una pregunta o describe un tarea.",
|
||||||
"begin_conversation_placeholder": "Inicia la conversación aquí...",
|
|
||||||
"brain": "cerebro",
|
"brain": "cerebro",
|
||||||
"brains": "cerebros",
|
"brains": "cerebros",
|
||||||
"chat": "Conversar",
|
"chat": "Conversar",
|
||||||
@ -34,5 +33,6 @@
|
|||||||
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
|
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
|
||||||
"thinking": "Pensando...",
|
"thinking": "Pensando...",
|
||||||
"title": "Conversa con {{brain}}",
|
"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.",
|
"ask": "Posez une question ou décrivez une tâche.",
|
||||||
"begin_conversation_placeholder": "Commencez la conversation ici...",
|
|
||||||
"brain": "cerveau",
|
"brain": "cerveau",
|
||||||
"brains": "cerveaux",
|
"brains": "cerveaux",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
@ -34,5 +33,6 @@
|
|||||||
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
||||||
"thinking": "Réflexion...",
|
"thinking": "Réflexion...",
|
||||||
"title": "Discuter avec {{brain}}",
|
"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",
|
"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.",
|
"ask": "Faça uma pergunta ou descreva uma tarefa.",
|
||||||
"begin_conversation_placeholder": "Inicie a conversa aqui...",
|
|
||||||
"brain": "cérebro",
|
"brain": "cérebro",
|
||||||
"brains": "cérebros",
|
"brains": "cérebros",
|
||||||
"chat": "Conversa",
|
"chat": "Conversa",
|
||||||
@ -34,5 +33,6 @@
|
|||||||
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
||||||
"thinking": "Pensando...",
|
"thinking": "Pensando...",
|
||||||
"title": "Converse com {{brain}}",
|
"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",
|
"actions_bar_placeholder": "Задайте вопрос @brains или /files и выберите ваш #prompt",
|
||||||
"ask": "Задайте вопрос или опишите задачу.",
|
"ask": "Задайте вопрос или опишите задачу.",
|
||||||
"begin_conversation_placeholder": "Начните беседу здесь...",
|
|
||||||
"brain": "мозг",
|
"brain": "мозг",
|
||||||
"brains": "мозги",
|
"brains": "мозги",
|
||||||
"chat": "Чат",
|
"chat": "Чат",
|
||||||
@ -34,5 +33,6 @@
|
|||||||
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
||||||
"thinking": "Думаю...",
|
"thinking": "Думаю...",
|
||||||
"title": "Чат с {{brain}}",
|
"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