mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
@@ -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 (
|
||||
<div className="relative h-screen overflow-auto dark:bg-raisin-black">
|
||||
<div className="relative h-screen overflow-hidden dark:bg-raisin-black">
|
||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||
<ActionButtons showNewChat={true} showShare={true} />
|
||||
<div
|
||||
className={`h-[calc(100dvh-64px)] md:h-screen ${
|
||||
className={`h-[calc(100dvh-64px)] overflow-auto lg:h-screen ${
|
||||
!isMobile
|
||||
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
|
||||
: 'ml-0 md:ml-16'
|
||||
? `ml-0 ${!navOpen ? 'lg:mx-auto' : 'lg:ml-72'}`
|
||||
: 'ml-0 lg:ml-16'
|
||||
}`}
|
||||
>
|
||||
<Outlet />
|
||||
@@ -46,14 +48,13 @@ function MainLayout() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [, , componentMounted] = useDarkTheme();
|
||||
if (!componentMounted) {
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<div className="relative h-full overflow-auto">
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
return (
|
||||
<>
|
||||
{!navOpen && (
|
||||
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all md:block">
|
||||
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all lg:block">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -573,10 +573,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
|
||||
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black lg:hidden">
|
||||
<div className="ml-6 flex h-full items-center gap-6">
|
||||
<button
|
||||
className="h-6 w-6 md:hidden"
|
||||
className="h-6 w-6 lg:hidden"
|
||||
onClick={() => setNavOpen(true)}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -65,22 +65,18 @@ export default function AgentPreview() {
|
||||
);
|
||||
|
||||
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({
|
||||
@@ -95,14 +91,6 @@ export default function AgentPreview() {
|
||||
index: undefined,
|
||||
});
|
||||
}
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,9 +123,7 @@ export default function AgentPreview() {
|
||||
</div>
|
||||
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
<MessageInput
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onSubmit={() => handleQuestionSubmission()}
|
||||
onSubmit={(text) => handleQuestionSubmission(text)}
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={selectedAgent ? false : true}
|
||||
showToolButton={selectedAgent ? false : true}
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="flex w-[95%] max-w-[1500px] flex-col items-center pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
<MessageInput
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onSubmit={() => handleQuestionSubmission()}
|
||||
onSubmit={(text) => handleQuestionSubmission(text)}
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={sharedAgent ? false : true}
|
||||
showToolButton={sharedAgent ? false : true}
|
||||
|
||||
89
frontend/src/components/ActionButtons.tsx
Normal file
89
frontend/src/components/ActionButtons.tsx
Normal file
@@ -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<AppDispatch>();
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const newChat = () => {
|
||||
dispatch(setConversation([]));
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: null },
|
||||
}),
|
||||
);
|
||||
navigate('/');
|
||||
};
|
||||
return (
|
||||
<div className="fixed right-4 top-4 z-10">
|
||||
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
|
||||
{showNewChat && (
|
||||
<button
|
||||
title="Open New Chat"
|
||||
onClick={newChat}
|
||||
className="flex items-center gap-1 rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E] lg:hidden"
|
||||
>
|
||||
<img
|
||||
className="filter dark:invert"
|
||||
alt="NewChat"
|
||||
width={21}
|
||||
height={21}
|
||||
src={newChatIcon}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showShare && conversationId && (
|
||||
<>
|
||||
<button
|
||||
title="Share"
|
||||
onClick={() => setShareModalState(true)}
|
||||
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="filter dark:invert"
|
||||
alt="share"
|
||||
width={16}
|
||||
height={16}
|
||||
src={ShareIcon}
|
||||
/>
|
||||
</button>
|
||||
{isShareModalOpen && (
|
||||
<ShareConversationModal
|
||||
close={() => setShareModalState(false)}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div>{/* <UserButton /> */}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,9 +30,7 @@ import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
|
||||
type MessageInputProps = {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => 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<HTMLTextAreaElement>(null);
|
||||
const sourceButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const toolButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -232,6 +229,11 @@ export default function MessageInput({
|
||||
handleInput();
|
||||
}, []);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value);
|
||||
handleInput();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<div className="mx-2 flex w-full flex-col">
|
||||
@@ -274,11 +279,11 @@ export default function MessageInput({
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
}
|
||||
}}
|
||||
aria-label="Remove attachment"
|
||||
aria-label={t('conversation.attachments.remove')}
|
||||
>
|
||||
<img
|
||||
src={ExitIcon}
|
||||
alt="Remove"
|
||||
alt={t('conversation.attachments.remove')}
|
||||
className="h-2.5 w-2.5 filter dark:invert"
|
||||
/>
|
||||
</button>
|
||||
@@ -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"
|
||||
/>
|
||||
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
Attach
|
||||
{t('conversation.attachments.attach')}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
@@ -406,7 +411,6 @@ export default function MessageInput({
|
||||
onChange={handleFileAttachment}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Additional badges can be added here in the future */}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
key={index}
|
||||
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
dispatch(setSelectedDocs(null));
|
||||
handlePostDocumentSelect(null);
|
||||
} else {
|
||||
dispatch(setSelectedDocs(option));
|
||||
handlePostDocumentSelect(option);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -176,10 +180,7 @@ export default function SourcesPopup({
|
||||
<div
|
||||
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
|
||||
>
|
||||
{selectedDocs &&
|
||||
(option.id
|
||||
? selectedDocs.id === option.id // For documents with MongoDB IDs
|
||||
: selectedDocs.date === option.date) && ( // For preloaded sources
|
||||
{isSelected && (
|
||||
<img
|
||||
src={CheckIcon}
|
||||
alt="Selected"
|
||||
@@ -192,28 +193,6 @@ export default function SourcesPopup({
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<div
|
||||
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
|
||||
onClick={handleEmptyDocumentSelect}
|
||||
>
|
||||
<img
|
||||
width={14}
|
||||
height={14}
|
||||
src={SourceIcon}
|
||||
alt="Source"
|
||||
className="mr-3 flex-shrink-0"
|
||||
/>
|
||||
<span className="mr-3 flex-grow font-medium text-[#5D5D5D] dark:text-bright-gray">
|
||||
{t('none')}
|
||||
</span>
|
||||
<div
|
||||
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
|
||||
>
|
||||
{selectedDocs === null && (
|
||||
<img src={CheckIcon} alt="Selected" className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="p-4 text-center text-gray-500 dark:text-[14px] dark:text-bright-gray">
|
||||
|
||||
@@ -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<string>('');
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
@@ -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 (
|
||||
<div className="flex h-full flex-col justify-end gap-1">
|
||||
{conversationId && queries.length > 0 && (
|
||||
<div className="absolute right-20 top-4">
|
||||
<div className="mt-2 flex items-center gap-4">
|
||||
{isMobile && queries.length > 0 && (
|
||||
<button
|
||||
title="Open New Chat"
|
||||
onClick={() => {
|
||||
newChat();
|
||||
}}
|
||||
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="h-5 w-5 filter dark:invert"
|
||||
alt="NewChat"
|
||||
src={newChatIcon}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
title="Share"
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="h-5 w-5 filter dark:invert"
|
||||
alt="share"
|
||||
src={ShareIcon}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isShareModalOpen && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConversationMessages
|
||||
handleQuestion={handleQuestion}
|
||||
handleQuestionSubmission={handleQuestionSubmission}
|
||||
@@ -258,9 +205,9 @@ export default function Conversation() {
|
||||
</label>
|
||||
<input {...getInputProps()} id="file-upload" />
|
||||
<MessageInput
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onSubmit={handleQuestionSubmission}
|
||||
onSubmit={(text) => {
|
||||
handleQuestionSubmission(text);
|
||||
}}
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={selectedAgent ? false : true}
|
||||
showToolButton={selectedAgent ? false : true}
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
@@ -300,14 +300,14 @@ const ConversationBubble = forwardRef<
|
||||
</p>
|
||||
<div
|
||||
className={`mt-[14px] flex flex-row items-center gap-[6px] underline-offset-2 ${
|
||||
source.source && source.source !== 'local'
|
||||
source.link && source.link !== 'local'
|
||||
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
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<
|
||||
<p
|
||||
className="mt-[2px] truncate text-xs"
|
||||
title={
|
||||
source.source && source.source !== 'local'
|
||||
? source.source
|
||||
source.link && source.link !== 'local'
|
||||
? source.link
|
||||
: source.title
|
||||
}
|
||||
>
|
||||
{source.source && source.source !== 'local'
|
||||
? source.source
|
||||
{source.link && source.link !== 'local'
|
||||
? source.link
|
||||
: source.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@ const ConversationBubble = forwardRef<
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
|
||||
<p className="line-clamp-6 max-h-[164px] overflow-hidden text-ellipsis break-words rounded-md text-sm">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="h-full w-full">
|
||||
<div className="w-full">
|
||||
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
|
||||
<p className="text-left text-xl">{`${sources.sources.length} ${t('conversation.sources.title')}`}</p>
|
||||
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%]"></div>
|
||||
</div>
|
||||
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
|
||||
{sources.sources.map((source, index) => (
|
||||
{sources.sources.map((source, index) => {
|
||||
const isExternalSource = source.link && source.link !== 'local';
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]"
|
||||
className={`group/card relative w-full rounded-[20px] bg-gray-1000 p-4 transition-colors hover:bg-[#F1F1F1] dark:bg-[#28292E] dark:hover:bg-[#2C2E3C] ${
|
||||
isExternalSource ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
isExternalSource && source.link && handleCardClick(source.link)
|
||||
}
|
||||
>
|
||||
<span className="flex flex-row">
|
||||
<p
|
||||
title={source.title}
|
||||
className="ellipsis-text break-words text-left text-sm font-semibold"
|
||||
className={`ellipsis-text break-words text-left text-sm font-semibold ${
|
||||
isExternalSource
|
||||
? 'group-hover/card:text-purple-30 dark:group-hover/card:text-[#8C67D7]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{`${index + 1}. ${source.title}`}
|
||||
</p>
|
||||
{source.source && source.source !== 'local' ? (
|
||||
{isExternalSource && (
|
||||
<img
|
||||
src={Link}
|
||||
alt={'Link'}
|
||||
className="h-3 w-3 cursor-pointer object-fill"
|
||||
onClick={() =>
|
||||
window.open(source.source, '_blank', 'noopener, noreferrer')
|
||||
}
|
||||
></img>
|
||||
) : null}
|
||||
</span>
|
||||
<p className="mt-3 max-h-16 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
|
||||
alt="External Link"
|
||||
className={`ml-1 inline h-3 w-3 object-fill dark:invert ${
|
||||
isExternalSource
|
||||
? 'group-hover/card:contrast-[50%] group-hover/card:hue-rotate-[235deg] group-hover/card:invert-[31%] group-hover/card:saturate-[752%] group-hover/card:sepia-[80%] group-hover/card:filter'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-3 line-clamp-4 break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConversationBubble;
|
||||
|
||||
function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
|
||||
|
||||
@@ -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 && (
|
||||
<button
|
||||
|
||||
@@ -35,7 +35,6 @@ export const SharedConversation = () => {
|
||||
const apiKey = useSelector(selectClientAPIKey);
|
||||
const status = useSelector(selectStatus);
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
@@ -76,15 +75,15 @@ export const SharedConversation = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuestionSubmission = () => {
|
||||
if (input && status !== 'loading') {
|
||||
const handleQuestionSubmission = (question?: string) => {
|
||||
if (question && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
query: {
|
||||
prompt: input,
|
||||
prompt: question,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -93,9 +92,8 @@ export const SharedConversation = () => {
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({ question: input });
|
||||
handleQuestion({ question });
|
||||
}
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,10 +154,12 @@ export const SharedConversation = () => {
|
||||
<div className="flex w-full max-w-[1200px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
{apiKey ? (
|
||||
<MessageInput
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onSubmit={() => handleQuestionSubmission()}
|
||||
onSubmit={(text) => {
|
||||
handleQuestionSubmission(text);
|
||||
}}
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={false}
|
||||
showToolButton={false}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface Query {
|
||||
conversationId?: string | null;
|
||||
title?: string | null;
|
||||
thought?: string;
|
||||
sources?: { title: string; text: string; source: string }[];
|
||||
sources?: { title: string; text: string; link: string }[];
|
||||
tool_calls?: ToolCallsType[];
|
||||
error?: string;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
|
||||
@@ -72,7 +72,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
dispatch(sharedConversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
sharedConversationSlice.actions.raiseError({
|
||||
index: state.conversation.queries.length - 1,
|
||||
index: state.sharedConversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -241,6 +241,11 @@
|
||||
"link": "Source link",
|
||||
"view_more": "{{count}} more sources"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Attach",
|
||||
"remove": "Remove attachment",
|
||||
"uploadFailed": "Upload failed"
|
||||
},
|
||||
"retry": "Retry"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,11 @@
|
||||
"link": "Enlace fuente",
|
||||
"view_more": "Ver {{count}} más fuentes"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Adjuntar",
|
||||
"remove": "Eliminar adjunto",
|
||||
"uploadFailed": "Error al subir"
|
||||
},
|
||||
"retry": "Reintentar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,12 @@
|
||||
"title": "ソース",
|
||||
"text": "ソーステキスト",
|
||||
"link": "ソースリンク",
|
||||
"view_more": "さらに{{count}}個のソースを表示"
|
||||
"view_more": "さらに{{count}}個のソース"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "添付",
|
||||
"remove": "添付ファイルを削除",
|
||||
"uploadFailed": "アップロード失敗"
|
||||
},
|
||||
"retry": "再試行"
|
||||
}
|
||||
|
||||
@@ -238,7 +238,12 @@
|
||||
"title": "Источники",
|
||||
"text": "Текст источника",
|
||||
"link": "Ссылка на источник",
|
||||
"view_more": "Показать еще {{count}} источников"
|
||||
"view_more": "ещё {{count}} источников"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Прикрепить",
|
||||
"remove": "Удалить вложение",
|
||||
"uploadFailed": "Ошибка загрузки"
|
||||
},
|
||||
"retry": "Повторить"
|
||||
}
|
||||
|
||||
@@ -240,6 +240,11 @@
|
||||
"link": "來源連結",
|
||||
"view_more": "查看更多 {{count}} 個來源"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "附件",
|
||||
"remove": "刪除附件",
|
||||
"uploadFailed": "上傳失敗"
|
||||
},
|
||||
"retry": "重試"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,12 @@
|
||||
"title": "来源",
|
||||
"text": "来源文本",
|
||||
"link": "来源链接",
|
||||
"view_more": "更多{{count}}个来源"
|
||||
"view_more": "还有{{count}}个来源"
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "附件",
|
||||
"remove": "删除附件",
|
||||
"uploadFailed": "上传失败"
|
||||
},
|
||||
"retry": "重试"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import Exit from '../assets/exit.svg';
|
||||
|
||||
@@ -45,7 +46,7 @@ export default function WrapperModal({
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
return (
|
||||
const modalContent = (
|
||||
<div className="fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
|
||||
<div
|
||||
ref={modalRef}
|
||||
@@ -63,4 +64,6 @@ export default function WrapperModal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(modalContent, document.body);
|
||||
}
|
||||
|
||||
@@ -17,12 +17,16 @@ type LogsProps = {
|
||||
|
||||
export default function Logs({ agentId, tableHeader }: LogsProps) {
|
||||
const token = useSelector(selectToken);
|
||||
const [logs, setLogs] = useState<LogData[]>([]);
|
||||
const [logsByPage, setLogsByPage] = useState<Record<number, LogData[]>>({});
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingLogs, setLoadingLogs] = useLoaderState(true);
|
||||
|
||||
const logs = Object.values(logsByPage).flat();
|
||||
|
||||
const fetchLogs = async () => {
|
||||
if (logsByPage[page] && logsByPage[page].length > 0) return;
|
||||
|
||||
setLoadingLogs(true);
|
||||
try {
|
||||
const response = await userService.getLogs(
|
||||
@@ -34,9 +38,13 @@ export default function Logs({ agentId, tableHeader }: LogsProps) {
|
||||
token,
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to fetch logs');
|
||||
const olderLogs = await response.json();
|
||||
setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]);
|
||||
setHasMore(olderLogs.has_more);
|
||||
const data = await response.json();
|
||||
|
||||
setLogsByPage((prev) => ({
|
||||
...prev,
|
||||
[page]: data.logs,
|
||||
}));
|
||||
setHasMore(data.has_more);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
@@ -73,16 +81,11 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
|
||||
const [openLogId, setOpenLogId] = useState<string | null>(null);
|
||||
|
||||
const handleLogToggle = (logId: string) => {
|
||||
if (openLogId && openLogId !== logId) {
|
||||
// If a different log is being opened, close the current one
|
||||
const currentOpenLog = document.getElementById(
|
||||
openLogId,
|
||||
) as HTMLDetailsElement;
|
||||
if (currentOpenLog) {
|
||||
currentOpenLog.open = false;
|
||||
}
|
||||
}
|
||||
if (openLogId === logId) {
|
||||
setOpenLogId(null);
|
||||
} else {
|
||||
setOpenLogId(logId);
|
||||
}
|
||||
};
|
||||
|
||||
const firstObserver = useCallback((node: HTMLDivElement | null) => {
|
||||
@@ -116,16 +119,27 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
|
||||
{tableHeader ? tableHeader : t('settings.logs.tableHeader')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-[51vh] flex-grow flex-col items-start gap-2 overflow-y-auto bg-transparent p-4">
|
||||
<div className="relative flex h-[51vh] flex-grow flex-col items-start gap-2 overflow-y-auto overscroll-contain bg-transparent p-4">
|
||||
{logs?.map((log, index) => {
|
||||
if (index === logs.length - 1) {
|
||||
return (
|
||||
<div ref={firstObserver} key={index} className="w-full">
|
||||
<Log log={log} onToggle={handleLogToggle} />
|
||||
<Log
|
||||
log={log}
|
||||
isOpen={openLogId === log.id}
|
||||
onToggle={handleLogToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else
|
||||
return <Log key={index} log={log} onToggle={handleLogToggle} />;
|
||||
return (
|
||||
<Log
|
||||
key={index}
|
||||
log={log}
|
||||
isOpen={openLogId === log.id}
|
||||
onToggle={handleLogToggle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{loading && <SkeletonLoader component="logs" />}
|
||||
</div>
|
||||
@@ -134,9 +148,11 @@ function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
|
||||
}
|
||||
function Log({
|
||||
log,
|
||||
isOpen,
|
||||
onToggle,
|
||||
}: {
|
||||
log: LogData;
|
||||
isOpen: boolean;
|
||||
onToggle: (id: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -148,20 +164,17 @@ function Log({
|
||||
const { id, action, timestamp, ...filteredLog } = log;
|
||||
|
||||
return (
|
||||
<details
|
||||
id={log.id}
|
||||
className="group w-full rounded-xl bg-transparent hover:bg-[#F9F9F9] group-open:opacity-80 hover:dark:bg-dark-charcoal [&[open]]:border [&[open]]:border-[#d9d9d9] [&_summary::-webkit-details-marker]:hidden"
|
||||
onToggle={(e) => {
|
||||
if ((e.target as HTMLDetailsElement).open) {
|
||||
onToggle(log.id);
|
||||
}
|
||||
}}
|
||||
<div className="group w-full rounded-xl bg-transparent hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
|
||||
<div
|
||||
onClick={() => onToggle(log.id)}
|
||||
className={`flex cursor-pointer flex-row items-start gap-2 p-2 px-4 py-3 text-gray-900 ${
|
||||
isOpen ? 'rounded-t-xl bg-[#F1F1F1] dark:bg-[#1B1B1B]' : ''
|
||||
}`}
|
||||
>
|
||||
<summary className="flex cursor-pointer flex-row items-start gap-2 p-2 px-4 py-3 text-gray-900 group-open:rounded-t-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
|
||||
<img
|
||||
src={ChevronRight}
|
||||
alt="Expand log entry"
|
||||
className="mt-[3px] h-3 w-3 transition duration-300 group-open:rotate-90"
|
||||
className={`mt-[3px] h-3 w-3 transition duration-300 ${isOpen ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
<span className="flex flex-row gap-2">
|
||||
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
|
||||
@@ -174,11 +187,14 @@ function Log({
|
||||
: log.question}
|
||||
</h2>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="px-4 py-3 group-open:rounded-b-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
|
||||
<p className="break-words px-2 text-xs leading-relaxed text-gray-700 dark:text-gray-400">
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="rounded-b-xl bg-[#F1F1F1] px-4 py-3 dark:bg-[#1B1B1B]">
|
||||
<div className="scrollbar-thin overflow-y-auto">
|
||||
<pre className="whitespace-pre-wrap break-words px-2 font-mono text-xs leading-relaxed text-gray-700 dark:text-gray-400">
|
||||
{JSON.stringify(filteredLog, null, 2)}
|
||||
</p>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="my-px w-fit">
|
||||
<CopyButton
|
||||
textToCopy={JSON.stringify(filteredLog)}
|
||||
@@ -186,6 +202,7 @@ function Log({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user