mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt
This commit is contained in:
@@ -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.)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -429,12 +430,70 @@ class TaskStatus(Resource):
|
||||
|
||||
|
||||
@user_ns.route("/api/combine")
|
||||
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"
|
||||
sort_field = request.args.get('sort', 'date') # Default to 'date'
|
||||
sort_order = request.args.get('order', "desc") # Default to 'desc'
|
||||
data = [
|
||||
{
|
||||
"name": "default",
|
||||
@@ -447,7 +506,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"]),
|
||||
@@ -485,6 +544,7 @@ class CombinedJson(Resource):
|
||||
"retriever": "brave_search",
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||
|
||||
@@ -1674,7 +1734,9 @@ class TextToSpeech(Resource):
|
||||
tts_model = api.model(
|
||||
"TextToSpeechModel",
|
||||
{
|
||||
"text": fields.String(required=True, description="Text to be synthesized as audio"),
|
||||
"text": fields.String(
|
||||
required=True, description="Text to be synthesized as audio"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1686,8 +1748,15 @@ class TextToSpeech(Resource):
|
||||
try:
|
||||
tts_instance = GoogleTTS()
|
||||
audio_base64, detected_language = tts_instance.text_to_speech(text)
|
||||
return make_response(jsonify({"success": True,'audio_base64': audio_base64,'lang':detected_language}), 200)
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"audio_base64": audio_base64,
|
||||
"lang": detected_language,
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
except Exception as err:
|
||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||
|
||||
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
5
frontend/src/assets/double-arrow-left.svg
Normal file
5
frontend/src/assets/double-arrow-left.svg
Normal 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 |
5
frontend/src/assets/double-arrow-right.svg
Normal file
5
frontend/src/assets/double-arrow-right.svg
Normal 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 |
3
frontend/src/assets/single-left-arrow.svg
Normal file
3
frontend/src/assets/single-left-arrow.svg
Normal 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 |
3
frontend/src/assets/single-right-arrow.svg
Normal file
3
frontend/src/assets/single-right-arrow.svg
Normal 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 |
119
frontend/src/components/DocumentPagination.tsx
Normal file
119
frontend/src/components/DocumentPagination.tsx
Normal 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;
|
||||
@@ -17,6 +17,7 @@ export default function useDefaultDocument() {
|
||||
getDocs().then((data) => {
|
||||
dispatch(setSourceDocs(data));
|
||||
if (!selectedDoc)
|
||||
Array.isArray(data) &&
|
||||
data?.forEach((doc: Doc) => {
|
||||
if (doc.model && doc.name === 'default') {
|
||||
dispatch(setSelectedDocs(doc));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') => {
|
||||
|
||||
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 {
|
||||
setSortOrder('desc');
|
||||
// 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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ const preloadedState: { preference: Preference } = {
|
||||
},
|
||||
],
|
||||
modalState: 'INACTIVE',
|
||||
paginatedDocuments: null,
|
||||
},
|
||||
};
|
||||
const store = configureStore({
|
||||
|
||||
@@ -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 (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
|
||||
// Select the doc not present in the intersection of current Docs and fetched data
|
||||
dispatch(setSelectedDocs(updatedDoc));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setProgress(
|
||||
(progress) =>
|
||||
|
||||
Reference in New Issue
Block a user