mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 17:43:15 +00:00
Merge branch 'main' into feat/agent-menu
This commit is contained in:
@@ -4,13 +4,20 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import endpoints from '../api/endpoints';
|
||||
import userService from '../api/services/userService';
|
||||
import AlertIcon from '../assets/alert.svg';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import ExitIcon from '../assets/exit.svg';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import { setAttachments } from '../conversation/conversationSlice';
|
||||
import {
|
||||
addAttachment,
|
||||
removeAttachment,
|
||||
selectAttachments,
|
||||
updateAttachment,
|
||||
} from '../conversation/conversationSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
@@ -18,6 +25,7 @@ import {
|
||||
selectToken,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import { getOS, isTouchDevice } from '../utils/browserUtils';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
|
||||
@@ -56,13 +64,35 @@ export default function MessageInput({
|
||||
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [uploads, setUploads] = useState<UploadState[]>([]);
|
||||
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const token = useSelector(selectToken);
|
||||
const attachments = useSelector(selectAttachments);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const browserOS = getOS();
|
||||
const isTouch = isTouchDevice();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
((browserOS === 'win' || browserOS === 'linux') &&
|
||||
event.ctrlKey &&
|
||||
event.key === 'k') ||
|
||||
(browserOS === 'mac' && event.metaKey && event.key === 'k')
|
||||
) {
|
||||
event.preventDefault();
|
||||
setIsSourcesPopupOpen(!isSourcesPopupOpen);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [browserOS]);
|
||||
|
||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
@@ -73,23 +103,23 @@ export default function MessageInput({
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const uploadState: UploadState = {
|
||||
taskId: '',
|
||||
const newAttachment = {
|
||||
fileName: file.name,
|
||||
progress: 0,
|
||||
status: 'uploading',
|
||||
status: 'uploading' as const,
|
||||
taskId: '',
|
||||
};
|
||||
|
||||
setUploads((prev) => [...prev, uploadState]);
|
||||
const uploadIndex = uploads.length;
|
||||
dispatch(addAttachment(newAttachment));
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progress = Math.round((event.loaded / event.total) * 100);
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, progress } : upload,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { progress },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -97,34 +127,35 @@ export default function MessageInput({
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
console.log('File uploaded successfully:', response);
|
||||
|
||||
if (response.task_id) {
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, taskId: response.task_id, status: 'processing' }
|
||||
: upload,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, status: 'failed' } : upload,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
console.error('Error uploading file:', xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, status: 'failed' } : upload,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
console.error('Network error during file upload');
|
||||
};
|
||||
|
||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||
@@ -134,69 +165,63 @@ export default function MessageInput({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutIds: number[] = [];
|
||||
|
||||
const checkTaskStatus = () => {
|
||||
const processingUploads = uploads.filter(
|
||||
(upload) => upload.status === 'processing' && upload.taskId,
|
||||
const processingAttachments = attachments.filter(
|
||||
(att) => att.status === 'processing' && att.taskId,
|
||||
);
|
||||
|
||||
processingUploads.forEach((upload) => {
|
||||
processingAttachments.forEach((attachment) => {
|
||||
userService
|
||||
.getTaskStatus(upload.taskId, null)
|
||||
.getTaskStatus(attachment.taskId!, null)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
console.log('Task status:', data);
|
||||
|
||||
setUploads((prev) =>
|
||||
prev.map((u) => {
|
||||
if (u.taskId !== upload.taskId) return u;
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
return {
|
||||
...u,
|
||||
if (data.status === 'SUCCESS') {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
attachment_id: data.result?.attachment_id,
|
||||
id: data.result?.attachment_id,
|
||||
token_count: data.result?.token_count,
|
||||
};
|
||||
} else if (data.status === 'FAILURE') {
|
||||
return { ...u, status: 'failed' };
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
return { ...u, progress: data.result.current };
|
||||
}
|
||||
return u;
|
||||
}),
|
||||
);
|
||||
|
||||
if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') {
|
||||
const timeoutId = window.setTimeout(
|
||||
() => checkTaskStatus(),
|
||||
2000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (data.status === 'FAILURE') {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { progress: data.result.current },
|
||||
}),
|
||||
);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking task status:', error);
|
||||
setUploads((prev) =>
|
||||
prev.map((u) =>
|
||||
u.taskId === upload.taskId ? { ...u, status: 'failed' } : u,
|
||||
),
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (uploads.some((upload) => upload.status === 'processing')) {
|
||||
const timeoutId = window.setTimeout(checkTaskStatus, 2000);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
if (attachments.some((att) => att.status === 'processing')) {
|
||||
checkTaskStatus();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
timeoutIds.forEach((id) => clearTimeout(id));
|
||||
};
|
||||
}, [uploads]);
|
||||
return () => clearInterval(interval);
|
||||
}, [attachments, dispatch]);
|
||||
|
||||
const handleInput = () => {
|
||||
if (inputRef.current) {
|
||||
@@ -230,42 +255,56 @@ export default function MessageInput({
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const completedAttachments = uploads
|
||||
.filter((upload) => upload.status === 'completed' && upload.attachment_id)
|
||||
.map((upload) => ({
|
||||
fileName: upload.fileName,
|
||||
id: upload.attachment_id as string,
|
||||
}));
|
||||
|
||||
dispatch(setAttachments(completedAttachments));
|
||||
|
||||
onSubmit();
|
||||
};
|
||||
return (
|
||||
<div className="mx-2 flex w-full flex-col">
|
||||
<div className="relative flex w-full flex-col rounded-[23px] border border-dark-gray bg-lotion dark:border-grey dark:bg-transparent">
|
||||
<div className="flex flex-wrap gap-1.5 px-4 pb-0 pt-3 sm:gap-2 sm:px-6">
|
||||
{uploads.map((upload, index) => (
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px]"
|
||||
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||
}`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||
{upload.fileName}
|
||||
{attachment.fileName}
|
||||
</span>
|
||||
|
||||
{upload.status === 'completed' && (
|
||||
<span className="ml-2 text-green-500">✓</span>
|
||||
{attachment.status === 'completed' && (
|
||||
<button
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
|
||||
onClick={() => {
|
||||
if (attachment.id) {
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
}
|
||||
}}
|
||||
aria-label="Remove attachment"
|
||||
>
|
||||
<img
|
||||
src={ExitIcon}
|
||||
alt="Remove"
|
||||
className="h-2.5 w-2.5 filter dark:invert"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{upload.status === 'failed' && (
|
||||
<span className="ml-2 text-red-500">✗</span>
|
||||
{attachment.status === 'failed' && (
|
||||
<img
|
||||
src={AlertIcon}
|
||||
alt="Upload failed"
|
||||
className="ml-2 h-3.5 w-3.5"
|
||||
title="Upload failed"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(upload.status === 'uploading' ||
|
||||
upload.status === 'processing') && (
|
||||
{(attachment.status === 'uploading' ||
|
||||
attachment.status === 'processing') && (
|
||||
<div className="relative ml-2 h-4 w-4">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
cx="12"
|
||||
@@ -284,7 +323,7 @@ export default function MessageInput({
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeDasharray="62.83"
|
||||
strokeDashoffset={62.83 - (upload.progress / 100) * 62.83}
|
||||
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
@@ -317,19 +356,29 @@ export default function MessageInput({
|
||||
{showSourceButton && (
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
|
||||
className="xs:px-3 xs:py-1.5 flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C] sm:max-w-[150px]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
title={
|
||||
selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={SourceIcon}
|
||||
alt="Sources"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
{!isTouch && (
|
||||
<span className="ml-1 hidden text-[10px] text-gray-500 dark:text-gray-400 sm:inline-block">
|
||||
{browserOS === 'mac' ? '(⌘K)' : '(ctrl+K)'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function SourcesPopup({
|
||||
<div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200 flex-shrink-0">
|
||||
<a
|
||||
href="/settings/documents"
|
||||
className="text-violets-are-blue text-base font-medium flex items-center gap-2"
|
||||
className="text-violets-are-blue text-base font-medium inline-flex items-center gap-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Go to Documents
|
||||
|
||||
@@ -217,10 +217,10 @@ export default function ToolsPopup({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 flex-shrink-0">
|
||||
<div className="p-4 flex-shrink-0 opacity-75 hover:opacity-100 transition-opacity duration-200">
|
||||
<a
|
||||
href="/settings/tools"
|
||||
className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center"
|
||||
className="text-base text-purple-30 font-medium inline-flex items-center"
|
||||
>
|
||||
{t('settings.tools.manageTools')}
|
||||
<img
|
||||
@@ -233,4 +233,4 @@ export default function ToolsPopup({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ const ConversationBubble = forwardRef<
|
||||
updated?: boolean,
|
||||
index?: number,
|
||||
) => void;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{
|
||||
@@ -72,7 +71,6 @@ const ConversationBubble = forwardRef<
|
||||
retryBtn,
|
||||
questionNumber,
|
||||
handleUpdatedQuestionSubmission,
|
||||
attachments,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -99,36 +97,6 @@ const ConversationBubble = forwardRef<
|
||||
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
||||
};
|
||||
let bubble;
|
||||
const renderAttachments = () => {
|
||||
if (!attachments || attachments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center rounded-md bg-gray-100 px-2 py-1 text-sm dark:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
className="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
/>
|
||||
</svg>
|
||||
<span>{attachment.fileName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
if (type === 'QUESTION') {
|
||||
bubble = (
|
||||
<div
|
||||
@@ -157,7 +125,6 @@ const ConversationBubble = forwardRef<
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{renderAttachments()}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -171,7 +171,6 @@ export default function ConversationMessages({
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
questionNumber={index}
|
||||
sources={query.sources}
|
||||
attachments={query.attachments}
|
||||
/>
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
|
||||
@@ -9,11 +9,21 @@ export interface Message {
|
||||
type: MESSAGE_TYPE;
|
||||
}
|
||||
|
||||
|
||||
export interface Attachment {
|
||||
id?: string;
|
||||
fileName: string;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
progress: number;
|
||||
taskId?: string;
|
||||
token_count?: number;
|
||||
}
|
||||
|
||||
export interface ConversationState {
|
||||
queries: Query[];
|
||||
status: Status;
|
||||
conversationId: string | null;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
attachments: Attachment[];
|
||||
}
|
||||
|
||||
export interface Answer {
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
} from './conversationHandlers';
|
||||
import { Answer, ConversationState, Query, Status } from './conversationModels';
|
||||
import {
|
||||
Answer,
|
||||
Query,
|
||||
Status,
|
||||
ConversationState,
|
||||
Attachment,
|
||||
} from './conversationModels';
|
||||
|
||||
const initialState: ConversationState = {
|
||||
queries: [],
|
||||
@@ -38,7 +44,9 @@ export const fetchAnswer = createAsyncThunk<
|
||||
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachments = state.conversation.attachments?.map((a) => a.id) || [];
|
||||
const attachmentIds = state.conversation.attachments
|
||||
.filter((a) => a.id && a.status === 'completed')
|
||||
.map((a) => a.id) as string[];
|
||||
const currentConversationId = state.conversation.conversationId;
|
||||
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||
const save_conversation = isPreview ? false : true;
|
||||
@@ -129,7 +137,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
},
|
||||
indx,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
} else {
|
||||
@@ -144,7 +152,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
if (answer) {
|
||||
@@ -301,12 +309,34 @@ export const conversationSlice = createSlice({
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
setAttachments: (
|
||||
state,
|
||||
action: PayloadAction<{ fileName: string; id: string }[]>,
|
||||
) => {
|
||||
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
|
||||
state.attachments = action.payload;
|
||||
},
|
||||
addAttachment: (state, action: PayloadAction<Attachment>) => {
|
||||
state.attachments.push(action.payload);
|
||||
},
|
||||
updateAttachment: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
taskId: string;
|
||||
updates: Partial<Attachment>;
|
||||
}>,
|
||||
) => {
|
||||
const index = state.attachments.findIndex(
|
||||
(att) => att.taskId === action.payload.taskId,
|
||||
);
|
||||
if (index !== -1) {
|
||||
state.attachments[index] = {
|
||||
...state.attachments[index],
|
||||
...action.payload.updates,
|
||||
};
|
||||
}
|
||||
},
|
||||
removeAttachment: (state, action: PayloadAction<string>) => {
|
||||
state.attachments = state.attachments.filter(
|
||||
(att) => att.taskId !== action.payload && att.id !== action.payload,
|
||||
);
|
||||
},
|
||||
resetConversation: (state) => {
|
||||
state.queries = initialState.queries;
|
||||
state.status = initialState.status;
|
||||
@@ -337,6 +367,11 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
|
||||
|
||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
|
||||
export const selectAttachments = (state: RootState) =>
|
||||
state.conversation.attachments;
|
||||
export const selectCompletedAttachments = (state: RootState) =>
|
||||
state.conversation.attachments.filter((att) => att.status === 'completed');
|
||||
|
||||
export const {
|
||||
addQuery,
|
||||
updateQuery,
|
||||
@@ -348,6 +383,9 @@ export const {
|
||||
updateToolCalls,
|
||||
setConversation,
|
||||
setAttachments,
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
resetConversation,
|
||||
} = conversationSlice.actions;
|
||||
export default conversationSlice.reducer;
|
||||
|
||||
10
frontend/src/utils/browserUtils.ts
Normal file
10
frontend/src/utils/browserUtils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function getOS() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
if (userAgent.indexOf('Mac') !== -1) return 'mac';
|
||||
if (userAgent.indexOf('Win') !== -1) return 'win';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
export function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user