Compare commits

..

No commits in common. 'main' and '0.9.0' have entirely different histories.
main ... 0.9.0

@ -13,6 +13,7 @@ jobs:
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
@ -35,6 +36,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Runs a single command using the runners shell
- name: Build and push Docker images to docker.io and ghcr.io
uses: docker/build-push-action@v4
with:

@ -8,11 +8,11 @@ on:
jobs:
deploy:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
@ -40,7 +40,7 @@ jobs:
uses: docker/build-push-action@v4
with:
file: './frontend/Dockerfile'
platforms: linux/amd64, linux/arm64
platforms: linux/amd64
context: ./frontend
push: true
tags: |

@ -7,9 +7,9 @@
</p>
<p align="left">
<strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> is a cutting-edge open-source solution that streamlines the process of finding information in the project documentation. With its integration of the powerful <strong>GPT</strong> models, developers can easily ask questions about a project and receive accurate answers.
<strong><a href="https://docsgpt.arc53.com/">DocsGPT</a></strong> is a cutting-edge open-source solution that streamlines the process of finding information in the project documentation. With its integration of the powerful <strong>GPT</strong> models, developers can easily ask questions about a project and receive accurate answers.
Say goodbye to time-consuming manual searches, and let <strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance.
Say goodbye to time-consuming manual searches, and let <strong><a href="https://docsgpt.arc53.com/">DocsGPT</a></strong> help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance.
</p>
<div align="center">
@ -27,7 +27,7 @@ Say goodbye to time-consuming manual searches, and let <strong><a href="https://
We're eager to provide personalized assistance when deploying your DocsGPT to a live environment.
- [Get Enterprise / teams Demo :wave:](https://www.docsgpt.cloud/contact)
- [Book Demo :wave:](https://airtable.com/appdeaL0F1qV8Bl2C/shrrJF1Ll7btCJRbP)
- [Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions)
![video-example-of-docs-gpt](https://d3dg1063dc54p9.cloudfront.net/videos/demov3.gif)
@ -52,17 +52,17 @@ If you don't have enough resources to run it, you can use bitsnbytes to quantize
## Useful Links
- :mag: :fire: [Cloud Version](https://app.docsgpt.cloud/)
- :mag: :fire: [Live preview](https://docsgpt.arc53.com/)
- :speech_balloon: :tada: [Join our Discord](https://discord.gg/n5BX8dh8rU)
- :books: :sunglasses: [Guides](https://docs.docsgpt.cloud/)
- :books: :sunglasses: [Guides](https://docs.docsgpt.co.uk/)
- :couple: [Interested in contributing?](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md)
- :file_folder: :rocket: [How to use any other documentation](https://docs.docsgpt.cloud/Guides/How-to-train-on-other-documentation)
- :file_folder: :rocket: [How to use any other documentation](https://docs.docsgpt.co.uk/Guides/How-to-train-on-other-documentation)
- :house: :closed_lock_with_key: [How to host it locally (so all data will stay on-premises)](https://docs.docsgpt.cloud/Guides/How-to-use-different-LLM)
- :house: :closed_lock_with_key: [How to host it locally (so all data will stay on-premises)](https://docs.docsgpt.co.uk/Guides/How-to-use-different-LLM)
## Project Structure
@ -85,7 +85,7 @@ On Mac OS or Linux, write:
It will install all the dependencies and allow you to download the local model, use OpenAI or use our LLM API.
Otherwise, refer to this Guide for Windows:
Otherwise, refer to this Guide:
1. Download and open this repository with `git clone https://github.com/arc53/DocsGPT.git`
2. Create a `.env` file in your root directory and set the env variables and `VITE_API_STREAMING` to true or false, depending on whether you want streaming answers or not.

@ -1,93 +1,31 @@
# Builder Stage
FROM ubuntu:24.04 as builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y software-properties-common
RUN add-apt-repository ppa:deadsnakes/ppa
# Install necessary packages and Python
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc curl wget unzip libc6-dev python3.11 python3.11-distutils python3.11-venv && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Verify Python installation and setup symlink
RUN if [ -f /usr/bin/python3.11 ]; then \
ln -s /usr/bin/python3.11 /usr/bin/python; \
else \
echo "Python 3.11 not found"; exit 1; \
fi
# Download and unzip the model
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip && \
unzip mpnet-base-v2.zip -d model && \
rm mpnet-base-v2.zip
# Install Rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
# Clean up to reduce container size
RUN apt-get remove --purge -y wget unzip && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
# Copy requirements.txt
FROM python:3.11-slim-bullseye as builder
# Tiktoken requires Rust toolchain, so build it in a separate stage
RUN apt-get update && apt-get install -y gcc curl
RUN apt-get install -y wget unzip
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip
RUN unzip mpnet-base-v2.zip -d model
RUN rm mpnet-base-v2.zip
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && apt-get install --reinstall libc6-dev -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN pip install --upgrade pip && pip install tiktoken==0.5.2
COPY requirements.txt .
RUN pip install -r requirements.txt
# Setup Python virtual environment
RUN python3.11 -m venv /venv
# Activate virtual environment and install Python packages
ENV PATH="/venv/bin:$PATH"
# Install Python packages
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir tiktoken && \
pip install --no-cache-dir -r requirements.txt
FROM python:3.11-slim-bullseye
# Final Stage
FROM ubuntu:24.04 as final
# Copy pre-built packages and binaries from builder stage
COPY --from=builder /usr/local/ /usr/local/
RUN apt-get update && \
apt-get install -y software-properties-common
RUN add-apt-repository ppa:deadsnakes/ppa
# Install Python
RUN apt-get update && apt-get install -y --no-install-recommends python3.11 && \
ln -s /usr/bin/python3.11 /usr/bin/python && \
rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Create a non-root user: `appuser` (Feel free to choose a name)
RUN groupadd -r appuser && \
useradd -r -g appuser -d /app -s /sbin/nologin -c "Docker image user" appuser
# Copy the virtual environment and model from the builder stage
COPY --from=builder /venv /venv
COPY --from=builder /model /app/model
# Copy your application code
COPY . /app/application
ENV FLASK_APP=app.py
ENV FLASK_DEBUG=true
# Change the ownership of the /app directory to the appuser
RUN mkdir -p /app/application/inputs/local
RUN chown -R appuser:appuser /app
# Set environment variables
ENV FLASK_APP=app.py \
FLASK_DEBUG=true \
PATH="/venv/bin:$PATH"
# Expose the port the app runs on
EXPOSE 7091
# Switch to non-root user
USER appuser
# Start Gunicorn
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]

@ -1,6 +1,5 @@
import asyncio
import os
import sys
from flask import Blueprint, request, Response
import json
import datetime
@ -10,11 +9,13 @@ import traceback
from pymongo import MongoClient
from bson.objectid import ObjectId
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from application.retriever.retriever_creator import RetrieverCreator
from application.error import bad_request
logger = logging.getLogger(__name__)
mongo = MongoClient(settings.MONGO_URI)
@ -74,10 +75,8 @@ def run_async_chain(chain, question, chat_history):
def get_data_from_api_key(api_key):
data = api_key_collection.find_one({"key": api_key})
# # Raise custom exception if the API key is not found
if data is None:
raise Exception("Invalid API Key, please generate new key", 401)
return bad_request(401, "Invalid API key")
return data
@ -129,10 +128,10 @@ def save_conversation(conversation_id, question, response, source_log_docs, llm)
"content": "Summarise following conversation in no more than 3 "
"words, respond ONLY with the summary, use the same "
"language as the system \n\nUser: "
+question
+"\n\n"
+"AI: "
+response,
+ question
+ "\n\n"
+ "AI: "
+ response,
},
{
"role": "user",
@ -174,40 +173,33 @@ def get_prompt(prompt_id):
def complete_stream(question, retriever, conversation_id, user_api_key):
try:
response_full = ""
source_log_docs = []
answer = retriever.gen()
for line in answer:
if "answer" in line:
response_full += str(line["answer"])
data = json.dumps(line)
yield f"data: {data}\n\n"
elif "source" in line:
source_log_docs.append(line["source"])
response_full = ""
source_log_docs = []
answer = retriever.gen()
for line in answer:
if "answer" in line:
response_full += str(line["answer"])
data = json.dumps(line)
yield f"data: {data}\n\n"
elif "source" in line:
source_log_docs.append(line["source"])
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
data = json.dumps({"type": "error","error":"Please try again later. We apologize for any inconvenience.",
"error_exception": str(e)})
yield f"data: {data}\n\n"
return
@answer.route("/stream", methods=["POST"])
def stream():
try:
data = request.get_json()
# get parameter from url question
question = data["question"]
@ -230,17 +222,13 @@ def stream():
chunks = int(data["chunks"])
else:
chunks = 2
if "token_limit" in data:
token_limit = data["token_limit"]
else:
token_limit = settings.DEFAULT_MAX_HISTORY
# check if active_docs or api_key is set
prompt = get_prompt(prompt_id)
# check if active_docs is set
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key["chunks"])
prompt_id = data_key["prompt_id"]
source = {"active_docs": data_key["source"]}
user_api_key = data["api_key"]
elif "active_docs" in data:
@ -258,8 +246,6 @@ def stream():
else:
retriever_name = source["active_docs"]
prompt = get_prompt(prompt_id)
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
@ -267,7 +253,6 @@ def stream():
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
@ -281,30 +266,7 @@ def stream():
),
mimetype="text/event-stream",
)
except ValueError:
message = "Malformed request body"
print("\033[91merr", str(message), file=sys.stderr)
return Response(
error_stream_generate(message),
status=400,
mimetype="text/event-stream",
)
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
message = e.args[0]
status_code = 400
# # Custom exceptions with two arguments, index 1 as status code
if(len(e.args) >= 2):
status_code = e.args[1]
return Response(
error_stream_generate(message),
status=status_code,
mimetype="text/event-stream",
)
def error_stream_generate(err_response):
data = json.dumps({"type": "error", "error":err_response})
yield f"data: {data}\n\n"
@answer.route("/api/answer", methods=["POST"])
def api_answer():
@ -327,22 +289,18 @@ def api_answer():
chunks = int(data["chunks"])
else:
chunks = 2
if "token_limit" in data:
token_limit = data["token_limit"]
else:
token_limit = settings.DEFAULT_MAX_HISTORY
prompt = get_prompt(prompt_id)
# use try and except to check for exception
try:
# check if the vectorstore is set
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key["chunks"])
prompt_id = data_key["prompt_id"]
source = {"active_docs": data_key["source"]}
user_api_key = data["api_key"]
else:
source = data
source = {data}
user_api_key = None
if (
@ -353,8 +311,6 @@ def api_answer():
else:
retriever_name = source["active_docs"]
prompt = get_prompt(prompt_id)
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
@ -362,7 +318,6 @@ def api_answer():
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
@ -396,13 +351,9 @@ def api_search():
data = request.get_json()
# get parameter from url question
question = data["question"]
if "chunks" in data:
chunks = int(data["chunks"])
else:
chunks = 2
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key["chunks"])
source = {"active_docs": data_key["source"]}
user_api_key = data["api_key"]
elif "active_docs" in data:
@ -411,6 +362,10 @@ def api_search():
else:
source = {}
user_api_key = None
if "chunks" in data:
chunks = int(data["chunks"])
else:
chunks = 2
if (
source["active_docs"].split("/")[0] == "default"
@ -419,10 +374,6 @@ def api_search():
retriever_name = "classic"
else:
retriever_name = source["active_docs"]
if "token_limit" in data:
token_limit = data["token_limit"]
else:
token_limit = settings.DEFAULT_MAX_HISTORY
retriever = RetrieverCreator.create_retriever(
retriever_name,
@ -431,7 +382,6 @@ def api_search():
chat_history=[],
prompt="default",
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)

@ -34,7 +34,6 @@ def upload_index_files():
if "name" not in request.form:
return {"status": "no name"}
job_name = secure_filename(request.form["name"])
tokens = secure_filename(request.form["tokens"])
save_dir = os.path.join(current_dir, "indexes", user, job_name)
if settings.VECTOR_STORE == "faiss":
if "file_faiss" not in request.files:
@ -65,7 +64,6 @@ def upload_index_files():
"date": datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"model": settings.EMBEDDINGS_NAME,
"type": "local",
"tokens": tokens
}
)
return {"status": "ok"}

@ -20,12 +20,9 @@ vectors_collection = db["vectors"]
prompts_collection = db["prompts"]
feedback_collection = db["feedback"]
api_key_collection = db["api_keys"]
user = Blueprint("user", __name__)
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
user = Blueprint('user', __name__)
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@user.route("/api/delete_conversation", methods=["POST"])
def delete_conversation():
@ -40,25 +37,15 @@ def delete_conversation():
return {"status": "ok"}
@user.route("/api/delete_all_conversations", methods=["POST"])
def delete_all_conversations():
user_id = "local"
conversations_collection.delete_many({"user": user_id})
return {"status": "ok"}
@user.route("/api/get_conversations", methods=["get"])
def get_conversations():
# provides a list of conversations
conversations = conversations_collection.find().sort("date", -1).limit(30)
list_conversations = []
for conversation in conversations:
list_conversations.append(
{"id": str(conversation["_id"]), "name": conversation["name"]}
)
list_conversations.append({"id": str(conversation["_id"]), "name": conversation["name"]})
# list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}]
#list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}]
return jsonify(list_conversations)
@ -68,8 +55,7 @@ def get_single_conversation():
# provides data for a conversation
conversation_id = request.args.get("id")
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
return jsonify(conversation["queries"])
return jsonify(conversation['queries'])
@user.route("/api/update_conversation_name", methods=["POST"])
def update_conversation_name():
@ -77,7 +63,7 @@ def update_conversation_name():
data = request.get_json()
id = data["id"]
name = data["name"]
conversations_collection.update_one({"_id": ObjectId(id)}, {"$set": {"name": name}})
conversations_collection.update_one({"_id": ObjectId(id)},{"$set":{"name":name}})
return {"status": "ok"}
@ -88,6 +74,7 @@ def api_feedback():
answer = data["answer"]
feedback = data["feedback"]
feedback_collection.insert_one(
{
"question": question,
@ -97,7 +84,6 @@ def api_feedback():
)
return {"status": "ok"}
@user.route("/api/delete_by_ids", methods=["get"])
def delete_by_ids():
"""Delete by ID. These are the IDs in the vectorstore"""
@ -112,7 +98,6 @@ def delete_by_ids():
return {"status": "ok"}
return {"status": "error"}
@user.route("/api/delete_old", methods=["get"])
def delete_old():
"""Delete old indexes."""
@ -128,7 +113,7 @@ def delete_old():
if dirs_clean[0] not in ["indexes", "vectors"]:
return {"status": "error"}
path_clean = "/".join(dirs_clean)
vectors_collection.delete_one({"name": dirs_clean[-1], "user": dirs_clean[-2]})
vectors_collection.delete_one({"name": dirs_clean[-1], 'user': dirs_clean[-2]})
if settings.VECTOR_STORE == "faiss":
try:
shutil.rmtree(os.path.join(current_dir, path_clean))
@ -139,10 +124,9 @@ def delete_old():
settings.VECTOR_STORE, path=os.path.join(current_dir, path_clean)
)
vetorstore.delete_index()
return {"status": "ok"}
@user.route("/api/upload", methods=["POST"])
def upload_file():
"""Upload a file to get vectorized and indexed."""
@ -154,29 +138,27 @@ def upload_file():
job_name = secure_filename(request.form["name"])
# check if the post request has the file part
files = request.files.getlist("file")
if not files or all(file.filename == "" for file in files):
if not files or all(file.filename == '' for file in files):
return {"status": "no file name"}
# Directory where files will be saved
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, job_name)
os.makedirs(save_dir, exist_ok=True)
if len(files) > 1:
# Multiple files; prepare them for zip
temp_dir = os.path.join(save_dir, "temp")
os.makedirs(temp_dir, exist_ok=True)
for file in files:
filename = secure_filename(file.filename)
file.save(os.path.join(temp_dir, filename))
# Use shutil.make_archive to zip the temp directory
zip_path = shutil.make_archive(
base_name=os.path.join(save_dir, job_name), format="zip", root_dir=temp_dir
)
zip_path = shutil.make_archive(base_name=os.path.join(save_dir, job_name), format='zip', root_dir=temp_dir)
final_filename = os.path.basename(zip_path)
# Clean up the temporary directory after zipping
shutil.rmtree(temp_dir)
else:
@ -185,19 +167,14 @@ def upload_file():
final_filename = secure_filename(file.filename)
file_path = os.path.join(save_dir, final_filename)
file.save(file_path)
# Call ingest with the single file or zipped file
task = ingest.delay(
settings.UPLOAD_FOLDER,
[".rst", ".md", ".pdf", ".txt", ".docx", ".csv", ".epub", ".html", ".mdx"],
job_name,
final_filename,
user,
)
task = ingest.delay(settings.UPLOAD_FOLDER, [".rst", ".md", ".pdf", ".txt", ".docx",
".csv", ".epub", ".html", ".mdx"],
job_name, final_filename, user)
return {"status": "ok", "task_id": task.id}
@user.route("/api/remote", methods=["POST"])
def upload_remote():
"""Upload a remote source to get vectorized and indexed."""
@ -210,27 +187,25 @@ def upload_remote():
if "name" not in request.form:
return {"status": "no name"}
job_name = secure_filename(request.form["name"])
# check if the post request has the file part
if "data" not in request.form:
print("No data")
return {"status": "no data"}
source_data = request.form["data"]
if source_data:
task = ingest_remote.delay(
source_data=source_data, job_name=job_name, user=user, loader=source
)
task = ingest_remote.delay(source_data=source_data, job_name=job_name, user=user, loader=source)
# task id
task_id = task.id
return {"status": "ok", "task_id": task_id}
else:
return {"status": "error"}
@user.route("/api/task_status", methods=["GET"])
def task_status():
"""Get celery job status."""
task_id = request.args.get("task_id")
from application.celery_init import celery
from application.celery import celery
task = celery.AsyncResult(task_id)
task_meta = task.info
return {"status": task.status, "result": task_meta}
@ -253,12 +228,11 @@ def combined_json():
"docLink": "default",
"model": settings.EMBEDDINGS_NAME,
"location": "remote",
"tokens":""
}
]
# structure: name, language, version, description, fullName, date, docLink
# append data from vectors_collection in sorted order in descending order of date
for index in vectors_collection.find({"user": user}).sort("date", -1):
# append data from vectors_collection
for index in vectors_collection.find({"user": user}):
data.append(
{
"name": index["name"],
@ -270,17 +244,14 @@ def combined_json():
"docLink": index["location"],
"model": settings.EMBEDDINGS_NAME,
"location": "local",
"tokens" : index["tokens"] if ("tokens" in index.keys()) else ""
}
)
if settings.VECTOR_STORE == "faiss":
data_remote = requests.get(
"https://d3dg1063dc54p9.cloudfront.net/combined.json"
).json()
data_remote = requests.get("https://d3dg1063dc54p9.cloudfront.net/combined.json").json()
for index in data_remote:
index["location"] = "remote"
data.append(index)
if "duckduck_search" in settings.RETRIEVERS_ENABLED:
if 'duckduck_search' in settings.RETRIEVERS_ENABLED:
data.append(
{
"name": "DuckDuckGo Search",
@ -292,10 +263,9 @@ def combined_json():
"docLink": "duckduck_search",
"model": settings.EMBEDDINGS_NAME,
"location": "custom",
"tokens":""
}
)
if "brave_search" in settings.RETRIEVERS_ENABLED:
if 'brave_search' in settings.RETRIEVERS_ENABLED:
data.append(
{
"name": "Brave Search",
@ -307,7 +277,6 @@ def combined_json():
"docLink": "brave_search",
"model": settings.EMBEDDINGS_NAME,
"location": "custom",
"tokens":""
}
)
@ -327,11 +296,11 @@ def check_docs():
return {"status": "exists"}
else:
file_url = urlparse(base_path + vectorstore + "index.faiss")
if (
file_url.scheme in ["https"]
and file_url.netloc == "raw.githubusercontent.com"
and file_url.path.startswith("/arc53/DocsHUB/main/")
file_url.scheme in ['https'] and
file_url.netloc == 'raw.githubusercontent.com' and
file_url.path.startswith('/arc53/DocsHUB/main/')
):
r = requests.get(file_url.geturl())
if r.status_code != 200:
@ -350,7 +319,6 @@ def check_docs():
return {"status": "loaded"}
@user.route("/api/create_prompt", methods=["POST"])
def create_prompt():
data = request.get_json()
@ -369,7 +337,6 @@ def create_prompt():
new_id = str(resp.inserted_id)
return {"id": new_id}
@user.route("/api/get_prompts", methods=["GET"])
def get_prompts():
user = "local"
@ -379,39 +346,30 @@ def get_prompts():
list_prompts.append({"id": "creative", "name": "creative", "type": "public"})
list_prompts.append({"id": "strict", "name": "strict", "type": "public"})
for prompt in prompts:
list_prompts.append(
{"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"}
)
list_prompts.append({"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"})
return jsonify(list_prompts)
@user.route("/api/get_single_prompt", methods=["GET"])
def get_single_prompt():
prompt_id = request.args.get("id")
if prompt_id == "default":
with open(
os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r"
) as f:
if prompt_id == 'default':
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
chat_combine_template = f.read()
return jsonify({"content": chat_combine_template})
elif prompt_id == "creative":
with open(
os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r"
) as f:
elif prompt_id == 'creative':
with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r") as f:
chat_reduce_creative = f.read()
return jsonify({"content": chat_reduce_creative})
elif prompt_id == "strict":
with open(
os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r"
) as f:
chat_reduce_strict = f.read()
elif prompt_id == 'strict':
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
chat_reduce_strict = f.read()
return jsonify({"content": chat_reduce_strict})
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})
return jsonify({"content": prompt["content"]})
@user.route("/api/delete_prompt", methods=["POST"])
def delete_prompt():
data = request.get_json()
@ -423,7 +381,6 @@ def delete_prompt():
)
return {"status": "ok"}
@user.route("/api/update_prompt", methods=["POST"])
def update_prompt_name():
data = request.get_json()
@ -433,31 +390,27 @@ def update_prompt_name():
# check if name is null
if name == "":
return {"status": "error"}
prompts_collection.update_one(
{"_id": ObjectId(id)}, {"$set": {"name": name, "content": content}}
)
prompts_collection.update_one({"_id": ObjectId(id)},{"$set":{"name":name, "content": content}})
return {"status": "ok"}
@user.route("/api/get_api_keys", methods=["GET"])
def get_api_keys():
user = "local"
keys = api_key_collection.find({"user": user})
list_keys = []
for key in keys:
list_keys.append(
{
"id": str(key["_id"]),
"name": key["name"],
"key": key["key"][:4] + "..." + key["key"][-4:],
"source": key["source"],
"prompt_id": key["prompt_id"],
"chunks": key["chunks"],
}
)
list_keys.append({
"id": str(key["_id"]),
"name": key["name"],
"key": key["key"][:4] + "..." + key["key"][-4:],
"source": key["source"],
"prompt_id": key["prompt_id"],
"chunks": key["chunks"]
})
return jsonify(list_keys)
@user.route("/api/create_api_key", methods=["POST"])
def create_api_key():
data = request.get_json()
@ -474,13 +427,12 @@ def create_api_key():
"source": source,
"user": user,
"prompt_id": prompt_id,
"chunks": chunks,
"chunks": chunks
}
)
new_id = str(resp.inserted_id)
return {"id": new_id, "key": key}
@user.route("/api/delete_api_key", methods=["POST"])
def delete_api_key():
data = request.get_json()
@ -491,3 +443,4 @@ def delete_api_key():
}
)
return {"status": "ok"}

