diff --git a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/Editor.tsx b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/Editor.tsx index bb3d5a6c2bfe..c1b5617615b3 100644 --- a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/Editor.tsx +++ b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/Editor.tsx @@ -4,39 +4,34 @@ import "./styles.css"; import { useChatStateUpdater } from "./hooks/useChatStateUpdater"; import { useCreateEditorState } from "./hooks/useCreateEditorState"; import { useEditor } from "./hooks/useEditor"; -import { useEditorStateUpdater } from "./hooks/useEditorStateUpdater"; type EditorProps = { onSubmit: () => void; setMessage: (text: string) => void; message: string; + placeholder?: string; }; export const Editor = ({ setMessage, - message, onSubmit, + placeholder, }: EditorProps): JSX.Element => { - const { editor } = useCreateEditorState(); + const { editor } = useCreateEditorState(placeholder); useChatStateUpdater({ editor, setMessage, }); - useEditorStateUpdater({ - editor, - message, - }); - const { submitOnEnter } = useEditor({ onSubmit, }); return ( void submitOnEnter(event)} editor={editor} /> ); diff --git a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useCreateEditorState.ts b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useCreateEditorState.ts index 359a0495eb11..d11d2a6880fd 100644 --- a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useCreateEditorState.ts +++ b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useCreateEditorState.ts @@ -1,20 +1,28 @@ -import Document from "@tiptap/extension-document"; -import HardBreak from "@tiptap/extension-hard-break"; -import Paragraph from "@tiptap/extension-paragraph"; -import Placeholder from "@tiptap/extension-placeholder"; -import Text from "@tiptap/extension-text"; -import { useEditor } from "@tiptap/react"; +import { Document } from "@tiptap/extension-document"; +import { HardBreak } from "@tiptap/extension-hard-break"; +import { Paragraph } from "@tiptap/extension-paragraph"; +import { Placeholder } from "@tiptap/extension-placeholder"; +import { Text } from "@tiptap/extension-text"; +import { Extension, useEditor } from "@tiptap/react"; import { useTranslation } from "react-i18next"; import { useBrainMention } from "./useBrainMention"; import { usePromptMention } from "./usePromptSuggestionsConfig"; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const useCreateEditorState = () => { +export const useCreateEditorState = (placeholder?: string) => { const { t } = useTranslation(["chat"]); const { BrainMention, items } = useBrainMention(); const { PromptMention } = usePromptMention(); + const PreventEnter = Extension.create({ + addKeyboardShortcuts: () => { + return { + Enter: () => true, + }; + }, + }); + const editor = useEditor( { autofocus: true, @@ -22,9 +30,10 @@ export const useCreateEditorState = () => { editor?.commands.focus("end"); }, extensions: [ + PreventEnter, Placeholder.configure({ showOnlyWhenEditable: true, - placeholder: t("actions_bar_placeholder"), + placeholder: placeholder ?? t("actions_bar_placeholder"), }), Document, Text, diff --git a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditor.ts b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditor.ts index 41373010dd60..3a4b7ac206c3 100644 --- a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditor.ts +++ b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditor.ts @@ -7,8 +7,7 @@ type UseEditorProps = { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const useEditor = ({ onSubmit }: UseEditorProps) => { const submitOnEnter = (ev: KeyboardEvent) => { - if (ev.key === "Enter" && !ev.shiftKey) { - ev.preventDefault(); + if (ev.key === "Enter" && !ev.shiftKey && !ev.metaKey) { onSubmit(); } }; diff --git a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditorStateUpdater.ts b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditorStateUpdater.ts deleted file mode 100644 index 4bc59a435478..000000000000 --- a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/hooks/useEditorStateUpdater.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { useCallback, useEffect } from "react"; - -import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; - -import { getChatInputAttributesFromEditorState } from "../utils/getChatInputAttributesFromEditorState"; - -type UseEditorStateUpdaterProps = { - editor: Editor | null; - message: string; -}; - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const useEditorStateUpdater = ({ - message, - editor, -}: UseEditorStateUpdaterProps) => { - const { currentBrain, currentPrompt } = useBrainContext(); - - const setCurrentBrainAndPrompt = useCallback(() => { - const { promptId, brainId } = getChatInputAttributesFromEditorState(editor); - if (currentBrain !== undefined && currentBrain.id !== brainId) { - editor - ?.chain() - .focus() - .insertContent({ - type: "mention@", - attrs: { - id: currentBrain.id, - label: currentBrain.name, - }, - }) - .insertContent({ - type: "text", - text: " ", - }) - .run(); - } - - if ( - currentPrompt !== undefined && - currentPrompt.id !== promptId && - promptId === "" - ) { - editor - ?.chain() - .focus() - .insertContent({ - type: "mention#", - attrs: { - id: currentPrompt.id, - label: currentPrompt.title, - }, - }) - .insertContent({ - type: "text", - text: " ", - }) - .run(); - } - }, [currentBrain, currentPrompt, editor]); - - useEffect(() => { - setCurrentBrainAndPrompt(); - }, [setCurrentBrainAndPrompt]); - - useEffect(() => { - const { text } = getChatInputAttributesFromEditorState(editor); - if (text !== message) { - editor?.commands.setContent(message); - } - setCurrentBrainAndPrompt(); - }, [editor, message, setCurrentBrainAndPrompt]); -}; diff --git a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/MenuControlButton/MenuControlButton.module.scss b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/MenuControlButton/MenuControlButton.module.scss index d539ead5219a..8568abb6c588 100644 --- a/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/MenuControlButton/MenuControlButton.module.scss +++ b/frontend/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/MenuControlButton/MenuControlButton.module.scss @@ -1,13 +1,13 @@ -@use '@/styles/Colors.module.scss'; -@use '@/styles/IconSizes.module.scss'; -@use '@/styles/Spacings.module.scss'; +@use "@/styles/Colors.module.scss"; +@use "@/styles/IconSizes.module.scss"; +@use "@/styles/Spacings.module.scss"; .menu_icon { - width: IconSizes.$medium; - height: IconSizes.$medium; - cursor: pointer; + width: IconSizes.$large; + height: IconSizes.$large; + cursor: pointer; - &:hover { - color: Colors.$accent; - } -} \ No newline at end of file + &:hover { + color: Colors.$accent; + } +} diff --git a/frontend/lib/components/ui/Button.tsx b/frontend/lib/components/ui/Button.tsx index a0f004dffd1b..acb95a0ab9e1 100644 --- a/frontend/lib/components/ui/Button.tsx +++ b/frontend/lib/components/ui/Button.tsx @@ -7,9 +7,9 @@ import { } from "@radix-ui/react-tooltip"; import { cva, type VariantProps } from "class-variance-authority"; import { ButtonHTMLAttributes, Ref, RefAttributes, forwardRef } from "react"; -import { FaSpinner } from "react-icons/fa"; import { cn } from "@/lib/utils"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; const ButtonVariants = cva( "px-8 py-3 text-sm disabled:opacity-80 text-center font-medium rounded-md focus:ring ring-primary/10 outline-none flex items-center justify-center gap-2 transition-opacity focus:ring-0", @@ -65,7 +65,8 @@ const Button = forwardRef( const buttonChildren = ( <> - {children} {isLoading && } + {children}{" "} + {isLoading && } ); diff --git a/frontend/lib/components/ui/LoaderIcon/LoaderIcon.module.scss b/frontend/lib/components/ui/LoaderIcon/LoaderIcon.module.scss new file mode 100644 index 000000000000..ea786e3fc46d --- /dev/null +++ b/frontend/lib/components/ui/LoaderIcon/LoaderIcon.module.scss @@ -0,0 +1,38 @@ +@use "@/styles/Colors.module.scss"; +@use "@/styles/IconSizes.module.scss"; + +.loader_icon { + animation: spin 1s linear infinite; + width: IconSizes.$big; + height: IconSizes.$big; + color: Colors.$accent; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + &.small { + width: IconSizes.$small; + height: IconSizes.$small; + } + + &.normal { + width: IconSizes.$normal; + height: IconSizes.$normal; + } + + &.large { + width: IconSizes.$large; + height: IconSizes.$large; + } + + &.big { + width: IconSizes.$big; + height: IconSizes.$big; + } +} diff --git a/frontend/lib/components/ui/LoaderIcon/LoaderIcon.tsx b/frontend/lib/components/ui/LoaderIcon/LoaderIcon.tsx new file mode 100644 index 000000000000..6cc9b555d7c2 --- /dev/null +++ b/frontend/lib/components/ui/LoaderIcon/LoaderIcon.tsx @@ -0,0 +1,17 @@ +import { AiOutlineLoading3Quarters } from "react-icons/ai"; + +import { IconSize } from "@/lib/types/Icons"; + +import styles from "./LoaderIcon.module.scss"; + +interface LoaderIconProps { + size: IconSize; +} + +export const LoaderIcon = (props: LoaderIconProps): JSX.Element => { + return ( + + ); +}; diff --git a/frontend/lib/components/ui/SearchBar/SearchBar.module.scss b/frontend/lib/components/ui/SearchBar/SearchBar.module.scss index ec137c25916f..1c81396b2b26 100644 --- a/frontend/lib/components/ui/SearchBar/SearchBar.module.scss +++ b/frontend/lib/components/ui/SearchBar/SearchBar.module.scss @@ -1,41 +1,32 @@ -@use '@/styles/Colors.module.scss'; -@use '@/styles/IconSizes.module.scss'; -@use '@/styles/Spacings.module.scss'; +@use "@/styles/Colors.module.scss"; +@use "@/styles/IconSizes.module.scss"; +@use "@/styles/Spacings.module.scss"; .search_bar_wrapper { - display: flex; - justify-content: space-between; - align-items: center; - gap: Spacings.$spacing03; - background-color: Colors.$white; - padding: Spacings.$spacing05; - border-radius: 12px; - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + display: flex; + justify-content: space-between; + align-items: center; + gap: Spacings.$spacing03; + background-color: Colors.$white; + padding: Spacings.$spacing05; + border-radius: 12px; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - &:hover { - box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - } - - .search_input { - border: none; - flex: 1; - caret-color: Colors.$accent; - - &:focus { - box-shadow: none; - } - } + &:hover { + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), + 0 8px 10px -6px rgb(0 0 0 / 0.1); + } - .search_icon { - width: IconSizes.$big; - height: IconSizes.$big; - color: Colors.$accent; - cursor: pointer; + .search_icon { + width: IconSizes.$big; + height: IconSizes.$big; + color: Colors.$accent; + cursor: pointer; - &.disabled { - color: Colors.$black; - pointer-events: none; - opacity: 0.2; - } + &.disabled { + color: Colors.$black; + pointer-events: none; + opacity: 0.2; } -} \ No newline at end of file + } +} diff --git a/frontend/lib/components/ui/SearchBar/SearchBar.tsx b/frontend/lib/components/ui/SearchBar/SearchBar.tsx index fdde60ca923e..a3e56829f0aa 100644 --- a/frontend/lib/components/ui/SearchBar/SearchBar.tsx +++ b/frontend/lib/components/ui/SearchBar/SearchBar.tsx @@ -1,14 +1,18 @@ -import { ChangeEvent, useEffect } from "react"; +import { useEffect, useState } from "react"; import { LuSearch } from "react-icons/lu"; +import { Editor } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/ChatEditor/components/Editor/Editor"; import { useChatInput } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/hooks/useChatInput"; import { useChat } from "@/app/chat/[chatId]/hooks/useChat"; import { useChatContext } from "@/lib/context"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; +import { LoaderIcon } from "../LoaderIcon/LoaderIcon"; +// eslint-disable-next-line import/order import styles from "./SearchBar.module.scss"; export const SearchBar = (): JSX.Element => { + const [searching, setSearching] = useState(false); const { message, setMessage } = useChatInput(); const { setMessages } = useChatContext(); const { addQuestion } = useChat(); @@ -18,24 +22,15 @@ export const SearchBar = (): JSX.Element => { setCurrentBrainId(null); }); - const handleChange = (event: ChangeEvent): void => { - setMessage(event.target.value); - }; - - const handleEnter = async ( - event: React.KeyboardEvent - ): Promise => { - if (event.key === "Enter") { - await submit(); - } - }; - const submit = async (): Promise => { + setSearching(true); setMessages([]); try { await addQuestion(message); } catch (error) { console.error(error); + } finally { + setSearching(false); } }; @@ -43,18 +38,20 @@ export const SearchBar = (): JSX.Element => { return (
- void submit()} placeholder="Search" - value={message} - onChange={handleChange} - onKeyDown={(event) => void handleEnter(event)} - /> - void submit()} - /> + > + {searching ? ( + + ) : ( + void submit()} + /> + )}
); }; diff --git a/frontend/lib/types/Icons.ts b/frontend/lib/types/Icons.ts new file mode 100644 index 000000000000..bc527f7edc84 --- /dev/null +++ b/frontend/lib/types/Icons.ts @@ -0,0 +1 @@ +export type IconSize = "small" | "normal" | "large" | "big"; diff --git a/frontend/styles/_IconSizes.module.scss b/frontend/styles/_IconSizes.module.scss index 34aa7faf5a99..77a59260f0c8 100644 --- a/frontend/styles/_IconSizes.module.scss +++ b/frontend/styles/_IconSizes.module.scss @@ -1,2 +1,4 @@ +$small: 12px; +$normal: 18px; +$large: 24px; $big: 30px; -$medium: 24px; \ No newline at end of file