mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' into tool-use
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>DocsGPT 🦖</title>
|
||||
<title>DocsGPT</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
|
||||
|
||||
887
frontend/package-lock.json
generated
887
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,8 @@
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-i18next": "^15.0.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^8.0.5",
|
||||
@@ -41,28 +42,29 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import SourceDropdown from './components/SourceDropdown';
|
||||
import {
|
||||
setConversation,
|
||||
updateConversationId,
|
||||
handleAbort,
|
||||
} from './conversation/conversationSlice';
|
||||
import ConversationTile from './conversation/ConversationTile';
|
||||
import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks';
|
||||
@@ -32,7 +33,6 @@ import {
|
||||
selectConversations,
|
||||
selectModalStateDeleteConv,
|
||||
selectSelectedDocs,
|
||||
selectSelectedDocsStatus,
|
||||
selectSourceDocs,
|
||||
selectPaginatedDocuments,
|
||||
setConversations,
|
||||
@@ -51,21 +51,7 @@ interface NavigationProps {
|
||||
navOpen: boolean;
|
||||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
/* const NavImage: React.FC<{
|
||||
Light: string | undefined;
|
||||
Dark: string | undefined;
|
||||
}> = ({ Light, Dark }) => {
|
||||
return (
|
||||
<>
|
||||
<img src={Dark} alt="icon" className="ml-2 hidden w-5 dark:block " />
|
||||
<img src={Light} alt="icon" className="ml-2 w-5 dark:hidden filter dark:invert" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
NavImage.propTypes = {
|
||||
Light: PropTypes.string,
|
||||
Dark: PropTypes.string,
|
||||
}; */
|
||||
|
||||
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const dispatch = useDispatch();
|
||||
const queries = useSelector(selectQueries);
|
||||
@@ -85,10 +71,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const [apiKeyModalState, setApiKeyModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
|
||||
const isSelectedDocsSet = useSelector(selectSelectedDocsStatus);
|
||||
const [selectedDocsModalState, setSelectedDocsModalState] =
|
||||
useState<ActiveState>(isSelectedDocsSet ? 'INACTIVE' : 'ACTIVE');
|
||||
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
|
||||
@@ -180,6 +162,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
};
|
||||
|
||||
const resetConversation = () => {
|
||||
handleAbort();
|
||||
dispatch(setConversation([]));
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
@@ -491,11 +474,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
setModalState={setModalStateDeleteConv}
|
||||
handleDeleteAllConv={handleDeleteAllConversations}
|
||||
/>
|
||||
<Upload
|
||||
modalState={uploadModalState}
|
||||
setModalState={setUploadModalState}
|
||||
isOnboarding={false}
|
||||
></Upload>
|
||||
{uploadModalState === 'ACTIVE' && (
|
||||
<Upload
|
||||
receivedFile={[]}
|
||||
setModalState={setUploadModalState}
|
||||
isOnboarding={false}
|
||||
renderTab={null}
|
||||
close={() => setUploadModalState('INACTIVE')}
|
||||
></Upload>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
4
frontend/src/assets/DragFileUpload.svg
Normal file
4
frontend/src/assets/DragFileUpload.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" fill="current" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M33.8788 9.87836C34.4413 9.31595 35.2043 9 35.9998 9C36.7952 9 37.5582 9.31595 38.1208 9.87836L50.1208 21.8784C50.6672 22.4442 50.9696 23.202 50.9628 23.9886C50.9559 24.7752 50.6404 25.5276 50.0842 26.0838C49.528 26.64 48.7755 26.9555 47.989 26.9624C47.2024 26.9692 46.4446 26.6668 45.8788 26.1204L38.9998 19.2414V47.9994C38.9998 48.795 38.6837 49.5581 38.1211 50.1207C37.5585 50.6833 36.7954 50.9994 35.9998 50.9994C35.2041 50.9994 34.441 50.6833 33.8784 50.1207C33.3158 49.5581 32.9998 48.795 32.9998 47.9994V19.2414L26.1208 26.1204C25.555 26.6668 24.7971 26.9692 24.0106 26.9624C23.224 26.9555 22.4715 26.64 21.9153 26.0838C21.3591 25.5276 21.0436 24.7752 21.0367 23.9886C21.0299 23.202 21.3323 22.4442 21.8788 21.8784L33.8788 9.87836Z" fill="#313131"/>
|
||||
<path d="M18 51C18 50.2044 17.6839 49.4413 17.1213 48.8787C16.5587 48.3161 15.7956 48 15 48C14.2044 48 13.4413 48.3161 12.8787 48.8787C12.3161 49.4413 12 50.2044 12 51V52.8C12 58.446 16.554 63 22.2 63H49.8C55.446 63 60 58.446 60 52.8V51C60 50.2044 59.6839 49.4413 59.1213 48.8787C58.5587 48.3161 57.7957 48 57 48C56.2043 48 55.4413 48.3161 54.8787 48.8787C54.3161 49.4413 54 50.2044 54 51V52.8C54 55.134 52.134 57 49.8 57H22.2C19.866 57 18 55.134 18 52.8V51Z" fill="#313131"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/src/assets/user.png
Normal file
BIN
frontend/src/assets/user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 735 B |
@@ -5,7 +5,7 @@ export default function Avatar({
|
||||
size,
|
||||
className,
|
||||
}: {
|
||||
avatar: string | ReactNode;
|
||||
avatar: ReactNode;
|
||||
size?: 'SMALL' | 'MEDIUM' | 'LARGE';
|
||||
className: string;
|
||||
}) {
|
||||
|
||||
@@ -19,7 +19,10 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
onPageChange,
|
||||
onRowsPerPageChange,
|
||||
}) => {
|
||||
const [rowsPerPageOptions] = useState([5, 10, 15, 20]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const rowsPerPageOptions = [5, 10, 20, 50];
|
||||
|
||||
const toggleDropdown = () => setIsDropdownOpen((prev) => !prev);
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 1) {
|
||||
@@ -41,31 +44,51 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
onPageChange(totalPages);
|
||||
};
|
||||
|
||||
const handleSelectRowsPerPage = (rows: number) => {
|
||||
setIsDropdownOpen(false);
|
||||
onRowsPerPageChange(rows);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-xs justify-end gap-4 mt-2 p-2 border-gray-200">
|
||||
<div className="flex items-center gap-2 ">
|
||||
{/* Rows per page dropdown */}
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<span className="text-gray-900 dark:text-gray-50">Rows per page:</span>
|
||||
<select
|
||||
value={rowsPerPage}
|
||||
onChange={(e) => onRowsPerPageChange(Number(e.target.value))}
|
||||
className="border border-gray-300 rounded px-2 py-1 dark:bg-dark-charcoal dark:text-gray-50"
|
||||
>
|
||||
{rowsPerPageOptions.map((option) => (
|
||||
<option
|
||||
className="bg-white dark:bg-dark-charcoal dark:text-gray-50"
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className="px-3 py-1 border rounded dark:bg-dark-charcoal dark:text-light-gray hover:bg-gray-200 dark:hover:bg-neutral-700"
|
||||
>
|
||||
{rowsPerPage}
|
||||
</button>
|
||||
<div
|
||||
className={`absolute z-50 right-0 mt-1 w-28 transform bg-white dark:bg-dark-charcoal shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${
|
||||
isDropdownOpen
|
||||
? 'scale-100 opacity-100 block'
|
||||
: 'scale-95 opacity-0 hidden'
|
||||
}`}
|
||||
>
|
||||
{rowsPerPageOptions.map((option) => (
|
||||
<div
|
||||
key={option}
|
||||
onClick={() => handleSelectRowsPerPage(option)}
|
||||
className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${
|
||||
rowsPerPage === option
|
||||
? 'bg-gray-100 dark:bg-neutral-700 dark:text-light-gray'
|
||||
: 'bg-white dark:bg-dark-charcoal dark:text-light-gray'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination controls */}
|
||||
<div className="text-gray-900 dark:text-gray-50">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-gray-900 dark:text-gray-50">
|
||||
<button
|
||||
onClick={handleFirstPage}
|
||||
@@ -74,7 +97,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
>
|
||||
<img
|
||||
src={DoubleArrowLeft}
|
||||
alt="arrow"
|
||||
alt="First page"
|
||||
className="dark:invert dark:sepia dark:brightness-200"
|
||||
/>
|
||||
</button>
|
||||
@@ -85,7 +108,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
>
|
||||
<img
|
||||
src={SingleArrowLeft}
|
||||
alt="arrow"
|
||||
alt="Previous page"
|
||||
className="dark:invert dark:sepia dark:brightness-200"
|
||||
/>
|
||||
</button>
|
||||
@@ -96,7 +119,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
>
|
||||
<img
|
||||
src={SingleArrowRight}
|
||||
alt="arrow"
|
||||
alt="Next page"
|
||||
className="dark:invert dark:sepia dark:brightness-200"
|
||||
/>
|
||||
</button>
|
||||
@@ -107,7 +130,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
>
|
||||
<img
|
||||
src={DoubleArrowRight}
|
||||
alt="arrow"
|
||||
alt="Last page"
|
||||
className="dark:invert dark:sepia dark:brightness-200"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function DropdownMenu({
|
||||
<div className="static inline-block text-left" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="flex w-20 cursor-pointer flex-row items-center gap-px rounded-3xl border-purple-30/25 bg-purple-30 p-2 text-xs text-white hover:bg-[#6F3FD1] focus:outline-none"
|
||||
className="flex w-20 cursor-pointer flex-row gap-1 rounded-3xl border-purple-30/25 bg-purple-30 p-2 text-xs text-white hover:bg-[#6F3FD1] focus:outline-none"
|
||||
>
|
||||
{icon && <img src={icon} alt="OptionIcon" className="h-4 w-4" />}
|
||||
{selectedOption.value !== 'never' ? selectedOption.label : name}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Hero from '../Hero';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import DragFileUpload from '../assets/DragFileUpload.svg';
|
||||
import ArrowDown from '../assets/arrow-down.svg';
|
||||
import newChatIcon from '../assets/openNewChat.svg';
|
||||
import Send from '../assets/send.svg';
|
||||
@@ -21,12 +23,15 @@ import { FEEDBACK, Query } from './conversationModels';
|
||||
import {
|
||||
addQuery,
|
||||
fetchAnswer,
|
||||
resendQuery,
|
||||
selectQueries,
|
||||
selectStatus,
|
||||
setConversation,
|
||||
updateConversationId,
|
||||
updateQuery,
|
||||
} from './conversationSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import { ActiveState } from '../models/misc';
|
||||
|
||||
export default function Conversation() {
|
||||
const queries = useSelector(selectQueries);
|
||||
@@ -44,6 +49,47 @@ export default function Conversation() {
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setUploadModalState('ACTIVE');
|
||||
setFiles(acceptedFiles);
|
||||
setHandleDragActive(false);
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
multiple: true,
|
||||
onDragEnter: () => {
|
||||
setHandleDragActive(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setHandleDragActive(false);
|
||||
},
|
||||
maxSize: 25000000,
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
'text/plain': ['.txt'],
|
||||
'text/x-rst': ['.rst'],
|
||||
'text/x-markdown': ['.md'],
|
||||
'application/zip': ['.zip'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
['.docx'],
|
||||
'application/json': ['.json'],
|
||||
'text/csv': ['.csv'],
|
||||
'text/html': ['.html'],
|
||||
'application/epub+zip': ['.epub'],
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
|
||||
'.xlsx',
|
||||
],
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
||||
['.pptx'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserInterruption = () => {
|
||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
||||
@@ -85,27 +131,57 @@ export default function Conversation() {
|
||||
const handleQuestion = ({
|
||||
question,
|
||||
isRetry = false,
|
||||
updated = null,
|
||||
indx = undefined,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
updated?: boolean | null;
|
||||
indx?: number;
|
||||
}) => {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
setEventInterrupt(false);
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
||||
if (updated === true) {
|
||||
!isRetry &&
|
||||
dispatch(resendQuery({ index: indx as number, prompt: question })); //dispatch only new queries
|
||||
fetchStream.current = dispatch(fetchAnswer({ question, indx }));
|
||||
} else {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
setEventInterrupt(false);
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||
const prevFeedback = query.feedback;
|
||||
dispatch(updateQuery({ index, query: { feedback } }));
|
||||
handleSendFeedback(query.prompt, query.response!, feedback).catch(() =>
|
||||
dispatch(updateQuery({ index, query: { feedback: prevFeedback } })),
|
||||
handleSendFeedback(
|
||||
query.prompt,
|
||||
query.response!,
|
||||
feedback,
|
||||
conversationId as string,
|
||||
index,
|
||||
).catch(() =>
|
||||
handleSendFeedback(
|
||||
query.prompt,
|
||||
query.response!,
|
||||
feedback,
|
||||
conversationId as string,
|
||||
index,
|
||||
).catch(() =>
|
||||
dispatch(updateQuery({ index, query: { feedback: prevFeedback } })),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleQuestionSubmission = () => {
|
||||
if (inputRef.current?.value && status !== 'loading') {
|
||||
const handleQuestionSubmission = (
|
||||
updatedQuestion?: string,
|
||||
updated?: boolean,
|
||||
indx?: number,
|
||||
) => {
|
||||
if (updated === true) {
|
||||
handleQuestion({ question: updatedQuestion as string, updated, indx });
|
||||
} else if (inputRef.current?.value && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
@@ -290,6 +366,8 @@ export default function Conversation() {
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
questionNumber={index}
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
|
||||
@@ -303,8 +381,12 @@ export default function Conversation() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 sm:w-[62%] h-auto">
|
||||
<div className="flex w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 sm:w-[62%] h-auto py-1">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex w-full items-center rounded-[40px] border border-silver bg-white dark:bg-raisin-black"
|
||||
>
|
||||
<input {...getInputProps()}></input>
|
||||
<textarea
|
||||
id="inputbox"
|
||||
ref={inputRef}
|
||||
@@ -328,7 +410,7 @@ export default function Conversation() {
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||
<img
|
||||
className="ml-[4px] h-6 w-6 text-white "
|
||||
onClick={handleQuestionSubmission}
|
||||
onClick={() => handleQuestionSubmission()}
|
||||
src={isDarkTheme ? SendDark : Send}
|
||||
></img>
|
||||
</div>
|
||||
@@ -339,6 +421,26 @@ export default function Conversation() {
|
||||
{t('tagline')}
|
||||
</p>
|
||||
</div>
|
||||
{handleDragActive && (
|
||||
<div className="pointer-events-none fixed top-0 left-0 z-30 flex flex-col size-full items-center justify-center bg-opacity-50 bg-white dark:bg-gray-alpha">
|
||||
<img className="filter dark:invert" src={DragFileUpload} />
|
||||
<span className="px-2 text-2xl font-bold text-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.drag.title')}
|
||||
</span>
|
||||
<span className="p-2 text-s w-48 text-center text-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.drag.description')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{uploadModalState === 'ACTIVE' && (
|
||||
<Upload
|
||||
receivedFile={files}
|
||||
setModalState={setUploadModalState}
|
||||
isOnboarding={false}
|
||||
renderTab={'file'}
|
||||
close={() => setUploadModalState('INACTIVE')}
|
||||
></Upload>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { forwardRef, useRef, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
@@ -15,6 +15,8 @@ import Document from '../assets/document.svg';
|
||||
import Like from '../assets/like.svg?react';
|
||||
import Link from '../assets/link.svg';
|
||||
import Sources from '../assets/sources.svg';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import UserIcon from '../assets/user.png';
|
||||
import Avatar from '../components/Avatar';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
} from '../preferences/preferenceSlice';
|
||||
import classes from './ConversationBubble.module.css';
|
||||
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
|
||||
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
|
||||
|
||||
@@ -38,36 +41,113 @@ const ConversationBubble = forwardRef<
|
||||
handleFeedback?: (feedback: FEEDBACK) => void;
|
||||
sources?: { title: string; text: string; source: string }[];
|
||||
retryBtn?: React.ReactElement;
|
||||
questionNumber?: number;
|
||||
handleUpdatedQuestionSubmission?: (
|
||||
updatedquestion?: string,
|
||||
updated?: boolean,
|
||||
index?: number,
|
||||
) => void;
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
|
||||
{
|
||||
message,
|
||||
type,
|
||||
className,
|
||||
feedback,
|
||||
handleFeedback,
|
||||
sources,
|
||||
retryBtn,
|
||||
questionNumber,
|
||||
handleUpdatedQuestionSubmission,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
// const bubbleRef = useRef<HTMLDivElement | null>(null);
|
||||
const chunks = useSelector(selectChunks);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const [isLikeHovered, setIsLikeHovered] = useState(false);
|
||||
const [isEditClicked, setIsEditClicked] = useState(false);
|
||||
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
|
||||
const [isQuestionHovered, setIsQuestionHovered] = useState(false);
|
||||
const [editInputBox, setEditInputBox] = useState<string>('');
|
||||
|
||||
const [isLikeClicked, setIsLikeClicked] = useState(false);
|
||||
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
|
||||
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||
const editableQueryRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useOutsideAlerter(editableQueryRef, () => setIsEditClicked(false), [], true);
|
||||
const handleEditClick = () => {
|
||||
setIsEditClicked(false);
|
||||
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
||||
};
|
||||
let bubble;
|
||||
if (type === 'QUESTION') {
|
||||
bubble = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-row-reverse self-end flex-wrap ${className}`}
|
||||
onMouseEnter={() => setIsQuestionHovered(true)}
|
||||
onMouseLeave={() => setIsQuestionHovered(false)}
|
||||
>
|
||||
<Avatar className="mt-2 text-2xl" avatar="🧑💻"></Avatar>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
className="ml-10 mr-2 flex items-center rounded-[28px] bg-purple-30 py-[14px] px-[19px] text-white max-w-full whitespace-pre-wrap leading-normal"
|
||||
ref={ref}
|
||||
className={`flex flex-row-reverse self-end flex-wrap ${className}`}
|
||||
>
|
||||
{message}
|
||||
<Avatar
|
||||
size="SMALL"
|
||||
className="mt-2 text-2xl"
|
||||
avatar={
|
||||
<img className="rounded-full mr-1" width={30} src={UserIcon} />
|
||||
}
|
||||
/>
|
||||
{!isEditClicked && (
|
||||
<div
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
className="text-sm sm:text-base ml-2 mr-2 flex items-center rounded-[28px] bg-purple-30 py-[14px] px-[19px] text-white max-w-full whitespace-pre-wrap leading-normal"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
{isEditClicked && (
|
||||
<div ref={editableQueryRef} className="w-[75%] flex flex-col">
|
||||
<textarea
|
||||
placeholder="Type the updated query..."
|
||||
onChange={(e) => {
|
||||
setEditInputBox(e.target.value);
|
||||
}}
|
||||
rows={1}
|
||||
value={editInputBox}
|
||||
className="ml-2 mr-12 text-[15px] resize-y h-12 min-h-max rounded-3xl p-3 no-scrollbar leading-relaxed dark:border-[0.5px] dark:border-white dark:bg-raisin-black dark:text-white px-[18px] border-[1.5px] border-black"
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-row-reverse justify-end gap-1 mt-3 text-sm font-medium`}
|
||||
>
|
||||
<button
|
||||
className="rounded-full bg-[#CDB5FF] hover:bg-[#E1D3FF] py-[10px] px-[15px] text-purple-30 max-w-full whitespace-pre-wrap leading-none"
|
||||
onClick={() => handleEditClick()}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
className="py-[10px] px-[15px] no-underline hover:underline text-purple-30 max-w-full whitespace-pre-wrap leading-normal"
|
||||
onClick={() => setIsEditClicked(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditClicked(true);
|
||||
setEditInputBox(message);
|
||||
}}
|
||||
className={`h-fit mt-3 p-2 cursor-pointer rounded-full hover:bg-[#35363B] flex items-center ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -241,7 +321,7 @@ const ConversationBubble = forwardRef<
|
||||
}`}
|
||||
>
|
||||
<ReactMarkdown
|
||||
className="fade-in whitespace-pre-wrap break-normal leading-normal"
|
||||
className="fade-in whitespace-pre-wrap break-words leading-normal"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
@@ -269,12 +349,7 @@ const ConversationBubble = forwardRef<
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code
|
||||
className={className ? className : 'whitespace-pre-line'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
<code className="whitespace-pre-line">{children}</code>
|
||||
);
|
||||
},
|
||||
ul({ children }) {
|
||||
@@ -361,7 +436,8 @@ const ConversationBubble = forwardRef<
|
||||
feedback === 'LIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
}
|
||||
${feedback === 'DISLIKE' && type !== 'ERROR' ? 'hidden' : ''}`}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
@@ -379,9 +455,15 @@ const ConversationBubble = forwardRef<
|
||||
: 'fill-none stroke-gray-4000'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleFeedback?.('LIKE');
|
||||
setIsLikeClicked(true);
|
||||
setIsDislikeClicked(false);
|
||||
if (feedback === undefined || feedback === null) {
|
||||
handleFeedback?.('LIKE');
|
||||
setIsLikeClicked(true);
|
||||
setIsDislikeClicked(false);
|
||||
} else if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
setIsLikeClicked(false);
|
||||
setIsDislikeClicked(false);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsLikeHovered(true)}
|
||||
onMouseLeave={() => setIsLikeHovered(false)}
|
||||
@@ -396,7 +478,7 @@ const ConversationBubble = forwardRef<
|
||||
feedback === 'DISLIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
} ${feedback === 'LIKE' && type !== 'ERROR' ? ' hidden' : ''} `}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
@@ -413,9 +495,15 @@ const ConversationBubble = forwardRef<
|
||||
: 'fill-none stroke-gray-4000'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleFeedback?.('DISLIKE');
|
||||
setIsDislikeClicked(true);
|
||||
setIsLikeClicked(false);
|
||||
if (feedback === undefined || feedback === null) {
|
||||
handleFeedback?.('DISLIKE');
|
||||
setIsDislikeClicked(true);
|
||||
setIsLikeClicked(false);
|
||||
} else if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
setIsLikeClicked(false);
|
||||
setIsDislikeClicked(false);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsDislikeHovered(true)}
|
||||
onMouseLeave={() => setIsDislikeHovered(false)}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
selectQueries,
|
||||
} from './sharedConversationSlice';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export const SharedConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -176,94 +177,111 @@ export const SharedConversation = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
||||
<div
|
||||
ref={sharedConversationRef}
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex w-full justify-center overflow-auto"
|
||||
>
|
||||
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
|
||||
<div className="mb-2 w-full border-b pb-2 dark:border-b-silver">
|
||||
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
||||
{title}
|
||||
</h1>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{t('sharedConv.subtitle')}{' '}
|
||||
<a href="/" className="text-[#007DFF]">
|
||||
DocsGPT
|
||||
</a>
|
||||
</h2>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{date}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="">
|
||||
{queries?.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={'mb-1 last:mb-28 md:mb-7'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`DocsGPT | ${title}`}</title>
|
||||
<meta name="description" content="Shared conversations with DocsGPT" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Shared conversations with DocsGPT"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Shared conversations with DocsGPT"
|
||||
/>
|
||||
</Helmet>
|
||||
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
||||
<div
|
||||
ref={sharedConversationRef}
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex w-full justify-center overflow-auto"
|
||||
>
|
||||
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
|
||||
<div className="mb-2 w-full border-b pb-2 dark:border-b-silver">
|
||||
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
||||
{title}
|
||||
</h1>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{t('sharedConv.subtitle')}{' '}
|
||||
<a href="/" className="text-[#007DFF]">
|
||||
DocsGPT
|
||||
</a>
|
||||
</h2>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{date}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="">
|
||||
{queries?.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={'mb-1 last:mb-28 md:mb-7'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
|
||||
{apiKey ? (
|
||||
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<div
|
||||
id="inputbox"
|
||||
ref={inputRef}
|
||||
tabIndex={1}
|
||||
onPaste={handlePaste}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
contentEditable
|
||||
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{status === 'loading' ? (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert"
|
||||
></img>
|
||||
) : (
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||
<div className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
|
||||
{apiKey ? (
|
||||
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<div
|
||||
id="inputbox"
|
||||
ref={inputRef}
|
||||
tabIndex={1}
|
||||
onPaste={handlePaste}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
contentEditable
|
||||
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{status === 'loading' ? (
|
||||
<img
|
||||
onClick={handleQuestionSubmission}
|
||||
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
|
||||
src={Send}
|
||||
src={Spinner}
|
||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert"
|
||||
></img>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe"
|
||||
>
|
||||
{t('sharedConv.button')}
|
||||
</button>
|
||||
)}
|
||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
) : (
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||
<img
|
||||
onClick={handleQuestionSubmission}
|
||||
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
|
||||
src={Send}
|
||||
></img>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe"
|
||||
>
|
||||
{t('sharedConv.button')}
|
||||
</button>
|
||||
)}
|
||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,6 +75,7 @@ export function handleFetchAnswerSteaming(
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
indx?: number,
|
||||
): Promise<Answer> {
|
||||
history = history.map((item) => {
|
||||
return { prompt: item.prompt, response: item.response };
|
||||
@@ -87,6 +88,7 @@ export function handleFetchAnswerSteaming(
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
index: indx,
|
||||
};
|
||||
if (selectedDocs && 'id' in selectedDocs) {
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
@@ -200,12 +202,16 @@ export function handleSendFeedback(
|
||||
prompt: string,
|
||||
response: string,
|
||||
feedback: FEEDBACK,
|
||||
conversation_id: string,
|
||||
prompt_index: number,
|
||||
) {
|
||||
return conversationService
|
||||
.feedback({
|
||||
question: prompt,
|
||||
answer: response,
|
||||
feedback: feedback,
|
||||
conversation_id: conversation_id,
|
||||
question_index: prompt_index,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
|
||||
export type Status = 'idle' | 'loading' | 'failed';
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE';
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE' | null;
|
||||
|
||||
export interface Message {
|
||||
text: string;
|
||||
@@ -41,4 +41,5 @@ export interface RetrievalPayload {
|
||||
chunks: string;
|
||||
token_limit: number;
|
||||
isNoneDoc: boolean;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
@@ -17,133 +17,147 @@ const initialState: ConversationState = {
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
|
||||
export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
'fetchAnswer',
|
||||
async ({ question }, { dispatch, getState, signal }) => {
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
let abortController: AbortController | null = null;
|
||||
export function handleAbort() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
export const fetchAnswer = createAsyncThunk<
|
||||
Answer,
|
||||
{ question: string; indx?: number }
|
||||
>('fetchAnswer', async ({ question, indx }, { dispatch, getState }) => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
if (data.type === 'end') {
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations()
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
if (!isSourceUpdated) {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
index: state.conversation.queries.length - 1,
|
||||
query: { sources: [] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (data.type === 'id') {
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: data.id },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'source') {
|
||||
isSourceUpdated = true;
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'end') {
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations()
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
if (!isSourceUpdated) {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
index: state.conversation.queries.length - 1,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'error') {
|
||||
// set status to 'failed'
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
index: state.conversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const result = data.answer;
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
index: state.conversation.queries.length - 1,
|
||||
query: { response: result },
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
query: { sources: [] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
});
|
||||
} else if (data.type === 'id') {
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: data.id },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'source') {
|
||||
isSourceUpdated = true;
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'error') {
|
||||
// set status to 'failed'
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const result = data.answer;
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
query: { response: result },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
indx,
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
});
|
||||
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: state.conversation.queries.length - 1,
|
||||
query: { response: answer.answer, sources: sourcesPrepped },
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: answer.conversationId },
|
||||
}),
|
||||
);
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations()
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
}
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
query: { response: answer.answer, sources: sourcesPrepped },
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: answer.conversationId },
|
||||
}),
|
||||
);
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations()
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
sources: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
sources: [],
|
||||
};
|
||||
});
|
||||
|
||||
export const conversationSlice = createSlice({
|
||||
name: 'conversation',
|
||||
@@ -155,6 +169,15 @@ export const conversationSlice = createSlice({
|
||||
setConversation(state, action: PayloadAction<Query[]>) {
|
||||
state.queries = action.payload;
|
||||
},
|
||||
resendQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
|
||||
) {
|
||||
state.queries = [
|
||||
...state.queries.splice(0, action.payload.index),
|
||||
action.payload,
|
||||
];
|
||||
},
|
||||
updateStreamingQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
@@ -236,6 +259,7 @@ export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
export const {
|
||||
addQuery,
|
||||
updateQuery,
|
||||
resendQuery,
|
||||
updateStreamingQuery,
|
||||
updateConversationId,
|
||||
updateStreamingSource,
|
||||
|
||||
@@ -50,19 +50,23 @@ body.dark {
|
||||
|
||||
@layer components {
|
||||
.table-default {
|
||||
@apply block w-full mx-auto table-auto content-start justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto;
|
||||
@apply block w-full table-auto content-start justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto;
|
||||
}
|
||||
|
||||
.table-default th {
|
||||
@apply p-4 w-full font-normal text-gray-400 text-nowrap; /* Remove border-r */
|
||||
@apply p-4 font-normal text-gray-400 text-nowrap; /* Remove border-r */
|
||||
}
|
||||
|
||||
.table-default th {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-default th:last-child {
|
||||
@apply w-[auto];
|
||||
flex: 0; /* Ensure the last column does not stretch unnecessarily */
|
||||
}
|
||||
|
||||
.table-default td {
|
||||
@apply border-t border-silver dark:border-silver/40 px-4 py-2; /* Remove border-r */
|
||||
@apply border-t w-full border-silver dark:border-silver/40 px-4 py-2; /* Remove border-r */
|
||||
}
|
||||
|
||||
.table-default td:last-child {
|
||||
|
||||
@@ -102,6 +102,10 @@
|
||||
"agent": "User agent",
|
||||
"searchQueries": "Search queries",
|
||||
"numberOfPosts": "Number of posts"
|
||||
},
|
||||
"drag": {
|
||||
"title": "Upload a source file",
|
||||
"description": "Drop your file here to add it as a source"
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
"agent": "Agente de Usuario",
|
||||
"searchQueries": "Consultas de Búsqueda",
|
||||
"numberOfPosts": "Número de publicaciones"
|
||||
},
|
||||
"drag": {
|
||||
"title": "Cargar un archivo fuente",
|
||||
"description": "Suelta tu archivo aquí para agregarlo como fuente."
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
|
||||
@@ -7,6 +7,7 @@ import es from './es.json'; //Spanish
|
||||
import jp from './jp.json'; //Japanese
|
||||
import zh from './zh.json'; //Mandarin
|
||||
import zhTW from './zh-TW.json'; //Traditional Chinese
|
||||
import ru from './ru.json'; //Russian
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
@@ -25,9 +26,12 @@ i18n
|
||||
zh: {
|
||||
translation: zh,
|
||||
},
|
||||
'zh-TW': {
|
||||
zhTW: {
|
||||
translation: zhTW,
|
||||
},
|
||||
ru: {
|
||||
translation: ru,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
detection: {
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
"agent": "ユーザーエージェント",
|
||||
"searchQueries": "検索クエリ",
|
||||
"numberOfPosts": "投稿数"
|
||||
},
|
||||
"drag": {
|
||||
"title": "ソースファイルをアップロードする",
|
||||
"description": "ファイルをここにドロップしてソースとして追加します"
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
|
||||
142
frontend/src/locale/ru.json
Normal file
142
frontend/src/locale/ru.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"language": "Русский",
|
||||
"chat": "Чат",
|
||||
"chats": "Чаты",
|
||||
"newChat": "Новый чат",
|
||||
"myPlan": "Мой план",
|
||||
"about": "О",
|
||||
"inputPlaceholder": "Введите свое сообщение здесь...",
|
||||
"tagline": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию, используя источники.",
|
||||
"sourceDocs": "Источник",
|
||||
"none": "Нет",
|
||||
"cancel": "Отмена",
|
||||
"demo": [
|
||||
{
|
||||
"header": "Узнайте о DocsGPT",
|
||||
"query": "Что такое DocsGPT?"
|
||||
},
|
||||
{
|
||||
"header": "Обобщить документацию",
|
||||
"query": "Обобщить текущий контекст"
|
||||
},
|
||||
{
|
||||
"header": "Написать код",
|
||||
"query": "Написать код для запроса API к /api/answer"
|
||||
},
|
||||
{
|
||||
"header": "Помощь в обучении",
|
||||
"query": "Написать потенциальные вопросы для контекста"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"label": "Настройки",
|
||||
"general": {
|
||||
"label": "Общие",
|
||||
"selectTheme": "Выбрать тему",
|
||||
"light": "Светлая",
|
||||
"dark": "Темная",
|
||||
"selectLanguage": "Выбрать язык",
|
||||
"chunks": "Обработанные фрагменты на запрос",
|
||||
"prompt": "Активная подсказка",
|
||||
"deleteAllLabel": "Удалить все беседы",
|
||||
"deleteAllBtn": "Удалить все",
|
||||
"addNew": "Добавить новый",
|
||||
"convHistory": "История разговоров",
|
||||
"none": "Нет",
|
||||
"low": "Низкий",
|
||||
"medium": "Средний",
|
||||
"high": "Высокий",
|
||||
"unlimited": "Без ограничений",
|
||||
"default": "по умолчанию"
|
||||
},
|
||||
"documents": {
|
||||
"label": "Документы",
|
||||
"name": "Название документа",
|
||||
"date": "Дата вектора",
|
||||
"type": "Тип",
|
||||
"tokenUsage": "Использование токена",
|
||||
"noData": "Нет существующих документов"
|
||||
},
|
||||
"apiKeys": {
|
||||
"label": "Чат-боты",
|
||||
"name": "Название",
|
||||
"key": "Ключ API",
|
||||
"sourceDoc": "Исходный документ",
|
||||
"createNew": "Создать новый",
|
||||
"noData": "Нет существующих чат-ботов"
|
||||
},
|
||||
"analytics": {
|
||||
"label": "Analytics"
|
||||
},
|
||||
"logs": {
|
||||
"label": "Журналы"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
"label": "Загрузить новую документацию",
|
||||
"select": "Выберите способ загрузки документа в DocsGPT",
|
||||
"file": "Загрузить с устройства",
|
||||
"back": "Назад",
|
||||
"wait": "Пожалуйста, подождите ...",
|
||||
"remote": "Собрать с веб-сайта",
|
||||
"start": "Начать чат",
|
||||
"name": "Имя",
|
||||
"choose": "Выбрать файлы",
|
||||
"info": "Загрузите .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .zip с ограничением до 25 МБ",
|
||||
"uploadedFiles": "Загруженные файлы",
|
||||
"cancel": "Отмена",
|
||||
"train": "Обучение",
|
||||
"link": "Ссылка",
|
||||
"urlLink": "URL-ссылка",
|
||||
"repoUrl": "URL-адрес репозитория",
|
||||
"reddit": {
|
||||
"id": "ID клиента",
|
||||
"secret": "Секрет клиента",
|
||||
"agent": "Агент пользователя",
|
||||
"searchQueries": "Поисковые запросы",
|
||||
"numberOfPosts": "Количество сообщений"
|
||||
},
|
||||
"drag": {
|
||||
"title": "Загрузите исходный файл",
|
||||
"description": "Перетащите сюда свой файл, чтобы добавить его в качестве источника."
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
"label": "Создать новый ключ API",
|
||||
"apiKeyName": "Имя ключа API",
|
||||
"chunks": "Обработано фрагментов на запрос",
|
||||
"prompt": "Выбрать активный запрос",
|
||||
"sourceDoc": "Исходный документ",
|
||||
"create": "Создать"
|
||||
},
|
||||
"saveKey": {
|
||||
"note": "Пожалуйста, сохраните свой ключ",
|
||||
"disclaimer": "Это единственный раз, когда будет показан ваш ключ.",
|
||||
"copy": "Копировать",
|
||||
"copied": "Скопировано",
|
||||
"confirm": "Я сохранил ключ"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "Вы уверены, что хотите удалить все разговоры?",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"shareConv": {
|
||||
"label": "Создать публичную страницу для общего доступа",
|
||||
"note": "Исходный документ, личная информация и дальнейший разговор останутся конфиденциальными",
|
||||
"create": "Создать",
|
||||
"option": "Разрешить пользователям запрашивать дальнейшие действия"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
"subtitle": "Создано с помощью",
|
||||
"button": "Начать работу с DocsGPT",
|
||||
"meta": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию с помощью источников."
|
||||
},
|
||||
"convTile": {
|
||||
"share": "Поделиться",
|
||||
"delete": "Удалить",
|
||||
"rename": "Переименовать",
|
||||
"deleteWarning": "Вы уверены, что хотите удалить этот разговор?"
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,10 @@
|
||||
"agent": "使用者代理(User-Agent)",
|
||||
"searchQueries": "搜尋查詢",
|
||||
"numberOfPosts": "貼文數量"
|
||||
},
|
||||
"drag": {
|
||||
"title": "上傳原始檔",
|
||||
"description": "將您的文件拖放到此處以將其添加為來源"
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
"agent": "用户代理",
|
||||
"searchQueries": "搜索查询",
|
||||
"numberOfPosts": "帖子数量"
|
||||
},
|
||||
"drag": {
|
||||
"title": "上传源文件",
|
||||
"description": "将您的文件拖放到此处以将其添加为源"
|
||||
}
|
||||
},
|
||||
"createAPIKey": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Exit from '../assets/exit.svg';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WrapperModal from './WrapperModal';
|
||||
function ConfirmationModal({
|
||||
message,
|
||||
modalState,
|
||||
@@ -20,49 +20,43 @@ function ConfirmationModal({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<article
|
||||
className={`${
|
||||
modalState === 'ACTIVE' ? 'visible' : 'hidden'
|
||||
} fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha`}
|
||||
>
|
||||
<article className="mx-auto mt-[35vh] flex w-[90vw] max-w-lg flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-outer-space">
|
||||
<div className="relative">
|
||||
<button
|
||||
className="absolute top-3 right-4 m-2 w-3"
|
||||
onClick={() => {
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
<div className="p-8">
|
||||
<p className="font-base mb-1 w-[90%] text-lg text-jet dark:text-bright-gray">
|
||||
{message}
|
||||
</p>
|
||||
<div>
|
||||
<div className="mt-6 flex flex-row-reverse gap-1">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{cancelLabel ? cancelLabel : t('cancel')}
|
||||
</button>
|
||||
<>
|
||||
{modalState === 'ACTIVE' && (
|
||||
<WrapperModal
|
||||
close={() => {
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="p-8">
|
||||
<p className="font-base mb-1 w-[90%] text-lg text-jet dark:text-bright-gray">
|
||||
{message}
|
||||
</p>
|
||||
<div>
|
||||
<div className="mt-6 flex flex-row-reverse gap-1">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{cancelLabel ? cancelLabel : t('cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</article>
|
||||
</WrapperModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Exit from '../assets/exit.svg';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import Input from '../components/Input';
|
||||
import { CreateAPIKeyModalProps, Doc } from '../models/misc';
|
||||
import { selectSourceDocs } from '../preferences/preferenceSlice';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
@@ -73,91 +73,82 @@ export default function CreateAPIKeyModal({
|
||||
handleFetchPrompts();
|
||||
}, []);
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
|
||||
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
|
||||
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
<div className="mb-6">
|
||||
<span className="text-xl text-jet dark:text-bright-gray">
|
||||
{t('modals.createAPIKey.label')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative mt-5 mb-4">
|
||||
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.createAPIKey.apiKeyName')}
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
className="rounded-md"
|
||||
value={APIKeyName}
|
||||
onChange={(e) => setAPIKeyName(e.target.value)}
|
||||
></Input>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath ? sourcePath.name : null}
|
||||
onSelect={(selection: {
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
}) => {
|
||||
setSourcePath(selection);
|
||||
}}
|
||||
options={extractDocPaths()}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
options={activePrompts}
|
||||
selectedValue={prompt ? prompt.name : null}
|
||||
placeholder={t('modals.createAPIKey.prompt')}
|
||||
onSelect={(value: { name: string; id: string; type: string }) =>
|
||||
setPrompt(value)
|
||||
}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<p className="mb-2 ml-2 font-semibold text-jet dark:text-bright-gray">
|
||||
{t('modals.createAPIKey.chunks')}
|
||||
</p>
|
||||
<Dropdown
|
||||
options={chunkOptions}
|
||||
selectedValue={chunk}
|
||||
onSelect={(value: string) => setChunk(value)}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={!sourcePath || APIKeyName.length === 0 || !prompt}
|
||||
onClick={() => {
|
||||
if (sourcePath && prompt) {
|
||||
const payload: any = {
|
||||
name: APIKeyName,
|
||||
prompt_id: prompt.id,
|
||||
chunks: chunk,
|
||||
};
|
||||
if (sourcePath.type === 'default') {
|
||||
payload.retriever = sourcePath.id;
|
||||
}
|
||||
if (sourcePath.type === 'local') {
|
||||
payload.source = sourcePath.id;
|
||||
}
|
||||
createAPIKey(payload);
|
||||
}
|
||||
}}
|
||||
className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
|
||||
>
|
||||
{t('modals.createAPIKey.create')}
|
||||
</button>
|
||||
<WrapperModal close={close}>
|
||||
<div className="mb-6">
|
||||
<span className="text-xl text-jet dark:text-bright-gray">
|
||||
{t('modals.createAPIKey.label')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-5 mb-4">
|
||||
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
|
||||
{t('modals.createAPIKey.apiKeyName')}
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
className="rounded-md"
|
||||
value={APIKeyName}
|
||||
onChange={(e) => setAPIKeyName(e.target.value)}
|
||||
></Input>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath ? sourcePath.name : null}
|
||||
onSelect={(selection: { name: string; id: string; type: string }) => {
|
||||
setSourcePath(selection);
|
||||
}}
|
||||
options={extractDocPaths()}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
options={activePrompts}
|
||||
selectedValue={prompt ? prompt.name : null}
|
||||
placeholder={t('modals.createAPIKey.prompt')}
|
||||
onSelect={(value: { name: string; id: string; type: string }) =>
|
||||
setPrompt(value)
|
||||
}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<p className="mb-2 ml-2 font-semibold text-jet dark:text-bright-gray">
|
||||
{t('modals.createAPIKey.chunks')}
|
||||
</p>
|
||||
<Dropdown
|
||||
options={chunkOptions}
|
||||
selectedValue={chunk}
|
||||
onSelect={(value: string) => setChunk(value)}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={!sourcePath || APIKeyName.length === 0 || !prompt}
|
||||
onClick={() => {
|
||||
if (sourcePath && prompt) {
|
||||
const payload: any = {
|
||||
name: APIKeyName,
|
||||
prompt_id: prompt.id,
|
||||
chunks: chunk,
|
||||
};
|
||||
if (sourcePath.type === 'default') {
|
||||
payload.retriever = sourcePath.id;
|
||||
}
|
||||
if (sourcePath.type === 'local') {
|
||||
payload.source = sourcePath.id;
|
||||
}
|
||||
createAPIKey(payload);
|
||||
}
|
||||
}}
|
||||
className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
|
||||
>
|
||||
{t('modals.createAPIKey.create')}
|
||||
</button>
|
||||
</WrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import { Doc } from '../models/misc';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import Exit from '../assets/exit.svg';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
@@ -19,6 +18,7 @@ const embeddingsName =
|
||||
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
|
||||
|
||||
import conversationService from '../api/services/conversationService';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
export const ShareConversationModal = ({
|
||||
close,
|
||||
@@ -99,85 +99,78 @@ export const ShareConversationModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-40 flex h-screen w-screen cursor-default items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver">
|
||||
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
|
||||
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2>
|
||||
<p className="text-sm">{t('modals.shareConv.note')}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg">{t('modals.shareConv.option')}</span>
|
||||
<label className=" cursor-pointer select-none items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allowPrompt}
|
||||
onChange={togglePromptPermission}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div
|
||||
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
|
||||
allowPrompt
|
||||
? 'bg-purple-30 dark:bg-purple-30'
|
||||
: 'dark:bg-transparent'
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
|
||||
allowPrompt ? 'translate-x-full bg-silver' : 'bg-purple-30'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{allowPrompt && (
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath}
|
||||
onSelect={(selection: { label: string; value: string }) =>
|
||||
setSourcePath(selection)
|
||||
}
|
||||
options={extractDocPaths(sourceDocs ?? [])}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
<WrapperModal close={close}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2>
|
||||
<p className="text-sm">{t('modals.shareConv.note')}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg">{t('modals.shareConv.option')}</span>
|
||||
<label className=" cursor-pointer select-none items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allowPrompt}
|
||||
onChange={togglePromptPermission}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div
|
||||
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
|
||||
allowPrompt
|
||||
? 'bg-purple-30 dark:bg-purple-30'
|
||||
: 'dark:bg-transparent'
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
|
||||
allowPrompt ? 'translate-x-full bg-silver' : 'bg-purple-30'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4">
|
||||
{`${domain}/share/${identifier ?? '....'}`}
|
||||
</span>
|
||||
{status === 'fetched' ? (
|
||||
<button
|
||||
className="my-1 h-10 w-28 rounded-full border border-solid bg-purple-30 p-2 text-sm text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
|
||||
>
|
||||
{isCopied
|
||||
? t('modals.saveKey.copied')
|
||||
: t('modals.saveKey.copy')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full border border-solid bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => {
|
||||
shareCoversationPublicly(allowPrompt);
|
||||
}}
|
||||
>
|
||||
{t('modals.shareConv.create')}
|
||||
{status === 'loading' && (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="inline animate-spin cursor-pointer bg-transparent filter dark:invert"
|
||||
></img>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{allowPrompt && (
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath}
|
||||
onSelect={(selection: { label: string; value: string }) =>
|
||||
setSourcePath(selection)
|
||||
}
|
||||
options={extractDocPaths(sourceDocs ?? [])}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4">
|
||||
{`${domain}/share/${identifier ?? '....'}`}
|
||||
</span>
|
||||
{status === 'fetched' ? (
|
||||
<button
|
||||
className="my-1 h-10 w-28 rounded-full border border-solid bg-purple-30 p-2 text-sm text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
|
||||
>
|
||||
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full border border-solid bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => {
|
||||
shareCoversationPublicly(allowPrompt);
|
||||
}}
|
||||
>
|
||||
{t('modals.shareConv.create')}
|
||||
{status === 'loading' && (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="inline animate-spin cursor-pointer bg-transparent filter dark:invert"
|
||||
></img>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WrapperModal>
|
||||
);
|
||||
};
|
||||
|
||||
55
frontend/src/modals/WrapperModal.tsx
Normal file
55
frontend/src/modals/WrapperModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { WrapperModalProps } from './types';
|
||||
import Exit from '../assets/exit.svg';
|
||||
|
||||
const WrapperModal: React.FC<WrapperModalProps> = ({
|
||||
children,
|
||||
close,
|
||||
isPerformingTask,
|
||||
}) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPerformingTask) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
modalRef.current &&
|
||||
!modalRef.current.contains(event.target as Node)
|
||||
) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscapePress = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscapePress);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscapePress);
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]"
|
||||
>
|
||||
{!isPerformingTask && (
|
||||
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrapperModal;
|
||||
@@ -8,4 +8,9 @@ export type AvailableTool = {
|
||||
description: string;
|
||||
parameters: object;
|
||||
}[];
|
||||
|
||||
export type WrapperModalProps = {
|
||||
children?: React.ReactNode;
|
||||
isPerformingTask?: boolean;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@@ -25,9 +25,10 @@ export async function getDocsWithPagination(
|
||||
order = 'desc',
|
||||
pageNumber = 1,
|
||||
rowsPerPage = 10,
|
||||
searchTerm = '',
|
||||
): Promise<GetDocsResponse | null> {
|
||||
try {
|
||||
const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}`;
|
||||
const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}&search=${searchTerm}`;
|
||||
const response = await userService.getDocsWithPagination(query);
|
||||
const data = await response.json();
|
||||
const docs: Doc[] = [];
|
||||
|
||||
@@ -109,41 +109,56 @@ export default function APIKeys() {
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} component={'chatbot'} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.apiKeys.name')}</th>
|
||||
<th>{t('settings.apiKeys.sourceDoc')}</th>
|
||||
<th>{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!apiKeys?.length && (
|
||||
<tr>
|
||||
<td colSpan={4} className="!p-4">
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{apiKeys?.map((element, index) => (
|
||||
<tr key={index}>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-grow">
|
||||
<div className="dark:border-silver/40 border-silver rounded-md border overflow-auto">
|
||||
<table className="min-w-full divide-y divide-silver dark:divide-silver/40 ">
|
||||
<thead>
|
||||
<tr className="text-start text-sm font-medium text-gray-700 dark:text-gray-50 uppercase">
|
||||
<th className="p-2">{t('settings.apiKeys.name')}</th>
|
||||
<th className="p-2">
|
||||
{t('settings.apiKeys.sourceDoc')}
|
||||
</th>
|
||||
<th className="p-2">{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{!apiKeys?.length && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="!p-4 text-gray-800 dark:text-neutral-200 text-center"
|
||||
>
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Array.isArray(apiKeys) &&
|
||||
apiKeys?.map((element, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="text-nowrap whitespace-nowrap text-center text-sm font-medium text-gray-800 dark:text-neutral-200 p-2"
|
||||
>
|
||||
<td className="p-1">{element.name}</td>
|
||||
<td className="p-2">{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import userService from '../api/services/userService';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
@@ -15,6 +15,7 @@ import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure Act
|
||||
import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi';
|
||||
import { setSourceDocs } from '../preferences/preferenceSlice';
|
||||
import { setPaginatedDocuments } from '../preferences/preferenceSlice';
|
||||
import { truncate } from '../utils/stringUtils';
|
||||
|
||||
// Utility function to format numbers
|
||||
const formatTokens = (tokens: number): string => {
|
||||
@@ -40,25 +41,18 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
// State for search input
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
// State for modal: active/inactive
|
||||
const [modalState, setModalState] = useState<ActiveState>('INACTIVE'); // Initialize with inactive state
|
||||
const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOnboarding, setIsOnboarding] = useState<boolean>(false); // State for onboarding flag
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||
const [totalPages, setTotalPages] = useState<number>(1);
|
||||
// const [totalDocuments, setTotalDocuments] = useState<number>(0);
|
||||
// Filter documents based on the search term
|
||||
const filteredDocuments = paginatedDocuments?.filter((document) =>
|
||||
document.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
// State for documents
|
||||
const currentDocuments = filteredDocuments ?? [];
|
||||
console.log('currentDocuments', currentDocuments);
|
||||
const currentDocuments = paginatedDocuments ?? [];
|
||||
const syncOptions = [
|
||||
{ label: 'Never', value: 'never' },
|
||||
{ label: 'Daily', value: 'daily' },
|
||||
@@ -66,36 +60,50 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
{ label: 'Monthly', value: 'monthly' },
|
||||
];
|
||||
|
||||
const refreshDocs = (
|
||||
field: 'date' | 'tokens' | undefined,
|
||||
pageNumber?: number,
|
||||
rows?: number,
|
||||
) => {
|
||||
const page = pageNumber ?? currentPage;
|
||||
const rowsPerPg = rows ?? rowsPerPage;
|
||||
const refreshDocs = useCallback(
|
||||
(
|
||||
field: 'date' | 'tokens' | undefined,
|
||||
pageNumber?: number,
|
||||
rows?: number,
|
||||
) => {
|
||||
const page = pageNumber ?? currentPage;
|
||||
const rowsPerPg = rows ?? rowsPerPage;
|
||||
|
||||
if (field !== undefined) {
|
||||
if (field === sortField) {
|
||||
// Toggle sort order
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
// Change sort field and reset order to 'desc'
|
||||
setSortField(field);
|
||||
setSortOrder('desc');
|
||||
// If field is undefined, (Pagination or Search) use the current sortField
|
||||
const newSortField = field ?? sortField;
|
||||
|
||||
// If field is undefined, (Pagination or Search) use the current sortOrder
|
||||
const newSortOrder =
|
||||
field === sortField
|
||||
? sortOrder === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
: sortOrder;
|
||||
|
||||
// If field is defined, update the sortField and sortOrder
|
||||
if (field) {
|
||||
setSortField(newSortField);
|
||||
setSortOrder(newSortOrder);
|
||||
}
|
||||
}
|
||||
getDocsWithPagination(sortField, sortOrder, page, rowsPerPg)
|
||||
.then((data) => {
|
||||
//dispatch(setSourceDocs(data ? data.docs : []));
|
||||
dispatch(setPaginatedDocuments(data ? data.docs : []));
|
||||
setTotalPages(data ? data.totalPages : 0);
|
||||
//setTotalDocuments(data ? data.totalDocuments : 0);
|
||||
})
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
setLoading(true);
|
||||
getDocsWithPagination(
|
||||
newSortField,
|
||||
newSortOrder,
|
||||
page,
|
||||
rowsPerPg,
|
||||
searchTerm,
|
||||
)
|
||||
.then((data) => {
|
||||
dispatch(setPaginatedDocuments(data ? data.docs : []));
|
||||
setTotalPages(data ? data.totalPages : 0);
|
||||
})
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[currentPage, rowsPerPage, sortField, sortOrder, searchTerm],
|
||||
);
|
||||
|
||||
const handleManageSync = (doc: Doc, sync_frequency: string) => {
|
||||
setLoading(true);
|
||||
@@ -130,7 +138,12 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
if (modalState === 'INACTIVE') {
|
||||
refreshDocs(sortField, currentPage, rowsPerPage);
|
||||
}
|
||||
}, [modalState, sortField, currentPage, rowsPerPage]);
|
||||
}, [modalState]);
|
||||
|
||||
useEffect(() => {
|
||||
// undefine to prevent reset the sort order
|
||||
refreshDocs(undefined, 1, rowsPerPage);
|
||||
}, [searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
@@ -145,11 +158,18 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
type="text"
|
||||
id="document-search-input"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)} // Handle search input change
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
// refreshDocs(sortField, 1, rowsPerPage);
|
||||
// do not call refreshDocs here the state is async
|
||||
// so it will not have the updated value
|
||||
}} // Handle search input change
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
title="Add New Document"
|
||||
onClick={() => {
|
||||
setIsOnboarding(false); // Set onboarding flag if needed
|
||||
setModalState('ACTIVE'); // Open the upload modal
|
||||
@@ -161,131 +181,160 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.documents.name')}</th>
|
||||
<th>
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.date')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('date')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('tokens')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.type')}
|
||||
</div>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!currentDocuments?.length && (
|
||||
<tr>
|
||||
<td colSpan={5} className="!p-4">
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Array.isArray(currentDocuments) &&
|
||||
currentDocuments.map((document, index) => (
|
||||
<tr key={index} className="text-nowrap font-normal">
|
||||
<td>{document.name}</td>
|
||||
<td>{document.date}</td>
|
||||
<td>
|
||||
{document.tokens ? formatTokens(+document.tokens) : ''}
|
||||
</td>
|
||||
<td>
|
||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="w-full flex flex-row-reverse items-center justify-items-end ml-auto gap-2">
|
||||
{document.type !== 'remote' && (
|
||||
<button
|
||||
className="h-8 w-8 border border-[#747474] rounded-full p-2 opacity-60 hover:opacity-100"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-grow">
|
||||
<div className="dark:border-silver/40 border-silver rounded-md border overflow-auto">
|
||||
<table className="min-w-full divide-y divide-silver dark:divide-silver/40 text-xs sm:text-sm ">
|
||||
<thead>
|
||||
<tr className="text-nowrap">
|
||||
<th className="px-5 py-3 text-start font-medium text-gray-700 dark:text-gray-50 uppercase w-96">
|
||||
{t('settings.documents.name')}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-start font-medium text-gray-700 dark:text-gray-50 uppercase">
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.date')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('date')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-5 py-2 text-center font-medium text-gray-700 dark:text-gray-50 uppercase"
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('tokens')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
{/*}
|
||||
<th className="px-5 py-2 text-start text-sm font-medium text-gray-700 dark:text-gray-50 uppercase">
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.type')}
|
||||
</div>
|
||||
</th>
|
||||
*/}
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-2 text-start font-medium text-gray-700 dark:text-gray-50 uppercase"
|
||||
>
|
||||
{' '}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{!currentDocuments?.length && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="!py-4 text-gray-800 dark:text-neutral-200 text-center"
|
||||
>
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Array.isArray(currentDocuments) &&
|
||||
currentDocuments.map((document, index) => (
|
||||
<tr key={index} className="text-nowrap font-normal">
|
||||
<td
|
||||
title={document.name}
|
||||
className="px-6 py-4 whitespace-nowrap text-left font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
<img
|
||||
id={`img-${index}`}
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-full w-full cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{truncate(document.name, 50)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.date}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.tokens
|
||||
? formatTokens(+document.tokens)
|
||||
: ''}
|
||||
</td>
|
||||
{/*}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.type === 'remote'
|
||||
? 'Pre-loaded'
|
||||
: 'Private'}
|
||||
</td>
|
||||
*/}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-left text-sm font-medium flex">
|
||||
<div className="min-w-[150px] flex flex-row items-center ml-auto gap-10">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer opacity-60 hover:opacity-100"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* outside scrollable area */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
refreshDocs(undefined, page, rowsPerPage);
|
||||
}}
|
||||
onRowsPerPageChange={(rows) => {
|
||||
setRowsPerPage(rows);
|
||||
setCurrentPage(1);
|
||||
refreshDocs(undefined, 1, rows);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Conditionally render the Upload modal based on modalState */}
|
||||
{modalState === 'ACTIVE' && (
|
||||
<div className="fixed top-0 left-0 w-screen h-screen z-50 flex items-center justify-center bg-transparent">
|
||||
<div className="w-full h-full bg-transparent flex flex-col items-center justify-center p-8">
|
||||
{/* Your Upload component */}
|
||||
<Upload
|
||||
modalState={modalState}
|
||||
receivedFile={[]}
|
||||
setModalState={setModalState}
|
||||
isOnboarding={isOnboarding}
|
||||
renderTab={null}
|
||||
close={() => setModalState('INACTIVE')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Pagination component with props:
|
||||
# Note: Every time the page changes,
|
||||
the refreshDocs function is called with the updated page number and rows per page.
|
||||
and reset cursor paginated query parameter to undefined.
|
||||
*/}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
refreshDocs(sortField, page, rowsPerPage);
|
||||
}}
|
||||
onRowsPerPageChange={(rows) => {
|
||||
setRowsPerPage(rows);
|
||||
setCurrentPage(1);
|
||||
refreshDocs(sortField, 1, rows);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -40,6 +40,14 @@ export default function General() {
|
||||
label: 'Mandarin',
|
||||
value: 'zh',
|
||||
},
|
||||
{
|
||||
label: 'Traditional Chinese',
|
||||
value: 'zhTW',
|
||||
},
|
||||
{
|
||||
label: 'Russian',
|
||||
value: 'ru',
|
||||
},
|
||||
];
|
||||
const chunks = ['0', '2', '4', '6', '8', '10'];
|
||||
const token_limits = new Map([
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Exit from '../assets/exit.svg';
|
||||
import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import FileUpload from '../assets/file_upload.svg';
|
||||
import WebsiteCollect from '../assets/website_collect.svg';
|
||||
@@ -17,17 +16,22 @@ import {
|
||||
setSourceDocs,
|
||||
selectSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import WrapperModal from '../modals/WrapperModal';
|
||||
|
||||
function Upload({
|
||||
modalState,
|
||||
receivedFile = [],
|
||||
setModalState,
|
||||
isOnboarding,
|
||||
renderTab = null,
|
||||
close,
|
||||
}: {
|
||||
modalState: ActiveState;
|
||||
receivedFile: File[];
|
||||
setModalState: (state: ActiveState) => void;
|
||||
isOnboarding: boolean;
|
||||
renderTab: string | null;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [docName, setDocName] = useState('');
|
||||
const [docName, setDocName] = useState(receivedFile[0]?.name);
|
||||
const [urlName, setUrlName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [repoUrl, setRepoUrl] = useState(''); // P3f93
|
||||
@@ -38,8 +42,8 @@ function Upload({
|
||||
search_queries: [''],
|
||||
number_posts: 10,
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<string | null>(null);
|
||||
const [files, setfiles] = useState<File[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(renderTab);
|
||||
const [files, setfiles] = useState<File[]>(receivedFile);
|
||||
const [progress, setProgress] = useState<{
|
||||
type: 'UPLOAD' | 'TRAINING';
|
||||
percentage: number;
|
||||
@@ -332,6 +336,9 @@ function Upload({
|
||||
],
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
||||
['.pptx'],
|
||||
'image/png': ['.png'],
|
||||
'image/jpeg': ['.jpeg'],
|
||||
'image/jpg': ['.jpg'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -600,7 +607,30 @@ function Upload({
|
||||
) : (
|
||||
<button
|
||||
onClick={uploadRemote}
|
||||
className={`ml-2 cursor-pointer rounded-3xl bg-purple-30 py-2 px-6 text-sm text-white hover:bg-[#6F3FD1]`}
|
||||
className={`ml-2 cursor-pointer rounded-3xl bg-purple-30 py-2 px-6 text-sm text-white hover:bg-[#6F3FD1] ${
|
||||
urlName.trim().length === 0 ||
|
||||
url.trim().length === 0 ||
|
||||
(urlType.label === 'Reddit' &&
|
||||
(redditData.client_id.length === 0 ||
|
||||
redditData.client_secret.length === 0 ||
|
||||
redditData.user_agent.length === 0 ||
|
||||
redditData.search_queries.length === 0 ||
|
||||
redditData.number_posts === 0)) ||
|
||||
(urlType.label === 'GitHub' && repoUrl.trim().length === 0)
|
||||
? 'bg-opacity-80 text-opacity-80'
|
||||
: ''
|
||||
}`}
|
||||
disabled={
|
||||
urlName.trim().length === 0 ||
|
||||
url.trim().length === 0 ||
|
||||
(urlType.label === 'Reddit' &&
|
||||
(redditData.client_id.length === 0 ||
|
||||
redditData.client_secret.length === 0 ||
|
||||
redditData.user_agent.length === 0 ||
|
||||
redditData.search_queries.length === 0 ||
|
||||
redditData.number_posts === 0)) ||
|
||||
(urlType.label === 'GitHub' && repoUrl.trim().length === 0)
|
||||
}
|
||||
>
|
||||
{t('modals.uploadDoc.train')}
|
||||
</button>
|
||||
@@ -626,28 +656,18 @@ function Upload({
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`${
|
||||
modalState === 'ACTIVE' ? 'visible' : 'hidden'
|
||||
} absolute z-30 bg-gray-alpha flex items-center justify-center h-[calc(100vh-4rem)] md:h-screen w-full`}
|
||||
<WrapperModal
|
||||
isPerformingTask={progress !== undefined && progress.percentage < 100}
|
||||
close={() => {
|
||||
close();
|
||||
setDocName('');
|
||||
setfiles([]);
|
||||
setModalState('INACTIVE');
|
||||
setActiveTab(null);
|
||||
}}
|
||||
>
|
||||
<article className="relative mx-auto flex w-[90vw] max-w-lg flex-col gap-4 rounded-lg bg-white p-6 shadow-lg dark:bg-outer-space h-fit-content">
|
||||
{!isOnboarding && !progress && (
|
||||
<button
|
||||
className="absolute top-4 right-4 m-1 w-3"
|
||||
onClick={() => {
|
||||
setDocName('');
|
||||
setfiles([]);
|
||||
setModalState('INACTIVE');
|
||||
setActiveTab(null);
|
||||
}}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
)}
|
||||
{view}
|
||||
</article>
|
||||
</article>
|
||||
{view}
|
||||
</WrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
4
frontend/src/utils/stringUtils.ts
Normal file
4
frontend/src/utils/stringUtils.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function truncate(str: string, n: number) {
|
||||
// slices long strings and ends with ...
|
||||
return str.length > n ? str.slice(0, n - 1) + '...' : str;
|
||||
}
|
||||
Reference in New Issue
Block a user