Merge branch 'main' into tool-use

This commit is contained in:
Alex
2024-12-20 17:29:41 +00:00
committed by GitHub
84 changed files with 4317 additions and 5522 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -5,7 +5,7 @@ export default function Avatar({
size,
className,
}: {
avatar: string | ReactNode;
avatar: ReactNode;
size?: 'SMALL' | 'MEDIUM' | 'LARGE';
className: string;
}) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -99,6 +99,10 @@
"agent": "ユーザーエージェント",
"searchQueries": "検索クエリ",
"numberOfPosts": "投稿数"
},
"drag": {
"title": "ソースファイルをアップロードする",
"description": "ファイルをここにドロップしてソースとして追加します"
}
},
"createAPIKey": {

142
frontend/src/locale/ru.json Normal file
View 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": "Вы уверены, что хотите удалить этот разговор?"
}
}

View File

@@ -92,6 +92,10 @@
"agent": "使用者代理(User-Agent)",
"searchQueries": "搜尋查詢",
"numberOfPosts": "貼文數量"
},
"drag": {
"title": "上傳原始檔",
"description": "將您的文件拖放到此處以將其添加為來源"
}
},
"createAPIKey": {

View File

@@ -99,6 +99,10 @@
"agent": "用户代理",
"searchQueries": "搜索查询",
"numberOfPosts": "帖子数量"
},
"drag": {
"title": "上传源文件",
"description": "将您的文件拖放到此处以将其添加为源"
}
},
"createAPIKey": {

View File

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

View File

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

View File

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

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

View File

@@ -8,4 +8,9 @@ export type AvailableTool = {
description: string;
parameters: object;
}[];
export type WrapperModalProps = {
children?: React.ReactNode;
isPerformingTask?: boolean;
close: () => void;
};

View File

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

View File

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

View File

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

View File

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

View File

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

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