diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 41b05ac0..1bba5f44 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import Navigation from './Navigation'; import PageNotFound from './PageNotFound'; import Setting from './settings'; import Agents from './agents'; +import ActionButtons from './components/ActionButtons'; function AuthWrapper({ children }: { children: React.ReactNode }) { const { isAuthLoading } = useTokenAuth(); @@ -32,13 +33,14 @@ function MainLayout() { const [navOpen, setNavOpen] = useState(!isMobile); return ( -
+
+
@@ -46,14 +48,13 @@ function MainLayout() {
); } - export default function App() { const [, , componentMounted] = useDarkTheme(); if (!componentMounted) { return
; } return ( -
+
{!navOpen && ( -
+
-
+
setInput(e.target.value)} - onSubmit={() => handleQuestionSubmission()} + onSubmit={(text) => handleQuestionSubmission(text)} loading={status === 'loading'} showSourceButton={selectedAgent ? false : true} showToolButton={selectedAgent ? false : true} diff --git a/frontend/src/agents/SharedAgent.tsx b/frontend/src/agents/SharedAgent.tsx index 98827f1d..8c7e76c9 100644 --- a/frontend/src/agents/SharedAgent.tsx +++ b/frontend/src/agents/SharedAgent.tsx @@ -91,22 +91,18 @@ export default function SharedAgent() { ); const handleQuestionSubmission = ( - updatedQuestion?: string, + question?: string, updated?: boolean, indx?: number, ) => { - if ( - updated === true && - updatedQuestion !== undefined && - indx !== undefined - ) { + if (updated === true && question !== undefined && indx !== undefined) { handleQuestion({ - question: updatedQuestion, + question, index: indx, isRetry: false, }); - } else if (input.trim() && status !== 'loading') { - const currentInput = input.trim(); + } else if (question && status !== 'loading') { + const currentInput = question.trim(); if (lastQueryReturnedErr && queries.length > 0) { const lastQueryIndex = queries.length - 1; handleQuestion({ @@ -183,9 +179,7 @@ export default function SharedAgent() {
setInput(e.target.value)} - onSubmit={() => handleQuestionSubmission()} + onSubmit={(text) => handleQuestionSubmission(text)} loading={status === 'loading'} showSourceButton={sharedAgent ? false : true} showToolButton={sharedAgent ? false : true} diff --git a/frontend/src/components/ActionButtons.tsx b/frontend/src/components/ActionButtons.tsx new file mode 100644 index 00000000..3bd71515 --- /dev/null +++ b/frontend/src/components/ActionButtons.tsx @@ -0,0 +1,89 @@ +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import newChatIcon from '../assets/openNewChat.svg'; +import ShareIcon from '../assets/share.svg'; +import { ShareConversationModal } from '../modals/ShareConversationModal'; +import { useState } from 'react'; +import { selectConversationId } from '../preferences/preferenceSlice'; +import { useDispatch } from 'react-redux'; +import { AppDispatch } from '../store'; +import { + setConversation, + updateConversationId, +} from '../conversation/conversationSlice'; + +interface ActionButtonsProps { + className?: string; + showNewChat?: boolean; + showShare?: boolean; +} + +import { useNavigate } from 'react-router-dom'; + +export default function ActionButtons({ + className = '', + showNewChat = true, + showShare = true, +}: ActionButtonsProps) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const conversationId = useSelector(selectConversationId); + const [isShareModalOpen, setShareModalState] = useState(false); + const navigate = useNavigate(); + + const newChat = () => { + dispatch(setConversation([])); + dispatch( + updateConversationId({ + query: { conversationId: null }, + }), + ); + navigate('/'); + }; + return ( +
+
+ {showNewChat && ( + + )} + + {showShare && conversationId && ( + <> + + {isShareModalOpen && ( + setShareModalState(false)} + conversationId={conversationId} + /> + )} + + )} +
{/* */}
+
+
+ ); +} diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 60cd4b81..79487188 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -30,9 +30,7 @@ import SourcesPopup from './SourcesPopup'; import ToolsPopup from './ToolsPopup'; type MessageInputProps = { - value: string; - onChange: (e: React.ChangeEvent) => void; - onSubmit: () => void; + onSubmit: (text: string) => void; loading: boolean; showSourceButton?: boolean; showToolButton?: boolean; @@ -40,8 +38,6 @@ type MessageInputProps = { }; export default function MessageInput({ - value, - onChange, onSubmit, loading, showSourceButton = true, @@ -50,6 +46,7 @@ export default function MessageInput({ }: MessageInputProps) { const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); + const [value, setValue] = useState(''); const inputRef = useRef(null); const sourceButtonRef = useRef(null); const toolButtonRef = useRef(null); @@ -232,6 +229,11 @@ export default function MessageInput({ handleInput(); }, []); + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + handleInput(); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -248,7 +250,10 @@ export default function MessageInput({ }; const handleSubmit = () => { - onSubmit(); + if (value.trim() && !loading) { + onSubmit(value); + setValue(''); + } }; return (
@@ -274,11 +279,11 @@ export default function MessageInput({ dispatch(removeAttachment(attachment.id)); } }} - aria-label="Remove attachment" + aria-label={t('conversation.attachments.remove')} > Remove @@ -334,7 +339,7 @@ export default function MessageInput({ id="message-input" ref={inputRef} value={value} - onChange={onChange} + onChange={handleChange} tabIndex={1} placeholder={t('inputPlaceholder')} className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5" @@ -398,7 +403,7 @@ export default function MessageInput({ className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4" /> - Attach + {t('conversation.attachments.attach')} - {/* Additional badges can be added here in the future */}
diff --git a/frontend/src/components/SourcesPopup.tsx b/frontend/src/components/SourcesPopup.tsx index 6846cf84..83966730 100644 --- a/frontend/src/components/SourcesPopup.tsx +++ b/frontend/src/components/SourcesPopup.tsx @@ -81,12 +81,6 @@ export default function SourcesPopup({ return () => window.removeEventListener('resize', updatePosition); }, [isOpen, anchorRef]); - const handleEmptyDocumentSelect = () => { - dispatch(setSelectedDocs(null)); - handlePostDocumentSelect(null); - onClose(); - }; - const handleClickOutside = (event: MouseEvent) => { if ( popupRef.current && @@ -153,14 +147,24 @@ export default function SourcesPopup({ <> {filteredOptions?.map((option: any, index: number) => { if (option.model === embeddingsName) { + const isSelected = + selectedDocs && + (option.id + ? selectedDocs.id === option.id + : selectedDocs.date === option.date); + return (
{ - dispatch(setSelectedDocs(option)); - handlePostDocumentSelect(option); - onClose(); + if (isSelected) { + dispatch(setSelectedDocs(null)); + handlePostDocumentSelect(null); + } else { + dispatch(setSelectedDocs(option)); + handlePostDocumentSelect(option); + } }} > - {selectedDocs && - (option.id - ? selectedDocs.id === option.id // For documents with MongoDB IDs - : selectedDocs.date === option.date) && ( // For preloaded sources - Selected - )} + {isSelected && ( + Selected + )}
); } return null; })} -
- Source - - {t('none')} - -
- {selectedDocs === null && ( - Selected - )} -
-
) : (
diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 21fc3a1f..753a3725 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -4,11 +4,8 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import DragFileUpload from '../assets/DragFileUpload.svg'; -import newChatIcon from '../assets/openNewChat.svg'; -import ShareIcon from '../assets/share.svg'; import MessageInput from '../components/MessageInput'; import { useMediaQuery } from '../hooks'; -import { ShareConversationModal } from '../modals/ShareConversationModal'; import { ActiveState } from '../models/misc'; import { selectConversationId, @@ -42,7 +39,6 @@ export default function Conversation() { const conversationId = useSelector(selectConversationId); const selectedAgent = useSelector(selectSelectedAgent); - const [input, setInput] = useState(''); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); const [files, setFiles] = useState([]); @@ -146,19 +142,19 @@ export default function Conversation() { }; const handleQuestionSubmission = ( - updatedQuestion?: string, + question?: string, updated?: boolean, indx?: number, ) => { if (updated === true) { - handleQuestion({ question: updatedQuestion as string, index: indx }); - } else if (input && status !== 'loading') { + handleQuestion({ question: question as string, index: indx }); + } else if (question && status !== 'loading') { if (lastQueryReturnedErr) { dispatch( updateQuery({ index: queries.length - 1, query: { - prompt: input, + prompt: question, }, }), ); @@ -168,10 +164,9 @@ export default function Conversation() { }); } else { handleQuestion({ - question: input, + question, }); } - setInput(''); } }; @@ -184,10 +179,6 @@ export default function Conversation() { ); }; - const newChat = () => { - if (queries && queries.length > 0) resetConversation(); - }; - useEffect(() => { if (queries.length) { queries[queries.length - 1].error && setLastQueryReturnedErr(true); @@ -196,50 +187,6 @@ export default function Conversation() { }, [queries[queries.length - 1]]); return (
- {conversationId && queries.length > 0 && ( -
-
- {isMobile && queries.length > 0 && ( - - )} - - -
- {isShareModalOpen && ( - { - setShareModalState(false); - }} - conversationId={conversationId} - /> - )} -
- )} - setInput(e.target.value)} - onSubmit={handleQuestionSubmission} + onSubmit={(text) => { + handleQuestionSubmission(text); + }} loading={status === 'loading'} showSourceButton={selectedAgent ? false : true} showToolButton={selectedAgent ? false : true} diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 2f9891b5..920005e3 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -49,7 +49,7 @@ const ConversationBubble = forwardRef< feedback?: FEEDBACK; handleFeedback?: (feedback: FEEDBACK) => void; thought?: string; - sources?: { title: string; text: string; source: string }[]; + sources?: { title: string; text: string; link: string }[]; toolCalls?: ToolCallsType[]; retryBtn?: React.ReactElement; questionNumber?: number; @@ -233,7 +233,7 @@ const ConversationBubble = forwardRef< {DisableSourceFE || type === 'ERROR' || sources?.length === 0 || - sources?.some((source) => source.source === 'None') ? null : !sources && + sources?.some((source) => source.link === 'None') ? null : !sources && chunks !== '0' && selectedDocs ? (
@@ -300,14 +300,14 @@ const ConversationBubble = forwardRef<

- source.source && source.source !== 'local' + source.link && source.link !== 'local' ? window.open( - source.source, + source.link, '_blank', 'noopener, noreferrer', ) @@ -322,13 +322,13 @@ const ConversationBubble = forwardRef<

- {source.source && source.source !== 'local' - ? source.source + {source.link && source.link !== 'local' + ? source.link : source.title}

@@ -339,7 +339,7 @@ const ConversationBubble = forwardRef< onMouseOver={() => setActiveTooltip(index)} onMouseOut={() => setActiveTooltip(null)} > -

+

{source.text}

@@ -649,50 +649,68 @@ const ConversationBubble = forwardRef< }); type AllSourcesProps = { - sources: { title: string; text: string; source: string }[]; + sources: { title: string; text: string; link?: string }[]; }; function AllSources(sources: AllSourcesProps) { + const { t } = useTranslation(); + + const handleCardClick = (link: string) => { + if (link && link !== 'local') { + window.open(link, '_blank', 'noopener,noreferrer'); + } + }; + return (
-

{`${sources.sources.length} Sources`}

+

{`${sources.sources.length} ${t('conversation.sources.title')}`}

- {sources.sources.map((source, index) => ( -
- + {sources.sources.map((source, index) => { + const isExternalSource = source.link && source.link !== 'local'; + return ( +
+ isExternalSource && source.link && handleCardClick(source.link) + } + >

{`${index + 1}. ${source.title}`} + {isExternalSource && ( + External Link + )}

- {source.source && source.source !== 'local' ? ( - {'Link'} - window.open(source.source, '_blank', 'noopener, noreferrer') - } - > - ) : null} - -

- {source.text} -

-
- ))} +

+ {source.text} +

+
+ ); + })}
); } - export default ConversationBubble; function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) { diff --git a/frontend/src/conversation/ConversationMessages.tsx b/frontend/src/conversation/ConversationMessages.tsx index 2369dbc6..5c2150a6 100644 --- a/frontend/src/conversation/ConversationMessages.tsx +++ b/frontend/src/conversation/ConversationMessages.tsx @@ -190,7 +190,7 @@ export default function ConversationMessages({ ref={conversationRef} onWheel={handleUserScrollInterruption} onTouchMove={handleUserScrollInterruption} - className="flex h-full w-full justify-center overflow-y-auto sm:pt-12" + className="flex h-full w-full justify-center overflow-y-auto will-change-scroll sm:pt-6 lg:pt-12" > {queries.length > 0 && !hasScrolledToLast && (