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.
|
> 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:
|
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.)
|
(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 os
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
|
import math
|
||||||
|
|
||||||
from bson.binary import Binary, UuidRepresentation
|
from bson.binary import Binary, UuidRepresentation
|
||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
from bson.objectid import ObjectId
|
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 flask_restx import inputs, fields, Namespace, Resource
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@@ -429,12 +430,70 @@ class TaskStatus(Resource):
|
|||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/combine")
|
@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):
|
class CombinedJson(Resource):
|
||||||
@api.doc(description="Provide JSON file with combined available indexes")
|
@api.doc(description="Provide JSON file with combined available indexes")
|
||||||
def get(self):
|
def get(self):
|
||||||
user = "local"
|
user = "local"
|
||||||
sort_field = request.args.get('sort', 'date') # Default to 'date'
|
|
||||||
sort_order = request.args.get('order', "desc") # Default to 'desc'
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
@@ -447,7 +506,7 @@ class CombinedJson(Resource):
|
|||||||
]
|
]
|
||||||
|
|
||||||
try:
|
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(
|
data.append(
|
||||||
{
|
{
|
||||||
"id": str(index["_id"]),
|
"id": str(index["_id"]),
|
||||||
@@ -485,6 +544,7 @@ class CombinedJson(Resource):
|
|||||||
"retriever": "brave_search",
|
"retriever": "brave_search",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||||
|
|
||||||
@@ -1674,7 +1734,9 @@ class TextToSpeech(Resource):
|
|||||||
tts_model = api.model(
|
tts_model = api.model(
|
||||||
"TextToSpeechModel",
|
"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:
|
try:
|
||||||
tts_instance = GoogleTTS()
|
tts_instance = GoogleTTS()
|
||||||
audio_base64, detected_language = tts_instance.text_to_speech(text)
|
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:
|
except Exception as err:
|
||||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Component {...pageProps} />
|
<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,
|
selectSelectedDocs,
|
||||||
selectSelectedDocsStatus,
|
selectSelectedDocsStatus,
|
||||||
selectSourceDocs,
|
selectSourceDocs,
|
||||||
|
selectPaginatedDocuments,
|
||||||
setConversations,
|
setConversations,
|
||||||
setModalStateDeleteConv,
|
setModalStateDeleteConv,
|
||||||
setSelectedDocs,
|
setSelectedDocs,
|
||||||
setSourceDocs,
|
setSourceDocs,
|
||||||
|
setPaginatedDocuments,
|
||||||
} from './preferences/preferenceSlice';
|
} from './preferences/preferenceSlice';
|
||||||
import Spinner from './assets/spinner.svg';
|
import Spinner from './assets/spinner.svg';
|
||||||
import SpinnerDark from './assets/spinner-dark.svg';
|
import SpinnerDark from './assets/spinner-dark.svg';
|
||||||
@@ -72,6 +74,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
const conversations = useSelector(selectConversations);
|
const conversations = useSelector(selectConversations);
|
||||||
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
||||||
const conversationId = useSelector(selectConversationId);
|
const conversationId = useSelector(selectConversationId);
|
||||||
|
const paginatedDocuments = useSelector(selectPaginatedDocuments);
|
||||||
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
|
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
|
||||||
|
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
@@ -143,9 +146,18 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
})
|
})
|
||||||
.then((updatedDocs) => {
|
.then((updatedDocs) => {
|
||||||
dispatch(setSourceDocs(updatedDocs));
|
dispatch(setSourceDocs(updatedDocs));
|
||||||
|
const updatedPaginatedDocs = paginatedDocuments?.filter(
|
||||||
|
(document) => document.id !== doc.id,
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
setPaginatedDocuments(updatedPaginatedDocs || paginatedDocuments),
|
||||||
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
setSelectedDocs(
|
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 = {
|
const endpoints = {
|
||||||
USER: {
|
USER: {
|
||||||
DOCS: '/api/combine',
|
DOCS: '/api/sources',
|
||||||
DOCS_CHECK: '/api/docs_check',
|
DOCS_CHECK: '/api/docs_check',
|
||||||
|
DOCS_PAGINATED: '/api/sources/paginated',
|
||||||
API_KEYS: '/api/get_api_keys',
|
API_KEYS: '/api/get_api_keys',
|
||||||
CREATE_API_KEY: '/api/create_api_key',
|
CREATE_API_KEY: '/api/create_api_key',
|
||||||
DELETE_API_KEY: '/api/delete_api_key',
|
DELETE_API_KEY: '/api/delete_api_key',
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import apiClient from '../client';
|
|||||||
import endpoints from '../endpoints';
|
import endpoints from '../endpoints';
|
||||||
|
|
||||||
const userService = {
|
const userService = {
|
||||||
getDocs: (sort = 'date', order = 'desc'): Promise<any> =>
|
getDocs: (): Promise<any> => apiClient.get(`${endpoints.USER.DOCS}`),
|
||||||
apiClient.get(`${endpoints.USER.DOCS}?sort=${sort}&order=${order}`),
|
getDocsWithPagination: (query: string): Promise<any> =>
|
||||||
|
apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`),
|
||||||
checkDocs: (data: any): Promise<any> =>
|
checkDocs: (data: any): Promise<any> =>
|
||||||
apiClient.post(endpoints.USER.DOCS_CHECK, data),
|
apiClient.post(endpoints.USER.DOCS_CHECK, data),
|
||||||
getAPIKeys: (): Promise<any> => apiClient.get(endpoints.USER.API_KEYS),
|
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) => {
|
getDocs().then((data) => {
|
||||||
dispatch(setSourceDocs(data));
|
dispatch(setSourceDocs(data));
|
||||||
if (!selectedDoc)
|
if (!selectedDoc)
|
||||||
|
Array.isArray(data) &&
|
||||||
data?.forEach((doc: Doc) => {
|
data?.forEach((doc: Doc) => {
|
||||||
if (doc.model && doc.name === 'default') {
|
if (doc.model && doc.name === 'default') {
|
||||||
dispatch(setSelectedDocs(doc));
|
dispatch(setSelectedDocs(doc));
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ body.dark {
|
|||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.table-default {
|
.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 {
|
.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 {
|
.table-default th:last-child {
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export type Doc = {
|
|||||||
syncFrequency?: string;
|
syncFrequency?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetDocsResponse = {
|
||||||
|
docs: Doc[];
|
||||||
|
totalDocuments: number;
|
||||||
|
totalPages: number;
|
||||||
|
nextCursor: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PromptProps = {
|
export type PromptProps = {
|
||||||
prompts: { name: string; id: string; type: string }[];
|
prompts: { name: string; id: string; type: string }[];
|
||||||
selectedPrompt: { name: string; id: string; type: string };
|
selectedPrompt: { name: string; id: string; type: string };
|
||||||
@@ -22,7 +29,7 @@ export type PromptProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentsProps = {
|
export type DocumentsProps = {
|
||||||
documents: Doc[] | null;
|
paginatedDocuments: Doc[] | null;
|
||||||
handleDeleteDocument: (index: number, document: Doc) => void;
|
handleDeleteDocument: (index: number, document: Doc) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import conversationService from '../api/services/conversationService';
|
import conversationService from '../api/services/conversationService';
|
||||||
import userService from '../api/services/userService';
|
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.
|
//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(
|
export async function getDocs(): Promise<Doc[] | null> {
|
||||||
sort = 'date',
|
|
||||||
order = 'desc',
|
|
||||||
): Promise<Doc[] | null> {
|
|
||||||
try {
|
try {
|
||||||
const response = await userService.getDocs(sort, order);
|
const response = await userService.getDocs();
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const docs: Doc[] = [];
|
const docs: Doc[] = [];
|
||||||
|
|
||||||
data.forEach((doc: object) => {
|
data.forEach((doc: object) => {
|
||||||
docs.push(doc as Doc);
|
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<{
|
export async function getConversations(): Promise<{
|
||||||
data: { name: string; id: string }[] | null;
|
data: { name: string; id: string }[] | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface Preference {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
modalState: ActiveState;
|
modalState: ActiveState;
|
||||||
|
paginatedDocuments: Doc[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Preference = {
|
const initialState: Preference = {
|
||||||
@@ -42,6 +43,7 @@ const initialState: Preference = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
modalState: 'INACTIVE',
|
modalState: 'INACTIVE',
|
||||||
|
paginatedDocuments: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prefSlice = createSlice({
|
export const prefSlice = createSlice({
|
||||||
@@ -57,6 +59,9 @@ export const prefSlice = createSlice({
|
|||||||
setSourceDocs: (state, action) => {
|
setSourceDocs: (state, action) => {
|
||||||
state.sourceDocs = action.payload;
|
state.sourceDocs = action.payload;
|
||||||
},
|
},
|
||||||
|
setPaginatedDocuments: (state, action) => {
|
||||||
|
state.paginatedDocuments = action.payload;
|
||||||
|
},
|
||||||
setConversations: (state, action) => {
|
setConversations: (state, action) => {
|
||||||
state.conversations = action.payload;
|
state.conversations = action.payload;
|
||||||
},
|
},
|
||||||
@@ -84,6 +89,7 @@ export const {
|
|||||||
setChunks,
|
setChunks,
|
||||||
setTokenLimit,
|
setTokenLimit,
|
||||||
setModalStateDeleteConv,
|
setModalStateDeleteConv,
|
||||||
|
setPaginatedDocuments,
|
||||||
} = prefSlice.actions;
|
} = prefSlice.actions;
|
||||||
export default prefSlice.reducer;
|
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 selectChunks = (state: RootState) => state.preference.chunks;
|
||||||
export const selectTokenLimit = (state: RootState) =>
|
export const selectTokenLimit = (state: RootState) =>
|
||||||
state.preference.token_limit;
|
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 PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import SyncIcon from '../assets/sync.svg';
|
import SyncIcon from '../assets/sync.svg';
|
||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/trash.svg';
|
||||||
import caretSort from '../assets/caret-sort.svg';
|
import caretSort from '../assets/caret-sort.svg';
|
||||||
import DropdownMenu from '../components/DropdownMenu';
|
import DropdownMenu from '../components/DropdownMenu';
|
||||||
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
|
|
||||||
import SkeletonLoader from '../components/SkeletonLoader';
|
import SkeletonLoader from '../components/SkeletonLoader';
|
||||||
import { getDocs } from '../preferences/preferenceApi';
|
|
||||||
import { setSourceDocs } from '../preferences/preferenceSlice';
|
|
||||||
import Input from '../components/Input';
|
import Input from '../components/Input';
|
||||||
import Upload from '../upload/Upload'; // Import the Upload component
|
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
|
// Utility function to format numbers
|
||||||
const formatTokens = (tokens: number): string => {
|
const formatTokens = (tokens: number): string => {
|
||||||
@@ -33,12 +34,11 @@ const formatTokens = (tokens: number): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Documents: React.FC<DocumentsProps> = ({
|
const Documents: React.FC<DocumentsProps> = ({
|
||||||
documents,
|
paginatedDocuments,
|
||||||
handleDeleteDocument,
|
handleDeleteDocument,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// State for search input
|
// State for search input
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
// State for modal: active/inactive
|
// State for modal: active/inactive
|
||||||
@@ -47,37 +47,49 @@ const Documents: React.FC<DocumentsProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
|
const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
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 = [
|
const syncOptions = [
|
||||||
{ label: 'Never', value: 'never' },
|
{ label: 'Never', value: 'never' },
|
||||||
{ label: 'Daily', value: 'daily' },
|
{ label: 'Daily', value: 'daily' },
|
||||||
{ label: 'Weekly', value: 'weekly' },
|
{ label: 'Weekly', value: 'weekly' },
|
||||||
{ label: 'Monthly', value: 'monthly' },
|
{ 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) {
|
if (field === sortField) {
|
||||||
|
// Toggle sort order
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||||
} else {
|
} else {
|
||||||
setSortOrder('desc');
|
// Change sort field and reset order to 'desc'
|
||||||
setSortField(field);
|
setSortField(field);
|
||||||
|
setSortOrder('desc');
|
||||||
}
|
}
|
||||||
getDocs(sortField, sortOrder)
|
}
|
||||||
|
getDocsWithPagination(sortField, sortOrder, page, rowsPerPg)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(setSourceDocs(data));
|
//dispatch(setSourceDocs(data ? data.docs : []));
|
||||||
})
|
dispatch(setPaginatedDocuments(data ? data.docs : []));
|
||||||
.catch((error) => console.error(error))
|
setTotalPages(data ? data.totalPages : 0);
|
||||||
.finally(() => {
|
//setTotalDocuments(data ? data.totalDocuments : 0);
|
||||||
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));
|
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error))
|
.catch((error) => console.error(error))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -85,10 +97,40 @@ const Documents: React.FC<DocumentsProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter documents based on the search term
|
const handleManageSync = (doc: Doc, sync_frequency: string) => {
|
||||||
const filteredDocuments = documents?.filter((document) =>
|
setLoading(true);
|
||||||
document.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
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 (
|
return (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
@@ -154,16 +196,16 @@ const Documents: React.FC<DocumentsProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{!filteredDocuments?.length && (
|
{!currentDocuments?.length && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="!p-4">
|
<td colSpan={5} className="!p-4">
|
||||||
{t('settings.documents.noData')}
|
{t('settings.documents.noData')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{filteredDocuments &&
|
{Array.isArray(currentDocuments) &&
|
||||||
filteredDocuments.map((document, index) => (
|
currentDocuments.map((document, index) => (
|
||||||
<tr key={index}>
|
<tr key={index} className="text-nowrap font-normal">
|
||||||
<td>{document.name}</td>
|
<td>{document.name}</td>
|
||||||
<td>{document.date}</td>
|
<td>{document.date}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -173,7 +215,7 @@ const Documents: React.FC<DocumentsProps> = ({
|
|||||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||||
</td>
|
</td>
|
||||||
<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' && (
|
{document.type !== 'remote' && (
|
||||||
<img
|
<img
|
||||||
src={Trash}
|
src={Trash}
|
||||||
@@ -221,12 +263,31 @@ const Documents: React.FC<DocumentsProps> = ({
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Documents.propTypes = {
|
Documents.propTypes = {
|
||||||
documents: PropTypes.array.isRequired,
|
//documents: PropTypes.array.isRequired,
|
||||||
handleDeleteDocument: PropTypes.func.isRequired,
|
handleDeleteDocument: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import i18n from '../locale/i18n';
|
|||||||
import { Doc } from '../models/misc';
|
import { Doc } from '../models/misc';
|
||||||
import {
|
import {
|
||||||
selectSourceDocs,
|
selectSourceDocs,
|
||||||
|
selectPaginatedDocuments,
|
||||||
|
setPaginatedDocuments,
|
||||||
setSourceDocs,
|
setSourceDocs,
|
||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
@@ -26,20 +28,29 @@ export default function Settings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const documents = useSelector(selectSourceDocs);
|
const documents = useSelector(selectSourceDocs);
|
||||||
|
const paginatedDocuments = useSelector(selectPaginatedDocuments);
|
||||||
const updateWidgetScreenshot = (screenshot: File | null) => {
|
const updateWidgetScreenshot = (screenshot: File | null) => {
|
||||||
setWidgetScreenshot(screenshot);
|
setWidgetScreenshot(screenshot);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateDocumentsList = (documents: Doc[], index: number) => [
|
||||||
|
...documents.slice(0, index),
|
||||||
|
...documents.slice(index + 1),
|
||||||
|
];
|
||||||
|
|
||||||
const handleDeleteClick = (index: number, doc: Doc) => {
|
const handleDeleteClick = (index: number, doc: Doc) => {
|
||||||
userService
|
userService
|
||||||
.deletePath(doc.id ?? '')
|
.deletePath(doc.id ?? '')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok && documents) {
|
if (response.ok && documents) {
|
||||||
const updatedDocuments = [
|
if (paginatedDocuments) {
|
||||||
...documents.slice(0, index),
|
dispatch(
|
||||||
...documents.slice(index + 1),
|
setPaginatedDocuments(
|
||||||
];
|
updateDocumentsList(paginatedDocuments, index),
|
||||||
dispatch(setSourceDocs(updatedDocuments));
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dispatch(setSourceDocs(updateDocumentsList(documents, index)));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
@@ -72,7 +83,7 @@ export default function Settings() {
|
|||||||
case t('settings.documents.label'):
|
case t('settings.documents.label'):
|
||||||
return (
|
return (
|
||||||
<Documents
|
<Documents
|
||||||
documents={documents}
|
paginatedDocuments={paginatedDocuments}
|
||||||
handleDeleteDocument={handleDeleteClick}
|
handleDeleteDocument={handleDeleteClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const preloadedState: { preference: Preference } = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
modalState: 'INACTIVE',
|
modalState: 'INACTIVE',
|
||||||
|
paginatedDocuments: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ function Upload({
|
|||||||
dispatch(setSourceDocs(data));
|
dispatch(setSourceDocs(data));
|
||||||
dispatch(
|
dispatch(
|
||||||
setSelectedDocs(
|
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) => {
|
getDocs().then((data) => {
|
||||||
dispatch(setSourceDocs(data));
|
dispatch(setSourceDocs(data));
|
||||||
const docIds = new Set(
|
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)) {
|
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));
|
dispatch(setSelectedDocs(updatedDoc));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setProgress(
|
setProgress(
|
||||||
(progress) =>
|
(progress) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user