Merge branch 'main' into multiple-files-bug

This commit is contained in:
Nonthachai Plodthong
2024-11-14 12:28:51 -05:00
committed by GitHub
35 changed files with 577 additions and 151 deletions

View File

@@ -128,7 +128,7 @@ docker compose -f docker-compose-dev.yaml up -d
> Make sure you have Python 3.10 or 3.11 installed.
1. Export required environment variables or prepare a `.env` file in the project folder:
- Copy [.env_sample](https://github.com/arc53/DocsGPT/blob/main/application/.env_sample) and create `.env`.
- Copy [.env-template](https://github.com/arc53/DocsGPT/blob/main/application/.env-template) and create `.env`.
(check out [`application/core/settings.py`](application/core/settings.py) if you want to see more config options.)

View File

@@ -241,6 +241,7 @@ def complete_stream(
yield f"data: {data}\n\n"
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
traceback.print_exc()
data = json.dumps(
{
"type": "error",

View File

@@ -2,11 +2,12 @@ import datetime
import os
import shutil
import uuid
import math
from bson.binary import Binary, UuidRepresentation
from bson.dbref import DBRef
from bson.objectid import ObjectId
from flask import Blueprint, jsonify, make_response, request
from flask import Blueprint, jsonify, make_response, request, redirect
from flask_restx import inputs, fields, Namespace, Resource
from werkzeug.utils import secure_filename
@@ -455,12 +456,70 @@ class TaskStatus(Resource):
@user_ns.route("/api/combine")
class CombinedJson(Resource):
@api.doc(description="Provide JSON file with combined available indexes")
class RedirectToSources(Resource):
@api.doc(
description="Redirects /api/combine to /api/sources for backward compatibility"
)
def get(self):
return redirect("/api/sources", code=301)
@user_ns.route("/api/sources/paginated")
class PaginatedSources(Resource):
@api.doc(description="Get document with pagination, sorting and filtering")
def get(self):
user = "local"
sort_field = request.args.get("sort", "date") # Default to 'date'
sort_order = request.args.get("order", "desc") # Default to 'desc'
page = int(request.args.get("page", 1)) # Default to 1
rows_per_page = int(request.args.get("rows", 10)) # Default to 10
# Prepare
query = {"user": user}
total_documents = sources_collection.count_documents(query)
total_pages = max(1, math.ceil(total_documents / rows_per_page))
sort_order = 1 if sort_order == "asc" else -1
skip = (page - 1) * rows_per_page
try:
documents = (
sources_collection.find(query)
.sort(sort_field, sort_order)
.skip(skip)
.limit(rows_per_page)
)
paginated_docs = []
for doc in documents:
doc_data = {
"id": str(doc["_id"]),
"name": doc.get("name", ""),
"date": doc.get("date", ""),
"model": settings.EMBEDDINGS_NAME,
"location": "local",
"tokens": doc.get("tokens", ""),
"retriever": doc.get("retriever", "classic"),
"syncFrequency": doc.get("sync_frequency", ""),
}
paginated_docs.append(doc_data)
response = {
"total": total_documents,
"totalPages": total_pages,
"currentPage": page,
"paginated": paginated_docs,
}
return make_response(jsonify(response), 200)
except Exception as err:
return make_response(jsonify({"success": False, "error": str(err)}), 400)
@user_ns.route("/api/sources")
class CombinedJson(Resource):
@api.doc(description="Provide JSON file with combined available indexes")
def get(self):
user = "local"
data = [
{
"name": "default",
@@ -473,9 +532,7 @@ class CombinedJson(Resource):
]
try:
for index in sources_collection.find({"user": user}).sort(
sort_field, 1 if sort_order == "asc" else -1
):
for index in sources_collection.find({"user": user}).sort("date", -1):
data.append(
{
"id": str(index["_id"]),
@@ -513,6 +570,7 @@ class CombinedJson(Resource):
"retriever": "brave_search",
}
)
except Exception as err:
return make_response(jsonify({"success": False, "error": str(err)}), 400)

View File

@@ -45,7 +45,6 @@ class ClassicRAG(BaseRetriever):
settings.VECTOR_STORE, self.vectorstore, settings.EMBEDDINGS_KEY
)
docs_temp = docsearch.search(self.question, k=self.chunks)
print(docs_temp)
docs = [
{
"title": i.metadata.get(
@@ -60,8 +59,6 @@ class ClassicRAG(BaseRetriever):
}
for i in docs_temp
]
if settings.LLM_NAME == "llama.cpp":
docs = [docs[0]]
return docs

View File

@@ -7,7 +7,7 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.4.5",
"docsgpt": "^0.4.7",
"next": "^14.2.12",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
@@ -3576,9 +3576,9 @@
}
},
"node_modules/docsgpt": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.4.5.tgz",
"integrity": "sha512-vf09E7jGn7pBqfvFIbpDPdvIv8dbbjhrMKva9x5hjiW5eroC8q9s1KwAzwkWidFRe0lM/A1mZUvluIAR8v4TuQ==",
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.4.7.tgz",
"integrity": "sha512-4YZzLZo6ybudFrJVUQflDFeWzFiTATRWB9myrGSpLigyuMMzax1ZAY2xFallZLuEG9VVm0mOgkx3ssWHLrXWkQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/plugin-transform-flow-strip-types": "^7.23.3",

View File

@@ -7,7 +7,7 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.4.6",
"docsgpt": "^0.4.7",
"next": "^14.2.12",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",

View File

@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" theme="dark" />
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" theme="dark" size="medium" />
</>
)
}

View File

@@ -1,12 +1,12 @@
{
"name": "docsgpt",
"version": "0.4.6",
"version": "0.4.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "docsgpt",
"version": "0.4.6",
"version": "0.4.7",
"license": "Apache-2.0",
"dependencies": {
"@babel/plugin-transform-flow-strip-types": "^7.23.3",

View File

@@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.4.6",
"version": "0.4.7",
"private": false,
"description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.",
"source": "./src/index.html",

View File

@@ -81,7 +81,7 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
text-align: left;
@keyframes createBox {
0% {
transform: scale(0.5);
transform: scale(0.6);
}
90% {
transform: scale(1.02);
@@ -99,35 +99,89 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
transform: scale(1.02);
}
100% {
transform: scale(0);
transform: scale(0.6);
}
}
`;
const StyledContainer = styled.div`
const StyledContainer = styled.div<{ isOpen: boolean }>`
all: initial;
max-height: ${(props) => props.theme.dimensions.maxHeight};
max-width: ${(props) => props.theme.dimensions.maxWidth};
height: ${(props) => props.theme.dimensions.height} ;
width: ${(props) => props.theme.dimensions.width} ;
display: flex;
position: relative;
flex-direction: column;
justify-content: space-between;
bottom: 0;
left: 0;
border-radius: 12px;
background-color: ${props => props.theme.primary.bg};
background-color: ${(props) => props.theme.primary.bg};
font-family: sans-serif;
display: flex;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1);
transition: visibility 0.3s, opacity 0.3s;
padding: 26px 26px 0px 26px ;
padding: 26px 26px 0px 26px;
animation: ${({ isOpen, theme }) =>
theme.dimensions.size === 'large'
? isOpen
? 'fadeIn 150ms ease-in forwards'
: 'fadeOut 150ms ease-in forwards'
: isOpen
? 'openContainer 150ms ease-in forwards'
: 'closeContainer 250ms ease-in forwards'};
@keyframes openContainer {
0% {
width: 200px;
height: 100px;
}
100% {
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
border-radius: 12px;
}
}
@keyframes closeContainer {
0% {
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
border-radius: 12px;
}
100% {
width: 200px;
height: 100px;
}
}
@keyframes fadeIn {
from {
opacity: 0;
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
}
}
@keyframes fadeOut {
from {
opacity: 1;
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
}
to {
opacity: 0;
transform: scale(0.9);
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
}
}
@media only screen and (max-width: 768px) {
max-height: 100vh ;
max-width: 80vw;
overflow: auto;
max-height: 100vh;
max-width: 80vw;
overflow: auto;
}
`;
const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean }>`
const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatingButton: boolean }>`
position: fixed;
display: ${props => props.hidden ? "none" : "flex"};
z-index: 500;
@@ -144,9 +198,22 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean }>`
background: ${props => props.bgcolor};
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
animation: ${props => props.isAnimatingButton ? 'scaleAnimation 200ms forwards' : 'none'};
&:hover {
transform: scale(1.1);
transition: transform 0.2s ease-in-out;
transform: scale(1.1);
transition: transform 0.2s ease-in-out;
}
&:not(:hover) {
transition: transform 0.2s ease-in-out;
}
@keyframes scaleAnimation {
from {
transform: scale(1.2);
}
to {
transform: scale(1);
}
}
`;
const CancelButton = styled.button`
@@ -433,6 +500,8 @@ export const DocsGPTWidget = ({
const [conversationId, setConversationId] = React.useState<string | null>(null)
const [open, setOpen] = React.useState<boolean>(deafultOpen)
const [eventInterrupt, setEventInterrupt] = React.useState<boolean>(false); //click or scroll by user while autoScrolling
const [isAnimatingButton, setIsAnimatingButton] = React.useState(false);
const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true);
const isBubbleHovered = useRef<boolean>(false)
const widgetRef = useRef<HTMLDivElement>(null)
const endMessageRef = React.useRef<HTMLDivElement | null>(null);
@@ -548,15 +617,16 @@ export const DocsGPTWidget = ({
};
const handleClose = () => {
setOpen(false);
size !== "large" ? setTimeout(() => {
if (widgetRef.current)
widgetRef.current.style.display = "none"
setTimeout(() => {
if (widgetRef.current) widgetRef.current.style.display = "none";
setIsFloatingButtonVisible(true);
setIsAnimatingButton(true);
setTimeout(() => setIsAnimatingButton(false), 200);
}, 250)
:
widgetRef.current && (widgetRef.current.style.display = "none")
};
const handleOpen = () => {
setOpen(true);
setIsFloatingButtonVisible(false);
if (widgetRef.current)
widgetRef.current.style.display = 'block'
}
@@ -570,12 +640,12 @@ export const DocsGPTWidget = ({
{open && size === 'large' &&
<Overlay onClick={handleClose} />
}
<FloatingButton bgcolor={buttonBg} onClick={handleOpen} hidden={open}>
<FloatingButton bgcolor={buttonBg} onClick={handleOpen} hidden={!isFloatingButtonVisible} isAnimatingButton={isAnimatingButton}>
<img width={24} src={buttonIcon} />
<span>{buttonText}</span>
</FloatingButton>
<WidgetContainer ref={widgetRef} className={`${size != "large" && (open ? "open" : "close")}`} modal={size == 'large'}>
{<StyledContainer>
{<StyledContainer isOpen={open}>
<div>
<CancelButton onClick={handleClose}>
<Cross2Icon width={24} height={24} color={theme === 'light' ? 'black' : 'white'} />
@@ -669,7 +739,7 @@ export const DocsGPTWidget = ({
</PromptContainer>
<Tagline>
Powered by&nbsp;
<Hyperlink target='_blank' href='https://github.com/arc53/DocsGPT'>DocsGPT</Hyperlink>
<Hyperlink target='_blank' href='https://www.docsgpt.cloud/'>DocsGPT</Hyperlink>
</Tagline>
</div>
</StyledContainer>}

View File

@@ -34,10 +34,12 @@ import {
selectSelectedDocs,
selectSelectedDocsStatus,
selectSourceDocs,
selectPaginatedDocuments,
setConversations,
setModalStateDeleteConv,
setSelectedDocs,
setSourceDocs,
setPaginatedDocuments,
} from './preferences/preferenceSlice';
import Spinner from './assets/spinner.svg';
import SpinnerDark from './assets/spinner-dark.svg';
@@ -72,6 +74,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const conversations = useSelector(selectConversations);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const conversationId = useSelector(selectConversationId);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const { isMobile } = useMediaQuery();
@@ -143,9 +146,18 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
})
.then((updatedDocs) => {
dispatch(setSourceDocs(updatedDocs));
const updatedPaginatedDocs = paginatedDocuments?.filter(
(document) => document.id !== doc.id,
);
dispatch(
setPaginatedDocuments(updatedPaginatedDocs || paginatedDocuments),
);
dispatch(
setSelectedDocs(
updatedDocs?.find((doc) => doc.name.toLowerCase() === 'default'),
Array.isArray(updatedDocs) &&
updatedDocs?.find(
(doc: Doc) => doc.name.toLowerCase() === 'default',
),
),
);
})

View File

@@ -1,7 +1,8 @@
const endpoints = {
USER: {
DOCS: '/api/combine',
DOCS: '/api/sources',
DOCS_CHECK: '/api/docs_check',
DOCS_PAGINATED: '/api/sources/paginated',
API_KEYS: '/api/get_api_keys',
CREATE_API_KEY: '/api/create_api_key',
DELETE_API_KEY: '/api/delete_api_key',

View File

@@ -2,8 +2,9 @@ import apiClient from '../client';
import endpoints from '../endpoints';
const userService = {
getDocs: (sort = 'date', order = 'desc'): Promise<any> =>
apiClient.get(`${endpoints.USER.DOCS}?sort=${sort}&order=${order}`),
getDocs: (): Promise<any> => apiClient.get(`${endpoints.USER.DOCS}`),
getDocsWithPagination: (query: string): Promise<any> =>
apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`),
checkDocs: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.DOCS_CHECK, data),
getAPIKeys: (): Promise<any> => apiClient.get(endpoints.USER.API_KEYS),

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54" />
<path d="M15.41 10.59L10.83 6L15.41 1.41L14 0L8 6L14 12L15.41 10.59Z" fill="black"
fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59 10.59L13.17 6L8.59 1.41L10 0L16 6L10 12L8.59 10.59Z" fill="black"
fill-opacity="0.54" />
<path d="M0.59 10.59L5.17 6L0.59 1.41L2 0L8 6L2 12L0.59 10.59Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -1,3 +1,3 @@
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 26.0003H18C19.1 26.0003 20 25.1003 20 24.0003V14.0003H23.18C24.96 14.0003 25.86 11.8403 24.6 10.5803L15.42 1.40032C15.235 1.21491 15.0152 1.06782 14.7732 0.967453C14.5313 0.86709 14.2719 0.81543 14.01 0.81543C13.7481 0.81543 13.4887 0.86709 13.2468 0.967453C13.0048 1.06782 12.785 1.21491 12.6 1.40032L3.42 10.5803C2.16 11.8403 3.04 14.0003 4.82 14.0003H8V24.0003C8 25.1003 8.9 26.0003 10 26.0003ZM2 30.0003H26C27.1 30.0003 28 30.9003 28 32.0003C28 33.1003 27.1 34.0003 26 34.0003H2C0.9 34.0003 0 33.1003 0 32.0003C0 30.9003 0.9 30.0003 2 30.0003Z" fill="#828282"/>
<path d="M10 26.0003H18C19.1 26.0003 20 25.1003 20 24.0003V14.0003H23.18C24.96 14.0003 25.86 11.8403 24.6 10.5803L15.42 1.40032C15.235 1.21491 15.0152 1.06782 14.7732 0.967453C14.5313 0.86709 14.2719 0.81543 14.01 0.81543C13.7481 0.81543 13.4887 0.86709 13.2468 0.967453C13.0048 1.06782 12.785 1.21491 12.6 1.40032L3.42 10.5803C2.16 11.8403 3.04 14.0003 4.82 14.0003H8V24.0003C8 25.1003 8.9 26.0003 10 26.0003ZM2 30.0003H26C27.1 30.0003 28 30.9003 28 32.0003C28 33.1003 27.1 34.0003 26 34.0003H2C0.9 34.0003 0 33.1003 0 32.0003C0 30.9003 0.9 30.0003 2 30.0003Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.59 1.41L5.17 6L0.59 10.59L2 12L8 6L2 0L0.59 1.41Z" fill="black" fill-opacity="0.54" />
</svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -1,6 +1,6 @@
<svg width="58" height="49" viewBox="0 0 58 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M33.0002 45.3337H6.3335C5.27263 45.3337 4.25521 44.9122 3.50507 44.1621C2.75492 43.4119 2.3335 42.3945 2.3335 41.3337V6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V24.0003M42.3335 40.0003L49.0002 46.667M49.0002 46.667L55.6668 40.0003M49.0002 46.667V30.667" stroke="#626262" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.3335 6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V18.667H2.3335V6.66699Z" stroke="#626262" stroke-width="4"/>
<path d="M7.66667 10.6673C7.66667 9.19456 8.86057 8.00065 10.3333 8.00065C11.8061 8.00065 13 9.19456 13 10.6673C13 12.1401 11.8061 13.334 10.3333 13.334C8.86057 13.334 7.66667 12.1401 7.66667 10.6673Z" fill="#626262"/>
<path d="M15.6667 10.6673C15.6667 9.19456 16.8606 8.00065 18.3333 8.00065C19.8061 8.00065 21 9.19456 21 10.6673C21 12.1401 19.8061 13.334 18.3333 13.334C16.8606 13.334 15.6667 12.1401 15.6667 10.6673Z" fill="#626262"/>
<path d="M33.0002 45.3337H6.3335C5.27263 45.3337 4.25521 44.9122 3.50507 44.1621C2.75492 43.4119 2.3335 42.3945 2.3335 41.3337V6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V24.0003M42.3335 40.0003L49.0002 46.667M49.0002 46.667L55.6668 40.0003M49.0002 46.667V30.667" stroke="#949494" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.3335 6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V18.667H2.3335V6.66699Z" stroke="#949494" stroke-width="4"/>
<path d="M7.66667 10.6673C7.66667 9.19456 8.86057 8.00065 10.3333 8.00065C11.8061 8.00065 13 9.19456 13 10.6673C13 12.1401 11.8061 13.334 10.3333 13.334C8.86057 13.334 7.66667 12.1401 7.66667 10.6673Z" fill="#949494"/>
<path d="M15.6667 10.6673C15.6667 9.19456 16.8606 8.00065 18.3333 8.00065C19.8061 8.00065 21 9.19456 21 10.6673C21 12.1401 19.8061 13.334 18.3333 13.334C16.8606 13.334 15.6667 12.1401 15.6667 10.6673Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import SingleArrowLeft from '../assets/single-left-arrow.svg';
import SingleArrowRight from '../assets/single-right-arrow.svg';
import DoubleArrowLeft from '../assets/double-arrow-left.svg';
import DoubleArrowRight from '../assets/double-arrow-right.svg';
interface PaginationProps {
currentPage: number;
totalPages: number;
rowsPerPage: number;
onPageChange: (page: number) => void;
onRowsPerPageChange: (rows: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
}) => {
const [rowsPerPageOptions] = useState([5, 10, 15, 20]);
const handlePreviousPage = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNextPage = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirstPage = () => {
onPageChange(1);
};
const handleLastPage = () => {
onPageChange(totalPages);
};
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 ">
<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>
<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}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={DoubleArrowLeft}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={SingleArrowLeft}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handleNextPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={SingleArrowRight}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
<button
onClick={handleLastPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
>
<img
src={DoubleArrowRight}
alt="arrow"
className="dark:invert dark:sepia dark:brightness-200"
/>
</button>
</div>
</div>
);
};
export default Pagination;

