diff --git a/README.md b/README.md index eeecb598..99baf811 100644 --- a/README.md +++ b/README.md @@ -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.) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 182cdf2b..f109db26 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -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", diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 0868333e..6a2f3bea 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -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) diff --git a/application/retriever/classic_rag.py b/application/retriever/classic_rag.py index 6a67cb38..42e318d2 100644 --- a/application/retriever/classic_rag.py +++ b/application/retriever/classic_rag.py @@ -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 diff --git a/docs/package-lock.json b/docs/package-lock.json index 888c7725..10418138 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -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", diff --git a/docs/package.json b/docs/package.json index 4eeb4df0..cc3e786d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -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", diff --git a/docs/pages/_app.mdx b/docs/pages/_app.mdx index ac2be195..1cb8cadd 100644 --- a/docs/pages/_app.mdx +++ b/docs/pages/_app.mdx @@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) { return ( <> - + ) } \ No newline at end of file diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index 5b6f3fb2..de4c228d 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -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", diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index a8e437da..a1097403 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -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", diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 69980359..1baa4c62 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -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(null) const [open, setOpen] = React.useState(deafultOpen) const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling + const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); + const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); const isBubbleHovered = useRef(false) const widgetRef = useRef(null) const endMessageRef = React.useRef(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' && } -