@ -1,5 +1,5 @@
from application.worker import ingest_worker, remote_worker
from application.celery_init import celery
from application.celery import celery
@celery.task(bind=True)
def ingest(self, directory, formats, name_job, filename, user):

@ -1,6 +1,6 @@
import platform
import dotenv
from application.celery_init import celery
from application.celery import celery
from flask import Flask, request, redirect
from application.core.settings import settings
from application.api.user.routes import user

@ -15,8 +15,7 @@ class Settings(BaseSettings):
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
DEFAULT_MAX_HISTORY: int = 150
MODEL_TOKEN_LIMITS: dict = {"gpt-3.5-turbo": 4096, "claude-2": 1e5}
TOKENS_MAX_HISTORY: int = 150
UPLOAD_FOLDER: str = "inputs"
VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant"
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search

@ -1,30 +1,9 @@
from application.llm.base import BaseLLM
from application.core.settings import settings
import threading
class LlamaSingleton:
_instances = {}
_lock = threading.Lock() # Add a lock for thread synchronization
@classmethod
def get_instance(cls, llm_name):
if llm_name not in cls._instances:
try:
from llama_cpp import Llama
except ImportError:
raise ImportError(
"Please install llama_cpp using pip install llama-cpp-python"
)
cls._instances[llm_name] = Llama(model_path=llm_name, n_ctx=2048)
return cls._instances[llm_name]
@classmethod
def query_model(cls, llm, prompt, **kwargs):
with cls._lock:
return llm(prompt, **kwargs)
class LlamaCpp(BaseLLM):
def __init__(
self,
api_key=None,
@ -33,23 +12,41 @@ class LlamaCpp(BaseLLM):
*args,
**kwargs,
):
global llama
try:
from llama_cpp import Llama
except ImportError:
raise ImportError(
"Please install llama_cpp using pip install llama-cpp-python"
)
super().__init__(*args, **kwargs)
self.api_key = api_key
self.user_api_key = user_api_key
self.llama = LlamaSingleton.get_instance(llm_name)
llama = Llama(model_path=llm_name, n_ctx=2048)
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
context = messages[0]["content"]
user_question = messages[-1]["content"]
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False)
result = llama(prompt, max_tokens=150, echo=False)
# import sys
# print(result['choices'][0]['text'].split('### Answer \n')[-1], file=sys.stderr)
return result["choices"][0]["text"].split("### Answer \n")[-1]
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
context = messages[0]["content"]
user_question = messages[-1]["content"]
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False, stream=stream)
result = llama(prompt, max_tokens=150, echo=False, stream=stream)
# import sys
# print(list(result), file=sys.stderr)
for item in result:
for choice in item["choices"]:
yield choice["text"]
yield choice["text"]

@ -1,5 +1,6 @@
import os
import tiktoken
from application.vectorstore.vector_creator import VectorCreator
from application.core.settings import settings
from retry import retry
@ -10,6 +11,14 @@ from retry import retry
# from langchain_community.embeddings import CohereEmbeddings
def num_tokens_from_string(string: str, encoding_name: str) -> int:
# Function to convert string to tokens and estimate user cost.
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
total_price = ((num_tokens / 1000) * 0.0004)
return num_tokens, total_price
@retry(tries=10, delay=60)
def store_add_texts_with_retry(store, i):
store.add_texts([i.page_content], metadatas=[i.metadata])
@ -17,13 +26,13 @@ def store_add_texts_with_retry(store, i):
def call_openai_api(docs, folder_name, task_status):
# Function to create a vector store from the documents and save it to disk
# Function to create a vector store from the documents and save it to disk.
# create output folder if it doesn't exist
if not os.path.exists(f"{folder_name}"):
os.makedirs(f"{folder_name}")
from tqdm import tqdm
c1 = 0
if settings.VECTOR_STORE == "faiss":
docs_init = [docs[0]]
@ -31,32 +40,25 @@ def call_openai_api(docs, folder_name, task_status):
store = VectorCreator.create_vectorstore(
settings.VECTOR_STORE,
docs_init=docs_init,
docs_init = docs_init,
path=f"{folder_name}",
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
embeddings_key=os.getenv("EMBEDDINGS_KEY")
)
else:
store = VectorCreator.create_vectorstore(
settings.VECTOR_STORE,
path=f"{folder_name}",
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
embeddings_key=os.getenv("EMBEDDINGS_KEY")
)
# Uncomment for MPNet embeddings
# model_name = "sentence-transformers/all-mpnet-base-v2"
# hf = HuggingFaceEmbeddings(model_name=model_name)
# store = FAISS.from_documents(docs_test, hf)
s1 = len(docs)
for i in tqdm(
docs,
desc="Embedding 🦖",
unit="docs",
total=len(docs),
bar_format="{l_bar}{bar}| Time Left: {remaining}",
):
for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs),
bar_format='{l_bar}{bar}| Time Left: {remaining}'):
try:
task_status.update_state(
state="PROGRESS", meta={"current": int((c1 / s1) * 100)}
)
task_status.update_state(state='PROGRESS', meta={'current': int((c1 / s1) * 100)})
store_add_texts_with_retry(store, i)
except Exception as e:
print(e)
@ -70,3 +72,23 @@ def call_openai_api(docs, folder_name, task_status):
store.save_local(f"{folder_name}")
def get_user_permission(docs, folder_name):
# Function to ask user permission to call the OpenAI api and spend their OpenAI funds.
# Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents.
# docs_content = (" ".join(docs))
docs_content = ""
for doc in docs:
docs_content += doc.page_content
tokens, total_price = num_tokens_from_string(string=docs_content, encoding_name="cl100k_base")
# Here we print the number of tokens and the approx user cost with some visually appealing formatting.
print(f"Number of Tokens = {format(tokens, ',d')}")
print(f"Approx Cost = ${format(total_price, ',.2f')}")
# Here we check for user permission before calling the API.
user_input = input("Price Okay? (Y/N) \n").lower()
if user_input == "y":
call_openai_api(docs, folder_name)
elif user_input == "":
call_openai_api(docs, folder_name)
else:
print("The API was not called. No money was spent.")

@ -1,32 +1,22 @@
from application.parser.remote.base import BaseRemote
from langchain_community.document_loaders import WebBaseLoader
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*"
";q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Referer": "https://www.google.com/",
"DNT": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
class WebLoader(BaseRemote):
def __init__(self):
from langchain.document_loaders import WebBaseLoader
self.loader = WebBaseLoader
def load_data(self, inputs):
urls = inputs
if isinstance(urls, str):
urls = [urls]
urls = [urls] # Convert string to list if a single URL is passed
documents = []
for url in urls:
try:
loader = self.loader([url], header_template=headers)
loader = self.loader([url]) # Process URLs one by one
documents.extend(loader.load())
except Exception as e:
print(f"Error processing URL {url}: {e}")
continue
return documents
continue # Continue with the next URL if an error occurs
return documents

@ -10,25 +10,26 @@ escodegen==1.0.11
esprima==4.0.1
faiss-cpu==1.7.4
Flask==3.0.1
gunicorn==22.0.0
gunicorn==21.2.0
html2text==2020.1.16
javalang==0.13.0
langchain==0.1.4
langchain-openai==0.0.5
nltk==3.8.1
openapi3_parser==1.1.16
pandas==2.2.0
pydantic_settings==2.1.0
pymongo==4.6.3
PyPDF2==3.0.1
python-dotenv==1.0.1
qdrant-client==1.9.0
qdrant-client==1.8.2
redis==5.0.1
Requests==2.32.0
Requests==2.31.0
retry==0.9.2
sentence-transformers
tiktoken
torch
tqdm==4.66.3
tiktoken==0.5.2
torch==2.1.2
tqdm==4.66.1
transformers==4.36.2
unstructured==0.12.2
Werkzeug==3.0.3
Werkzeug==3.0.1

@ -15,7 +15,6 @@ class BraveRetSearch(BaseRetriever):
chat_history,
prompt,
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
):
@ -25,16 +24,6 @@ class BraveRetSearch(BaseRetriever):
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
self.user_api_key = user_api_key
def _get_data(self):
@ -81,7 +70,10 @@ class BraveRetSearch(BaseRetriever):
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
i["response"]
)
if tokens_current_history + tokens_batch < self.token_limit:
if (
tokens_current_history + tokens_batch
< settings.TOKENS_MAX_HISTORY
):
tokens_current_history += tokens_batch
messages_combine.append(
{"role": "user", "content": i["prompt"]}

@ -16,7 +16,6 @@ class ClassicRAG(BaseRetriever):
chat_history,
prompt,
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
):
@ -26,16 +25,6 @@ class ClassicRAG(BaseRetriever):
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
self.user_api_key = user_api_key
def _get_vectorstore(self, source):
@ -69,11 +58,6 @@ class ClassicRAG(BaseRetriever):
else i.page_content
),
"text": i.page_content,
"source": (
i.metadata.get("source")
if i.metadata.get("source")
else "local"
),
}
for i in docs_temp
]
@ -101,7 +85,10 @@ class ClassicRAG(BaseRetriever):
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
i["response"]
)
if tokens_current_history + tokens_batch < self.token_limit:
if (
tokens_current_history + tokens_batch
< settings.TOKENS_MAX_HISTORY
):
tokens_current_history += tokens_batch
messages_combine.append(
{"role": "user", "content": i["prompt"]}

@ -15,7 +15,6 @@ class DuckDuckSearch(BaseRetriever):
chat_history,
prompt,
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
):
@ -25,16 +24,6 @@ class DuckDuckSearch(BaseRetriever):
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
self.user_api_key = user_api_key
def _parse_lang_string(self, input_string):
@ -98,7 +87,10 @@ class DuckDuckSearch(BaseRetriever):
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
i["response"]
)
if tokens_current_history + tokens_batch < self.token_limit:
if (
tokens_current_history + tokens_batch
< settings.TOKENS_MAX_HISTORY
):
tokens_current_history += tokens_batch
messages_combine.append(
{"role": "user", "content": i["prompt"]}

@ -1,6 +1,6 @@
from transformers import GPT2TokenizerFast
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
tokenizer.model_max_length = 100000
def count_tokens(string):
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
return len(tokenizer(string)['input_ids'])

@ -8,30 +8,6 @@ from langchain_community.embeddings import (
from langchain_openai import OpenAIEmbeddings
from application.core.settings import settings
class EmbeddingsSingleton:
_instances = {}
@staticmethod
def get_instance(embeddings_name, *args, **kwargs):
if embeddings_name not in EmbeddingsSingleton._instances:
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(embeddings_name, *args, **kwargs)
return EmbeddingsSingleton._instances[embeddings_name]
@staticmethod
def _create_instance(embeddings_name, *args, **kwargs):
embeddings_factory = {
"openai_text-embedding-ada-002": OpenAIEmbeddings,
"huggingface_sentence-transformers/all-mpnet-base-v2": HuggingFaceEmbeddings,
"huggingface_sentence-transformers-all-mpnet-base-v2": HuggingFaceEmbeddings,
"huggingface_hkunlp/instructor-large": HuggingFaceInstructEmbeddings,
"cohere_medium": CohereEmbeddings
}
if embeddings_name not in embeddings_factory:
raise ValueError(f"Invalid embeddings_name: {embeddings_name}")
return embeddings_factory[embeddings_name](*args, **kwargs)
class BaseVectorStore(ABC):
def __init__(self):
pass
@ -44,36 +20,37 @@ class BaseVectorStore(ABC):
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME
def _get_embeddings(self, embeddings_name, embeddings_key=None):
embeddings_factory = {
"openai_text-embedding-ada-002": OpenAIEmbeddings,
"huggingface_sentence-transformers/all-mpnet-base-v2": HuggingFaceEmbeddings,
"huggingface_hkunlp/instructor-large": HuggingFaceInstructEmbeddings,
"cohere_medium": CohereEmbeddings
}
if embeddings_name not in embeddings_factory:
raise ValueError(f"Invalid embeddings_name: {embeddings_name}")
if embeddings_name == "openai_text-embedding-ada-002":
if self.is_azure_configured():
os.environ["OPENAI_API_TYPE"] = "azure"
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
embedding_instance = embeddings_factory[embeddings_name](
model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME
)
else:
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
embedding_instance = embeddings_factory[embeddings_name](
openai_api_key=embeddings_key
)
elif embeddings_name == "cohere_medium":
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
embedding_instance = embeddings_factory[embeddings_name](
cohere_api_key=embeddings_key
)
elif embeddings_name == "huggingface_sentence-transformers/all-mpnet-base-v2":
if os.path.exists("./model/all-mpnet-base-v2"):
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
model_name="./model/all-mpnet-base-v2",
model_kwargs={"device": "cpu"}
)
else:
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
model_kwargs={"device": "cpu"}
)
embedding_instance = embeddings_factory[embeddings_name](
#model_name="./model/all-mpnet-base-v2",
model_kwargs={"device": "cpu"},
)
else:
embedding_instance = EmbeddingsSingleton.get_instance(embeddings_name)
embedding_instance = embeddings_factory[embeddings_name]()
return embedding_instance
return embedding_instance