View File

@@ -1,29 +1,30 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import newChatIcon from '../assets/openNewChat.svg';
import { useNavigate } from 'react-router-dom';
import Hero from '../Hero';
import ArrowDown from '../assets/arrow-down.svg';
import newChatIcon from '../assets/openNewChat.svg';
import Send from '../assets/send.svg';
import SendDark from '../assets/send_dark.svg';
import ShareIcon from '../assets/share.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
import RetryIcon from '../components/RetryIcon';
import { useNavigate } from 'react-router-dom';
import Hero from '../Hero';
import { useDarkTheme, useMediaQuery } from '../hooks';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { setConversation, updateConversationId } from './conversationSlice';
import { selectConversationId } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import ConversationBubble from './ConversationBubble';
import { handleSendFeedback } from './conversationHandlers';
import { FEEDBACK, Query } from './conversationModels';
import ShareIcon from '../assets/share.svg';
import {
addQuery,
fetchAnswer,
selectQueries,
selectStatus,
setConversation,
updateConversationId,
updateQuery,
} from './conversationSlice';
@@ -216,9 +217,9 @@ export default function Conversation() {
return (
<div className="flex flex-col gap-1 h-full justify-end ">
{conversationId && queries.length > 0 && (
<div className="absolute top-4 right-20 z-20 ">
<div className="absolute top-4 right-20 z-10 ">
{' '}
<div className="flex items-center gap-4 ">
<div className="flex mt-2 items-center gap-4 ">
{isMobile && queries.length > 0 && (
<button
title="Open New Chat"
@@ -302,14 +303,14 @@ export default function Conversation() {
)}
</div>
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-[62%] h-auto">
<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">
<textarea
id="inputbox"
ref={inputRef}
tabIndex={1}
placeholder={t('inputPlaceholder')}
className={`inputbox-style h-16 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`}
className={`inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-transparent py-5 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray`}
onInput={handleInput}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {

View File

@@ -1,12 +1,14 @@
import 'katex/dist/katex.min.css';
import { forwardRef, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
import Dislike from '../assets/dislike.svg?react';
import Document from '../assets/document.svg';
@@ -16,13 +18,13 @@ import Sources from '../assets/sources.svg';
import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
import Sidebar from '../components/Sidebar';
import SpeakButton from '../components/TextToSpeechButton';
import {
selectChunks,
selectSelectedDocs,
} from '../preferences/preferenceSlice';
import classes from './ConversationBubble.module.css';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import SpeakButton from '../components/TextToSpeechButton';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
@@ -41,6 +43,7 @@ const ConversationBubble = forwardRef<
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
ref,
) {
// const bubbleRef = useRef<HTMLDivElement | null>(null);
const chunks = useSelector(selectChunks);
const selectedDocs = useSelector(selectSelectedDocs);
const [isLikeHovered, setIsLikeHovered] = useState(false);
@@ -141,7 +144,7 @@ const ConversationBubble = forwardRef<
/>
<p className="text-base font-semibold">Sources</p>
</div>
<div className="ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
<div className="fade-in ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{sources?.slice(0, 3)?.map((source, index) => (
<div key={index} className="relative">
@@ -190,7 +193,7 @@ const ConversationBubble = forwardRef<
</div>
{activeTooltip === index && (
<div
className={`absolute left-1/2 z-30 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver sm:w-56`}
className={`absolute left-1/2 z-50 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver sm:w-56`}
onMouseOver={() => setActiveTooltip(index)}
onMouseOut={() => setActiveTooltip(null)}
>
@@ -231,14 +234,14 @@ const ConversationBubble = forwardRef<
<p className="text-base font-semibold">Answer</p>
</div>
<div
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
className={`fade-in-bubble ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
type === 'ERROR'
? 'relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
: 'flex-col rounded-3xl'
}`}
>
<ReactMarkdown
className="whitespace-pre-wrap break-normal leading-normal"
className="fade-in whitespace-pre-wrap break-normal leading-normal"
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{

View File

@@ -17,11 +17,12 @@ export default function useDefaultDocument() {
getDocs().then((data) => {
dispatch(setSourceDocs(data));
if (!selectedDoc)
data?.forEach((doc: Doc) => {
if (doc.model && doc.name === 'default') {
dispatch(setSelectedDocs(doc));
}
});
Array.isArray(data) &&
data?.forEach((doc: Doc) => {
if (doc.model && doc.name === 'default') {
dispatch(setSelectedDocs(doc));
}
});
});
};

View File

@@ -50,11 +50,11 @@ body.dark {
@layer components {
.table-default {
@apply block w-max table-auto content-center justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray;
@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;
}
.table-default th {
@apply p-4 w-[244px] font-normal text-gray-400; /* Remove border-r */
@apply p-4 w-full font-normal text-gray-400 text-nowrap; /* Remove border-r */
}
.table-default th:last-child {
@@ -514,3 +514,29 @@ input:-webkit-autofill:focus {
.logs-table {
font-family: 'IBMPlexMono-Medium', system-ui;
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-in-bubble {
opacity: 0;
transform: translateY(10px);
animation: fadeInUp 0.5s forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -77,12 +77,12 @@
},
"modals": {
"uploadDoc": {
"label": "Upload New Documentation",
"label": "Upload new document",
"select": "Choose how to upload your document to DocsGPT",
"file": "Upload from device",
"back": "Back",
"wait": "Please wait ...",
"remote": "Collect from a website",
"remote": "Collect from website",
"start": "Start Chatting",
"name": "Name",
"choose": "Choose Files",

View File

@@ -77,7 +77,7 @@
},
"modals": {
"uploadDoc": {
"label": "Subir Nueva Documentación",
"label": "Subir nuevo documento",
"select": "Elija cómo cargar su documento en DocsGPT",
"file": "Subir desde el dispositivo",
"back": "Atrás",

View File

@@ -77,7 +77,7 @@
},
"modals": {
"uploadDoc": {
"label": "新規書類のアップロード",
"label": "新しい文書をアップロードする",
"select": "ドキュメントを DocsGPT にアップロードする方法を選択します",
"file": "デバイスからアップロード",
"back": "戻る",

View File

@@ -77,7 +77,7 @@
},
"modals": {
"uploadDoc": {
"label": "上传新文档资料",
"label": "上传新文档",
"select": "选择如何将文档上传到 DocsGPT",
"file": "从设备上传",
"back": "后退",

View File

@@ -14,6 +14,13 @@ export type Doc = {
syncFrequency?: string;
};
export type GetDocsResponse = {
docs: Doc[];
totalDocuments: number;
totalPages: number;
nextCursor: string;
};
export type PromptProps = {
prompts: { name: string; id: string; type: string }[];
selectedPrompt: { name: string; id: string; type: string };
@@ -22,7 +29,7 @@ export type PromptProps = {
};
export type DocumentsProps = {
documents: Doc[] | null;
paginatedDocuments: Doc[] | null;
handleDeleteDocument: (index: number, document: Doc) => void;
};

View File

@@ -1,18 +1,14 @@
import conversationService from '../api/services/conversationService';
import userService from '../api/services/userService';
import { Doc } from '../models/misc';
import { Doc, GetDocsResponse } from '../models/misc';
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
export async function getDocs(
sort = 'date',
order = 'desc',
): Promise<Doc[] | null> {
export async function getDocs(): Promise<Doc[] | null> {
try {
const response = await userService.getDocs(sort, order);
const response = await userService.getDocs();
const data = await response.json();
const docs: Doc[] = [];
data.forEach((doc: object) => {
docs.push(doc as Doc);
});
@@ -24,6 +20,33 @@ export async function getDocs(
}
}
export async function getDocsWithPagination(
sort = 'date',
order = 'desc',
pageNumber = 1,
rowsPerPage = 10,
): Promise<GetDocsResponse | null> {
try {
const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}`;
const response = await userService.getDocsWithPagination(query);
const data = await response.json();
const docs: Doc[] = [];
Array.isArray(data.paginated) &&
data.paginated.forEach((doc: Doc) => {
docs.push(doc as Doc);
});
return {
docs: docs,
totalDocuments: data.total,
totalPages: data.totalPages,
nextCursor: data.nextCursor,
};
} catch (error) {
console.log(error);
return null;
}
}
export async function getConversations(): Promise<{
data: { name: string; id: string }[] | null;
loading: boolean;

View File

@@ -20,6 +20,7 @@ export interface Preference {
loading: boolean;
};
modalState: ActiveState;
paginatedDocuments: Doc[] | null;
}
const initialState: Preference = {
@@ -42,6 +43,7 @@ const initialState: Preference = {
loading: false,
},
modalState: 'INACTIVE',
paginatedDocuments: null,
};
export const prefSlice = createSlice({
@@ -57,6 +59,9 @@ export const prefSlice = createSlice({
setSourceDocs: (state, action) => {
state.sourceDocs = action.payload;
},
setPaginatedDocuments: (state, action) => {
state.paginatedDocuments = action.payload;
},
setConversations: (state, action) => {
state.conversations = action.payload;
},
@@ -84,6 +89,7 @@ export const {
setChunks,
setTokenLimit,
setModalStateDeleteConv,
setPaginatedDocuments,
} = prefSlice.actions;
export default prefSlice.reducer;
@@ -155,3 +161,5 @@ export const selectPrompt = (state: RootState) => state.preference.prompt;
export const selectChunks = (state: RootState) => state.preference.chunks;
export const selectTokenLimit = (state: RootState) =>
state.preference.token_limit;
export const selectPaginatedDocuments = (state: RootState) =>
state.preference.paginatedDocuments;

View File

@@ -1,19 +1,20 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import userService from '../api/services/userService';
import SyncIcon from '../assets/sync.svg';
import Trash from '../assets/trash.svg';
import caretSort from '../assets/caret-sort.svg';
import DropdownMenu from '../components/DropdownMenu';
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
import SkeletonLoader from '../components/SkeletonLoader';
import { getDocs } from '../preferences/preferenceApi';
import { setSourceDocs } from '../preferences/preferenceSlice';
import Input from '../components/Input';
import Upload from '../upload/Upload'; // Import the Upload component
import Pagination from '../components/DocumentPagination';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi';
import { setSourceDocs } from '../preferences/preferenceSlice';
import { setPaginatedDocuments } from '../preferences/preferenceSlice';
// Utility function to format numbers
const formatTokens = (tokens: number): string => {
@@ -33,12 +34,11 @@ const formatTokens = (tokens: number): string => {
};
const Documents: React.FC<DocumentsProps> = ({
documents,
paginatedDocuments,
handleDeleteDocument,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
// State for search input
const [searchTerm, setSearchTerm] = useState('');
// State for modal: active/inactive
@@ -47,37 +47,49 @@ const Documents: React.FC<DocumentsProps> = ({
const [loading, setLoading] = useState(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 syncOptions = [
{ label: 'Never', value: 'never' },
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
];
const refreshDocs = (field: 'date' | 'tokens') => {
if (field === sortField) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortOrder('desc');
setSortField(field);
const refreshDocs = (
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');
}
}
getDocs(sortField, sortOrder)
getDocsWithPagination(sortField, sortOrder, page, rowsPerPg)
.then((data) => {
dispatch(setSourceDocs(data));
})
.catch((error) => console.error(error))
.finally(() => {
setLoading(false);
});
};
const handleManageSync = (doc: Doc, sync_frequency: string) => {
setLoading(true);
userService
.manageSync({ source_id: doc.id, sync_frequency })
.then(() => {
return getDocs();
})
.then((data) => {
dispatch(setSourceDocs(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(() => {
@@ -85,10 +97,40 @@ const Documents: React.FC<DocumentsProps> = ({
});
};
// Filter documents based on the search term
const filteredDocuments = documents?.filter((document) =>
document.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const handleManageSync = (doc: Doc, sync_frequency: string) => {
setLoading(true);
userService
.manageSync({ source_id: doc.id, sync_frequency })
.then(() => {
// First, fetch the updated source docs
return getDocs();
})
.then((data) => {
dispatch(setSourceDocs(data));
return getDocsWithPagination(
sortField,
sortOrder,
currentPage,
rowsPerPage,
);
})
.then((paginatedData) => {
dispatch(
setPaginatedDocuments(paginatedData ? paginatedData.docs : []),
);
setTotalPages(paginatedData ? paginatedData.totalPages : 0);
})
.catch((error) => console.error('Error in handleManageSync:', error))
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (modalState === 'INACTIVE') {
refreshDocs(sortField, currentPage, rowsPerPage);
}
}, [modalState, sortField, currentPage, rowsPerPage]);
return (
<div className="mt-8">
@@ -154,16 +196,16 @@ const Documents: React.FC<DocumentsProps> = ({
</tr>
</thead>
<tbody>
{!filteredDocuments?.length && (
{!currentDocuments?.length && (
<tr>
<td colSpan={5} className="!p-4">
{t('settings.documents.noData')}
</td>
</tr>
)}
{filteredDocuments &&
filteredDocuments.map((document, index) => (
<tr key={index}>
{Array.isArray(currentDocuments) &&
currentDocuments.map((document, index) => (
<tr key={index} className="text-nowrap font-normal">
<td>{document.name}</td>
<td>{document.date}</td>
<td>
@@ -173,7 +215,7 @@ const Documents: React.FC<DocumentsProps> = ({
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
</td>
<td>
<div className="flex flex-row items-center">
<div className="min-w-[70px] flex flex-row items-end justify-end ml-auto">
{document.type !== 'remote' && (
<img
src={Trash}
@@ -221,12 +263,31 @@ const Documents: React.FC<DocumentsProps> = ({
</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>
);
};
Documents.propTypes = {
documents: PropTypes.array.isRequired,
//documents: PropTypes.array.isRequired,
handleDeleteDocument: PropTypes.func.isRequired,
};

View File

@@ -8,6 +8,8 @@ import i18n from '../locale/i18n';
import { Doc } from '../models/misc';
import {
selectSourceDocs,
selectPaginatedDocuments,
setPaginatedDocuments,
setSourceDocs,
} from '../preferences/preferenceSlice';
import Analytics from './Analytics';
@@ -26,20 +28,29 @@ export default function Settings() {
);
const documents = useSelector(selectSourceDocs);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const updateWidgetScreenshot = (screenshot: File | null) => {
setWidgetScreenshot(screenshot);
};
const updateDocumentsList = (documents: Doc[], index: number) => [
...documents.slice(0, index),
...documents.slice(index + 1),
];
const handleDeleteClick = (index: number, doc: Doc) => {
userService
.deletePath(doc.id ?? '')
.then((response) => {
if (response.ok && documents) {
const updatedDocuments = [
...documents.slice(0, index),
...documents.slice(index + 1),
];
dispatch(setSourceDocs(updatedDocuments));
if (paginatedDocuments) {
dispatch(
setPaginatedDocuments(
updateDocumentsList(paginatedDocuments, index),
),
);
}
dispatch(setSourceDocs(updateDocumentsList(documents, index)));
}
})
.catch((error) => console.error(error));
@@ -72,7 +83,7 @@ export default function Settings() {
case t('settings.documents.label'):
return (
<Documents
documents={documents}
paginatedDocuments={paginatedDocuments}
handleDeleteDocument={handleDeleteClick}
/>
);

View File

@@ -38,6 +38,7 @@ const preloadedState: { preference: Preference } = {
},
],
modalState: 'INACTIVE',
paginatedDocuments: null,
},
};
const store = configureStore({

View File

@@ -166,7 +166,10 @@ function Upload({
dispatch(setSourceDocs(data));
dispatch(
setSelectedDocs(
data?.find((d) => d.type?.toLowerCase() === 'local'),
Array.isArray(data) &&
data?.find(
(d: Doc) => d.type?.toLowerCase() === 'local',
),
),
);
});
@@ -182,15 +185,21 @@ function Upload({
getDocs().then((data) => {
dispatch(setSourceDocs(data));
const docIds = new Set(
sourceDocs?.map((doc: Doc) => (doc.id ? doc.id : null)),
(Array.isArray(sourceDocs) &&
sourceDocs?.map((doc: Doc) =>
doc.id ? doc.id : null,
)) ||
[],
);
data?.map((updatedDoc: Doc) => {
if (updatedDoc.id && !docIds.has(updatedDoc.id)) {
//select the doc not present in the intersection of current Docs and fetched data
dispatch(setSelectedDocs(updatedDoc));
return;
}
});
if (data && Array.isArray(data)) {
data.map((updatedDoc: Doc) => {
if (updatedDoc.id && !docIds.has(updatedDoc.id)) {
// Select the doc not present in the intersection of current Docs and fetched data
dispatch(setSelectedDocs(updatedDoc));
return;
}
});
}
});
setProgress(
(progress) =>
@@ -351,32 +360,32 @@ function Upload({
} else {
view = (
<div className="flex flex-col gap-4 w-full">
<p className="text-xl text-jet dark:text-bright-gray text-center font-semibold">
<p className="text-2xl text-jet dark:text-bright-gray text-center font-semibold">
{t('modals.uploadDoc.label')}
</p>
{!activeTab && (
<div>
<p className="text-gray-6000 dark:text-light-gray text-sm text-center">
<p className="text-gray-6000 dark:text-bright-gray text-sm text-center font-medium">
{t('modals.uploadDoc.select')}
</p>
<div className="w-full gap-4 h-full p-4 flex flex-col md:flex-row md:gap-4 justify-center items-center">
<button
onClick={() => setActiveTab('file')}
className="rounded-3xl text-md font-medium border flex flex-col items-center justify-center hover:shadow-lg dark:hover:shadow-lg p-8 dark:active:shadow-lg gap-4 bg-white dark:bg-outer-space dark:text-light-gray hover:bg-gray-100 dark:hover:bg-[#767183]/50 hover:text-purple-30 dark:hover:text-gray-30 border-purple-30 dark:border-gray-700 h-40 w-40 md:w-52 md:h-52"
className="opacity-85 hover:opacity-100 rounded-3xl text-sm font-medium border flex flex-col items-center justify-center hover:shadow-purple-30/30 hover:shadow-lg p-8 gap-4 bg-white text-[#777777] dark:bg-outer-space dark:text-[#c3c3c3] hover:border-purple-30 border-[#D7D7D7] h-40 w-40 md:w-52 md:h-52"
>
<img
src={FileUpload}
className="w-8 h-8 mr-2 dark:filter dark:invert"
className="w-12 h-12 mr-2 dark:filter dark:invert dark:brightness-50"
/>
{t('modals.uploadDoc.file')}
</button>
<button
onClick={() => setActiveTab('remote')}
className="rounded-3xl text-md font-medium border flex flex-col items-center justify-center hover:shadow-lg dark:hover:shadow-lg p-8 dark:active:shadow-lg gap-4 bg-white dark:bg-outer-space dark:text-light-gray hover:bg-gray-100 dark:hover:bg-[#767183]/50 hover:text-purple-30 dark:hover:text-gray-30 border-purple-30 dark:border-gray-700 h-40 w-40 md:w-52 md:h-52"
className="opacity-85 hover:opacity-100 rounded-3xl text-sm font-medium border flex flex-col items-center justify-center hover:shadow-purple-30/30 hover:shadow-lg p-8 gap-4 bg-white text-[#777777] dark:bg-outer-space dark:text-[#c3c3c3] hover:border-purple-30 border-[#D7D7D7] h-40 w-40 md:w-52 md:h-52"
>
<img
src={WebsiteCollect}
className="w-8 h-8 mr-2 dark:filter dark:invert"
className="w-14 h-14 mr-2 dark:filter dark:invert dark:brightness-50"
/>
{t('modals.uploadDoc.remote')}
</button>