Merge pull request #1789 from ManishMadan2882/main

Frontend Fixes
This commit is contained in:
Alex
2025-05-21 01:05:47 +03:00
committed by GitHub
21 changed files with 313 additions and 245 deletions

View File

@@ -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={

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View 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>
);
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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[] }) {

View File

@@ -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

View File

@@ -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

View File

@@ -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 }[];

View File

@@ -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,
}),
);

View File

@@ -241,6 +241,11 @@
"link": "Source link",
"view_more": "{{count}} more sources"
},
"attachments": {
"attach": "Attach",
"remove": "Remove attachment",
"uploadFailed": "Upload failed"
},
"retry": "Retry"
}
}

View File

@@ -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"
}
}

View File

@@ -238,7 +238,12 @@
"title": "ソース",
"text": "ソーステキスト",
"link": "ソースリンク",
"view_more": "さらに{{count}}個のソースを表示"
"view_more": "さらに{{count}}個のソース"
},
"attachments": {
"attach": "添付",
"remove": "添付ファイルを削除",
"uploadFailed": "アップロード失敗"
},
"retry": "再試行"
}

View File

@@ -238,7 +238,12 @@
"title": "Источники",
"text": "Текст источника",
"link": "Ссылка на источник",
"view_more": "Показать еще {{count}} источников"
"view_more": "ещё {{count}} источников"
},
"attachments": {
"attach": "Прикрепить",
"remove": "Удалить вложение",
"uploadFailed": "Ошибка загрузки"
},
"retry": "Повторить"
}

View File

@@ -240,6 +240,11 @@
"link": "來源連結",
"view_more": "查看更多 {{count}} 個來源"
},
"attachments": {
"attach": "附件",
"remove": "刪除附件",
"uploadFailed": "上傳失敗"
},
"retry": "重試"
}
}

View File

@@ -238,7 +238,12 @@
"title": "来源",
"text": "来源文本",
"link": "来源链接",
"view_more": "更多{{count}}个来源"
"view_more": "还有{{count}}个来源"
},
"attachments": {
"attach": "附件",
"remove": "删除附件",
"uploadFailed": "上传失败"
},
"retry": "重试"
}

View File

@@ -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);
}

View File

@@ -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>
);
}