@ -2,9 +2,9 @@ import os
import shutil
import string
import zipfile
import tiktoken
from urllib.parse import urljoin
import nltk
import requests
from application.core.settings import settings
@ -14,6 +14,13 @@ from application.parser.open_ai_func import call_openai_api
from application.parser.schema.base import Document
from application.parser.token_func import group_split
try:
nltk.download("punkt", quiet=True)
nltk.download("averaged_perceptron_tagger", quiet=True)
except FileExistsError:
pass
# Define a function to extract metadata from a given filename.
def metadata_from_filename(title):
store = "/".join(title.split("/")[1:3])
@ -29,7 +36,6 @@ current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
"""
Recursively extract zip files with a limit on recursion depth.
@ -44,7 +50,7 @@ def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
print(f"Reached maximum recursion depth of {max_depth}")
return
with zipfile.ZipFile(zip_path, "r") as zip_ref:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
os.remove(zip_path) # Remove the zip file after extracting
@ -90,6 +96,7 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
full_path = os.path.join(directory, user, name_job)
import sys
print(full_path, file=sys.stderr)
# check if API_URL env variable is set
file_data = {"name": name_job, "file": filename, "user": user}
@ -107,9 +114,7 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
# check if file is .zip and extract it
if filename.endswith(".zip"):
extract_zip_recursive(
os.path.join(full_path, filename), full_path, 0, recursion_depth
)
extract_zip_recursive(os.path.join(full_path, filename), full_path, 0, recursion_depth)
self.update_state(state="PROGRESS", meta={"current": 1})
@ -132,7 +137,6 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
call_openai_api(docs, full_path, self)
tokens = count_tokens_docs(docs)
self.update_state(state="PROGRESS", meta={"current": 100})
if sample:
@ -141,7 +145,7 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
# get files from outputs/inputs/index.faiss and outputs/inputs/index.pkl
# and send them to the server (provide user and name in form)
file_data = {"name": name_job, "user": user, "tokens":tokens}
file_data = {"name": name_job, "user": user}
if settings.VECTOR_STORE == "faiss":
files = {
"file_faiss": open(full_path + "/index.faiss", "rb"),
@ -172,6 +176,7 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
# sample = False
token_check = True
min_tokens = 150
max_tokens = 1250
@ -179,8 +184,12 @@ def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
if not os.path.exists(full_path):
os.makedirs(full_path)
self.update_state(state="PROGRESS", meta={"current": 1})
# source_data {"data": [url]} for url type task just urls
# Use RemoteCreator to load data from URL
remote_loader = RemoteCreator.create_loader(loader)
raw_docs = remote_loader.load_data(source_data)
@ -190,19 +199,19 @@ def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
max_tokens=max_tokens,
token_check=token_check,
)
# docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
call_openai_api(docs, full_path, self)
tokens = count_tokens_docs(docs)
self.update_state(state="PROGRESS", meta={"current": 100})
# Proceed with uploading and cleaning as in the original function
file_data = {"name": name_job, "user": user, "tokens":tokens}
file_data = {"name": name_job, "user": user}
if settings.VECTOR_STORE == "faiss":
files = {
"file_faiss": open(full_path + "/index.faiss", "rb"),
"file_pkl": open(full_path + "/index.pkl", "rb"),
}
requests.post(
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
)
@ -213,25 +222,3 @@ def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
shutil.rmtree(full_path)
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
def count_tokens_docs(docs):
# Here we convert the docs list to a string and calculate the number of tokens the string represents.
# docs_content = (" ".join(docs))
docs_content = ""
for doc in docs:
docs_content += doc.page_content
tokens, total_price = num_tokens_from_string(
string=docs_content, encoding_name="cl100k_base"
)
# Here we print the number of tokens and the approx user cost with some visually appealing formatting.
return tokens
def num_tokens_from_string(string: str, encoding_name: str) -> int:
# Function to convert string to tokens and estimate user cost.
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
total_price = (num_tokens / 1000) * 0.0004
return num_tokens, total_price

@ -8,7 +8,7 @@
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.3.7",
"next": "^14.1.1",
"next": "^14.0.4",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",
@ -936,14 +936,14 @@
}
},
"node_modules/@next/env": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.1.tgz",
"integrity": "sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA=="
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
"integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ=="
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz",
"integrity": "sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz",
"integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==",
"cpu": [
"arm64"
],
@ -956,9 +956,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz",
"integrity": "sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz",
"integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==",
"cpu": [
"x64"
],
@ -971,9 +971,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz",
"integrity": "sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz",
"integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==",
"cpu": [
"arm64"
],
@ -986,9 +986,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz",
"integrity": "sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz",
"integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==",
"cpu": [
"arm64"
],
@ -1001,9 +1001,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz",
"integrity": "sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz",
"integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==",
"cpu": [
"x64"
],
@ -1016,9 +1016,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz",
"integrity": "sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz",
"integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==",
"cpu": [
"x64"
],
@ -1031,9 +1031,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz",
"integrity": "sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz",
"integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==",
"cpu": [
"arm64"
],
@ -1046,9 +1046,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz",
"integrity": "sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz",
"integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==",
"cpu": [
"ia32"
],
@ -1061,9 +1061,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz",
"integrity": "sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz",
"integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==",
"cpu": [
"x64"
],
@ -4266,6 +4266,69 @@
"node": ">=4"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"optional": true,
"peer": true,
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-select/node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"optional": true,
"peer": true,
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/css-select/node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"optional": true,
"peer": true,
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/css-select/node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"optional": true,
"peer": true,
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@ -4276,6 +4339,20 @@
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"optional": true,
"peer": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@ -4287,6 +4364,42 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"optional": true,
"peer": true,
"dependencies": {
"css-tree": "~2.2.0"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"optional": true,
"peer": true,
"dependencies": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"optional": true,
"peer": true
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -5259,6 +5372,11 @@
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -6965,6 +7083,13 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"optional": true,
"peer": true
},
"node_modules/mermaid": {
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.6.1.tgz",
@ -7827,17 +7952,18 @@
}
},
"node_modules/next": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/next/-/next-14.1.1.tgz",
"integrity": "sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz",
"integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==",
"dependencies": {
"@next/env": "14.1.1",
"@next/env": "14.0.4",
"@swc/helpers": "0.5.2",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"caniuse-lite": "^1.0.30001406",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
"styled-jsx": "5.1.1",
"watchpack": "2.4.0"
},
"bin": {
"next": "dist/bin/next"
@ -7846,15 +7972,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.1.1",
"@next/swc-darwin-x64": "14.1.1",
"@next/swc-linux-arm64-gnu": "14.1.1",
"@next/swc-linux-arm64-musl": "14.1.1",
"@next/swc-linux-x64-gnu": "14.1.1",
"@next/swc-linux-x64-musl": "14.1.1",
"@next/swc-win32-arm64-msvc": "14.1.1",
"@next/swc-win32-ia32-msvc": "14.1.1",
"@next/swc-win32-x64-msvc": "14.1.1"
"@next/swc-darwin-arm64": "14.0.4",
"@next/swc-darwin-x64": "14.0.4",
"@next/swc-linux-arm64-gnu": "14.0.4",
"@next/swc-linux-arm64-musl": "14.0.4",
"@next/swc-linux-x64-gnu": "14.0.4",
"@next/swc-linux-x64-musl": "14.0.4",
"@next/swc-win32-arm64-msvc": "14.0.4",
"@next/swc-win32-ia32-msvc": "14.0.4",
"@next/swc-win32-x64-msvc": "14.0.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -11608,6 +11734,42 @@
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="
},
"node_modules/svgo": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
"integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==",
"optional": true,
"peer": true,
"dependencies": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.3.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
},
"bin": {
"svgo": "bin/svgo"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/svgo"
}
},
"node_modules/svgo/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
@ -11712,6 +11874,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
@ -12146,6 +12321,18 @@
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg=="
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/weak-lru-cache": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",

@ -8,7 +8,7 @@
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.3.7",
"next": "^14.1.1",
"next": "^14.0.4",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",

@ -1,10 +0,0 @@
{
"API-docs": {
"title": "🗂️️ API-docs",
"href": "/API/API-docs"
},
"api-key-guide": {
"title": "🔐 API Keys guide",
"href": "/API/api-key-guide"
}
}

@ -1,100 +0,0 @@
# Self-hosting DocsGPT on Kubernetes
This guide will walk you through deploying DocsGPT on Kubernetes.
## Prerequisites
Ensure you have the following installed before proceeding:
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- Access to a Kubernetes cluster
## Folder Structure
The `k8s` folder contains the necessary deployment and service configuration files:
- `deployments/`
- `services/`
- `docsgpt-secrets.yaml`
## Deployment Instructions
1. **Clone the Repository**
```sh
git clone https://github.com/arc53/DocsGPT.git
cd docsgpt/k8s
```
2. **Configure Secrets (optional)**
Ensure that you have all the necessary secrets in `docsgpt-secrets.yaml`. Update it with your secrets before applying if you want. By default we will use qdrant as a vectorstore and public docsgpt llm as llm for inference.
3. **Apply Kubernetes Deployments**
Deploy your DocsGPT resources using the following commands:
```sh
kubectl apply -f deployments/
```
4. **Apply Kubernetes Services**
Set up your services using the following commands:
```sh
kubectl apply -f services/
```
5. **Apply Secrets**
Apply the secret configurations:
```sh
kubectl apply -f docsgpt-secrets.yaml
```
6. **Substitute API URL**
After deploying the services, you need to update the environment variable `VITE_API_HOST` in your deployment file `deployments/docsgpt-deploy.yaml` with the actual endpoint URL created by your `docsgpt-api-service`.
```sh
kubectl get services/docsgpt-api-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' | xargs -I {} sed -i "s|<your-api-endpoint>|{}|g" deployments/docsgpt-deploy.yaml
```
7. **Rerun Deployment**
After making the changes, reapply the deployment configuration to update the environment variables:
```sh
kubectl apply -f deployments/
```
## Verifying the Deployment
To verify if everything is set up correctly, you can run the following:
```sh
kubectl get pods
kubectl get services
```
Ensure that the pods are running and the services are available.
## Accessing DocsGPT
To access DocsGPT, you need to find the external IP address of the frontend service. You can do this by running:
```sh
kubectl get services/docsgpt-frontend-service | awk 'NR>1 {print "http://" $4}'
```
## Troubleshooting
If you encounter any issues, you can check the logs of the pods for more details:
```sh
kubectl logs <pod-name>
```
Replace `<pod-name>` with the actual name of your DocsGPT pod.

@ -110,3 +110,19 @@ Option 2: Using Git Bash or Command Prompt (CMD):
6. To stop the setup, just press **Ctrl + C** in the Git Bash or Command Prompt terminal.
These steps should help you set up and run the project on Windows using either WSL or Git Bash/Command Prompt. Make sure you have Docker installed and properly configured on your Windows system for this to work.
### Chrome Extension
#### Installing the Chrome extension:
To enhance your DocsGPT experience, you can install the DocsGPT Chrome extension. Here's how:
1. In the DocsGPT GitHub repository, click on the **Code** button and select **Download ZIP**.
2. Unzip the downloaded file to a location you can easily access.
3. Open the Google Chrome browser and click on the three dots menu (upper right corner).
4. Select **More Tools** and then **Extensions**.
5. Turn on the **Developer mode** switch in the top right corner of the **Extensions page**.
6. Click on the **Load unpacked** button.
7. Select the **Chrome** folder where the DocsGPT files have been unzipped (docsgpt-main > extensions > chrome).
8. The extension should now be added to Google Chrome and can be managed on the Extensions page.
9. To disable or remove the extension, simply turn off the toggle switch on the extension card or click the **Remove** button.

@ -10,9 +10,5 @@
"Railway-Deploying": {
"title": "🚂Deploying on Railway",
"href": "/Deploying/Railway-Deploying"
},
"Kubernetes-Deploying": {
"title": "☸Deploying on Kubernetes",
"href": "/Deploying/Kubernetes-Deploying"
}
}
}

@ -0,0 +1,6 @@
{
"API-docs": {
"title": "🗂️️ API-docs",
"href": "/Developing/API-docs"
}
}

@ -1,34 +0,0 @@
import {Steps} from 'nextra/components'
import { Callout } from 'nextra/components'
## Chrome Extension Setup Guide
To enhance your DocsGPT experience, you can install the DocsGPT Chrome extension. Here's how:
<Steps >
### Step 1
In the DocsGPT GitHub repository, click on the **Code** button and select **Download ZIP**.
### Step 2
Unzip the downloaded file to a location you can easily access.
### Step 3
Open the Google Chrome browser and click on the three dots menu (upper right corner).
### Step 4
Select **More Tools** and then **Extensions**.
### Step 5
Turn on the **Developer mode** switch in the top right corner of the **Extensions page**.
### Step 6
Click on the **Load unpacked** button.
### Step 7
7. Select the **Chrome** folder where the DocsGPT files have been unzipped (docsgpt-main > extensions > chrome).
### Step 8
The extension should now be added to Google Chrome and can be managed on the Extensions page.
### Step 9
To disable or remove the extension, simply turn off the toggle switch on the extension card or click the **Remove** button.
</Steps>

@ -7,8 +7,8 @@
"title": "🏗️ Widget setup",
"href": "/Extensions/react-widget"
},
"Chrome-extension": {
"title": "🌐 Chrome Extension",
"href": "/Extensions/Chrome-extension"
"api-key-guide": {
"title": "🔐 API Keys guide",
"href": "/Extensions/api-key-guide"
}
}

@ -14,7 +14,7 @@ Before creating your first API key, you must upload the document that will be li
After uploading your document, you can obtain an API key either through the graphical user interface or via an API call:
- **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the API Keys option, and press 'Create New' to generate your key.
- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://docs.docsgpt.cloud/API/API-docs#8-apicreate_api_key).
- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://docs.docsgpt.co.uk/Developing/API-docs#8-apicreate_api_key).
### Understanding Key Variables
@ -27,4 +27,4 @@ Upon creating your API key, you will encounter several key variables. Each serve
With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the API key, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation.
Congratulations on taking the first step towards enhancing your applications with DocsGPT! With this guide, you're now equipped to navigate the process of obtaining and understanding DocsGPT API keys.
Congratulations on taking the first step towards enhancing your applications with DocsGPT! With this guide, you're now equipped to navigate the process of obtaining and understanding DocsGPT API keys.

@ -51,59 +51,6 @@ export default function MyApp({ Component, pageProps }) {
)
}
```
### How to use DocsGPTWidget with HTML
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT Widget</title>
</head>
<body>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app');
}
</script>
</body>
</html>
```
To link the widget to your api and your documents you can pass parameters to the renderDocsGPTWidget('div id', { parameters }).
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT Widget</title>
</head>
<body>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app', {
apiHost: 'http://localhost:7001',
selectDocs: 'default',
apiKey: '',
avatar: 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
title: 'Get AI assistance',
description: "DocsGPT's AI Chatbot is here to help",
heroTitle: 'Welcome to DocsGPT!',
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.'
});
}
</script>
</body>
</html>
```
For more information about React, refer to this [link here](https://react.dev/learn)

@ -1,25 +1,10 @@
import Image from 'next/image'
# Customizing the Main Prompt
Customizing the main prompt for DocsGPT gives you the ability to tailor the AI's responses to your specific requirements. By modifying the prompt text, you can achieve more accurate and relevant answers. Here's how you can do it:
1. Navigate to `SideBar -> Settings`.
2.In Settings select the `Active Prompt` now you will be able to see various prompts style.x
3.Click on the `edit icon` on the prompt of your choice and you will be able to see the current prompt for it,you can now customise the prompt as per your choice.
### Video Demo
<Image src="/prompts.gif" alt="prompts" width={800} height={500} />
1. Navigate to `/application/prompts/combine_prompt.txt`.
2. Open the `combine_prompt.txt` file and modify the prompt text to suit your needs. You can experiment with different phrasings and structures to observe how the model responds. The main prompt serves as guidance to the AI model on how to generate responses.
## Example Prompt Modification

@ -0,0 +1,63 @@
## How to train on other documentation
This AI can utilize any documentation, but it requires preparation for similarity search. Follow these steps to get your documentation ready:
**Step 1: Prepare Your Documentation**
![video-example-of-how-to-do-it](https://d3dg1063dc54p9.cloudfront.net/videos/how-to-vectorise.gif)
Start by going to `/scripts/` folder.
If you open this file, you will see that it uses RST files from the folder to create a `index.faiss` and `index.pkl`.
It currently uses OPENAI to create the vector store, so make sure your documentation is not too large. Using Pandas cost me around $3-$4.
You can typically find documentation on GitHub in the `docs/` folder for most open-source projects.
### 1. Find documentation in .rst/.md format and create a folder with it in your scripts directory.
- Name it `inputs/`.
- Put all your .rst/.md files in there.
- The search is recursive, so you don't need to flatten them.
If there are no .rst/.md files, convert whatever you find to a .txt file and feed it. (Don't forget to change the extension in the script).
### Step 2: Configure Your OpenAI API Key
1. Create a .env file in the scripts/ folder.
- Add your OpenAI API key inside: OPENAI_API_KEY=<your-api-key>.
### Step 3: Run the Ingestion Script
`python ingest.py ingest`
It will provide you with the estimated cost.
### Step 4: Move `index.faiss` and `index.pkl` generated in `scripts/output` to `application/` folder.
### Step 5: Run the Web App
Once you run it, it will use new context relevant to your documentation.Make sure you select default in the dropdown in the UI.
## Customization
You can learn more about options while running ingest.py by running:
- Make sure you select 'default' from the dropdown in the UI.
## Customization
You can learn more about options while running ingest.py by executing:
`python ingest.py --help`
| Options | |
|:--------------------------------:|:------------------------------------------------------------------------------------------------------------------------------:|
| **ingest** | Runs 'ingest' function, converting documentation to Faiss plus Index format |
| --dir TEXT | List of paths to directory for index creation. E.g. --dir inputs --dir inputs2 [default: inputs] |
| --file TEXT | File paths to use (Optional; overrides directory) E.g. --files inputs/1.md --files inputs/2.md |
| --recursive / --no-recursive | Whether to recursively search in subdirectories [default: recursive] |
| --limit INTEGER | Maximum number of files to read |
| --formats TEXT | List of required extensions (list with .) Currently supported: .rst, .md, .pdf, .docx, .csv, .epub, .html [default: .rst, .md] |
| --exclude / --no-exclude | Whether to exclude hidden files (dotfiles) [default: exclude] |
| -y, --yes | Whether to skip price confirmation |
| --sample / --no-sample | Whether to output sample of the first 5 split documents. [default: no-sample] |
| --token-check / --no-token-check | Whether to group small documents and split large. Improves semantics. [default: token-check] |
| --min_tokens INTEGER | Minimum number of tokens to not group. [default: 150] |
| --max_tokens INTEGER | Maximum number of tokens to not split. [default: 2000] |
| | |
| **convert** | Creates documentation in .md format from source code |
| --dir TEXT | Path to a directory with source code. E.g. --dir inputs [default: inputs] |
| --formats TEXT | Source code language from which to create documentation. Supports py, js and java. E.g. --formats py [default: py] |

@ -1,44 +0,0 @@
import { Callout } from 'nextra/components'
import Image from 'next/image'
import { Steps } from 'nextra/components'
## How to train on other documentation
Training on other documentation sources can greatly enhance the versatility and depth of DocsGPT's knowledge. By incorporating diverse materials, you can broaden the AI's understanding and improve its ability to generate insightful responses across a range of topics. Here's a step-by-step guide on how to effectively train DocsGPT on additional documentation sources:
**Get your document ready**:
Make sure you have the document on which you want to train on ready with you on the device which you are using .You can also use links to the documentation to train on.
<Callout type="warning" emoji="⚠️">
Note: The document should be either of the given file formats .pdf, .txt, .rst, .docx, .md, .zip and limited to 25mb.You can also train using the link of the documentation.
</Callout>
### Video Demo
<Image src="/docs.gif" alt="prompts" width={800} height={500} />
<Steps>
### Step1
Navigate to the sidebar where you will find `Source Docs` option,here you will find 3 options built in which are default,Web Search and None.
### Step 2
Click on the `Upload icon` just beside the source docs options,now borwse and upload the document which you want to train on or select the `remote` option if you have to insert the link of the documentation.
### Step 3
Now you will be able to see the name of the file uploaded under the Uploaded Files ,now click on `Train`,once you click on train it might take some time to train on the document. You will be able to see the `Training progress` and once the training is completed you can click the `finish` button and there you go your docuemnt is uploaded.
### Step 4
Go to `New chat` and from the side bar select the document you uploaded under the `Source Docs` and go ahead with your chat, now you can ask qestions regarding the document you uploaded and you will get the effective answer based on it.
</Steps>

@ -0,0 +1,48 @@
# Setting Up Local Language Models for Your App
Your app relies on two essential models: Embeddings and Text Generation. While OpenAI's default models work seamlessly, you have the flexibility to switch providers or even run the models locally.
## Step 1: Configure Environment Variables
Navigate to the `.env` file or set the following environment variables:
```env
LLM_NAME=<your Text Generation model>
API_KEY=<API key for Text Generation>
EMBEDDINGS_NAME=<LLM for Embeddings>
EMBEDDINGS_KEY=<API key for Embeddings>
VITE_API_STREAMING=<true or false>
```
You can omit the keys if users provide their own. Ensure you set `LLM_NAME` and `EMBEDDINGS_NAME`.
## Step 2: Choose Your Models
**Options for `LLM_NAME`:**
- openai ([More details](https://platform.openai.com/docs/models))
- anthropic ([More details](https://docs.anthropic.com/claude/reference/selecting-a-model))
- manifest ([More details](https://python.langchain.com/docs/integrations/llms/manifest))
- cohere ([More details](https://docs.cohere.com/docs/llmu))
- llama.cpp ([More details](https://python.langchain.com/docs/integrations/llms/llamacpp))
- huggingface (Arc53/DocsGPT-7B by default)
- sagemaker ([Mode details](https://aws.amazon.com/sagemaker/))
Note: for huggingface you can choose any model inside application/llm/huggingface.py or pass llm_name on init, loads
**Options for `EMBEDDINGS_NAME`:**
- openai_text-embedding-ada-002
- huggingface_sentence-transformers/all-mpnet-base-v2
- huggingface_hkunlp/instructor-large
- cohere_medium
If you want to be completely local, set `EMBEDDINGS_NAME` to `huggingface_sentence-transformers/all-mpnet-base-v2`.
For llama.cpp Download the required model and place it in the `models/` folder.
Alternatively, for local Llama setup, run `setup.sh` and choose option 1. The script handles the DocsGPT model addition.
## Step 3: Local Hosting for Privacy
If working with sensitive data, host everything locally by setting `LLM_NAME`, llama.cpp or huggingface, use any model available on Hugging Face, for llama.cpp you need to convert it into gguf format.
That's it! Your app is now configured for local and private hosting, ensuring optimal security for critical data.

@ -1,41 +0,0 @@
import { Callout } from 'nextra/components'
import Image from 'next/image'
import { Steps } from 'nextra/components'
# Setting Up Local Language Models for Your App
Setting up local language models for your app can significantly enhance its capabilities, enabling it to understand and generate text in multiple languages without relying on external APIs. By integrating local language models, you can improve privacy, reduce latency, and ensure continuous functionality even in offline environments. Here's a comprehensive guide on how to set up local language models for your application:
## Steps:
### For cloud version LLM change:
<Steps >
### Step 1
Visit the chat screen and you will be to see the default LLM selected.
### Step 2
Click on it and you will get a drop down of various LLM's available to choose.
### Step 3
Choose the LLM of your choice.
</Steps>
### Video Demo
<Image src="/llms.gif" alt="prompts" width={800} height={500} />
### For Open source llm change:
<Steps >
### Step 1
For open source you have to edit .env file with LLM_NAME with their desired LLM name.
### Step 2
All the supported LLM providers are here application/llm and you can check what env variable are needed for each
List of latest supported LLMs are https://github.com/arc53/DocsGPT/blob/main/application/llm/llm_creator.py
### Step 3
Visit application/llm and select the file of your selected llm and there you will find the speicifc requirements needed to be filled in order to use it,i.e API key of that llm.
</Steps>

@ -1,6 +1,6 @@
{
"Customising-prompts": {
"title": "💻 Customising Prompts",
"title": "🏗️ Customising Prompts",
"href": "/Guides/Customising-prompts"
},
"How-to-train-on-other-documentation": {
@ -8,7 +8,7 @@
"href": "/Guides/How-to-train-on-other-documentation"
},
"How-to-use-different-LLM": {
"title": "🤖 How to use different LLM's",
"title": "⚙️ How to use different LLM's",
"href": "/Guides/How-to-use-different-LLM"
},
"My-AI-answers-questions-using-external-knowledge": {

@ -2,16 +2,14 @@
title: 'Home'
---
import { Cards, Card } from 'nextra/components'
import Image from 'next/image'
import deployingGuides from './Deploying/_meta.json';
import developingGuides from './API/_meta.json';
import developingGuides from './Developing/_meta.json';
import extensionGuides from './Extensions/_meta.json';
import mainGuides from './Guides/_meta.json';
export const allGuides = {
...deployingGuides,
...developingGuides,
@ -23,12 +21,9 @@ export const allGuides = {
DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖. Eliminate lengthy manual searches 🔍 and enhance your documentation experience with DocsGPT, and consider contributing to its AI-powered future 🚀.
![video-example-of-docs-gpt](https://d3dg1063dc54p9.cloudfront.net/videos/demov3.gif)
<Image src="/homevideo.gif" alt="homedemo" width={800} height={500}/>
Try it yourself: [https://www.docsgpt.cloud/](https://www.docsgpt.cloud/)
Try it yourself: [https://docsgpt.arc53.com/](https://docsgpt.arc53.com/)
<Cards
num={3}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

@ -1,5 +1,6 @@
# DocsGPT react widget
This widget will allow you to embed a DocsGPT assistant in your React app.
## Installation
@ -10,8 +11,6 @@ npm install docsgpt
## Usage
### React
```javascript
import { DocsGPTWidget } from "docsgpt";
@ -26,9 +25,9 @@ To link the widget to your api and your documents you can pass parameters to the
import { DocsGPTWidget } from "docsgpt";
const App = () => {
return <DocsGPTWidget
return <DocsGPTWidget
apiHost = 'http://localhost:7001',
selectDocs = 'default',
selectDocs = 'default',
apiKey = '',
avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
title = 'Get AI assistance',
@ -39,65 +38,10 @@ To link the widget to your api and your documents you can pass parameters to the
};
```
### Html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT Widget</title>
</head>
<body>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app');
}
</script>
</body>
</html>
```
To link the widget to your api and your documents you can pass parameters to the **renderDocsGPTWidget('div id', { parameters })**.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT Widget</title>
</head>
<body>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app', , {
apiHost: 'http://localhost:7001',
selectDocs: 'default',
apiKey: '',
avatar: 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
title: 'Get AI assistance',
description: "DocsGPT's AI Chatbot is here to help",
heroTitle: 'Welcome to DocsGPT !',
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.'
});
}
</script>
</body>
</html>
```
## Our github
[DocsGPT](https://github.com/arc53/DocsGPT)
You can find the source code in the extensions/react-widget folder.

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.3.9",
"version": "0.3.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",
@ -11,18 +11,6 @@
"dist",
"package.json"
],
"targets": {
"modern": {
"engines": {
"browsers": "Chrome 80"
}
},
"legacy": {
"engines": {
"browsers": "> 0.5%, last 2 versions, not dead"
}
}
},
"@parcel/resolver-default": {
"packageExports": true
},
@ -30,7 +18,7 @@
"styled-components": "^5"
},
"scripts": {
"build": "parcel build src/main.tsx --public-url ./",
"build": "parcel build src/index.ts",
"dev": "parcel src/index.html -p 3000",
"test": "jest",
"lint": "eslint",
@ -45,13 +33,16 @@
"@parcel/transformer-typescript-tsc": "^2.12.0",
"@parcel/validator-typescript": "^2.12.0",
"@radix-ui/react-icons": "^1.3.0",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dompurify": "^3.1.5",
"dompurify": "^3.0.9",
"flow-bin": "^0.229.2",
"i": "^0.3.7",
"install": "^0.13.0",
"npm": "^10.5.0",
"parcel": "^2.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^6.1.8"
@ -63,10 +54,7 @@
"@parcel/packager-ts": "^2.12.0",
"@parcel/transformer-typescript-types": "^2.12.0",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"babel-loader": "^8.0.4",
"parcel": "^2.12.0",
"process": "^0.11.10",
"typescript": "^5.3.3"
},

@ -1,13 +1,12 @@
"use client";
import React from 'react'
import DOMPurify from 'dompurify';
import snarkdown from '@bpmn-io/snarkdown';
import styled, { keyframes, createGlobalStyle } from 'styled-components';
import { Fragment, useEffect, useRef, useState } from 'react'
import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons';
import MessageIcon from '../assets/message.svg';
import { MESSAGE_TYPE, Query, Status } from '../types/index';
import MessageIcon from '../assets/message.svg'
import { fetchAnswerStreaming } from '../requests/streamingApi';
import styled, { keyframes, createGlobalStyle } from 'styled-components';
import snarkdown from '@bpmn-io/snarkdown';
import { sanitize } from 'dompurify';
const GlobalStyles = createGlobalStyle`
.response pre {
padding: 8px;
@ -293,13 +292,13 @@ export const DocsGPTWidget = ({
heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.'
}) => {
const [prompt, setPrompt] = React.useState('');
const [status, setStatus] = React.useState<Status>('idle');
const [queries, setQueries] = React.useState<Query[]>([])
const [conversationId, setConversationId] = React.useState<string | null>(null)
const [open, setOpen] = React.useState<boolean>(false)
const [eventInterrupt, setEventInterrupt] = React.useState<boolean>(false); //click or scroll by user while autoScrolling
const endMessageRef = React.useRef<HTMLDivElement | null>(null);
const [prompt, setPrompt] = useState('');
const [status, setStatus] = useState<Status>('idle');
const [queries, setQueries] = useState<Query[]>([])
const [conversationId, setConversationId] = useState<string | null>(null)
const [open, setOpen] = useState<boolean>(false)
const [eventInterrupt, setEventInterrupt] = useState<boolean>(false); //click or scroll by user while autoScrolling
const endMessageRef = useRef<HTMLDivElement | null>(null);
const handleUserInterrupt = () => {
(status === 'loading') && setEventInterrupt(true);
}
@ -317,7 +316,7 @@ export const DocsGPTWidget = ({
lastChild && scrollToBottom(lastChild)
};
React.useEffect(() => {
useEffect(() => {
!eventInterrupt && scrollToBottom(endMessageRef.current);
}, [queries.length, queries[queries.length - 1]?.response]);
@ -397,7 +396,7 @@ export const DocsGPTWidget = ({
{
queries.length > 0 ? queries?.map((query, index) => {
return (
<React.Fragment key={index}>
<Fragment key={index}>
{
query.prompt && <MessageBubble type='QUESTION'>
<Message
@ -413,7 +412,7 @@ export const DocsGPTWidget = ({
type='ANSWER'
ref={(index === queries.length - 1) ? endMessageRef : null}
>
<div className="response" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(snarkdown(query.response)) }} />
<div className="response" dangerouslySetInnerHTML={{ __html: sanitize(snarkdown(query.response)) }} />
</Message>
</MessageBubble>
: <div>
@ -437,7 +436,7 @@ export const DocsGPTWidget = ({
}
</div>
}
</React.Fragment>)
</Fragment>)
})
: <Hero title={heroTitle} description={heroDescription} />
}

@ -9,11 +9,5 @@
<body>
<div id="app"></div>
<script type="module" src="main.tsx"></script>
<script type="module" src="../dist/main.js"></script>
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app');
}
</script>
</body>
</html>

@ -1,11 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { DocsGPTWidget } from './components/DocsGPTWidget';
const renderWidget = (elementId: string, props = {}) => {
const root = createRoot(document.getElementById(elementId) as HTMLElement);
root.render(<DocsGPTWidget {...props} />);
};
import App from './App.tsx';
import React from 'react';
const root = createRoot(document.getElementById('app') as HTMLElement);
(window as any).renderDocsGPTWidget = renderWidget;
export { DocsGPTWidget };
root.render(<App />);

@ -1,17 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes">
<title>DocsGPT 🦖</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<div id="root" class="h-screen"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT 🦖</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<div id="root" class="h-screen"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

@ -10,12 +10,10 @@
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^14.1.2",
"react-markdown": "^8.0.7",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",
@ -356,12 +354,11 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
"integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
"license": "MIT",
"version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
"integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
@ -2154,12 +2151,12 @@
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
@ -3702,9 +3699,9 @@
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -4137,15 +4134,6 @@
"react-is": "^16.7.0"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/human-signals": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
@ -4170,29 +4158,6 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/i18next": {
"version": "23.11.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
"integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@ -6713,28 +6678,6 @@
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-i18next": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz",
"integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -6932,10 +6875,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
@ -7981,15 +7923,6 @@
"vite": "^2.6.0 || 3 || 4 || 5"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

@ -21,12 +21,10 @@
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^14.1.2",
"react-markdown": "^8.0.7",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",

@ -7,7 +7,6 @@ import { inject } from '@vercel/analytics';
import { useMediaQuery } from './hooks';
import { useState } from 'react';
import Setting from './settings';
import './locale/i18n';
inject();
@ -20,7 +19,7 @@ export default function App() {
<div
className={`transition-all duration-200 ${
!isMobile
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
? `ml-0 ${!navOpen ? '-mt-5 md:mx-auto lg:mx-auto' : 'md:ml-72'}`
: 'ml-0 md:ml-16'
}`}
>

@ -1,52 +1,191 @@
import { Fragment } from 'react';
import { useDarkTheme, useMediaQuery } from './hooks';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
import { useTranslation } from 'react-i18next';
export default function Hero({
handleQuestion,
}: {
handleQuestion: ({
question,
isRetry,
}: {
question: string;
isRetry?: boolean;
}) => void;
}) {
const { t } = useTranslation();
const demos = t('demo', { returnObjects: true }) as Array<{
header: string;
query: string;
}>;
export default function Hero({ className = '' }: { className?: string }) {
// const isMobile = window.innerWidth <= 768;
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
return (
<div
className={`mt-14 mb-4 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full lg:mt-6`}
className={`mt-14 mb-32 flex flex-col text-black-1000 dark:text-bright-gray lg:mt-6`}
>
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex items-center">
<span className="p-0 text-4xl font-semibold">DocsGPT</span>
<img className="mb-1 inline w-14 p-0" src={DocsGPT3} alt="docsgpt" />
</div>
<div className="mb-4 flex flex-col items-center justify-center dark:text-white"></div>
<div className=" mb-2 flex items-center justify-center sm:mb-10">
<p className="mr-2 text-4xl font-semibold">DocsGPT</p>
<img className="mb-2 h-14" src={DocsGPT3} alt="DocsGPT" />
</div>
<div className="mb-16 grid w-full grid-cols-1 items-center gap-4 self-center text-xs sm:w-auto sm:gap-6 md:mb-0 md:text-sm lg:grid-cols-2">
{demos?.map(
(demo: { header: string; query: string }, key: number) =>
demo.header &&
demo.query && (
<Fragment key={key}>
<button
onClick={() => handleQuestion({ question: demo.query })}
className="w-full rounded-full border-2 border-silver px-6 py-4 text-left hover:border-gray-4000 dark:hover:border-gray-3000 xl:min-w-[24vw]"
>
<p className="mb-1 font-semibold text-black dark:text-silver">
{demo.header}
</p>
<span className="text-gray-400">{demo.query}</span>
</button>
</Fragment>
),
)}
{isMobile ? (
<p className="mb-3 text-center leading-6">
Welcome to <span className="font-bold">DocsGPT</span>, your technical
documentation assistant! Start by entering your query in the input
field below, and we&apos;ll provide you with the most relevant
answers.
</p>
) : (
<>
<p className="mb-3 text-center leading-6">
Welcome to DocsGPT, your technical documentation assistant!
</p>
<p className="mb-3 text-center leading-6">
Enter a query related to the information in the documentation you
selected to receive
<br /> and we will provide you with the most relevant answers.
</p>
<p className="mb-3 text-center leading-6">
Start by entering your query in the input field below and we will do
the rest!
</p>
</>
)}
<div
className={`mt-0 flex flex-wrap items-center justify-center gap-2 sm:mt-1 sm:gap-4 md:gap-4 lg:gap-0`}
>
{/* first */}
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-tr-none lg:rounded-br-none">
<div
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 py-8'
} lg:rounded-tr-none lg:rounded-br-none`}
>
{/* Add Mobile check here */}
{isMobile ? (
<div className="flex justify-center">
<img
src={
isDarkTheme ? '/message-text-dark.svg' : '/message-text.svg'
}
alt="lock"
className="h-[24px] w-[24px] "
/>
<h2 className="mb-0 pl-1 text-lg font-bold">
Chat with Your Data
</h2>
</div>
) : (
<>
<img
src={
isDarkTheme ? '/message-text-dark.svg' : '/message-text.svg'
}
alt="lock"
className="h-[24px] w-[24px]"
/>
<h2 className="mt-2 mb-3 text-lg font-bold">
Chat with Your Data
</h2>
</>
)}
<p
className={
isMobile
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
}
>
DocsGPT will use your data to answer questions. Whether its
documentation, source code, or Microsoft files, DocsGPT allows you
to have interactive conversations and find answers based on the
provided data.
</p>
</div>
</div>
{/* second */}
<div className="h-auto rounded-[50px] bg-gradient-to-r from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-none lg:py-1 lg:px-0">
<div
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 py-6'
} lg:rounded-none`}
>
{/* Add Mobile check here */}
{isMobile ? (
<div className="flex justify-center ">
<img
src={isDarkTheme ? '/lock-dark.svg' : '/lock.svg'}
alt="lock"
className="h-[24px] w-[24px]"
/>
<h2 className="mb-0 pl-1 text-lg font-bold">
Secure Data Storage
</h2>
</div>
) : (
<>
<img
src={isDarkTheme ? '/lock-dark.svg' : '/lock.svg'}
alt="lock"
className="h-[24px] w-[24px]"
/>
<h2 className="mt-2 mb-3 text-lg font-bold">
Secure Data Storage
</h2>
</>
)}
<p
className={
isMobile
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
}
>
The security of your data is our top priority. DocsGPT ensures the
utmost protection for your sensitive information. With secure data
storage and privacy measures in place, you can trust that your
data is kept safe and confidential.
</p>
</div>
</div>
{/* third */}
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-tl-none lg:rounded-bl-none ">
<div
className={`firefox h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 px-6 '
} lg:rounded-tl-none lg:rounded-bl-none`}
>
{/* Add Mobile check here */}
{isMobile ? (
<div className="flex justify-center">
<img
src={
isDarkTheme
? 'message-programming-dark.svg'
: '/message-programming.svg'
}
alt="lock"
className="h-[24px] w-[24px]"
/>
<h2 className="mb-0 pl-1 text-lg font-bold">
Open Source Code
</h2>
</div>
) : (
<>
<img
src={
isDarkTheme
? '/message-programming-dark.svg'
: '/message-programming.svg'
}
alt="lock"
className="h-[24px] w-[24px]"
/>
<h2 className="mt-2 mb-3 text-lg font-bold">
Open Source Code
</h2>
</>
)}
<p
className={
isMobile
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
}
>
DocsGPT is built on open source principles, promoting transparency
and collaboration. The source code is freely available, enabling
developers to contribute, enhance, and customize the app to meet
their specific needs.
</p>
</div>
</div>
</div>
</div>
);

@ -1,5 +1,5 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
interface ModalProps {
handleSubmit: () => void;
isCancellable: boolean;
@ -8,11 +8,8 @@ interface ModalProps {
modalState: string;
isError: boolean;
errorMessage?: string;
textDelete?: boolean;
}
const Modal = (props: ModalProps) => {
const { t } = useTranslation();
return (
<div
className={`${
@ -20,20 +17,20 @@ const Modal = (props: ModalProps) => {
} absolute z-30 h-screen w-screen bg-gray-alpha`}
>
{props.render()}
<div className=" mx-auto flex w-[90vw] max-w-lg flex-row-reverse rounded-b-lg bg-white pb-5 pr-5 shadow-lg dark:bg-outer-space">
<div className=" mx-auto flex w-[90vw] max-w-lg flex-row-reverse rounded-b-lg bg-white pb-5 pr-5 shadow-lg">
<div>
<button
onClick={() => props.handleSubmit()}
className="ml-auto h-10 w-20 rounded-3xl bg-violet-800 text-white transition-all hover:bg-violet-700 dark:text-silver"
className="ml-auto h-10 w-20 rounded-3xl bg-violet-800 text-white transition-all hover:bg-violet-700"
>
{props.textDelete ? 'Delete' : 'Save'}
Save
</button>
{props.isCancellable && (
<button
onClick={() => props.handleCancel && props.handleCancel()}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
className="ml-5 h-10 w-20 rounded-lg border border-violet-700 bg-white text-violet-800 transition-all hover:bg-violet-700 hover:text-white"
>
{t('cancel')}
Cancel
</button>
)}
</div>

@ -1,21 +1,25 @@
import { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NavLink, useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
import Documentation from './assets/documentation.svg';
import DocumentationDark from './assets/documentation-dark.svg';
import Discord from './assets/discord.svg';
import DiscordDark from './assets/discord-dark.svg';
import Expand from './assets/expand.svg';
import Github from './assets/github.svg';
import GithubDark from './assets/github-dark.svg';
import Hamburger from './assets/hamburger.svg';
import HamburgerDark from './assets/hamburger-dark.svg';
import Info from './assets/info.svg';
import InfoDark from './assets/info-dark.svg';
import SettingGear from './assets/settingGear.svg';
import Twitter from './assets/TwitterX.svg';
import SettingGearDark from './assets/settingGear-dark.svg';
import Add from './assets/add.svg';
import UploadIcon from './assets/upload.svg';
import { ActiveState } from './models/misc';
import APIKeyModal from './preferences/APIKeyModal';
import DeleteConvModal from './modals/DeleteConvModal';
import {
selectApiKeyStatus,
selectSelectedDocs,
@ -25,9 +29,6 @@ import {
selectConversations,
setConversations,
selectConversationId,
selectModalStateDeleteConv,
setModalStateDeleteConv,
setSourceDocs,
} from './preferences/preferenceSlice';
import {
setConversation,
@ -35,43 +36,40 @@ import {
} from './conversation/conversationSlice';
import { useMediaQuery, useOutsideAlerter } from './hooks';
import Upload from './upload/Upload';
import { Doc, getConversations, getDocs } from './preferences/preferenceApi';
import { Doc, getConversations } from './preferences/preferenceApi';
import SelectDocsModal from './preferences/SelectDocsModal';
import ConversationTile from './conversation/ConversationTile';
import { useDarkTheme } from './hooks';
import SourceDropdown from './components/SourceDropdown';
import { useTranslation } from 'react-i18next';
interface NavigationProps {
navOpen: boolean;
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
/* const NavImage: React.FC<{
const NavImage: React.FC<{
Light: string | undefined;
Dark: string | undefined;
}> = ({ Light, Dark }) => {
return (
<>
<img src={Dark} alt="icon" className="ml-2 hidden w-5 dark:block " />
<img src={Light} alt="icon" className="ml-2 w-5 dark:hidden filter dark:invert" />
<img src={Light} alt="icon" className="ml-2 w-5 dark:hidden " />
</>
);
};
NavImage.propTypes = {
Light: PropTypes.string,
Dark: PropTypes.string,
}; */
};
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const dispatch = useDispatch();
const docs = useSelector(selectSourceDocs);
const selectedDocs = useSelector(selectSelectedDocs);
const conversations = useSelector(selectConversations);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const conversationId = useSelector(selectConversationId);
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
const { t } = useTranslation();
const isApiKeySet = useSelector(selectApiKeyStatus);
const [apiKeyModalState, setApiKeyModalState] =
@ -94,7 +92,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
fetchConversations();
}
}, [conversations, dispatch]);
async function fetchConversations() {
return await getConversations()
.then((fetchedConversations) => {
@ -105,16 +102,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
});
}
const handleDeleteAllConversations = () => {
fetch(`${apiHost}/api/delete_all_conversations`, {
method: 'POST',
})
.then(() => {
fetchConversations();
})
.catch((error) => console.error(error));
};
const handleDeleteConversation = (id: string) => {
fetch(`${apiHost}/api/delete_conversation?id=${id}`, {
method: 'POST',
@ -125,29 +112,19 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
.catch((error) => console.error(error));
};
const handleDeleteClick = (doc: Doc) => {
const docPath = `indexes/local/${doc.name}`;
const handleDeleteClick = (index: number, doc: Doc) => {
const docPath = 'indexes/' + 'local' + '/' + doc.name;
fetch(`${apiHost}/api/delete_old?path=${docPath}`, {
method: 'GET',
})
.then(() => {
// remove the image element from the DOM
// const imageElement = document.querySelector(
// `#img-${index}`,
// ) as HTMLElement;
// const parentElement = imageElement.parentNode as HTMLElement;
// parentElement.parentNode?.removeChild(parentElement);
return getDocs();
})
.then((updatedDocs) => {
dispatch(setSourceDocs(updatedDocs));
dispatch(
setSelectedDocs(
updatedDocs?.find((doc) => doc.name.toLowerCase() === 'default'),
),
);
const imageElement = document.querySelector(
`#img-${index}`,
) as HTMLElement;
const parentElement = imageElement.parentNode as HTMLElement;
parentElement.parentNode?.removeChild(parentElement);
})
.catch((error) => console.error(error));
};
@ -277,15 +254,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="opacity-80 group-hover:opacity-100"
/>
<p className=" text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray">
{t('newChat')}
New Chat
</p>
</NavLink>
<div className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white">
{conversations && conversations.length > 0 ? (
<div className="mb-auto h-[56vh] overflow-y-auto overflow-x-hidden dark:text-white">
{conversations && (
<div>
<div className=" my-auto mx-4 mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
<p className="mt-1 ml-4 text-sm font-semibold">{t('chats')}</p>
</div>
<p className="ml-6 mt-3 text-sm font-semibold">Chats</p>
<div className="conversations-container">
{conversations?.map((conversation) => (
<ConversationTile
@ -300,14 +275,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
))}
</div>
</div>
) : (
<></>
)}
</div>
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white">
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe">
<div className="relative my-4 mx-4 flex gap-2">
<div className="relative my-4 flex gap-2 px-2">
<SourceDropdown
options={docs}
selectedDocs={selectedDocs}
@ -322,84 +295,66 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
onClick={() => setUploadModalState('ACTIVE')}
></img>
</div>
<p className="ml-5 mt-3 text-sm font-semibold">{t('sourceDocs')}</p>
<p className="ml-6 mt-3 text-sm font-semibold">Source Docs</p>
</div>
<div className="flex flex-col gap-2 border-b-[1px] py-2 dark:border-b-purple-taupe">
<NavLink
to="/settings"
className={({ isActive }) =>
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
}`
}
>
<img
src={SettingGear}
alt="icon"
className="ml-2 w-5 filter dark:invert"
/>
<NavImage Light={SettingGear} Dark={SettingGearDark} />
<p className="my-auto text-sm text-eerie-black dark:text-white">
{t('settings.label')}
Settings
</p>
</NavLink>
</div>
<div className="flex justify-between gap-2 border-b-[1.5px] py-2 dark:border-b-purple-taupe">
<div className="flex flex-col gap-2 border-b-[1.5px] py-2 dark:border-b-purple-taupe">
<NavLink
to="/about"
className={({ isActive }) =>
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : ''
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
isActive ? 'bg-gray-3000 dark:bg-purple-taupe' : ''
}`
}
>
<img
src={Info}
alt="icon"
className="ml-2 w-5 filter dark:invert"
/>
<p className="my-auto pr-1 text-sm">{t('about')}</p>
<NavImage Light={Info} Dark={InfoDark} />
<p className="my-auto text-sm">About</p>
</NavLink>
<div className="flex items-center justify-evenly gap-1 px-1">
<NavLink
target="_blank"
to={'https://discord.gg/WHJdfbQDR4'}
className={
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
}
>
<img
src={Discord}
alt="discord"
className="m-2 w-6 self-center filter dark:invert"
/>
</NavLink>
<NavLink
target="_blank"
to={'https://twitter.com/docsgptai'}
className={
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
}
>
<img
src={Twitter}
alt="x"
className="m-2 w-5 self-center filter dark:invert"
/>
</NavLink>
<NavLink
target="_blank"
to={'https://github.com/arc53/docsgpt'}
className={
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
}
>
<img
src={Github}
alt="github"
className="m-2 w-6 self-center filter dark:invert"
/>
</NavLink>
</div>
<a
href="https://docs.docsgpt.co.uk/"
target="_blank"
rel="noreferrer"
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
>
<NavImage Light={Documentation} Dark={DocumentationDark} />
<p className="my-auto text-sm ">Documentation</p>
</a>
<a
href="https://discord.gg/WHJdfbQDR4"
target="_blank"
rel="noreferrer"
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
>
<NavImage Light={Discord} Dark={DiscordDark} />
{/* <img src={isDarkTheme ? DiscordDark : Discord} alt="discord-link" className="ml-2 w-5" /> */}
<p className="my-auto text-sm">Visit our Discord</p>
</a>
<a
href="https://github.com/arc53/DocsGPT"
target="_blank"
rel="noreferrer"
className="mx-4 mt-auto flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
>
<NavImage Light={Github} Dark={GithubDark} />
<p className="my-auto text-sm">Visit our Github</p>
</a>
</div>
</div>
</div>
@ -415,7 +370,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</button>
</div>
<SelectDocsModal
modalState={selectedDocsModalState}
setModalState={setSelectedDocsModalState}
@ -426,11 +380,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
setModalState={setApiKeyModalState}
isCancellable={isApiKeySet}
/>
<DeleteConvModal
modalState={modalStateDeleteConv}
setModalState={setModalStateDeleteConv}
handleDeleteAllConv={handleDeleteAllConversations}
/>
<Upload
modalState={uploadModalState}
setModalState={setUploadModalState}

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.175 0.843262H16.9354L10.9054 7.75269L18 17.1564H12.4457L8.09229 11.4543L3.11657 17.1564H0.353571L6.80271 9.76355L0 0.844547H5.69571L9.62486 6.05555L14.175 0.843262ZM13.2043 15.5004H14.7343L4.86 2.41312H3.21943L13.2043 15.5004Z" fill="#747474"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="white" d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>

Before

Width:  |  Height:  |  Size: 454 B

@ -1 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="black" d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>
<svg width="30" height="33" viewBox="0 0 30 33" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="30" height="33" fill="none"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_1_917" transform="scale(0.0166667 0.0151515)"/>
</pattern>
<image id="image0_1_917" width="60" height="66" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAABCCAYAAAAL1LXDAAAGuElEQVRoge2aSXPaSheG3wYJBJiSQAx2mCInhqLiSjZJKllml1+cn5BVUnE5k4sEDAZjZgkHMUiIvgtf9FkxYIwxn+H63bXULZ1H3eo+ffoQSinFf0i2/7cBq9YD8KbrAXjT9QC86WJW9SJKKTqdDlRVhc1mg9frhcvlQq/XA8MwYFl2JXbcGbAsy1AUBZVKBaqqQtd1MAwDTdNACIGmaWAYBjzPY2trC06nE6FQCD6fD5RS2Gw22GzLH4BkGZ7WaDRCu92GqqpotVqoVqtQFAW6rkPXdQAXPfz3qwgh5jWO4+B0OuH1ehGNRuH1euHz+eDxeG5rnvWdywCu1Wr49esXFEVBp9NBr9e7Aje3Qf9+BI/Hg1gshng8jkgkAkopCCG3NfV2wM1mE1+/fkWpVDJ7cpY4joPL5QKlFL1eD4PBACzLglKK4XB41ThCwPM8kskktre3wfM87Hb7rcBvDGwYBlqtFsrlMkqlEprNJgzDmAgnCAICgQAEQTAnJZZlYRiG+T+rqmqODEVRJo4OlmXh9Xqxv7+PaDQKp9O5OmBZlvH582dUKpUrver1ehEMBuHxeOD3+81/0GazgRACu91uqT8ajcyJqdlsQtd1yLKMTCZjTnSj0cisHwwG8fjxY8RiMfA8vxrgDx8+oFarWXqBYRiIoghJkrC9vQ2PxwOHw7GQQYZhoNFooFQqodFooF6vWz6s2+1GJBJBKpVCIBC48fCeG/jPnz/49OkTTk5OLF+d4zjs7u4imUyC5/mlLSWGYUBRFOTzeWQyGfT7ffMewzDw+Xx49+7djWfxuYBbrRZ+/PiBfD5vfm2Hw4FEIoF0Og1RFG+IM780TUOhUMDBwQE6nY7lniRJePXqFdxu99w9fW139Pt9ZLNZFItFE5ZlWUSjUaTTafh8vgUw5pfD4YAkSXj9+jUCgQAY5n++Uj6fRzabhaqqcz/vWuCzszPkcjlzSBFCIIoiUqkURFG8E2/obzEMg3g8jhcvXiAcDpuTH6UUP3/+xPHx8cRlbeKzZt0cDAY4PDxEt9s1r1FK8fLlSwQCgVsg3FyEEMTjcXNZq1QqAIBut4vj42OEQiGEw+FrnzOzew4ODtBut80yz/N4//49RFFciteziILBIPb398FxnHlNlmVks9m52k8FrtfrqFarFqciFoutbBhPE8MwiMVi2N3dNf/n0WiEYrGIer1uWUEmaaLlg8EAhUIBsiybPenz+RCJRBZeX5etRCIBQRBM+8ZD+7oJbCKw3W5HNBqFJEngeR4ulwuSJCEYDC7f8gUliiISiYSlAyqVChqNxsx2EycthmEQDofh9/vR7XZxdHSEp0+frmyTPo9YlsXOzg7Ozs5QLpcBAJ1OB7VaDZIkTW039WckhMDhcEAQBLx582bp+9JlSBAERKNRs6xpGhRFmdlmrWNaLMsiGAzC5XIBuFgyz8/PIcvy1DZrDQxcQF/eOXW7XdRqtan11x7Y4/FYnKDRaIRmszm1/toD22w2CIJgKV/2DK/UX4VRdylCiGX1oJRC1/WpMbW1Bx5vJMauJqUUDMNc2UqOtfbAwMXENZ6pAZixsknaCODhcGjx+QkhU/39jQC22Ww4Pz83y4ZhIBQKTa67KqPuSpRSy1aREIJerze1/toDE0LQ6XTMTQSlFOFweCr02gMDQLlcvhJJ3dhlqd1uo9/vmzEth8MBp9MJt9s9sf7aA6uqitPTU7PMcRx2dnam1l9rYMMwIMuyuSQRQsBx3MwA41oD9/t95HI5s0wpRSwWm3lUu7bAuq7j9+/flvV3fJg3K8i4tsCtVgsnJyfQNA3AxXBOJBLw+/2bBzwajVCtVq/EzB89enTt2fHKsniWpeFwiFwuh+/fv5tnXeM9cSQSubb9WgFTSiHLMj5+/GiZmARBwNu3b+d6xloBV6tVfPnyxXLN7Xbj+fPnFn96ltYCWNM0NBoNHB0dodlsmr3rdruxt7c309H4W2sBfHp6im/fvlmcDIZhEIlE8OTJk7l7F7jHwONkt0wmYzmfHsvn8yGVSt04ueXeAY/DM8Vi0UyLunzYTQjBs2fPkE6nFzoNuRfAlFIoigJZltFqtaCqqpnsdnk2drvdSKfTSCaTNxrGl3UvgIfDIQ4PD1Eul68MXeAiMikIAiRJgiRJt0pMuxfAhmHAMIyJsJcTaERRtCS1LKJ7ATz2lAqFAgghcLlc2NragiAI2NvbmxqQW0T3Anh8Hs2yLMLhsHk27fF4Fk4xnKalpA8vQ5RStFqtO01yA+4R8Kq0ltvD2+gBeNP1ALzp+s8B/wPYvPrTcSgesQAAAABJRU5ErkJggg=="/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -1,45 +0,0 @@
import { useState } from 'react';
import Copy from './../assets/copy.svg?react';
import CheckMark from './../assets/checkmark.svg?react';
import copy from 'copy-to-clipboard';
export default function CoppyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const [isCopyHovered, setIsCopyHovered] = useState(false);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
// Reset copied to false after a few seconds
setTimeout(() => {
setCopied(false);
}, 3000);
};
return (
<div
className={`flex items-center justify-center rounded-full p-2 ${
isCopyHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
{copied ? (
<CheckMark
className="cursor-pointer stroke-green-2000"
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
/>
) : (
<Copy
className="cursor-pointer fill-none"
onClick={() => {
handleCopyClick(text);
}}
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
/>
)}
</div>
);
}

@ -9,8 +9,6 @@ function Dropdown({
onSelect,
size = 'w-32',
rounded = 'xl',
border = 'border-2',
borderColor = 'silver',
showEdit,
onEdit,
showDelete,
@ -20,48 +18,21 @@ function Dropdown({
options:
| string[]
| { name: string; id: string; type: string }[]
| { label: string; value: string }[]
| { value: number; description: string }[];
selectedValue:
| string
| { label: string; value: string }
| { value: number; description: string }
| null;
| { label: string; value: string }[];
selectedValue: string | { label: string; value: string } | null;
onSelect:
| ((value: string) => void)
| ((value: { name: string; id: string; type: string }) => void)
| ((value: { label: string; value: string }) => void)
| ((value: { value: number; description: string }) => void);
| ((value: { label: string; value: string }) => void);
size?: string;
rounded?: 'xl' | '3xl';
border?: 'border' | 'border-2';
borderColor?: string;
showEdit?: boolean;
onEdit?: (value: { name: string; id: string; type: string }) => void;
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
}) {
const dropdownRef = React.useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = React.useState(false);
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
const borderTopRadius = rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl';
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
React.useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div
className={[
@ -70,12 +41,11 @@ function Dropdown({
: 'relative align-middle',
size,
].join(' ')}
ref={dropdownRef}
>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${borderColor}/40 dark:bg-transparent ${
isOpen ? `${borderTopRadius}` : `${borderRadius}`
className={`flex w-full cursor-pointer items-center justify-between border-2 border-silver bg-white px-5 py-3 dark:border-chinese-silver dark:bg-transparent ${
isOpen ? `rounded-t-${rounded}` : `rounded-${rounded}`
}`}
>
{typeof selectedValue === 'string' ? (
@ -88,14 +58,8 @@ function Dropdown({
!selectedValue && 'text-silver dark:text-gray-400'
}`}
>
{selectedValue && 'label' in selectedValue
{selectedValue
? selectedValue.label
: selectedValue && 'description' in selectedValue
? `${
selectedValue.value < 1e9
? selectedValue.value + ` (${selectedValue.description})`
: selectedValue.description
}`
: placeholder
? placeholder
: 'From URL'}
@ -110,9 +74,7 @@ function Dropdown({
/>
</button>
{isOpen && (
<div
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${borderColor}/40 dark:bg-dark-charcoal`}
>
<div className="absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border-2 border-silver bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
{options.map((option: any, index) => (
<div
key={index}
@ -129,13 +91,7 @@ function Dropdown({
? option
: option.name
? option.name
: option.label
? option.label
: `${
option.value < 1e9
? option.value + ` (${option.description})`
: option.description
}`}
: option.label}
</span>
{showEdit && onEdit && (
<img

@ -1,17 +0,0 @@
import * as React from 'react';
import { SVGProps } from 'react';
const RetryIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={16}
height={16}
fill={props.fill}
stroke={props.stroke}
viewBox="0 0 383.748 383.748"
{...props}
>
<path d="M62.772 95.042C90.904 54.899 137.496 30 187.343 30c83.743 0 151.874 68.13 151.874 151.874h30C369.217 81.588 287.629 0 187.343 0c-35.038 0-69.061 9.989-98.391 28.888a182.423 182.423 0 0 0-47.731 44.705L2.081 34.641v113.365h113.91L62.772 95.042zM381.667 235.742h-113.91l53.219 52.965c-28.132 40.142-74.724 65.042-124.571 65.042-83.744 0-151.874-68.13-151.874-151.874h-30c0 100.286 81.588 181.874 181.874 181.874 35.038 0 69.062-9.989 98.391-28.888a182.443 182.443 0 0 0 47.731-44.706l39.139 38.952V235.742z" />
</svg>
);
export default RetryIcon;

@ -1,9 +1,8 @@
import React from 'react';
import Trash from '../assets/trash.svg';
import Arrow2 from '../assets/dropdown-arrow.svg';
import { Doc } from '../preferences/preferenceApi';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
type Props = {
options: Doc[] | null;
selectedDocs: Doc | null;
@ -22,8 +21,6 @@ function SourceDropdown({
handleDeleteClick,
}: Props) {
const dispatch = useDispatch();
const { t } = useTranslation();
const dropdownRef = React.useRef<HTMLDivElement>(null);
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
@ -33,35 +30,18 @@ function SourceDropdown({
setIsDocsListOpen(false);
};
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDocsListOpen(false);
}
};
React.useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
<div className="relative w-5/6 rounded-3xl">
<button
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
className={`flex w-full cursor-pointer items-center border border-silver bg-white p-[14px] dark:bg-transparent ${
isDocsListOpen
? 'rounded-t-3xl dark:border-silver/40'
: 'rounded-3xl dark:border-purple-taupe'
className={`flex w-full cursor-pointer items-center border-2 bg-white p-3 dark:border-chinese-silver dark:bg-transparent ${
isDocsListOpen ? 'rounded-t-3xl' : 'rounded-3xl'
}`}
>
<span className="ml-1 mr-2 flex-1 overflow-hidden text-ellipsis text-left dark:text-bright-gray">
<div className="flex flex-row gap-2">
<p className="max-w-3/4 truncate whitespace-nowrap">
{selectedDocs?.name || 'None'}
{selectedDocs?.name || ''}
</p>
<p className="flex flex-col items-center justify-center">
{selectedDocs?.version}
@ -77,7 +57,7 @@ function SourceDropdown({
/>
</button>
{isDocsListOpen && (
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border-2 bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
{options ? (
options.map((option: any, index: number) => {
if (option.model === embeddingsName) {
@ -106,7 +86,7 @@ function SourceDropdown({
id={`img-${index}`}
onClick={(event) => {
event.stopPropagation();
handleDeleteClick(option);
handleDeleteClick(index, option);
}}
/>
)}
@ -115,14 +95,16 @@ function SourceDropdown({
}
})
) : (
<></>
<div className="h-10 w-full cursor-pointer border-b-[1px] hover:bg-gray-100 dark:border-b-purple-taupe dark:hover:bg-purple-taupe">
<p className="ml-5 py-3">No default documentation.</p>
</div>
)}
<div
className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe"
onClick={handleEmptyDocumentSelect}
>
<span className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3">
{t('none')}
Empty
</span>
</div>
</div>

@ -14,12 +14,9 @@ import {
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import Spinner from './../assets/spinner.svg';
import SpinnerDark from './../assets/spinner-dark.svg';
import { FEEDBACK, Query } from './conversationModels';
import { sendFeedback } from './conversationApi';
import { useTranslation } from 'react-i18next';
import ArrowDown from './../assets/arrow-down.svg';
import RetryIcon from '../components/RetryIcon';
export default function Conversation() {
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
@ -30,8 +27,6 @@ export default function Conversation() {
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
const fetchStream = useRef<any>(null);
const [eventInterrupt, setEventInterrupt] = useState(false);
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const { t } = useTranslation();
const handleUserInterruption = () => {
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
@ -75,13 +70,6 @@ export default function Conversation() {
};
}, [endMessageRef.current]);
useEffect(() => {
if (queries.length) {
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
}
}, [queries[queries.length - 1]]);
const scrollIntoView = () => {
endMessageRef?.current?.scrollIntoView({
behavior: 'smooth',
@ -89,20 +77,13 @@ export default function Conversation() {
});
};
const handleQuestion = ({
question,
isRetry = false,
}: {
question: string;
isRetry?: boolean;
}) => {
const handleQuestion = (question: string) => {
question = question.trim();
if (question === '') return;
setEventInterrupt(false);
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
dispatch(addQuery({ prompt: question }));
fetchStream.current = dispatch(fetchAnswer({ question }));
};
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
const prevFeedback = query.feedback;
dispatch(updateQuery({ index, query: { feedback } }));
@ -111,32 +92,19 @@ export default function Conversation() {
);
};
const handleQuestionSubmission = () => {
if (inputRef.current?.textContent && status !== 'loading') {
if (lastQueryReturnedErr) {
// update last failed query with new prompt
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: inputRef.current.textContent,
},
}),
);
handleQuestion({
question: queries[queries.length - 1].prompt,
isRetry: true,
});
} else {
handleQuestion({ question: inputRef.current.textContent });
}
inputRef.current.textContent = '';
}
};
const prepResponseView = (query: Query, index: number) => {
let responseView;
if (query.response) {
if (query.error) {
responseView = (
<ConversationBubble
ref={endMessageRef}
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
key={`${index}ERROR`}
message={query.error}
type="ERROR"
></ConversationBubble>
);
} else if (query.response) {
responseView = (
<ConversationBubble
ref={endMessageRef}
@ -151,35 +119,6 @@ export default function Conversation() {
}
></ConversationBubble>
);
} else if (query.error) {
const retryBtn = (
<button
className="flex items-center justify-center gap-3 self-center rounded-full border border-silver py-3 px-5 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed dark:text-bright-gray"
disabled={status === 'loading'}
onClick={() => {
handleQuestion({
question: queries[queries.length - 1].prompt,
isRetry: true,
});
}}
>
<RetryIcon
fill={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
stroke={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
/>
Retry
</button>
);
responseView = (
<ConversationBubble
ref={endMessageRef}
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
key={`${index}ERROR`}
message={query.error}
type="ERROR"
retryBtn={retryBtn}
></ConversationBubble>
);
}
return responseView;
};
@ -191,84 +130,86 @@ export default function Conversation() {
};
return (
<div className="flex h-screen flex-col gap-1">
<div
onWheel={handleUserInterruption}
onTouchMove={handleUserInterruption}
className="flex h-[90%] w-full justify-center overflow-y-auto p-4 md:h-[83vh]"
>
{queries.length > 0 && !hasScrolledToLast && (
<button
onClick={scrollIntoView}
aria-label="scroll to bottom"
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-purple-taupe md:h-9 md:w-9 md:bg-opacity-100 "
>
<img
src={ArrowDown}
alt="arrow down"
className="h-4 w-4 opacity-50 md:h-5 md:w-5"
/>
</button>
)}
{queries.length > 0 && (
<div className="mt-16 w-full md:w-8/12">
{queries.map((query, index) => {
return (
<Fragment key={index}>
<ConversationBubble
className={'mb-1 last:mb-28 md:mb-7'}
key={`${index}QUESTION`}
message={query.prompt}
type="QUESTION"
sources={query.sources}
></ConversationBubble>
{prepResponseView(query, index)}
</Fragment>
);
})}
</div>
)}
{queries.length === 0 && <Hero handleQuestion={handleQuestion} />}
</div>
<div className="bottom-safe fixed flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-6/12">
<div className="flex h-full w-full items-center rounded-full border border-silver bg-white dark:bg-raisin-black">
<div
onWheel={handleUserInterruption}
onTouchMove={handleUserInterruption}
className="flex w-full flex-col justify-center p-4 md:flex-row"
>
{queries.length > 0 && !hasScrolledToLast && (
<button
onClick={scrollIntoView}
aria-label="scroll to bottom"
className="fixed bottom-32 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-purple-taupe md:h-9 md:w-9 md:bg-opacity-100 "
>
<img
src={ArrowDown}
alt="arrow down"
className="h4- w-4 opacity-50 md:h-5 md:w-5"
/>
</button>
)}
{queries.length > 0 && (
<div className="mt-20 mb-9 flex flex-col transition-all md:w-3/4">
{queries.map((query, index) => {
return (
<Fragment key={index}>
<ConversationBubble
className={'mb-7 last:mb-28'}
key={`${index}QUESTION`}
message={query.prompt}
type="QUESTION"
sources={query.sources}
></ConversationBubble>
{prepResponseView(query, index)}
</Fragment>
);
})}
</div>
)}
{queries.length === 0 && <Hero className="mt-24 md:mt-52"></Hero>}
<div className="absolute bottom-0 flex w-11/12 flex-col items-end self-center bg-white pt-4 dark:bg-raisin-black md:fixed md:w-[65%]">
<div className="flex h-full w-full">
<div
id="inputbox"
ref={inputRef}
tabIndex={1}
placeholder={t('inputPlaceholder')}
placeholder="Type your message here..."
contentEditable
onPaste={handlePaste}
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
className={`border-000000 max-h-24 min-h-[2.6rem] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-3xl border bg-white py-2 pl-4 pr-9 text-base leading-7 opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleQuestionSubmission();
if (inputRef.current?.textContent && status !== 'loading') {
handleQuestion(inputRef.current.textContent);
inputRef.current.textContent = '';
}
}
}}
></div>
{status === 'loading' ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
src={Spinner}
className="relative right-[38px] bottom-[7px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
></img>
) : (
<div className="mx-1 cursor-pointer rounded-full p-4 text-center hover:bg-gray-3000">
<div className="relative right-[43px] bottom-[7px] -mr-[35px] h-[35px] w-[35px] cursor-pointer self-end rounded-full hover:bg-gray-3000">
<img
className="w-6 text-white "
onClick={handleQuestionSubmission}
className="ml-[9px] mt-[9px] text-white"
onClick={() => {
if (inputRef.current?.textContent) {
handleQuestion(inputRef.current.textContent);
inputRef.current.textContent = '';
}
}}
src={isDarkTheme ? SendDark : Send}
></img>
</div>
)}
</div>
<p className="text-gray-595959 hidden w-[100vw] self-center bg-white bg-transparent py-2 text-center text-xs dark:bg-raisin-black dark:text-bright-gray md:inline md:w-full">
{t('tagline')}
<p className="text-gray-595959 w-[100vw] self-center bg-white bg-transparent p-5 text-center text-xs dark:bg-raisin-black dark:text-bright-gray md:w-full">
DocsGPT uses GenAI, please review critial information using sources.
</p>
</div>
</div>

@ -1,14 +1,15 @@
import { forwardRef, useState } from 'react';
import Avatar from '../components/Avatar';
import CoppyButton from '../components/CopyButton';
import remarkGfm from 'remark-gfm';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import classes from './ConversationBubble.module.css';
import Alert from './../assets/alert.svg';
import Like from './../assets/like.svg?react';
import Dislike from './../assets/dislike.svg?react';
import Copy from './../assets/copy.svg?react';
import CheckMark from './../assets/checkmark.svg?react';
import ReactMarkdown from 'react-markdown';
import copy from 'copy-to-clipboard';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
@ -22,15 +23,24 @@ const ConversationBubble = forwardRef<
className?: string;
feedback?: FEEDBACK;
handleFeedback?: (feedback: FEEDBACK) => void;
sources?: { title: string; text: string; source: string }[];
retryBtn?: React.ReactElement;
sources?: { title: string; text: string }[];
}
>(function ConversationBubble(
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
{ message, type, className, feedback, handleFeedback, sources },
ref,
) {
const [openSource, setOpenSource] = useState<number | null>(null);
const [copied, setCopied] = useState(false);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
// Reset copied to false after a few seconds
setTimeout(() => {
setCopied(false);
}, 3000);
};
const [isCopyHovered, setIsCopyHovered] = useState(false);
const [isLikeHovered, setIsLikeHovered] = useState(false);
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
const [isLikeClicked, setIsLikeClicked] = useState(false);
@ -42,8 +52,8 @@ const ConversationBubble = forwardRef<
bubble = (
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
<Avatar className="mt-2 text-2xl" avatar="🧑‍💻"></Avatar>
<div className="ml-10 mr-2 flex items-center rounded-3xl bg-purple-30 p-3.5 text-white">
<ReactMarkdown className="whitespace-pre-wrap break-normal leading-normal">
<div className="mr-2 ml-10 flex items-center rounded-3xl bg-purple-30 p-3.5 text-white">
<ReactMarkdown className="whitespace-pre-wrap break-all">
{message}
</ReactMarkdown>
</div>
@ -70,44 +80,29 @@ const ConversationBubble = forwardRef<
<div
className={`ml-2 mr-5 flex max-w-[90vw] rounded-3xl bg-gray-1000 p-3.5 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-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'
}`}
>
{type === 'ERROR' && (
<>
<img src={Alert} alt="alert" className="mr-2 inline" />
<div className="absolute -right-32 top-1/2 -translate-y-1/2">
{retryBtn}
</div>
</>
<img src={Alert} alt="alert" className="mr-2 inline" />
)}
<ReactMarkdown
className="whitespace-pre-wrap break-normal leading-normal"
className="whitespace-pre-wrap break-words"
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<div className="group relative">
<SyntaxHighlighter
PreTag="div"
language={match[1]}
{...props}
style={vscDarkPlus}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
<div
className={`absolute right-3 top-3 lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
>
<CoppyButton
text={String(children).replace(/\n$/, '')}
/>
</div>
</div>
<SyntaxHighlighter
PreTag="div"
language={match[1]}
{...props}
style={vscDarkPlus}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className ? className : ''} {...props}>
{children}
@ -177,19 +172,13 @@ const ConversationBubble = forwardRef<
{sources?.map((source, index) => (
<div
key={index}
className={`max-w-xs cursor-pointer rounded-[28px] px-4 py-1 sm:max-w-sm md:max-w-md ${
className={`max-w-fit cursor-pointer rounded-[28px] py-1 px-4 ${
openSource === index
? 'bg-[#007DFF]'
: 'bg-[#D7EBFD] hover:bg-[#BFE1FF]'
}`}
onClick={() =>
source.source !== 'local'
? window.open(
source.source,
'_blank',
'noopener, noreferrer',
)
: setOpenSource(openSource === index ? null : index)
setOpenSource(openSource === index ? null : index)
}
>
<p
@ -214,7 +203,31 @@ const ConversationBubble = forwardRef<
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
>
<div className="absolute left-2 top-4">
<CoppyButton text={message} />
<div
className={`flex items-center justify-center rounded-full p-2
${
isCopyHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
{copied ? (
<CheckMark
className="cursor-pointer stroke-green-2000"
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
/>
) : (
<Copy
className={`cursor-pointer fill-none`}
onClick={() => {
handleCopyClick(message);
}}
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
></Copy>
)}
</div>
</div>
</div>
<div

@ -32,6 +32,15 @@ export default function ConversationTile({
const [isDarkTheme] = useDarkTheme();
const [isEdit, setIsEdit] = useState(false);
const [conversationName, setConversationsName] = useState('');
// useOutsideAlerter(
// tileRef,
// () =>
// handleSaveConversation({
// id: conversationId || conversation.id,
// name: conversationName,
// }),
// [conversationName],
// );
useEffect(() => {
setConversationsName(conversation.name);
@ -60,9 +69,9 @@ export default function ConversationTile({
onClick={() => {
selectConversation(conversation.id);
}}
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
conversationId === conversation.id
? 'bg-gray-100 dark:bg-[#28292E]'
? 'bg-gray-100 dark:bg-purple-taupe'
: ''
}`}
>

@ -38,7 +38,6 @@ export function fetchAnswerApi(
conversationId: string | null,
promptId: string | null,
chunks: string,
token_limit: number,
): Promise<
| {
result: any;
@ -74,7 +73,6 @@ export function fetchAnswerApi(
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
}),
signal,
})
@ -105,7 +103,6 @@ export function fetchAnswerSteaming(
conversationId: string | null,
promptId: string | null,
chunks: string,
token_limit: number,
onEvent: (event: MessageEvent) => void,
): Promise<Answer> {
const docPath = getDocPath(selectedDocs);
@ -122,7 +119,6 @@ export function fetchAnswerSteaming(
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
};
fetch(apiHost + '/stream', {
method: 'POST',
@ -185,7 +181,6 @@ export function searchEndpoint(
conversation_id: string | null,
history: Array<any> = [],
chunks: string,
token_limit: number,
) {
const docPath = getDocPath(selectedDocs);
@ -195,7 +190,6 @@ export function searchEndpoint(
conversation_id,
history,
chunks: chunks,
token_limit: token_limit,
};
return fetch(`${apiHost}/api/search`, {
method: 'POST',

@ -17,7 +17,7 @@ export interface Answer {
answer: string;
query: string;
result: string;
sources: { title: string; text: string; source: string }[];
sources: { title: string; text: string }[];
conversationId: string | null;
title: string | null;
}
@ -27,7 +27,7 @@ export interface Query {
response?: string;
feedback?: FEEDBACK;
error?: string;
sources?: { title: string; text: string; source: string }[];
sources?: { title: string; text: string }[];
conversationId?: string | null;
title?: string | null;
}

@ -28,7 +28,6 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
(event) => {
const data = JSON.parse(event.data);
@ -52,7 +51,6 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
state.conversation.conversationId,
state.conversation.queries,
state.preference.chunks,
state.preference.token_limit,
).then((sources) => {
//dispatch streaming sources
dispatch(
@ -68,15 +66,6 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
query: { conversationId: data.id },
}),
);
} else if (data.type === 'error') {
// set status to 'failed'
dispatch(conversationSlice.actions.setStatus('failed'));
dispatch(
conversationSlice.actions.raiseError({
index: state.conversation.queries.length - 1,
message: data.error,
}),
);
} else {
const result = data.answer;
dispatch(
@ -97,7 +86,6 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
);
if (answer) {
let sourcesPrepped = [];
@ -160,7 +148,7 @@ export const conversationSlice = createSlice({
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { index, query } = action.payload;
if (query.response != undefined) {
if (query.response) {
state.queries[index].response =
(state.queries[index].response || '') + query.response;
} else {
@ -200,13 +188,6 @@ export const conversationSlice = createSlice({
setStatus(state, action: PayloadAction<Status>) {
state.status = action.payload;
},
raiseError(
state,
action: PayloadAction<{ index: number; message: string }>,
) {
const { index, message } = action.payload;
state.queries[index].error = message;
},
},
extraReducers(builder) {
builder
@ -220,7 +201,7 @@ export const conversationSlice = createSlice({
}
state.status = 'failed';
state.queries[state.queries.length - 1].error =
'Something went wrong. Please check your internet connection.';
'Something went wrong. Please try again later.';
});
},
});

@ -398,22 +398,3 @@ template {
padding: 16px;
}
}
@font-face {
font-family: 'Inter';
font-weight: 100 200 300 400 500 600 700 800 900;
src: url('/fonts/Inter-Variable.ttf');
}
::-webkit-scrollbar {
width: 0;
}
.bottom-safe {
bottom: env(safe-area-inset-bottom, 0);
}
.inputbox-style[contenteditable] {
padding-left: 36px;
padding-right: 36px;
}

@ -1,108 +0,0 @@
{
"language": "English",
"chat": "Chat",
"chats": "Chats",
"newChat": "New Chat",
"myPlan": "My Plan",
"about": "About",
"inputPlaceholder": "Type your message here...",
"tagline": "DocsGPT uses GenAI, please review critical information using sources.",
"sourceDocs": "Source Docs",
"none": "None",
"cancel": "Cancel",
"demo": [
{
"header": "Learn about DocsGPT",
"query": "What is DocsGPT?"
},
{
"header": "Summarize documentation",
"query": "Summarize current context"
},
{
"header": "Write Code",
"query": "Write code for api request to /api/answer"
},
{
"header": "Learning Assistance",
"query": "Write potential questions for context"
}
],
"settings": {
"label": "Settings",
"general": {
"label": "General",
"selectTheme": "Select Theme",
"light": "Light",
"dark": "Dark",
"selectLanguage": "Select Language",
"chunks": "Chunks processed per query",
"prompt": "Active Prompt",
"deleteAllLabel": "Delete all Conversation",
"deleteAllBtn": "Delete all",
"addNew": "Add New",
"convHistory": "Conversational history",
"none": "None",
"low": "Low",
"medium": "Medium",
"high": "High",
"unlimited": "Unlimited",
"default": "default"
},
"documents": {
"label": "Documents",
"name": "Document Name",
"date": "Vector Date",
"type": "Type",
"tokenUsage": "Token Usage"
},
"apiKeys": {
"label": "API Keys",
"name": "Name",
"key": "API Key",
"sourceDoc": "Source Document",
"createNew": "Create New"
}
},
"modals": {
"uploadDoc": {
"label": "Upload New Documentation",
"file": "From File",
"remote": "Remote",
"name": "Name",
"choose": "Choose Files",
"info": "Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb",
"uploadedFiles": "Uploaded Files",
"cancel": "Cancel",
"train": "Train",
"link": "Link",
"urlLink": "URL Link",
"reddit": {
"id": "Client ID",
"secret": "Client Secret",
"agent": "User agent",
"searchQueries": "Search queries",
"numberOfPosts": "Number of posts"
}
},
"createAPIKey": {
"label": "Create New API Key",
"apiKeyName": "API Key Name",
"chunks": "Chunks processed per query",
"prompt": "Select active prompt",
"sourceDoc": "Source document",
"create": "Create"
},
"saveKey": {
"note": "Please save your Key",
"disclaimer": "This is the only time your key will be shown.",
"copy": "Copy",
"copied": "Copied",
"confirm": "I saved the Key"
},
"deleteConv": {
"confirm": "Are you sure you want to delete all the conversations?",
"delete": "Delete"
}
}
}

@ -1,108 +0,0 @@
{
"language": "Spanish",
"chat": "Chat",
"chats": "Chats",
"newChat": "Nuevo Chat",
"myPlan": "Mi Plan",
"about": "Acerca de",
"inputPlaceholder": "Escribe tu mensaje aquí...",
"tagline": "DocsGPT utiliza GenAI, por favor revisa información crítica utilizando fuentes.",
"sourceDocs": "Documentos Fuente",
"none": "Nada",
"cancel": "Cancelar",
"demo": [
{
"header": "Aprende sobre DocsGPT",
"query": "¿Qué es DocsGPT?"
},
{
"header": "Resumir documentación",
"query": "Resumir contexto actual"
},
{
"header": "Escribir Código",
"query": "Escribir código para solicitud de API a /api/answer"
},
{
"header": "Asistencia de Aprendizaje",
"query": "Escribe posibles preguntas para el contexto"
}
],
"settings": {
"label": "Configuración",
"general": {
"label": "General",
"selectTheme": "Seleccionar Tema",
"light": "de luz",
"dark": "oscura",
"selectLanguage": "Seleccionar Idioma",
"chunks": "Trozos procesados por consulta",
"prompt": "Prompt Activo",
"deleteAllLabel": "Eliminar toda la Conversación",
"deleteAllBtn": "Eliminar todo",
"addNew": "Agregar Nuevo",
"convHistory": "Historia conversacional",
"none": "ninguno",
"low": "Bajo",
"medium": "Medio",
"high": "Alto",
"unlimited": "Ilimitado",
"default": "predeterminada"
},
"documents": {
"label": "Documentos",
"name": "Nombre del Documento",
"date": "Fecha Vector",
"type": "Tipo",
"tokenUsage": "Uso de Tokens"
},
"apiKeys": {
"label": "Claves API",
"name": "Nombre",
"key": "Clave de API",
"sourceDoc": "Documento Fuente",
"createNew": "Crear Nuevo"
}
},
"modals": {
"uploadDoc": {
"label": "Subir Nueva Documentación",
"file": "Desde Archivo",
"remote": "Remota",
"name": "Nombre",
"choose": "Seleccionar Archivos",
"info": "Por favor, suba archivos .pdf, .txt, .rst, .docx, .md, .zip limitados a 25 MB",
"uploadedFiles": "Archivos Subidos",
"cancel": "Cancelar",
"train": "Entrenar",
"link": "Enlace",
"urlLink": "Enlace URL",
"reddit": {
"id": "ID de Cliente",
"secret": "Secreto de Cliente",
"agent": "Agente de Usuario",
"searchQueries": "Consultas de Búsqueda",
"numberOfPosts": "Número de publicaciones"
}
},
"createAPIKey": {
"label": "Crear Nueva Clave de API",
"apiKeyName": "Nombre de la Clave de API",
"chunks": "Fragmentos procesados por consulta",
"prompt": "Seleccione el prompt activo",
"sourceDoc": "Documento Fuente",
"create": "Crear"
},
"saveKey": {
"note": "Por favor, guarde su Clave",
"disclaimer": "Esta es la única vez que se mostrará su clave.",
"copy": "Copiar",
"copied": "Copiado",
"confirm": "He guardado la Clave"
},
"deleteConv": {
"confirm": "¿Está seguro de que desea eliminar todas las conversaciones?",
"delete": "Eliminar"
}
}
}

@ -1,29 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './en.json'; //English
import es from './es.json'; //Spanish
import jp from './jp.json'; //Japanese
import zh from './zh.json'; //Mandarin
i18n.use(initReactI18next).init({
resources: {
en: {
translation: en,
},
es: {
translation: es,
},
jp: {
translation: jp,
},
zh: {
translation: zh,
},
},
});
const locale = localStorage.getItem('docsgpt-locale') ?? 'en';
i18n.changeLanguage(locale);
export default i18n;

@ -1,108 +0,0 @@
{
"language": "日本語",
"chat": "チャット",
"chats": "チャット",
"newChat": "新しいチャット",
"myPlan": "私のプラン",
"about": "について",
"inputPlaceholder": "ここにメッセージを入力してください...",
"tagline": "DocsGPTはGenAIを使用しています。重要な情報はソースで確認してください。",
"sourceDocs": "ソースドキュメント",
"none": "なし",
"cancel": "キャンセル",
"demo": [
{
"header": "DocsGPTについて学ぶ",
"query": "DocsGPTとは何ですか?"
},
{
"header": "ドキュメントを要約する",
"query": "現在のコンテキストを要約してください"
},
{
"header": "コードを書く",
"query": "APIリクエストのコードを/api/answerに書いてください。"
},
{
"header": "学習支援",
"query": "コンテキストに対する潜在的な質問を書いてください"
}
],
"settings": {
"label": "設定",
"general": {
"label": "一般",
"selectTheme": "テーマを選択",
"light": "ライト",
"dark": "ダーク",
"selectLanguage": "言語を選択",
"chunks": "クエリごとに処理されるチャンク",
"prompt": "アクティブプロンプト",
"deleteAllLabel": "すべての会話を削除",
"deleteAllBtn": "すべて削除",
"addNew": "新規追加",
"convHistory": "会話履歴",
"none": "なし",
"low": "低",
"medium": "中",
"high": "高",
"unlimited": "無制限",
"default": "デフォルト"
},
"documents": {
"label": "ドキュメント",
"name": "ドキュメント名",
"date": "ベクトル日付",
"type": "タイプ",
"tokenUsage": "トークン使用量"
},
"apiKeys": {
"label": "APIキー",
"name": "名前",
"key": "APIキー",
"sourceDoc": "ソースドキュメント",
"createNew": "新規作成"
}
},
"modals": {
"uploadDoc": {
"label": "新規書類のアップロード",
"file": "ファイルから",
"remote": "リモート",
"name": "名前",
"choose": "ファイルを選択",
"info": ".pdf, .txt, .rst, .docx, .md, .zipファイルを25MBまでアップロードしてください",
"uploadedFiles": "アップロードされたファイル",
"cancel": "キャンセル",
"train": "トレーニング",
"link": "リンク",
"urlLink": "URLリンク",
"reddit": {
"id": "クライアントID",
"secret": "クライアントシークレット",
"agent": "ユーザーエージェント",
"searchQueries": "検索クエリ",
"numberOfPosts": "投稿数"
}
},
"createAPIKey": {
"label": "新しいAPIキーを作成",
"apiKeyName": "APIキー名",
"chunks": "クエリごとに処理されるチャンク",
"prompt": "アクティブプロンプトを選択",
"sourceDoc": "ソースドキュメント",
"create": "作成"
},
"saveKey": {
"note": "キーを保存してください",
"disclaimer": "キーが表示されるのはこのときだけです。",
"copy": "コピー",
"copied": "コピーしました",
"confirm": "キーを保存しました"
},
"deleteConv": {
"confirm": "すべての会話を削除してもよろしいですか?",
"delete": "削除"
}
}
}

@ -1,108 +0,0 @@
{
"language": "普通话",
"chat": "聊天",
"chats": "聊天",
"newChat": "新聊天",
"myPlan": "我的计划",
"about": "关于",
"inputPlaceholder": "在这里输入您的消息...",
"tagline": "DocsGPT 使用 GenAI, 请使用来源审核关键信息.",
"sourceDocs": "来源文档",
"none": "无",
"cancel": "取消",
"demo": [
{
"header": "了解 DocsGPT",
"query": "DocsGPT 是什么"
},
{
"header": "总结文档",
"query": "总结当前情况"
},
{
"header": "编写代码",
"query": "为 /api/answer API 请求编写代码"
},
{
"header": "学习帮助",
"query": "为背景写出潜在问题"
}
],
"settings": {
"label": "设置",
"general": {
"label": "般",
"selectTheme": "选择主题",
"light": "浅色",
"dark": "暗色",
"selectLanguage": "选择语言",
"chunks": "每个查询处理的块",
"prompt": "提示",
"deleteAllLabel": "删除所有对话",
"deleteAllBtn": "删除所有",
"addNew": "添加新的",
"convHistory": "对话历史",
"none": "无",
"low": "低",
"medium": "中",
"high": "高",
"unlimited": "无限",
"default": "默认"
},
"documents": {
"label": "文件",
"name": "文件名称",
"date": "向量日期",
"type": "类型",
"tokenUsage": "令牌使用"
},
"apiKeys": {
"label": "API 密钥",
"name": "名称",
"key": "API 密钥",
"sourceDoc": "源文档",
"createNew": "创建新的"
}
},
"modals": {
"uploadDoc": {
"label": "上传新文档资料",
"file": "从文件",
"remote": "远程",
"name": "名称",
"choose": "选择文件",
"info": "请上传 .pdf, .txt, .rst, .docx, .md, .zip 文件,限 25MB",
"uploadedFiles": "已上传文件",
"cancel": "取消",
"train": "训练",
"link": "链接",
"urlLink": "URL 链接",
"reddit": {
"id": "客户端 ID",
"secret": "客户端密钥",
"agent": "用户代理",
"searchQueries": "搜索查询",
"numberOfPosts": "帖子数量"
}
},
"createAPIKey": {
"label": "创建新的 API 密钥",
"apiKeyName": "API 密钥名称",
"chunks": "每个查询处理的块",
"prompt": "选择活动提示",
"sourceDoc": "源文档",
"create": "创建"
},
"saveKey": {
"note": "请保存您的密钥",
"disclaimer": "这是您的密钥唯一一次展示机会。",
"copy": "复制",
"copied": "已复制",
"confirm": "我已保存密钥"
},
"deleteConv": {
"confirm": "您确定要删除所有对话吗?",
"delete": "删除"
}
}
}

@ -1,69 +0,0 @@
import Exit from '../assets/exit.svg';
import { ActiveState } from '../models/misc';
import { useTranslation } from 'react-i18next';
function ConfirmationModal({
message,
modalState,
setModalState,
submitLabel,
handleSubmit,
cancelLabel,
handleCancel,
}: {
message: string;
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
submitLabel: string;
handleSubmit: () => void;
cancelLabel?: string;
handleCancel?: () => void;
}) {
const { t } = useTranslation();
return (
<article
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha`}
>
<article className="mx-auto mt-[35vh] flex w-[90vw] max-w-lg flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-outer-space">
<div className="relative">
<button
className="absolute top-3 right-4 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
handleCancel && handleCancel();
}}
>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="p-8">
<p className="font-base mb-1 w-[90%] text-lg text-jet dark:text-bright-gray">
{message}
</p>
<div>
<div className="mt-6 flex flex-row-reverse gap-1">
<button
onClick={handleSubmit}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]"
>
{submitLabel}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
handleCancel && handleCancel();
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
{cancelLabel ? cancelLabel : t('cancel')}
</button>
</div>
</div>
</div>
</div>
</article>
</article>
);
}
export default ConfirmationModal;

@ -1,51 +0,0 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { ActiveState } from '../models/misc';
import { useMediaQuery, useOutsideAlerter } from '../hooks';
import ConfirmationModal from './ConfirmationModal';
import { useTranslation } from 'react-i18next';
import { Action } from '@reduxjs/toolkit';
export default function DeleteConvModal({
modalState,
setModalState,
handleDeleteAllConv,
}: {
modalState: ActiveState;
setModalState: (val: ActiveState) => Action;
handleDeleteAllConv: () => void;
}) {
const modalRef = React.useRef(null);
const dispatch = useDispatch();
const { isMobile } = useMediaQuery();
const { t } = useTranslation();
useOutsideAlerter(
modalRef,
() => {
if (isMobile && modalState === 'ACTIVE') {
dispatch(setModalState('INACTIVE'));
}
},
[modalState],
);
function handleSubmit() {
handleDeleteAllConv();
dispatch(setModalState('INACTIVE'));
}
function handleCancel() {
dispatch(setModalState('INACTIVE'));
}
return (
<ConfirmationModal
message={t('modals.deleteConv.confirm')}
modalState={modalState}
setModalState={setModalState}
submitLabel={t('modals.deleteConv.delete')}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
/>
);
}

@ -13,7 +13,6 @@ export type Doc = {
date: string;
docLink: string;
model: string;
tokens?: string;
};
export type PromptProps = {

@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { ActiveState } from '../models/misc';
import { selectApiKey, setApiKey } from './preferenceSlice';
import { useMediaQuery, useOutsideAlerter } from './../hooks';
import Modal from '../modals';
import Modal from '../Modal';
export default function APIKeyModal({
modalState,

@ -37,7 +37,7 @@ function AddPrompt({
<input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={newPromptName}
onChange={(e) => setNewPromptName(e.target.value)}
></input>
@ -52,7 +52,7 @@ function AddPrompt({
</span>
</div>
<textarea
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:bg-transparent dark:text-silver"
value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)}
></textarea>
@ -108,7 +108,7 @@ function EditPrompt({
<input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={editPromptName}
onChange={(e) => setEditPromptName(e.target.value)}
></input>
@ -123,7 +123,7 @@ function EditPrompt({
</span>
</div>
<textarea
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:bg-transparent dark:text-silver"
value={editPromptContent}
onChange={(e) => setEditPromptContent(e.target.value)}
></textarea>

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ActiveState } from '../models/misc';
import Modal from '../modals';
import Modal from '../Modal';
import {
setSelectedDocs,
setSourceDocs,

@ -1,29 +1,24 @@
import {
PayloadAction,
createListenerMiddleware,
createSlice,
isAnyOf,
} from '@reduxjs/toolkit';
import { Doc, setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
import { RootState } from '../store';
import { ActiveState } from '../models/misc';
interface Preference {
apiKey: string;
prompt: { name: string; id: string; type: string };
chunks: string;
token_limit: number;
selectedDocs: Doc | null;
chunks: string;
sourceDocs: Doc[] | null;
conversations: { name: string; id: string }[] | null;
modalState: ActiveState;
}
const initialState: Preference = {
apiKey: 'xxx',
prompt: { name: 'default', id: 'default', type: 'public' },
chunks: '2',
token_limit: 2000,
selectedDocs: {
name: 'default',
language: 'default',
@ -37,7 +32,6 @@ const initialState: Preference = {
} as Doc,
sourceDocs: null,
conversations: null,
modalState: 'INACTIVE',
};
export const prefSlice = createSlice({
@ -62,12 +56,6 @@ export const prefSlice = createSlice({
setChunks: (state, action) => {
state.chunks = action.payload;
},
setTokenLimit: (state, action) => {
state.token_limit = action.payload;
},
setModalStateDeleteConv: (state, action: PayloadAction<ActiveState>) => {
state.modalState = action.payload;
},
},
});
@ -78,8 +66,6 @@ export const {
setConversations,
setPrompt,
setChunks,
setTokenLimit,
setModalStateDeleteConv,
} = prefSlice.actions;
export default prefSlice.reducer;
@ -121,18 +107,6 @@ prefListenerMiddleware.startListening({
},
});
prefListenerMiddleware.startListening({
matcher: isAnyOf(setTokenLimit),
effect: (action, listenerApi) => {
localStorage.setItem(
'DocsGPTTokenLimit',
JSON.stringify(
(listenerApi.getState() as RootState).preference.token_limit,
),
);
},
});
export const selectApiKey = (state: RootState) => state.preference.apiKey;
export const selectApiKeyStatus = (state: RootState) =>
!!state.preference.apiKey;
@ -140,8 +114,6 @@ export const selectSelectedDocsStatus = (state: RootState) =>
!!state.preference.selectedDocs;
export const selectSourceDocs = (state: RootState) =>
state.preference.sourceDocs;
export const selectModalStateDeleteConv = (state: RootState) =>
state.preference.modalState;
export const selectSelectedDocs = (state: RootState) =>
state.preference.selectedDocs;
export const selectConversations = (state: RootState) =>
@ -150,5 +122,3 @@ export const selectConversationId = (state: RootState) =>
state.conversation.conversationId;
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;

@ -9,14 +9,13 @@ import {
import { selectSourceDocs } from '../preferences/preferenceSlice';
import Exit from '../assets/exit.svg';
import Trash from '../assets/trash.svg';
import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const APIKeys: React.FC = () => {
const { t } = useTranslation();
const [isCreateModalOpen, setCreateModal] = React.useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
const [newKey, setNewKey] = React.useState('');
@ -96,9 +95,9 @@ const APIKeys: React.FC = () => {
<div className="flex justify-end">
<button
onClick={() => setCreateModal(true)}
className="rounded-full bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
className="rounded-full bg-purple-30 px-4 py-3 text-sm text-white hover:opacity-90"
>
{t('settings.apiKeys.createNew')}
Create new
</button>
</div>
{isCreateModalOpen && (
@ -118,15 +117,11 @@ const APIKeys: React.FC = () => {
<table className="block w-max table-auto content-center justify-center rounded-xl border text-center dark:border-chinese-silver dark:text-bright-gray">
<thead>
<tr>
<th className="border-r p-4 md:w-[244px]">
{t('settings.apiKeys.name')}
</th>
<th className="border-r p-4 md:w-[244px]">Name</th>
<th className="w-[244px] border-r px-4 py-2">
{t('settings.apiKeys.sourceDoc')}
</th>
<th className="w-[244px] border-r px-4 py-2">
{t('settings.apiKeys.key')}
Source document
</th>
<th className="w-[244px] border-r px-4 py-2">API Key</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
@ -221,7 +216,7 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
};
})
: [];
const { t } = useTranslation();
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
@ -230,23 +225,23 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
</button>
<div className="mb-6">
<span className="text-xl text-jet dark:text-bright-gray">
{t('modals.createAPIKey.label')}
Create New API Key
</span>
</div>
<div className="relative mt-5 mb-4">
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.createAPIKey.apiKeyName')}
API Key Name
</span>
<input
type="text"
className="h-[42px] w-full rounded-md border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-md border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={APIKeyName}
onChange={(e) => setAPIKeyName(e.target.value)}
/>
</div>
<div className="my-4">
<Dropdown
placeholder={t('modals.createAPIKey.sourceDoc')}
placeholder="Source document"
selectedValue={sourcePath}
onSelect={(selection: { label: string; value: string }) =>
setSourcePath(selection)
@ -260,7 +255,7 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
<Dropdown
options={activePrompts}
selectedValue={prompt ? prompt.name : null}
placeholder={t('modals.createAPIKey.prompt')}
placeholder="Select active prompt"
onSelect={(value: { name: string; id: string; type: string }) =>
setPrompt(value)
}
@ -269,7 +264,7 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
</div>
<div className="my-4">
<p className="mb-2 ml-2 font-bold text-jet dark:text-bright-gray">
{t('modals.createAPIKey.chunks')}
Chunks processed per query
</p>
<Dropdown
options={chunkOptions}
@ -290,9 +285,9 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
chunks: chunk,
})
}
className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
className="float-right mt-4 rounded-full bg-purple-30 px-4 py-3 text-white disabled:opacity-50"
>
{t('modals.createAPIKey.create')}
Create
</button>
</div>
</div>
@ -301,7 +296,6 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
const SaveAPIKeyModal: React.FC<SaveAPIKeyModalProps> = ({ apiKey, close }) => {
const [isCopied, setIsCopied] = React.useState(false);
const { t } = useTranslation();
const handleCopyKey = () => {
navigator.clipboard.writeText(apiKey);
setIsCopied(true);
@ -312,12 +306,9 @@ const SaveAPIKeyModal: React.FC<SaveAPIKeyModalProps> = ({ apiKey, close }) => {
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<h1 className="my-0 text-xl font-medium">
{' '}
{t('modals.saveKey.note')}
</h1>
<h1 className="my-0 text-xl font-medium">Please save your Key</h1>
<h3 className="text-sm font-normal text-outer-space">
{t('modals.saveKey.disclaimer')}
This is the only time your key will be shown.
</h3>
<div className="flex justify-between py-2">
<div>
@ -325,17 +316,17 @@ const SaveAPIKeyModal: React.FC<SaveAPIKeyModalProps> = ({ apiKey, close }) => {
<span className="text-sm font-normal leading-7 ">{apiKey}</span>
</div>
<button
className="my-1 h-10 w-20 rounded-full border border-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
className="my-1 h-10 w-20 rounded-full border border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white dark:border-purple-500 dark:text-purple-500"
onClick={handleCopyKey}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
{isCopied ? 'Copied' : 'Copy'}
</button>
</div>
<button
onClick={close}
className="rounded-full bg-philippine-yellow px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
>
{t('modals.saveKey.confirm')}
I saved the Key
</button>
</div>
</div>

@ -1,49 +1,20 @@
import { DocumentsProps } from '../models/misc';
import Trash from '../assets/trash.svg';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
// Utility function to format numbers
const formatTokens = (tokens: number): string => {
const roundToTwoDecimals = (num: number): string => {
return (Math.round((num + Number.EPSILON) * 100) / 100).toString();
};
if (tokens >= 1_000_000_000) {
return roundToTwoDecimals(tokens / 1_000_000_000) + 'b';
} else if (tokens >= 1_000_000) {
return roundToTwoDecimals(tokens / 1_000_000) + 'm';
} else if (tokens >= 1_000) {
return roundToTwoDecimals(tokens / 1_000) + 'k';
} else {
return tokens.toString();
}
};
const Documents: React.FC<DocumentsProps> = ({
documents,
handleDeleteDocument,
}) => {
const { t } = useTranslation();
return (
<div className="mt-8">
<div className="flex flex-col">
<div className="w-full overflow-x-auto">
<table className="block w-max table-auto content-center justify-center rounded-xl border text-center dark:border-chinese-silver dark:text-bright-gray">
<div className="mt-[27px] w-max overflow-x-auto rounded-xl border dark:border-chinese-silver">
<table className="block w-full table-auto content-center justify-center text-center dark:text-bright-gray">
<thead>
<tr>
<th className="border-r p-4 md:w-[244px]">
{t('settings.documents.name')}
</th>
<th className="w-[244px] border-r px-4 py-2">
{t('settings.documents.date')}
</th>
<th className="w-[244px] border-r px-4 py-2">
{t('settings.documents.tokenUsage')}
</th>
<th className="w-[244px] border-r px-4 py-2">
{t('settings.documents.type')}
</th>
<th className="border-r p-4 md:w-[244px]">Document Name</th>
<th className="w-[244px] border-r px-4 py-2">Vector Date</th>
<th className="w-[244px] border-r px-4 py-2">Type</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
@ -57,9 +28,6 @@ const Documents: React.FC<DocumentsProps> = ({
<td className="border-r border-t px-4 py-2">
{document.date}
</td>
<td className="border-r border-t px-4 py-2">
{document.tokens ? formatTokens(+document.tokens) : ''}
</td>
<td className="border-r border-t px-4 py-2">
{document.location === 'remote'
? 'Pre-loaded'
@ -88,8 +56,5 @@ const Documents: React.FC<DocumentsProps> = ({
</div>
);
};
Documents.propTypes = {
documents: PropTypes.array.isRequired,
handleDeleteDocument: PropTypes.func.isRequired,
};
export default Documents;

@ -2,70 +2,30 @@ import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Prompts from './Prompts';
import { useDarkTheme } from '../hooks';
import { useTranslation } from 'react-i18next';
import Dropdown from '../components/Dropdown';
import {
selectPrompt,
setPrompt,
setChunks,
selectChunks,
setTokenLimit,
selectTokenLimit,
setModalStateDeleteConv,
} from '../preferences/preferenceSlice';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const General: React.FC = () => {
const {
t,
i18n: { changeLanguage, language },
} = useTranslation();
const themes = ['Light', 'Dark'];
const languageOptions = [
{
label: 'English',
value: 'en',
},
{
label: 'Spanish',
value: 'es',
},
{
label: 'Japanese',
value: 'jp',
},
{
label: 'Mandarin',
value: 'zh',
},
];
const languages = ['English'];
const chunks = ['0', '2', '4', '6', '8', '10'];
const token_limits = new Map([
[0, t('settings.general.none')],
[100, t('settings.general.low')],
[1000, t('settings.general.medium')],
[2000, t('settings.general.default')],
[4000, t('settings.general.high')],
[1e9, t('settings.general.unlimited')],
]);
const [prompts, setPrompts] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const selectedChunks = useSelector(selectChunks);
const selectedTokenLimit = useSelector(selectTokenLimit);
const [isDarkTheme, toggleTheme] = useDarkTheme();
const [selectedTheme, setSelectedTheme] = React.useState(
isDarkTheme ? 'Dark' : 'Light',
);
const dispatch = useDispatch();
const locale = localStorage.getItem('docsgpt-locale');
const [selectedLanguage, setSelectedLanguage] = React.useState(
locale
? languageOptions.find((option) => option.value === locale)
: languageOptions[0],
);
const [selectedLanguage, setSelectedLanguage] = React.useState(languages[0]);
const selectedPrompt = useSelector(selectPrompt);
React.useEffect(() => {
@ -83,18 +43,10 @@ const General: React.FC = () => {
};
fetchPrompts();
}, []);
React.useEffect(() => {
localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string);
changeLanguage(selectedLanguage?.value);
}, [selectedLanguage, changeLanguage]);
return (
<div className="mt-[59px]">
<div className="mb-5">
<p className="font-bold text-jet dark:text-bright-gray">
{t('settings.general.selectTheme')}
</p>
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">Select Theme</p>
<Dropdown
options={themes}
selectedValue={selectedTheme}
@ -104,30 +56,23 @@ const General: React.FC = () => {
}}
size="w-56"
rounded="3xl"
border="border"
/>
</div>
<div className="mb-5">
<p className="mb-2 font-bold text-jet dark:text-bright-gray">
{t('settings.general.selectLanguage')}
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">
Select Language
</p>
<Dropdown
options={languageOptions.filter(
(languageOption) =>
languageOption.value !== selectedLanguage?.value,
)}
selectedValue={selectedLanguage ?? languageOptions[0]}
onSelect={(selectedOption: { label: string; value: string }) => {
setSelectedLanguage(selectedOption);
}}
options={languages}
selectedValue={selectedLanguage}
onSelect={setSelectedLanguage}
size="w-56"
rounded="3xl"
border="border"
/>
</div>
<div className="mb-5">
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">
{t('settings.general.chunks')}
Chunks processed per query
</p>
<Dropdown
options={chunks}
@ -135,35 +80,9 @@ const General: React.FC = () => {
onSelect={(value: string) => dispatch(setChunks(value))}
size="w-56"
rounded="3xl"
border="border"
/>
</div>
<div className="mb-5">
<p className="mb-2 font-bold text-jet dark:text-bright-gray">
{t('settings.general.convHistory')}
</p>
<Dropdown
options={Array.from(token_limits, ([value, desc]) => ({
value: value,
description: desc,
}))}
selectedValue={{
value: selectedTokenLimit,
description: token_limits.get(selectedTokenLimit) as string,
}}
onSelect={({
value,
description,
}: {
value: number;
description: string;
}) => dispatch(setTokenLimit(value))}
size="w-56"
rounded="3xl"
border="border"
/>
</div>
<div className="mb-5">
<div>
<Prompts
prompts={prompts}
selectedPrompt={selectedPrompt}
@ -174,19 +93,6 @@ const General: React.FC = () => {
apiHost={apiHost}
/>
</div>
<div className="w-56">
<p className="font-bold text-jet dark:text-bright-gray">
{t('settings.general.deleteAllLabel')}
</p>
<button
className="mt-2 flex w-full cursor-pointer items-center justify-between rounded-3xl border border-solid border-red-500 px-5 py-3 text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))}
>
<span className="overflow-hidden text-ellipsis ">
{t('settings.general.deleteAllBtn')}
</span>
</button>
</div>
</div>
);
};

@ -2,8 +2,9 @@ import React from 'react';
import { PromptProps, ActiveState } from '../models/misc';
import Dropdown from '../components/Dropdown';
import PromptsModal from '../preferences/PromptsModal';
import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const Prompts: React.FC<PromptProps> = ({
prompts,
selectedPrompt,
@ -33,10 +34,7 @@ const Prompts: React.FC<PromptProps> = ({
});
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
const {
t,
i18n: { changeLanguage, language },
} = useTranslation();
const handleAddPrompt = async () => {
try {
const response = await fetch(`${apiHost}/api/create_prompt`, {
@ -158,18 +156,15 @@ const Prompts: React.FC<PromptProps> = ({
return (
<>
<div>
<div className="flex flex-row items-center gap-8">
<div className="mb-4 flex flex-row items-center gap-8">
<div>
<p className="font-semibold dark:text-bright-gray">
{t('settings.general.prompt')}
</p>
<p className="font-semibold dark:text-bright-gray">Active Prompt</p>
<Dropdown
options={prompts}
selectedValue={selectedPrompt.name}
onSelect={handleSelectPrompt}
size="w-56"
rounded="3xl"
border="border"
showEdit
showDelete
onEdit={({
@ -191,13 +186,13 @@ const Prompts: React.FC<PromptProps> = ({
/>
</div>
<button
className="mt-[24px] rounded-3xl border border-solid border-purple-30 px-5 py-3 text-purple-30 hover:bg-purple-30 hover:text-white"
className="mt-[24px] rounded-3xl border-2 border-solid border-purple-30 px-5 py-3 text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => {
setModalType('ADD');
setModalState('ACTIVE');
}}
>
{t('settings.general.addNew')}
Add new
</button>
</div>
</div>

@ -11,20 +11,13 @@ import {
import { Doc } from '../preferences/preferenceApi';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
import { useTranslation } from 'react-i18next';
import i18n from '../locale/i18n';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const Settings: React.FC = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const tabs = [
t('settings.general.label'),
t('settings.documents.label'),
t('settings.apiKeys.label'),
];
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
const tabs = ['General', 'Documents', 'API Keys'];
const [activeTab, setActiveTab] = React.useState('General');
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
null,
);
@ -49,15 +42,10 @@ const Settings: React.FC = () => {
})
.catch((error) => console.error(error));
};
// persist active tab as the translated version of 'general' per language change
React.useEffect(() => {
setActiveTab(t('settings.general.label'));
}, [i18n.language]);
return (
<div className="wa p-4 pt-20 md:p-12">
<p className="text-2xl font-bold text-eerie-black dark:text-bright-gray">
{t('settings.label')}
Settings
</p>
<div className="mt-6 flex flex-row items-center space-x-4 overflow-x-auto md:space-x-8 ">
<div className="md:hidden">
@ -112,9 +100,9 @@ const Settings: React.FC = () => {
function renderActiveTab() {
switch (activeTab) {
case t('settings.general.label'):
case 'General':
return <General />;
case t('settings.documents.label'):
case 'Documents':
return (
<Documents
documents={documents}
@ -128,7 +116,7 @@ const Settings: React.FC = () => {
onWidgetScreenshotChange={updateWidgetScreenshot} // Add this line
/>
);
case t('settings.apiKeys.label'):
case 'API Keys':
return <APIKeys />;
default:
return null;

@ -7,21 +7,19 @@ import {
const key = localStorage.getItem('DocsGPTApiKey');
const prompt = localStorage.getItem('DocsGPTPrompt');
const chunks = localStorage.getItem('DocsGPTChunks');
const token_limit = localStorage.getItem('DocsGPTTokenLimit');
const doc = localStorage.getItem('DocsGPTRecentDocs');
const chunks = localStorage.getItem('DocsGPTChunks');
const store = configureStore({
preloadedState: {
preference: {
apiKey: key ?? '',
chunks: JSON.parse(chunks ?? '2').toString(),
selectedDocs: doc !== null ? JSON.parse(doc) : null,
prompt:
prompt !== null
? JSON.parse(prompt)
: { name: 'default', id: 'default', type: 'private' },
chunks: JSON.parse(chunks ?? '2').toString(),
token_limit: token_limit ? parseInt(token_limit) : 2000,
selectedDocs: doc !== null ? JSON.parse(doc) : null,
conversations: null,
sourceDocs: [
{
@ -36,7 +34,6 @@ const store = configureStore({
model: '1.0',
},
],
modalState: 'INACTIVE',
},
},
reducer: {

@ -1,14 +1,13 @@
import React, { useRef } from 'react';
import React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useDispatch } from 'react-redux';
import { ActiveState } from '../models/misc';
import { getDocs } from '../preferences/preferenceApi';
import { setSelectedDocs, setSourceDocs } from '../preferences/preferenceSlice';
import { setSourceDocs } from '../preferences/preferenceSlice';
import Dropdown from '../components/Dropdown';
import { useTranslation } from 'react-i18next';
function Upload({
export default function Upload({
modalState,
setModalState,
}: {
@ -25,54 +24,24 @@ function Upload({
search_queries: [''],
number_posts: 10,
});
const [activeTab, setActiveTab] = useState<string>('file');
const [files, setfiles] = useState<File[]>([]);
const [progress, setProgress] = useState<{
type: 'UPLOAD' | 'TRAINIING';
percentage: number;
taskId?: string;
failed?: boolean;
}>();
const { t } = useTranslation();
const setTimeoutRef = useRef<number | null>();
const urlOptions: { label: string; value: string }[] = [
{ label: 'Crawler', value: 'crawler' },
// { label: 'Sitemap', value: 'sitemap' },
{ label: 'Link', value: 'url' },
{ label: 'Reddit', value: 'reddit' },
];
const [urlType, setUrlType] = useState<{ label: string; value: string }>({
label: 'Link',
value: 'url',
});
useEffect(() => {
if (setTimeoutRef.current) {
clearTimeout(setTimeoutRef.current);
}
}, []);
function ProgressBar({ progress }: { progress: number }) {
return (
<div className="my-5 w-[50%]">
<div
className={`h-4 overflow-hidden rounded-lg border border-purple-30 text-xs text-white outline-none `}
>
<div
className={`h-full border-none p-1 w-${
progress || 0
}% flex items-center justify-center bg-purple-30 outline-none transition-all`}
style={{ width: `${progress || 0}%` }}
>
{progress >= 5 && `${progress}%`}
</div>
</div>
</div>
);
}
const [activeTab, setActiveTab] = useState<string>('file');
const [files, setfiles] = useState<File[]>([]);
const [progress, setProgress] = useState<{
type: 'UPLOAD' | 'TRAINIING';
percentage: number;
taskId?: string;
failed?: boolean;
}>();
function Progress({
title,
@ -90,10 +59,15 @@ function Upload({
<p className={`ml-5 text-xl text-red-400 ${isFailed ? '' : 'hidden'}`}>
Over the token limit, please consider uploading smaller document
</p>
{/* <p className="mt-10 text-2xl">{progress?.percentage || 0}%</p> */}
<p className="mt-10 text-2xl">{progress?.percentage || 0}%</p>
{/* progress bar */}
<ProgressBar progress={progress?.percentage as number} />
<div className="mb-10 w-[50%]">
<div className="h-1 w-[100%] bg-purple-30"></div>
<div
className={`relative bottom-1 h-1 bg-purple-30 transition-all`}
style={{ width: `${progress?.percentage || 0}%` }}
></div>
</div>
<button
onClick={() => {
@ -118,26 +92,16 @@ function Upload({
function TrainingProgress() {
const dispatch = useDispatch();
useEffect(() => {
let timeoutID: number | undefined;
if ((progress?.percentage ?? 0) < 100) {
timeoutID = setTimeout(() => {
(progress?.percentage ?? 0) < 100 &&
setTimeout(() => {
const apiHost = import.meta.env.VITE_API_HOST;
fetch(`${apiHost}/api/task_status?task_id=${progress?.taskId}`)
.then((data) => data.json())
.then((data) => {
if (data.status == 'SUCCESS') {
if (data.result.limited === true) {
getDocs().then((data) => {
dispatch(setSourceDocs(data));
dispatch(
setSelectedDocs(
data?.find((d) => d.location.toLowerCase() === 'local'),
),
);
});
getDocs().then((data) => dispatch(setSourceDocs(data)));
setProgress(
(progress) =>
progress && {
@ -147,14 +111,7 @@ function Upload({
},
);
} else {
getDocs().then((data) => {
dispatch(setSourceDocs(data));
dispatch(
setSelectedDocs(
data?.find((d) => d.location.toLowerCase() === 'local'),
),
);
});
getDocs().then((data) => dispatch(setSourceDocs(data)));
setProgress(
(progress) =>
progress && {
@ -175,14 +132,6 @@ function Upload({
}
});
}, 5000);
}
// cleanup
return () => {
if (timeoutID !== undefined) {
clearTimeout(timeoutID);
}
};
}, [progress, dispatch]);
return (
<Progress
@ -215,9 +164,7 @@ function Upload({
});
xhr.onload = () => {
const { task_id } = JSON.parse(xhr.responseText);
setTimeoutRef.current = setTimeout(() => {
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
}, 3000);
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
};
xhr.open('POST', `${apiHost + '/api/upload'}`);
xhr.send(formData);
@ -246,9 +193,7 @@ function Upload({
});
xhr.onload = () => {
const { task_id } = JSON.parse(xhr.responseText);
setTimeoutRef.current = setTimeout(() => {
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
}, 3000);
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
};
xhr.open('POST', `${apiHost + '/api/remote'}`);
xhr.send(formData);
@ -271,7 +216,6 @@ function Upload({
['.docx'],
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
if (name === 'search_queries' && value.length > 0) {
@ -285,9 +229,7 @@ function Upload({
[name]: value,
});
};
let view;
if (progress?.type === 'UPLOAD') {
view = <UploadProgress></UploadProgress>;
} else if (progress?.type === 'TRAINIING') {
@ -296,7 +238,7 @@ function Upload({
view = (
<>
<p className="text-xl text-jet dark:text-bright-gray">
{t('modals.uploadDoc.label')}
Upload New Documentation
</p>
<div>
<button
@ -307,7 +249,7 @@ function Upload({
: 'text-sonic-silver hover:text-purple-30'
} mr-4 rounded-full px-[20px] py-[5px] text-sm font-semibold`}
>
{t('modals.uploadDoc.file')}
From File
</button>
<button
onClick={() => setActiveTab('remote')}
@ -317,35 +259,34 @@ function Upload({
: 'text-sonic-silver hover:text-purple-30'
} mr-4 rounded-full px-[20px] py-[5px] text-sm font-semibold`}
>
{t('modals.uploadDoc.remote')}
Remote
</button>
</div>
{activeTab === 'file' && (
<>
<input
type="text"
className="h-[42px] w-full rounded-full border-2 border-gray-5000 px-3 outline-none dark:bg-transparent dark:text-silver"
className="h-10 w-full rounded-full border-2 border-gray-5000 px-3 outline-none dark:bg-transparent dark:text-silver"
value={docName}
onChange={(e) => setDocName(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
Name
</span>
</div>
<div {...getRootProps()}>
<span className="rounded-3xl border border-purple-30 px-4 py-2 font-medium text-purple-30 hover:cursor-pointer dark:bg-purple-taupe dark:text-silver">
<input type="button" {...getInputProps()} />
{t('modals.uploadDoc.choose')}
Choose Files
</span>
</div>
<p className="mb-0 text-xs italic text-gray-4000">
{t('modals.uploadDoc.info')}
Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb
</p>
<div className="mt-0">
<p className="mb-[14px] font-medium text-eerie-black dark:text-light-gray">
{t('modals.uploadDoc.uploadedFiles')}
Uploaded Files
</p>
{files.map((file) => (
<p key={file.name} className="text-gray-6000">
@ -353,9 +294,7 @@ function Upload({
</p>
))}
{files.length === 0 && (
<p className="text-gray-6000 dark:text-light-gray">
{t('none')}
</p>
<p className="text-gray-6000 dark:text-light-gray">None</p>
)}
</div>
</>
@ -374,27 +313,27 @@ function Upload({
{urlType.label !== 'Reddit' ? (
<>
<input
placeholder={`Enter ${t('modals.uploadDoc.name')}`}
placeholder="Enter name"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Name
</span>
</div>
<input
placeholder={t('modals.uploadDoc.urlLink')}
placeholder="URL Link"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={url}
onChange={(e) => setUrl(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.link')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Link
</span>
</div>
</>
@ -403,106 +342,96 @@ function Upload({
<input
placeholder="Enter client ID"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.id')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Client ID
</span>
</div>
<input
placeholder="Enter client secret"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.secret')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Client secret
</span>
</div>
<input
placeholder="Enter user agent"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.agent')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
User agent
</span>
</div>
<input
placeholder="Enter search queries"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.searchQueries')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Search queries
</span>
</div>
<input
placeholder="Enter number of posts"
type="number"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.numberOfPosts')}
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Number of posts
</span>
</div>
</>
)}
</>
)}
<div className="flex flex-row-reverse">
{activeTab === 'file' ? (
<button
onClick={uploadFile}
className={`ml-2 cursor-pointer rounded-3xl bg-purple-30 text-sm text-white ${
files.length > 0 && docName.trim().length > 0
? 'hover:bg-[#6F3FD1]'
: 'bg-opacity-75 text-opacity-80'
} py-2 px-6`}
disabled={
(files.length === 0 || docName.trim().length === 0) &&
activeTab === 'file'
}
>
{t('modals.uploadDoc.train')}
</button>
) : (
<button
onClick={uploadRemote}
className={`ml-2 cursor-pointer rounded-3xl bg-purple-30 py-2 px-6 text-sm text-white hover:bg-[#6F3FD1]`}
>
{t('modals.uploadDoc.train')}
</button>
)}
<button
onClick={activeTab === 'file' ? uploadFile : uploadRemote}
className={`ml-6 cursor-pointer rounded-3xl bg-purple-30 text-white ${
files.length > 0 && docName.trim().length > 0
? ''
: 'bg-opacity-75 text-opacity-80'
} py-2 px-6`}
disabled={
(files.length === 0 || docName.trim().length === 0) &&
activeTab === 'file'
}
>
Train
</button>
<button
onClick={() => {
setDocName('');
setfiles([]);
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
className="cursor-pointer font-medium dark:text-light-gray"
>
{t('modals.uploadDoc.cancel')}
Cancel
</button>
</div>
</>
@ -521,5 +450,3 @@ function Upload({
</article>
);
}
export default Upload;

@ -1,98 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: docsgpt-api
spec:
replicas: 1
selector:
matchLabels:
app: docsgpt-api
template:
metadata:
labels:
app: docsgpt-api
spec:
containers:
- name: docsgpt-api
image: arc53/docsgpt
ports:
- containerPort: 7091
resources:
limits:
memory: "4Gi"
cpu: "2"
requests:
memory: "2Gi"
cpu: "1"
envFrom:
- secretRef:
name: docsgpt-secrets
env:
- name: FLASK_APP
value: "application/app.py"
- name: DEPLOYMENT_TYPE
value: "cloud"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: docsgpt-worker
spec:
replicas: 1
selector:
matchLabels:
app: docsgpt-worker
template:
metadata:
labels:
app: docsgpt-worker
spec:
containers:
- name: docsgpt-worker
image: arc53/docsgpt
command: ["celery", "-A", "application.app.celery", "worker", "-l", "INFO", "-n", "worker.%h"]
resources:
limits:
memory: "4Gi"
cpu: "2"
requests:
memory: "2Gi"
cpu: "1"
envFrom:
- secretRef:
name: docsgpt-secrets
env:
- name: API_URL
value: "http://<your-api-endpoint>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: docsgpt-frontend
spec:
replicas: 1
selector:
matchLabels:
app: docsgpt-frontend
template:
metadata:
labels:
app: docsgpt-frontend
spec:
containers:
- name: docsgpt-frontend
image: arc53/docsgpt-fe
ports:
- containerPort: 5173
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "256Mi"
cpu: "100m"
env:
- name: VITE_API_HOST
value: "http://<your-api-endpoint>"
- name: VITE_API_STREAMING
value: "true"

@ -1,46 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi # Adjust size as needed
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongodb
spec:
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:latest
ports:
- containerPort: 27017
resources:
limits:
memory: "1Gi"
cpu: "0.5"
requests:
memory: "512Mi"
cpu: "250m"
volumeMounts:
- name: mongodb-data
mountPath: /data/db
volumes:
- name: mongodb-data
persistentVolumeClaim:
claimName: mongodb-pvc

@ -1,46 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qdrant-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: qdrant
spec:
replicas: 1
selector:
matchLabels:
app: qdrant
template:
metadata:
labels:
app: qdrant
spec:
containers:
- name: qdrant
image: qdrant/qdrant:latest
ports:
- containerPort: 6333
resources:
limits:
memory: "2Gi" # Adjust based on your needs
cpu: "1" # Adjust based on your needs
requests:
memory: "1Gi" # Adjust based on your needs
cpu: "500m" # Adjust based on your needs
volumeMounts:
- name: qdrant-data
mountPath: /qdrant/storage
volumes:
- name: qdrant-data
persistentVolumeClaim:
claimName: qdrant-pvc

@ -1,26 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:latest
ports:
- containerPort: 6379
resources:
limits:
memory: "1Gi"
cpu: "0.5"
requests:
memory: "512Mi"
cpu: "250m"

@ -1,15 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: docsgpt-secrets
type: Opaque
data:
LLM_NAME: ZG9jc2dwdA==
INTERNAL_KEY: aW50ZXJuYWw=
CELERY_BROKER_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
CELERY_RESULT_BACKEND: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
QDRANT_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
QDRANT_PORT: NjM3OQ==
MONGO_URI: bW9uZ29kYjovL21vbmdvZGItc2VydmljZToyNzAxNy9kb2NzZ3B0P3JldHJ5V3JpdGVzPXRydWUmdz1tYWpvcml0eQ==
mongo-user: bW9uZ28tdXNlcg==
mongo-password: bW9uZ28tcGFzc3dvcmQ=

@ -1,25 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: docsgpt-api-service
spec:
selector:
app: docsgpt-api
ports:
- protocol: TCP
port: 80
targetPort: 7091
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: docsgpt-frontend-service
spec:
selector:
app: docsgpt-frontend
ports:
- protocol: TCP
port: 80
targetPort: 5173
type: LoadBalancer

@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mongodb-service
spec:
selector:
app: mongodb
ports:
- protocol: TCP
port: 27017
targetPort: 27017
type: ClusterIP

@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: qdrant
spec:
selector:
app: qdrant
ports:
- protocol: TCP
port: 6333
targetPort: 6333
type: ClusterIP

@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save