mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' into Jackson
This commit is contained in:
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/application" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/frontend" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
1
.github/workflows/labeler.yml
vendored
1
.github/workflows/labeler.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
- pull_request_target
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
@@ -46,9 +46,9 @@ You can find our roadmap [here](https://github.com/orgs/arc53/projects/2). Pleas
|
||||
|
||||
If you don't have enough resources to run it, you can use bitsnbytes to quantize.
|
||||
|
||||
## Features
|
||||
## End to End AI Framework for Information Retrieval
|
||||
|
||||

|
||||

|
||||
|
||||
## Useful Links
|
||||
|
||||
|
||||
@@ -4,14 +4,11 @@ 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
|
||||
|
||||
apt-get install -y software-properties-common && \
|
||||
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 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc wget unzip libc6-dev python3.11 python3.11-distutils python3.11-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Verify Python installation and setup symlink
|
||||
@@ -27,7 +24,7 @@ RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.z
|
||||
rm mpnet-base-v2.zip
|
||||
|
||||
# Install Rust
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
RUN wget -q -O - https://sh.rustup.rs | 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/*
|
||||
@@ -50,12 +47,10 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
FROM ubuntu:24.04 as final
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
|
||||
apt-get install -y software-properties-common && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
# Install Python
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3.11 && \
|
||||
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/*
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from flask import Blueprint, request, Response
|
||||
from flask import Blueprint, request, Response, current_app
|
||||
import json
|
||||
import datetime
|
||||
import logging
|
||||
@@ -74,7 +74,7 @@ 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)
|
||||
@@ -129,10 +129,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",
|
||||
@@ -172,7 +172,9 @@ def get_prompt(prompt_id):
|
||||
return prompt
|
||||
|
||||
|
||||
def complete_stream(question, retriever, conversation_id, user_api_key):
|
||||
def complete_stream(
|
||||
question, retriever, conversation_id, user_api_key, isNoneDoc=False
|
||||
):
|
||||
|
||||
try:
|
||||
response_full = ""
|
||||
@@ -186,126 +188,142 @@ def complete_stream(question, retriever, conversation_id, user_api_key):
|
||||
elif "source" in line:
|
||||
source_log_docs.append(line["source"])
|
||||
|
||||
if isNoneDoc:
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
if(user_api_key is None):
|
||||
)
|
||||
if user_api_key is None:
|
||||
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)})
|
||||
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
|
||||
return
|
||||
|
||||
|
||||
@answer.route("/stream", methods=["POST"])
|
||||
def stream():
|
||||
try:
|
||||
data = request.get_json()
|
||||
# get parameter from url question
|
||||
question = data["question"]
|
||||
if "history" not in data:
|
||||
history = []
|
||||
else:
|
||||
history = data["history"]
|
||||
history = json.loads(history)
|
||||
if "conversation_id" not in data:
|
||||
conversation_id = None
|
||||
else:
|
||||
conversation_id = data["conversation_id"]
|
||||
if "prompt_id" in data:
|
||||
prompt_id = data["prompt_id"]
|
||||
else:
|
||||
prompt_id = "default"
|
||||
if "selectedDocs" in data and data["selectedDocs"] is None:
|
||||
chunks = 0
|
||||
elif "chunks" in data:
|
||||
chunks = int(data["chunks"])
|
||||
else:
|
||||
chunks = 2
|
||||
if "token_limit" in data:
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
try:
|
||||
data = request.get_json()
|
||||
question = data["question"]
|
||||
if "history" not in data:
|
||||
history = []
|
||||
else:
|
||||
history = data["history"]
|
||||
history = json.loads(history)
|
||||
if "conversation_id" not in data:
|
||||
conversation_id = None
|
||||
else:
|
||||
conversation_id = data["conversation_id"]
|
||||
if "prompt_id" in data:
|
||||
prompt_id = data["prompt_id"]
|
||||
else:
|
||||
prompt_id = "default"
|
||||
if "selectedDocs" in data and data["selectedDocs"] is None:
|
||||
chunks = 0
|
||||
elif "chunks" in data:
|
||||
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
|
||||
# check if active_docs or api_key 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:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
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:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
if source["active_docs"].split("/")[0] in ["default", "local"]:
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
current_app.logger.info(f"/stream - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
return Response(
|
||||
complete_stream(
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
retriever=retriever,
|
||||
conversation_id=conversation_id,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
),
|
||||
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)
|
||||
)
|
||||
|
||||
return Response(
|
||||
complete_stream(
|
||||
question=question,
|
||||
retriever=retriever,
|
||||
conversation_id=conversation_id,
|
||||
user_api_key=user_api_key,
|
||||
isNoneDoc=data.get("isNoneDoc"),
|
||||
),
|
||||
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:
|
||||
current_app.logger.error(f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()}
|
||||
)
|
||||
message = e.args[0]
|
||||
status_code = 400
|
||||
# # Custom exceptions with two arguments, index 1 as status code
|
||||
if(len(e.args) >= 2):
|
||||
if len(e.args) >= 2:
|
||||
status_code = e.args[1]
|
||||
return Response(
|
||||
error_stream_generate(message),
|
||||
status=status_code,
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
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"
|
||||
data = json.dumps({"type": "error", "error": err_response})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
|
||||
@answer.route("/api/answer", methods=["POST"])
|
||||
def api_answer():
|
||||
@@ -333,7 +351,6 @@ def api_answer():
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
# use try and except to check for exception
|
||||
try:
|
||||
# check if the vectorstore is set
|
||||
if "api_key" in data:
|
||||
@@ -346,16 +363,17 @@ def api_answer():
|
||||
source = data
|
||||
user_api_key = None
|
||||
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
if source["active_docs"].split("/")[0] in ["default", "local"]:
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
current_app.logger.info(f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
@@ -375,6 +393,10 @@ def api_answer():
|
||||
elif "answer" in line:
|
||||
response_full += line["answer"]
|
||||
|
||||
if data.get("isNoneDoc"):
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
@@ -386,16 +408,15 @@ def api_answer():
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
# print whole traceback
|
||||
traceback.print_exc()
|
||||
print(str(e))
|
||||
current_app.logger.error(f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||
extra={"error": str(e), "traceback": traceback.format_exc()}
|
||||
)
|
||||
return bad_request(500, str(e))
|
||||
|
||||
|
||||
@answer.route("/api/search", methods=["POST"])
|
||||
def api_search():
|
||||
data = request.get_json()
|
||||
# get parameter from url question
|
||||
question = data["question"]
|
||||
if "chunks" in data:
|
||||
chunks = int(data["chunks"])
|
||||
@@ -413,10 +434,7 @@ def api_search():
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
if source["active_docs"].split("/")[0] in ["default", "local"]:
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
@@ -424,6 +442,10 @@ def api_search():
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
current_app.logger.info(f"/api/answer - request_data: {data}, source: {source}",
|
||||
extra={"data": json.dumps({"request_data": data, "source": source})}
|
||||
)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
@@ -437,4 +459,9 @@ def api_search():
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
docs = retriever.search()
|
||||
|
||||
if data.get("isNoneDoc"):
|
||||
for doc in docs:
|
||||
doc["source"] = "None"
|
||||
|
||||
return docs
|
||||
|
||||
@@ -44,7 +44,7 @@ def delete_conversation():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/delete_all_conversations", methods=["POST"])
|
||||
@user.route("/api/delete_all_conversations", methods=["GET"])
|
||||
def delete_all_conversations():
|
||||
user_id = "local"
|
||||
conversations_collection.delete_many({"user": user_id})
|
||||
@@ -256,7 +256,7 @@ def combined_json():
|
||||
"docLink": "default",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "remote",
|
||||
"tokens":""
|
||||
"tokens": "",
|
||||
}
|
||||
]
|
||||
# structure: name, language, version, description, fullName, date, docLink
|
||||
@@ -273,7 +273,7 @@ def combined_json():
|
||||
"docLink": index["location"],
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "local",
|
||||
"tokens" : index["tokens"] if ("tokens" in index.keys()) else ""
|
||||
"tokens": index["tokens"] if ("tokens" in index.keys()) else "",
|
||||
}
|
||||
)
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
@@ -295,7 +295,7 @@ def combined_json():
|
||||
"docLink": "duckduck_search",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "custom",
|
||||
"tokens":""
|
||||
"tokens": "",
|
||||
}
|
||||
)
|
||||
if "brave_search" in settings.RETRIEVERS_ENABLED:
|
||||
@@ -310,7 +310,7 @@ def combined_json():
|
||||
"docLink": "brave_search",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "custom",
|
||||
"tokens":""
|
||||
"tokens": "",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -496,138 +496,204 @@ def delete_api_key():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
#route to share conversation
|
||||
# route to share conversation
|
||||
##isPromptable should be passed through queries
|
||||
@user.route("/api/share",methods=["POST"])
|
||||
@user.route("/api/share", methods=["POST"])
|
||||
def share_conversation():
|
||||
try:
|
||||
data = request.get_json()
|
||||
user = "local" if "user" not in data else data["user"]
|
||||
conversation_id = data["conversation_id"]
|
||||
isPromptable = request.args.get("isPromptable").lower() == "true"
|
||||
|
||||
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
|
||||
|
||||
conversation = conversations_collection.find_one(
|
||||
{"_id": ObjectId(conversation_id)}
|
||||
)
|
||||
current_n_queries = len(conversation["queries"])
|
||||
|
||||
##generate binary representation of uuid
|
||||
|
||||
##generate binary representation of uuid
|
||||
explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD)
|
||||
|
||||
if(isPromptable):
|
||||
|
||||
if isPromptable:
|
||||
source = "default" if "source" not in data else data["source"]
|
||||
prompt_id = "default" if "prompt_id" not in data else data["prompt_id"]
|
||||
chunks = "2" if "chunks" not in data else data["chunks"]
|
||||
|
||||
name = conversation["name"]+"(shared)"
|
||||
pre_existing_api_document = api_key_collection.find_one({
|
||||
"prompt_id":prompt_id,
|
||||
"chunks":chunks,
|
||||
"source":source,
|
||||
"user":user
|
||||
})
|
||||
|
||||
name = conversation["name"] + "(shared)"
|
||||
pre_existing_api_document = api_key_collection.find_one(
|
||||
{
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
"source": source,
|
||||
"user": user,
|
||||
}
|
||||
)
|
||||
api_uuid = str(uuid.uuid4())
|
||||
if(pre_existing_api_document):
|
||||
api_uuid = pre_existing_api_document["key"]
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
if(pre_existing is not None):
|
||||
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
|
||||
else:
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref":"conversations",
|
||||
"$id":ObjectId(conversation_id)
|
||||
} ,
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
return jsonify({"success":True,"identifier":str(explicit_binary.as_uuid())})
|
||||
if pre_existing_api_document:
|
||||
api_uuid = pre_existing_api_document["key"]
|
||||
pre_existing = shared_conversations_collections.find_one(
|
||||
{
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
if pre_existing is not None:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"identifier": str(pre_existing["uuid"].as_uuid()),
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
else:
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
return jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
)
|
||||
else:
|
||||
api_key_collection.insert_one(
|
||||
{
|
||||
"name": name,
|
||||
"key": api_uuid,
|
||||
"source": source,
|
||||
"user": user,
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
}
|
||||
)
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref":"conversations",
|
||||
"$id":ObjectId(conversation_id)
|
||||
} ,
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
{
|
||||
"name": name,
|
||||
"key": api_uuid,
|
||||
"source": source,
|
||||
"user": user,
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
}
|
||||
)
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
}
|
||||
)
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
|
||||
##isPromptable = False
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user
|
||||
})
|
||||
if(pre_existing is not None):
|
||||
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
|
||||
else:
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref":"conversations",
|
||||
"$id":ObjectId(conversation_id)
|
||||
} ,
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user
|
||||
})
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
return (
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
),
|
||||
201,
|
||||
)
|
||||
|
||||
#route to get publicly shared conversations
|
||||
@user.route("/api/shared_conversation/<string:identifier>",methods=["GET"])
|
||||
def get_publicly_shared_conversations(identifier : str):
|
||||
try:
|
||||
query_uuid = Binary.from_uuid(uuid.UUID(identifier), UuidRepresentation.STANDARD)
|
||||
shared = shared_conversations_collections.find_one({"uuid":query_uuid})
|
||||
conversation_queries=[]
|
||||
if shared and 'conversation_id' in shared and isinstance(shared['conversation_id'], DBRef):
|
||||
# Resolve the DBRef
|
||||
conversation_ref = shared['conversation_id']
|
||||
conversation = db.dereference(conversation_ref)
|
||||
if(conversation is None):
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
conversation_queries = conversation['queries'][:(shared["first_n_queries"])]
|
||||
for query in conversation_queries:
|
||||
query.pop("sources") ## avoid exposing sources
|
||||
##isPromptable = False
|
||||
pre_existing = shared_conversations_collections.find_one(
|
||||
{
|
||||
"conversation_id": DBRef("conversations", ObjectId(conversation_id)),
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
}
|
||||
)
|
||||
if pre_existing is not None:
|
||||
return (
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(pre_existing["uuid"].as_uuid())}
|
||||
),
|
||||
200,
|
||||
)
|
||||
else:
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": isPromptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
}
|
||||
)
|
||||
## Identifier as route parameter in frontend
|
||||
return (
|
||||
jsonify(
|
||||
{"success": True, "identifier": str(explicit_binary.as_uuid())}
|
||||
),
|
||||
201,
|
||||
)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
|
||||
# route to get publicly shared conversations
|
||||
@user.route("/api/shared_conversation/<string:identifier>", methods=["GET"])
|
||||
def get_publicly_shared_conversations(identifier: str):
|
||||
try:
|
||||
query_uuid = Binary.from_uuid(
|
||||
uuid.UUID(identifier), UuidRepresentation.STANDARD
|
||||
)
|
||||
shared = shared_conversations_collections.find_one({"uuid": query_uuid})
|
||||
conversation_queries = []
|
||||
if (
|
||||
shared
|
||||
and "conversation_id" in shared
|
||||
and isinstance(shared["conversation_id"], DBRef)
|
||||
):
|
||||
# Resolve the DBRef
|
||||
conversation_ref = shared["conversation_id"]
|
||||
conversation = db.dereference(conversation_ref)
|
||||
if conversation is None:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"sucess": False,
|
||||
"error": "might have broken url or the conversation does not exist",
|
||||
}
|
||||
),
|
||||
404,
|
||||
)
|
||||
conversation_queries = conversation["queries"][
|
||||
: (shared["first_n_queries"])
|
||||
]
|
||||
for query in conversation_queries:
|
||||
query.pop("sources") ## avoid exposing sources
|
||||
else:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"sucess": False,
|
||||
"error": "might have broken url or the conversation does not exist",
|
||||
}
|
||||
),
|
||||
404,
|
||||
)
|
||||
date = conversation["_id"].generation_time.isoformat()
|
||||
res = {
|
||||
"success":True,
|
||||
"queries":conversation_queries,
|
||||
"title":conversation["name"],
|
||||
"timestamp":date
|
||||
}
|
||||
if(shared["isPromptable"] and "api_key" in shared):
|
||||
"success": True,
|
||||
"queries": conversation_queries,
|
||||
"title": conversation["name"],
|
||||
"timestamp": date,
|
||||
}
|
||||
if shared["isPromptable"] and "api_key" in shared:
|
||||
res["api_key"] = shared["api_key"]
|
||||
return jsonify(res), 200
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
print(err)
|
||||
return jsonify({"success": False, "error": str(err)}), 400
|
||||
|
||||
@@ -6,12 +6,14 @@ from application.core.settings import settings
|
||||
from application.api.user.routes import user
|
||||
from application.api.answer.routes import answer
|
||||
from application.api.internal.routes import internal
|
||||
from application.core.logging_config import setup_logging
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import pathlib
|
||||
pathlib.PosixPath = pathlib.WindowsPath
|
||||
|
||||
dotenv.load_dotenv()
|
||||
setup_logging()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(user)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from celery import Celery
|
||||
from application.core.settings import settings
|
||||
from celery.signals import setup_logging
|
||||
|
||||
def make_celery(app_name=__name__):
|
||||
celery = Celery(app_name, broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND)
|
||||
celery.conf.update(settings)
|
||||
return celery
|
||||
|
||||
@setup_logging.connect
|
||||
def config_loggers(*args, **kwargs):
|
||||
from application.core.logging_config import setup_logging
|
||||
setup_logging()
|
||||
|
||||
celery = make_celery()
|
||||
|
||||
22
application/core/logging_config.py
Normal file
22
application/core/logging_config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from logging.config import dictConfig
|
||||
|
||||
def setup_logging():
|
||||
dictConfig({
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
'root': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['console'],
|
||||
},
|
||||
})
|
||||
@@ -29,6 +29,7 @@ class Settings(BaseSettings):
|
||||
OPENAI_API_VERSION: Optional[str] = None # azure openai api version
|
||||
AZURE_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for answering
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for embeddings
|
||||
OPENAI_BASE_URL: Optional[str] = None # openai base url for open ai compatable models
|
||||
|
||||
# elasticsearch
|
||||
ELASTIC_CLOUD_ID: Optional[str] = None # cloud id for elasticsearch
|
||||
|
||||
@@ -2,25 +2,23 @@ from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
|
||||
class OpenAILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
global openai
|
||||
from openai import OpenAI
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = OpenAI(
|
||||
api_key=api_key,
|
||||
)
|
||||
if settings.OPENAI_BASE_URL:
|
||||
self.client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=settings.OPENAI_BASE_URL
|
||||
)
|
||||
else:
|
||||
self.client = OpenAI(api_key=api_key)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _get_openai(self):
|
||||
# Import openai when needed
|
||||
import openai
|
||||
|
||||
return openai
|
||||
|
||||
def _raw_gen(
|
||||
self,
|
||||
baseself,
|
||||
@@ -29,7 +27,7 @@ class OpenAILLM(BaseLLM):
|
||||
stream=False,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs
|
||||
):
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
@@ -44,7 +42,7 @@ class OpenAILLM(BaseLLM):
|
||||
stream=True,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs
|
||||
):
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
@@ -73,8 +71,3 @@ class AzureOpenAILLM(OpenAILLM):
|
||||
api_base=settings.OPENAI_API_BASE,
|
||||
deployment_name=settings.AZURE_DEPLOYMENT_NAME,
|
||||
)
|
||||
|
||||
def _get_openai(self):
|
||||
openai = super()._get_openai()
|
||||
|
||||
return openai
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Contains parser for html files.
|
||||
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Union
|
||||
|
||||
@@ -18,66 +17,8 @@ class HTMLParser(BaseParser):
|
||||
return {}
|
||||
|
||||
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]:
|
||||
"""Parse file.
|
||||
from langchain_community.document_loaders import BSHTMLLoader
|
||||
|
||||
Returns:
|
||||
Union[str, List[str]]: a string or a List of strings.
|
||||
"""
|
||||
try:
|
||||
from unstructured.partition.html import partition_html
|
||||
from unstructured.staging.base import convert_to_isd
|
||||
from unstructured.cleaners.core import clean
|
||||
except ImportError:
|
||||
raise ValueError("unstructured package is required to parse HTML files.")
|
||||
|
||||
# Using the unstructured library to convert the html to isd format
|
||||
# isd sample : isd = [
|
||||
# {"text": "My Title", "type": "Title"},
|
||||
# {"text": "My Narrative", "type": "NarrativeText"}
|
||||
# ]
|
||||
with open(file, "r", encoding="utf-8") as fp:
|
||||
elements = partition_html(file=fp)
|
||||
isd = convert_to_isd(elements)
|
||||
|
||||
# Removing non ascii charactwers from isd_el['text']
|
||||
for isd_el in isd:
|
||||
isd_el['text'] = isd_el['text'].encode("ascii", "ignore").decode()
|
||||
|
||||
# Removing all the \n characters from isd_el['text'] using regex and replace with single space
|
||||
# Removing all the extra spaces from isd_el['text'] using regex and replace with single space
|
||||
for isd_el in isd:
|
||||
isd_el['text'] = re.sub(r'\n', ' ', isd_el['text'], flags=re.MULTILINE | re.DOTALL)
|
||||
isd_el['text'] = re.sub(r"\s{2,}", " ", isd_el['text'], flags=re.MULTILINE | re.DOTALL)
|
||||
|
||||
# more cleaning: extra_whitespaces, dashes, bullets, trailing_punctuation
|
||||
for isd_el in isd:
|
||||
clean(isd_el['text'], extra_whitespace=True, dashes=True, bullets=True, trailing_punctuation=True)
|
||||
|
||||
# Creating a list of all the indexes of isd_el['type'] = 'Title'
|
||||
title_indexes = [i for i, isd_el in enumerate(isd) if isd_el['type'] == 'Title']
|
||||
|
||||
# Creating 'Chunks' - List of lists of strings
|
||||
# each list starting with isd_el['type'] = 'Title' and all the data till the next 'Title'
|
||||
# Each Chunk can be thought of as an individual set of data, which can be sent to the model
|
||||
# Where Each Title is grouped together with the data under it
|
||||
|
||||
Chunks = [[]]
|
||||
final_chunks = list(list())
|
||||
|
||||
for i, isd_el in enumerate(isd):
|
||||
if i in title_indexes:
|
||||
Chunks.append([])
|
||||
Chunks[-1].append(isd_el['text'])
|
||||
|
||||
# Removing all the chunks with sum of length of all the strings in the chunk < 25
|
||||
# TODO: This value can be an user defined variable
|
||||
for chunk in Chunks:
|
||||
# sum of length of all the strings in the chunk
|
||||
sum = 0
|
||||
sum += len(str(chunk))
|
||||
if sum < 25:
|
||||
Chunks.remove(chunk)
|
||||
else:
|
||||
# appending all the approved chunks to final_chunks as a single string
|
||||
final_chunks.append(" ".join([str(item) for item in chunk]))
|
||||
return final_chunks
|
||||
loader = BSHTMLLoader(file)
|
||||
data = loader.load()
|
||||
return data
|
||||
|
||||
@@ -5,7 +5,7 @@ from application.parser.remote.base import BaseRemote
|
||||
|
||||
class CrawlerLoader(BaseRemote):
|
||||
def __init__(self, limit=10):
|
||||
from langchain.document_loaders import WebBaseLoader
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
self.loader = WebBaseLoader # Initialize the document loader
|
||||
self.limit = limit # Set the limit for the number of pages to scrape
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from application.parser.remote.base import BaseRemote
|
||||
|
||||
class SitemapLoader(BaseRemote):
|
||||
def __init__(self, limit=20):
|
||||
from langchain.document_loaders import WebBaseLoader
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
self.loader = WebBaseLoader
|
||||
self.limit = limit # Adding limit to control the number of URLs to process
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
anthropic==0.12.0
|
||||
boto3==1.34.6
|
||||
anthropic==0.34.0
|
||||
boto3==1.34.153
|
||||
beautifulsoup4==4.12.3
|
||||
celery==5.3.6
|
||||
dataclasses_json==0.6.3
|
||||
dataclasses_json==0.6.7
|
||||
docx2txt==0.8
|
||||
duckduckgo-search==5.3.0
|
||||
duckduckgo-search==6.2.6
|
||||
EbookLib==0.18
|
||||
elasticsearch==8.12.0
|
||||
elasticsearch==8.14.0
|
||||
escodegen==1.0.11
|
||||
esprima==4.0.1
|
||||
faiss-cpu==1.8.0.post1
|
||||
Flask==3.0.1
|
||||
gunicorn==22.0.0
|
||||
faiss-cpu==1.8.0.post1
|
||||
gunicorn==23.0.0
|
||||
html2text==2020.1.16
|
||||
javalang==0.13.0
|
||||
langchain==0.2.16
|
||||
@@ -18,12 +19,12 @@ langchain-community==0.2.16
|
||||
langchain-core==0.2.38
|
||||
langchain-openai==0.1.23
|
||||
openapi3_parser==1.1.16
|
||||
pandas==2.2.0
|
||||
pydantic_settings==2.1.0
|
||||
pymongo==4.6.3
|
||||
pandas==2.2.2
|
||||
pydantic_settings==2.4.0
|
||||
pymongo==4.8.0
|
||||
PyPDF2==3.0.1
|
||||
python-dotenv==1.0.1
|
||||
qdrant-client==1.9.0
|
||||
qdrant-client==1.11.0
|
||||
redis==5.0.1
|
||||
Requests==2.32.0
|
||||
retry==0.9.2
|
||||
@@ -31,6 +32,5 @@ sentence-transformers
|
||||
tiktoken==0.7.0
|
||||
torch
|
||||
tqdm==4.66.3
|
||||
transformers==4.36.2
|
||||
unstructured==0.12.2
|
||||
transformers==4.44.0
|
||||
Werkzeug==3.0.3
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from application.retriever.base import BaseRetriever
|
||||
from application.core.settings import settings
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.utils import count_tokens
|
||||
from application.utils import num_tokens_from_string
|
||||
from langchain_community.tools import BraveSearch
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class BraveRetSearch(BaseRetriever):
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
|
||||
@@ -4,7 +4,7 @@ from application.core.settings import settings
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
|
||||
from application.utils import count_tokens
|
||||
from application.utils import num_tokens_from_string
|
||||
|
||||
|
||||
class ClassicRAG(BaseRetriever):
|
||||
@@ -98,7 +98,7 @@ class ClassicRAG(BaseRetriever):
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from application.retriever.base import BaseRetriever
|
||||
from application.core.settings import settings
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.utils import count_tokens
|
||||
from application.utils import num_tokens_from_string
|
||||
from langchain_community.tools import DuckDuckGoSearchResults
|
||||
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
|
||||
|
||||
@@ -95,7 +95,7 @@ class DuckDuckSearch(BaseRetriever):
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
from pymongo import MongoClient
|
||||
from datetime import datetime
|
||||
from application.core.settings import settings
|
||||
from application.utils import count_tokens
|
||||
from application.utils import num_tokens_from_string
|
||||
|
||||
mongo = MongoClient(settings.MONGO_URI)
|
||||
db = mongo["docsgpt"]
|
||||
@@ -24,9 +24,9 @@ def update_token_usage(user_api_key, token_usage):
|
||||
def gen_token_usage(func):
|
||||
def wrapper(self, model, messages, stream, **kwargs):
|
||||
for message in messages:
|
||||
self.token_usage["prompt_tokens"] += count_tokens(message["content"])
|
||||
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
|
||||
result = func(self, model, messages, stream, **kwargs)
|
||||
self.token_usage["generated_tokens"] += count_tokens(result)
|
||||
self.token_usage["generated_tokens"] += num_tokens_from_string(result)
|
||||
update_token_usage(self.user_api_key, self.token_usage)
|
||||
return result
|
||||
|
||||
@@ -36,14 +36,14 @@ def gen_token_usage(func):
|
||||
def stream_token_usage(func):
|
||||
def wrapper(self, model, messages, stream, **kwargs):
|
||||
for message in messages:
|
||||
self.token_usage["prompt_tokens"] += count_tokens(message["content"])
|
||||
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
|
||||
batch = []
|
||||
result = func(self, model, messages, stream, **kwargs)
|
||||
for r in result:
|
||||
batch.append(r)
|
||||
yield r
|
||||
for line in batch:
|
||||
self.token_usage["generated_tokens"] += count_tokens(line)
|
||||
self.token_usage["generated_tokens"] += num_tokens_from_string(line)
|
||||
update_token_usage(self.user_api_key, self.token_usage)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
from transformers import GPT2TokenizerFast
|
||||
import tiktoken
|
||||
|
||||
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
|
||||
tokenizer.model_max_length = 100000
|
||||
def count_tokens(string):
|
||||
return len(tokenizer(string)['input_ids'])
|
||||
_encoding = None
|
||||
|
||||
def get_encoding():
|
||||
global _encoding
|
||||
if _encoding is None:
|
||||
_encoding = tiktoken.get_encoding("cl100k_base")
|
||||
return _encoding
|
||||
|
||||
def num_tokens_from_string(string: str) -> int:
|
||||
encoding = get_encoding()
|
||||
num_tokens = len(encoding.encode(string))
|
||||
return num_tokens
|
||||
|
||||
def count_tokens_docs(docs):
|
||||
docs_content = ""
|
||||
for doc in docs:
|
||||
docs_content += doc.page_content
|
||||
|
||||
tokens = num_tokens_from_string(docs_content)
|
||||
return tokens
|
||||
@@ -2,8 +2,8 @@ import os
|
||||
import shutil
|
||||
import string
|
||||
import zipfile
|
||||
import tiktoken
|
||||
from urllib.parse import urljoin
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
@@ -13,6 +13,8 @@ from application.parser.remote.remote_creator import RemoteCreator
|
||||
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
|
||||
from application.utils import count_tokens_docs
|
||||
|
||||
|
||||
# Define a function to extract metadata from a given filename.
|
||||
def metadata_from_filename(title):
|
||||
@@ -41,7 +43,7 @@ def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
|
||||
max_depth (int): Maximum allowed depth of recursion to prevent infinite loops.
|
||||
"""
|
||||
if current_depth > max_depth:
|
||||
print(f"Reached maximum recursion depth of {max_depth}")
|
||||
logging.warning(f"Reached maximum recursion depth of {max_depth}")
|
||||
return
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
@@ -88,16 +90,13 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
|
||||
max_tokens = 1250
|
||||
recursion_depth = 2
|
||||
full_path = os.path.join(directory, user, name_job)
|
||||
import sys
|
||||
|
||||
print(full_path, file=sys.stderr)
|
||||
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": name_job})
|
||||
# check if API_URL env variable is set
|
||||
file_data = {"name": name_job, "file": filename, "user": user}
|
||||
response = requests.get(
|
||||
urljoin(settings.API_URL, "/api/download"), params=file_data
|
||||
)
|
||||
# check if file is in the response
|
||||
print(response, file=sys.stderr)
|
||||
file = response.content
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
@@ -137,7 +136,7 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
|
||||
|
||||
if sample:
|
||||
for i in range(min(5, len(raw_docs))):
|
||||
print(raw_docs[i].text)
|
||||
logging.info(f"Sample document {i}: {raw_docs[i]}")
|
||||
|
||||
# get files from outputs/inputs/index.faiss and outputs/inputs/index.pkl
|
||||
# and send them to the server (provide user and name in form)
|
||||
@@ -180,6 +179,7 @@ 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})
|
||||
logging.info(f"Remote job: {full_path}", extra={"user": user, "job": name_job, source_data: source_data})
|
||||
|
||||
remote_loader = RemoteCreator.create_loader(loader)
|
||||
raw_docs = remote_loader.load_data(source_data)
|
||||
@@ -212,26 +212,4 @@ 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
|
||||
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
|
||||
1826
docs/package-lock.json
generated
1826
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"docsgpt": "^0.3.7",
|
||||
"docsgpt": "^0.4.1",
|
||||
"next": "^14.1.1",
|
||||
"nextra": "^2.13.2",
|
||||
"nextra-theme-docs": "^2.13.2",
|
||||
|
||||
@@ -17,25 +17,31 @@ Now, you can use the widget in your component like this :
|
||||
```jsx
|
||||
<DocsGPTWidget
|
||||
apiHost="https://your-docsgpt-api.com"
|
||||
selectDocs="local/docs.zip"
|
||||
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 !",
|
||||
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."
|
||||
theme = "dark"
|
||||
buttonIcon = "https://your-icon"
|
||||
buttonBg = "#222327"
|
||||
/>
|
||||
```
|
||||
DocsGPTWidget takes 8 **props** with default fallback values:
|
||||
To tailor the widget to your needs, you can configure the following props in your component:
|
||||
1. `apiHost` — The URL of your DocsGPT API.
|
||||
2. `selectDocs` — The documentation source that you want to use for your widget (e.g. `default` or `local/docs1.zip`).
|
||||
2. `theme` — Allows to select your specific theme (dark or light).
|
||||
3. `apiKey` — Usually, it's empty.
|
||||
4. `avatar`: Specifies the URL of the avatar or image representing the chatbot.
|
||||
5. `title`: Sets the title text displayed in the chatbot interface.
|
||||
6. `description`: Provides a brief description of the chatbot's purpose or functionality.
|
||||
7. `heroTitle`: Displays a welcome title when users interact with the chatbot.
|
||||
8. `heroDescription`: Provide additional introductory text or information about the chatbot's capabilities.
|
||||
9. `buttonIcon`: Specifies the url of the icon image for the widget.
|
||||
10. `buttonBg`: Allows to specify the Background color of the widget.
|
||||
11. `size`: Sets the size of the widget ( small, medium).
|
||||
|
||||
|
||||
### How to use DocsGPTWidget with [Nextra](https://nextra.site/) (Next.js + MDX)
|
||||
Install your widget as described above and then go to your `pages/` folder and create a new file `_app.js` with the following content:
|
||||
@@ -55,22 +61,30 @@ export default function MyApp({ Component, pageProps }) {
|
||||
```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>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>HTML + CSS</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>This is a simple HTML + CSS template!</h1>
|
||||
<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", {
|
||||
apiKey: "",
|
||||
size: "medium",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
To link the widget to your api and your documents you can pass parameters to the renderDocsGPTWidget('div id', { parameters }).
|
||||
@@ -82,22 +96,24 @@ To link the widget to your api and your documents you can pass parameters to the
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DocsGPT Widget</title>
|
||||
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
|
||||
</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: '',
|
||||
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.'
|
||||
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
|
||||
theme:"dark",
|
||||
buttonIcon:"https://your-icon",
|
||||
buttonBg:"#222327"
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,6 +36,14 @@ List of latest supported LLMs are https://github.com/arc53/DocsGPT/blob/main/app
|
||||
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>
|
||||
|
||||
### For OpenAI-Compatible Endpoints:
|
||||
DocsGPT supports the use of OpenAI-compatible endpoints through base URL substitution. This feature allows you to use alternative AI models or services that implement the OpenAI API interface.
|
||||
|
||||
|
||||
Set the OPENAI_BASE_URL in your environment. You can change .env file with OPENAI_BASE_URL with the desired base URL or docker-compose.yml file and add the environment variable to the backend container.
|
||||
|
||||
> Make sure you have the right API_KEY and correct LLM_NAME.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<>
|
||||
<Component {...pageProps} />
|
||||
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" />
|
||||
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" theme="dark" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
28
extensions/chrome/package-lock.json
generated
28
extensions/chrome/package-lock.json
generated
@@ -107,12 +107,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -260,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -884,12 +884,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"camelcase-css": {
|
||||
@@ -1000,9 +1000,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
||||
@@ -27,15 +27,18 @@ To link the widget to your api and your documents you can pass parameters to the
|
||||
|
||||
const App = () => {
|
||||
return <DocsGPTWidget
|
||||
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.'
|
||||
/>;
|
||||
apiHost="https://your-docsgpt-api.com"
|
||||
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."
|
||||
theme = "dark"
|
||||
buttonIcon = "https://your-icon"
|
||||
buttonBg = "#222327"
|
||||
/>;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -80,15 +83,17 @@ To link the widget to your api and your documents you can pass parameters to the
|
||||
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
|
||||
<script type="module">
|
||||
window.onload = function() {
|
||||
renderDocsGPTWidget('app', , {
|
||||
renderDocsGPTWidget('app', {
|
||||
apiHost: 'http://localhost:7001',
|
||||
selectDocs: 'default',
|
||||
apiKey: '',
|
||||
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.'
|
||||
heroTitle: 'Welcome to DocsGPT!',
|
||||
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
|
||||
theme:"dark",
|
||||
buttonIcon:"https://your-icon.svg",
|
||||
buttonBg:"#222327"
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
907
extensions/react-widget/package-lock.json
generated
907
extensions/react-widget/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docsgpt",
|
||||
"version": "0.3.9",
|
||||
"name": "docsgpt-react",
|
||||
"version": "0.4.2",
|
||||
"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
|
||||
},
|
||||
@@ -31,6 +19,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "parcel build src/main.tsx --public-url ./",
|
||||
"build:react": "parcel build src/index.ts",
|
||||
"dev": "parcel src/index.html -p 3000",
|
||||
"test": "jest",
|
||||
"lint": "eslint",
|
||||
@@ -39,7 +28,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.23.3",
|
||||
"@bpmn-io/snarkdown": "^2.2.0",
|
||||
"@parcel/resolver-glob": "^2.12.0",
|
||||
"@parcel/transformer-svg-react": "^2.12.0",
|
||||
"@parcel/transformer-typescript-tsc": "^2.12.0",
|
||||
@@ -51,6 +39,7 @@
|
||||
"flow-bin": "^0.229.2",
|
||||
"i": "^0.3.7",
|
||||
"install": "^0.13.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"npm": "^10.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -63,6 +52,7 @@
|
||||
"@parcel/packager-ts": "^2.12.0",
|
||||
"@parcel/transformer-typescript-types": "^2.12.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
|
||||
43
extensions/react-widget/publish.sh
Executable file
43
extensions/react-widget/publish.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
## chmod +x publish.sh - to upgrade ownership
|
||||
set -e
|
||||
cat package.json >> package_copy.json
|
||||
cat package-lock.json >> package-lock_copy.json
|
||||
publish_package() {
|
||||
PACKAGE_NAME=$1
|
||||
BUILD_COMMAND=$2
|
||||
# Update package name in package.json
|
||||
jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json
|
||||
|
||||
# Remove 'target' key if the package name is 'docsgpt-react'
|
||||
if [ "$PACKAGE_NAME" = "docsgpt-react" ]; then
|
||||
jq 'del(.targets)' package.json > temp.json && mv temp.json package.json
|
||||
fi
|
||||
|
||||
if [ -d "dist" ]; then
|
||||
echo "Deleting existing dist directory..."
|
||||
rm -rf dist
|
||||
fi
|
||||
|
||||
npm version patch
|
||||
|
||||
npm run "$BUILD_COMMAND"
|
||||
|
||||
# Publish to npm
|
||||
npm publish
|
||||
# Clean up
|
||||
mv package_copy.json package.json
|
||||
mv package-lock_copy.json package-lock.json
|
||||
echo "Published ${PACKAGE_NAME}"
|
||||
}
|
||||
|
||||
# Publish docsgpt package
|
||||
publish_package "docsgpt" "build"
|
||||
|
||||
# Publish docsgpt-react package
|
||||
publish_package "docsgpt-react" "build:react"
|
||||
|
||||
|
||||
rm -rf package_copy.json
|
||||
rm -rf package-lock_copy.json
|
||||
echo "---Process completed---"
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.37891 9.44824H7.75821" stroke="white" stroke-width="1.68965" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.1377 9.44824H12.8273" stroke="white" stroke-width="1.68965" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.37891 6.06934H6.06856" stroke="white" stroke-width="1.68965" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.44824 6.06934H12.8276" stroke="white" stroke-width="1.68965" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.2069 11.1379C16.2069 11.5861 16.0289 12.0158 15.712 12.3327C15.3951 12.6496 14.9654 12.8276 14.5172 12.8276H4.37931L1 16.2069V2.68965C1 2.24153 1.17802 1.81176 1.49489 1.49489C1.81176 1.17802 2.24153 1 2.68965 1H14.5172C14.9654 1 15.3951 1.17802 15.712 1.49489C16.0289 1.81176 16.2069 2.24153 16.2069 2.68965V11.1379Z" stroke="white" stroke-width="1.68965" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1009 B |
@@ -1,13 +1,38 @@
|
||||
"use client";
|
||||
import React from 'react'
|
||||
import DOMPurify from 'dompurify';
|
||||
import snarkdown from '@bpmn-io/snarkdown';
|
||||
import styled, { keyframes, createGlobalStyle } from 'styled-components';
|
||||
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 { MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index';
|
||||
import { fetchAnswerStreaming } from '../requests/streamingApi';
|
||||
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
const themes = {
|
||||
dark: {
|
||||
bg: '#222327',
|
||||
text: '#fff',
|
||||
primary: {
|
||||
text: "#FAFAFA",
|
||||
bg: '#222327'
|
||||
},
|
||||
secondary: {
|
||||
text: "#A1A1AA",
|
||||
bg: "#38383b"
|
||||
}
|
||||
},
|
||||
light: {
|
||||
bg: '#fff',
|
||||
text: '#000',
|
||||
primary: {
|
||||
text: "#222327",
|
||||
bg: "#fff"
|
||||
},
|
||||
secondary: {
|
||||
text: "#A1A1AA",
|
||||
bg: "#F6F6F6"
|
||||
}
|
||||
}
|
||||
}
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
.response pre {
|
||||
padding: 8px;
|
||||
@@ -16,6 +41,7 @@ const GlobalStyles = createGlobalStyle`
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
background-color: #1B1C1F;
|
||||
color: #fff !important;
|
||||
}
|
||||
.response h1{
|
||||
font-size: 20px;
|
||||
@@ -26,40 +52,60 @@ const GlobalStyles = createGlobalStyle`
|
||||
.response h3{
|
||||
font-size: 16px;
|
||||
}
|
||||
.response p{
|
||||
margin:0px;
|
||||
}
|
||||
.response code:not(pre code){
|
||||
border-radius: 6px;
|
||||
padding: 1px 3px 1px 3px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
background-color: #646464;
|
||||
color: #fff !important;
|
||||
}
|
||||
`;
|
||||
const WidgetContainer = styled.div`
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
transition: opacity 0.5s;
|
||||
`
|
||||
const WidgetContainer = styled.div<{ modal: boolean }>`
|
||||
display: block;
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
right: ${props => props.modal ? '50%' : '10px'};
|
||||
bottom: ${props => props.modal ? '50%' : '10px'};
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
${props => props.modal &&
|
||||
"transform : translate(50%,50%);"
|
||||
}
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
@media only screen and (max-width: 768px) {
|
||||
max-height: 100vh !important;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
const StyledContainer = styled.div`
|
||||
display: block;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 352px;
|
||||
height: 407px;
|
||||
max-height: 407px;
|
||||
border-radius: 0.75rem;
|
||||
background-color: #222327;
|
||||
background-color: ${props => props.theme.primary.bg};
|
||||
font-family: sans-serif;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: visibility 0.3s, opacity 0.3s;
|
||||
`;
|
||||
const FloatingButton = styled.div`
|
||||
const FloatingButton = styled.div<{ bgcolor: string }>`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
z-index: 500;
|
||||
@@ -70,7 +116,7 @@ const FloatingButton = styled.div`
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 9999px;
|
||||
background-image: linear-gradient(to bottom right, #5AF0EC, #E80D9D);
|
||||
background: ${props => props.bgcolor};
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
@@ -120,24 +166,34 @@ const ContentWrapper = styled.div`
|
||||
const Title = styled.h3`
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
color: #FAFAFA;
|
||||
color: ${props => props.theme.primary.text};
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.25rem;
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
font-size: 0.85rem;
|
||||
color: #A1A1AA;
|
||||
color: ${props => props.theme.secondary.text};
|
||||
margin-top: 0;
|
||||
`;
|
||||
const Conversation = styled.div`
|
||||
height: 16rem;
|
||||
|
||||
const Conversation = styled.div<{ size: string }>`
|
||||
min-height: 250px;
|
||||
max-width: 968px;
|
||||
height: ${props => props.size === 'large' ? '75vh' : props.size === 'medium' ? '70vh' : '320px'};
|
||||
width: ${props => props.size === 'large' ? '60vw' : props.size === 'medium' ? '28vw' : '400px'};
|
||||
padding-inline: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4a4a4a transparent; /* thumb color track color */
|
||||
@media only screen and (max-width: 768px) {
|
||||
width: 90vw !important;
|
||||
}
|
||||
@media only screen and (min-width:768px ) and (max-width: 1280px) {
|
||||
width:${props => props.size === 'large' ? '90vw' : props.size === 'medium' ? '60vw' : '400px'} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>`
|
||||
@@ -146,13 +202,13 @@ const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>`
|
||||
justify-content: ${props => props.type === 'QUESTION' ? 'flex-end' : 'flex-start'};
|
||||
margin: 0.5rem;
|
||||
`;
|
||||
const Message = styled.p<{ type: MESSAGE_TYPE }>`
|
||||
const Message = styled.div<{ type: MESSAGE_TYPE }>`
|
||||
background: ${props => props.type === 'QUESTION' ?
|
||||
'linear-gradient(to bottom right, #8860DB, #6D42C5)' :
|
||||
'#38383b'};
|
||||
color: #ffff;
|
||||
props.theme.secondary.bg};
|
||||
color: ${props => props.type === 'ANSWER' ? props.theme.primary.text : '#fff'};
|
||||
border: none;
|
||||
max-width: 80%;
|
||||
max-width: ${props => props.type === 'ANSWER' ? '100%' : '80'};
|
||||
overflow: auto;
|
||||
margin: 4px;
|
||||
display: block;
|
||||
@@ -190,35 +246,31 @@ const DotAnimation = styled.div`
|
||||
const Delay = styled(DotAnimation) <{ delay: number }>`
|
||||
animation-delay: ${props => props.delay + 'ms'};
|
||||
`;
|
||||
const PromptContainer = styled.form`
|
||||
const PromptContainer = styled.form<{ size: string }>`
|
||||
background-color: transparent;
|
||||
height: 36px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
height: ${props => props.size == 'large' ? '60px' : '40px'};
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
`;
|
||||
const StyledInput = styled.input`
|
||||
width: 260px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
border: 1px solid #686877;
|
||||
padding-left: 12px;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
color: #ffff;
|
||||
color: ${props => props.theme.text};
|
||||
outline: none;
|
||||
`;
|
||||
const StyledButton = styled.button`
|
||||
const StyledButton = styled.button<{ size: string }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-image: linear-gradient(to bottom right, #5AF0EC, #E80D9D);
|
||||
border-radius: 6px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: ${props => props.size === 'large' ? '60px' : '36px'};
|
||||
height: ${props => props.size === 'large' ? '60px' : '36px'};
|
||||
margin-left:8px;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
@@ -239,13 +291,14 @@ const HeroContainer = styled.div`
|
||||
align-items: middle;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
background-image: linear-gradient(to bottom right, #5AF0EC, #ff1bf4);
|
||||
border-radius: 10px;
|
||||
margin: 0 auto;
|
||||
padding: 2px;
|
||||
`;
|
||||
const HeroWrapper = styled.div`
|
||||
background-color: #222327;
|
||||
background-color: ${props => props.theme.primary.bg};
|
||||
border-radius: 10px;
|
||||
font-weight: normal;
|
||||
padding: 6px;
|
||||
@@ -253,23 +306,22 @@ const HeroWrapper = styled.div`
|
||||
justify-content: space-between;
|
||||
`
|
||||
const HeroTitle = styled.h3`
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
color: ${props => props.theme.text};
|
||||
margin-bottom: 5px;
|
||||
padding: 2px;
|
||||
`;
|
||||
const HeroDescription = styled.p`
|
||||
color: #fff;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
const Hero = ({ title, description }: { title: string, description: string }) => {
|
||||
const Hero = ({ title, description, theme }: { title: string, description: string, theme: string }) => {
|
||||
return (
|
||||
<>
|
||||
<HeroContainer>
|
||||
<HeroWrapper>
|
||||
<IconWrapper style={{ marginTop: '8px' }}>
|
||||
<RocketIcon color='white' width={20} height={20} />
|
||||
<IconWrapper style={{ marginTop: '12px' }}>
|
||||
<RocketIcon color={theme === 'light' ? 'black' : 'white'} width={20} height={20} />
|
||||
</IconWrapper>
|
||||
<div>
|
||||
<HeroTitle>{title}</HeroTitle>
|
||||
@@ -284,15 +336,17 @@ const Hero = ({ title, description }: { title: string, description: string }) =>
|
||||
};
|
||||
export const DocsGPTWidget = ({
|
||||
apiHost = 'https://gptcloud.arc53.com',
|
||||
selectDocs = 'default',
|
||||
apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a',
|
||||
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.'
|
||||
}) => {
|
||||
|
||||
heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
|
||||
size = 'small',
|
||||
theme = 'dark',
|
||||
buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/message.svg',
|
||||
buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)'
|
||||
}: WidgetProps) => {
|
||||
const [prompt, setPrompt] = React.useState('');
|
||||
const [status, setStatus] = React.useState<Status>('idle');
|
||||
const [queries, setQueries] = React.useState<Query[]>([])
|
||||
@@ -300,6 +354,8 @@ export const DocsGPTWidget = ({
|
||||
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 md = new MarkdownIt();
|
||||
|
||||
const handleUserInterrupt = () => {
|
||||
(status === 'loading') && setEventInterrupt(true);
|
||||
}
|
||||
@@ -316,7 +372,6 @@ export const DocsGPTWidget = ({
|
||||
const lastChild = element?.children?.[element.children.length - 1]
|
||||
lastChild && scrollToBottom(lastChild)
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
!eventInterrupt && scrollToBottom(endMessageRef.current);
|
||||
}, [queries.length, queries[queries.length - 1]?.response]);
|
||||
@@ -329,19 +384,24 @@ export const DocsGPTWidget = ({
|
||||
question: question,
|
||||
apiKey: apiKey,
|
||||
apiHost: apiHost,
|
||||
selectedDocs: selectDocs,
|
||||
history: queries,
|
||||
conversationId: conversationId,
|
||||
onEvent: (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
// check if the 'end' event has been received
|
||||
if (data.type === 'end') {
|
||||
// set status to 'idle'
|
||||
setStatus('idle');
|
||||
|
||||
} else if (data.type === 'id') {
|
||||
}
|
||||
else if (data.type === 'id') {
|
||||
setConversationId(data.id)
|
||||
} else {
|
||||
}
|
||||
else if (data.type === 'error') {
|
||||
const updatedQueries = [...queries];
|
||||
updatedQueries[updatedQueries.length - 1].error = data.error;
|
||||
setQueries(updatedQueries);
|
||||
setStatus('idle')
|
||||
}
|
||||
else {
|
||||
const result = data.answer;
|
||||
const streamingResponse = queries[queries.length - 1].response ? queries[queries.length - 1].response : '';
|
||||
const updatedQueries = [...queries];
|
||||
@@ -353,7 +413,7 @@ export const DocsGPTWidget = ({
|
||||
);
|
||||
} catch (error) {
|
||||
const updatedQueries = [...queries];
|
||||
updatedQueries[updatedQueries.length - 1].error = 'error'
|
||||
updatedQueries[updatedQueries.length - 1].error = 'Something went wrong !'
|
||||
setQueries(updatedQueries);
|
||||
setStatus('idle')
|
||||
//setEventInterrupt(false)
|
||||
@@ -372,16 +432,21 @@ export const DocsGPTWidget = ({
|
||||
event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png";
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<WidgetContainer>
|
||||
<ThemeProvider theme={themes[theme]}>
|
||||
{open && size === 'large' &&
|
||||
<Overlay onClick={() => {
|
||||
setOpen(false)
|
||||
}} />
|
||||
}
|
||||
<FloatingButton bgcolor={buttonBg} onClick={() => setOpen(!open)} hidden={open}>
|
||||
<img style={{ maxHeight: '4rem', maxWidth: '4rem' }} src={buttonIcon} />
|
||||
</FloatingButton>
|
||||
<WidgetContainer modal={size == 'large'}>
|
||||
<GlobalStyles />
|
||||
{!open && <FloatingButton onClick={() => setOpen(true)} hidden={open}>
|
||||
<MessageIcon style={{ marginTop: '8px' }} />
|
||||
</FloatingButton>}
|
||||
{open && <StyledContainer>
|
||||
<div>
|
||||
<CancelButton onClick={() => setOpen(false)}>
|
||||
<Cross2Icon width={24} height={24} color='white' />
|
||||
<Cross2Icon width={24} height={24} color={theme === 'light' ? 'black' : 'white'} />
|
||||
</CancelButton>
|
||||
<Header>
|
||||
<IconWrapper>
|
||||
@@ -393,7 +458,7 @@ export const DocsGPTWidget = ({
|
||||
</ContentWrapper>
|
||||
</Header>
|
||||
</div>
|
||||
<Conversation onWheel={handleUserInterrupt} onTouchMove={handleUserInterrupt}>
|
||||
<Conversation size={size} onWheel={handleUserInterrupt} onTouchMove={handleUserInterrupt}>
|
||||
{
|
||||
queries.length > 0 ? queries?.map((query, index) => {
|
||||
return (
|
||||
@@ -413,7 +478,10 @@ 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: DOMPurify.sanitize(md.render(query.response)) }}
|
||||
/>
|
||||
</Message>
|
||||
</MessageBubble>
|
||||
: <div>
|
||||
@@ -424,7 +492,7 @@ export const DocsGPTWidget = ({
|
||||
</IconWrapper>
|
||||
<div>
|
||||
<h5 style={{ margin: 2 }}>Network Error</h5>
|
||||
<span style={{ margin: 2, fontSize: '13px' }}>Something went wrong !</span>
|
||||
<span style={{ margin: 2, fontSize: '13px' }}>{query.error}</span>
|
||||
</div>
|
||||
</ErrorAlert>
|
||||
: <MessageBubble type='ANSWER'>
|
||||
@@ -439,22 +507,23 @@ export const DocsGPTWidget = ({
|
||||
}
|
||||
</React.Fragment>)
|
||||
})
|
||||
: <Hero title={heroTitle} description={heroDescription} />
|
||||
: <Hero title={heroTitle} description={heroDescription} theme={theme} />
|
||||
}
|
||||
</Conversation>
|
||||
|
||||
<PromptContainer
|
||||
size={size}
|
||||
onSubmit={handleSubmit}>
|
||||
<StyledInput
|
||||
value={prompt} onChange={(event) => setPrompt(event.target.value)}
|
||||
type='text' placeholder="What do you want to do?" />
|
||||
<StyledButton
|
||||
size={size}
|
||||
disabled={prompt.length == 0 || status !== 'idle'}>
|
||||
<PaperPlaneIcon width={15} height={15} color='white' />
|
||||
</StyledButton>
|
||||
</PromptContainer>
|
||||
</StyledContainer>}
|
||||
</WidgetContainer>
|
||||
</>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,11 @@ 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} />);
|
||||
};
|
||||
|
||||
(window as any).renderDocsGPTWidget = renderWidget;
|
||||
if (typeof window !== 'undefined') {
|
||||
const renderWidget = (elementId: string, props = {}) => {
|
||||
const root = createRoot(document.getElementById(elementId) as HTMLElement);
|
||||
root.render(<DocsGPTWidget {...props} />);
|
||||
};
|
||||
(window as any).renderDocsGPTWidget = renderWidget;
|
||||
}
|
||||
export { DocsGPTWidget };
|
||||
@@ -1,92 +1,83 @@
|
||||
interface HistoryItem {
|
||||
prompt: string;
|
||||
response?: string;
|
||||
}
|
||||
prompt: string;
|
||||
response?: string;
|
||||
}
|
||||
interface FetchAnswerStreamingProps {
|
||||
question?: string;
|
||||
apiKey?: string;
|
||||
selectedDocs?: string;
|
||||
history?: HistoryItem[];
|
||||
conversationId?: string | null;
|
||||
apiHost?: string;
|
||||
onEvent?: (event: MessageEvent) => void;
|
||||
}
|
||||
question?: string;
|
||||
apiKey?: string;
|
||||
selectedDocs?: string;
|
||||
history?: HistoryItem[];
|
||||
conversationId?: string | null;
|
||||
apiHost?: string;
|
||||
onEvent?: (event: MessageEvent) => void;
|
||||
}
|
||||
export function fetchAnswerStreaming({
|
||||
question = '',
|
||||
apiKey = '',
|
||||
selectedDocs = '',
|
||||
history = [],
|
||||
conversationId = null,
|
||||
apiHost = '',
|
||||
onEvent = () => {console.log("Event triggered, but no handler provided.");}
|
||||
}: FetchAnswerStreamingProps): Promise<void> {
|
||||
let docPath = 'default';
|
||||
if (selectedDocs) {
|
||||
docPath = selectedDocs;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const body = {
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
embeddings_key: apiKey,
|
||||
active_docs: docPath,
|
||||
history: JSON.stringify(history),
|
||||
conversation_id: conversationId,
|
||||
model: 'default'
|
||||
};
|
||||
|
||||
fetch(apiHost + '/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let counterrr = 0;
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) {
|
||||
resolve();
|
||||
return;
|
||||
question = '',
|
||||
apiKey = '',
|
||||
history = [],
|
||||
conversationId = null,
|
||||
apiHost = '',
|
||||
onEvent = () => { console.log("Event triggered, but no handler provided."); }
|
||||
}: FetchAnswerStreamingProps): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const body= {
|
||||
question: question,
|
||||
history: JSON.stringify(history),
|
||||
conversation_id: conversationId,
|
||||
model: 'default',
|
||||
api_key:apiKey
|
||||
};
|
||||
fetch(apiHost + '/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let counterrr = 0;
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
counterrr += 1;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim() == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
counterrr += 1;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim() == '') {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
line = line.substring(5);
|
||||
}
|
||||
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: line,
|
||||
});
|
||||
|
||||
onEvent(messageEvent); // handle each message
|
||||
if (line.startsWith('data:')) {
|
||||
line = line.substring(5);
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
};
|
||||
|
||||
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: line,
|
||||
});
|
||||
|
||||
onEvent(messageEvent); // handle each message
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Connection failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Connection failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
|
||||
export type Status = 'idle' | 'loading' | 'failed';
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE';
|
||||
|
||||
export type THEME = 'light' | 'dark';
|
||||
export interface Query {
|
||||
prompt: string;
|
||||
response?: string;
|
||||
@@ -10,4 +10,17 @@ export interface Query {
|
||||
sources?: { title: string; text: string }[];
|
||||
conversationId?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
export interface WidgetProps {
|
||||
apiHost?: string;
|
||||
apiKey?: string;
|
||||
avatar?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
heroTitle?: string;
|
||||
heroDescription?: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
theme?:THEME,
|
||||
buttonIcon?:string;
|
||||
buttonBg?:string;
|
||||
}
|
||||
3942
frontend/package-lock.json
generated
3942
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,45 +21,45 @@
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@vercel/analytics": "^0.1.10",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.0"
|
||||
"remark-gfm": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^15.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.1.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.4",
|
||||
"lint-staged": "^15.2.8",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^5.0.13",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import Navigation from './Navigation';
|
||||
import Conversation from './conversation/Conversation';
|
||||
import About from './About';
|
||||
@@ -34,17 +33,7 @@ function MainLayout() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
useEffect(() => {
|
||||
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
|
||||
if (isDarkTheme) {
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
} else {
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
}
|
||||
}, [isDarkTheme]);
|
||||
useDarkTheme();
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Hero({
|
||||
}>;
|
||||
return (
|
||||
<div
|
||||
className={`mt-16 mb-4 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full lg:mt-6`}
|
||||
className={`mb-1 mt-16 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full md:mb-10 lg:mt-6`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -10,7 +10,6 @@ import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
||||
import Discord from './assets/discord.svg';
|
||||
import Expand from './assets/expand.svg';
|
||||
import Github from './assets/github.svg';
|
||||
import HamburgerDark from './assets/hamburger-dark.svg';
|
||||
import Hamburger from './assets/hamburger.svg';
|
||||
import Info from './assets/info.svg';
|
||||
import SettingGear from './assets/settingGear.svg';
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
} from './conversation/conversationSlice';
|
||||
import ConversationTile from './conversation/ConversationTile';
|
||||
import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks';
|
||||
import useDefaultDocument from './hooks/useDefaultDocument';
|
||||
import DeleteConvModal from './modals/DeleteConvModal';
|
||||
import { ActiveState } from './models/misc';
|
||||
import APIKeyModal from './preferences/APIKeyModal';
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
setSelectedDocs,
|
||||
setSourceDocs,
|
||||
} from './preferences/preferenceSlice';
|
||||
import SelectDocsModal from './preferences/SelectDocsModal';
|
||||
import Upload from './upload/Upload';
|
||||
|
||||
interface NavigationProps {
|
||||
@@ -175,16 +174,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
useOutsideAlerter(
|
||||
navRef,
|
||||
() => {
|
||||
if (isMobile && navOpen && apiKeyModalState === 'INACTIVE') {
|
||||
setNavOpen(false);
|
||||
setIsDocsListOpen(false);
|
||||
}
|
||||
},
|
||||
[navOpen, isDocsListOpen, apiKeyModalState],
|
||||
);
|
||||
useOutsideAlerter(navRef, () => {
|
||||
if (isMobile && navOpen && apiKeyModalState === 'INACTIVE') {
|
||||
setNavOpen(false);
|
||||
setIsDocsListOpen(false);
|
||||
}
|
||||
}, [navOpen, isDocsListOpen, apiKeyModalState]);
|
||||
|
||||
/*
|
||||
Needed to fix bug where if mobile nav was closed and then window was resized to desktop, nav would still be closed but the button to open would be gone, as per #1 on issue #146
|
||||
@@ -193,6 +188,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
useEffect(() => {
|
||||
setNavOpen(!isMobile);
|
||||
}, [isMobile]);
|
||||
useDefaultDocument();
|
||||
return (
|
||||
<>
|
||||
{!navOpen && (
|
||||
@@ -393,18 +389,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
onClick={() => setNavOpen(true)}
|
||||
>
|
||||
<img
|
||||
src={isDarkTheme ? HamburgerDark : Hamburger}
|
||||
src={Hamburger}
|
||||
alt="menu toggle"
|
||||
className="w-7"
|
||||
className="w-7 filter dark:invert"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SelectDocsModal
|
||||
modalState={selectedDocsModalState}
|
||||
setModalState={setSelectedDocsModalState}
|
||||
isCancellable={isSelectedDocsSet}
|
||||
/>
|
||||
<APIKeyModal
|
||||
modalState={apiKeyModalState}
|
||||
setModalState={setApiKeyModalState}
|
||||
|
||||
3
frontend/src/assets/document.svg
Normal file
3
frontend/src/assets/document.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.9294 5.375V12.4583C12.9294 12.8341 12.7801 13.1944 12.5145 13.4601C12.2488 13.7257 11.8885 13.875 11.5127 13.875H3.01274C2.63701 13.875 2.27668 13.7257 2.011 13.4601C1.74532 13.1944 1.59607 12.8341 1.59607 12.4583V2.54167C1.59607 2.16594 1.74532 1.80561 2.011 1.53993C2.27668 1.27426 2.63701 1.125 3.01274 1.125H8.6794M12.9294 5.375V5.25317C12.9293 4.87747 12.78 4.5172 12.5143 4.25158L9.80282 1.54008C9.53721 1.27439 9.17693 1.12508 8.80124 1.125H8.6794M12.9294 5.375H10.0961C9.72035 5.375 9.36001 5.22574 9.09434 4.96007C8.82866 4.69439 8.6794 4.33406 8.6794 3.95833V1.125" stroke="#949494" stroke-width="1.41667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
25
frontend/src/assets/sources.svg
Normal file
25
frontend/src/assets/sources.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg width="24" height="27" viewBox="0 0 24 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.73517 17.2121L0.538918 17.9125C0.3753 18.0084 0.239616 18.1455 0.145332 18.31C0.0510491 18.4746 0.00144577 18.661 0.00144577 18.8506C0.00144577 19.0403 0.0510491 19.2267 0.145332 19.3912C0.239616 19.5558 0.3753 19.6929 0.538918 19.7888L11.0514 25.9513C11.2181 26.0489 11.4078 26.1004 11.601 26.1004C11.7941 26.1004 11.9838 26.0489 12.1505 25.9513L22.663 19.7888C22.8266 19.6929 22.9623 19.5558 23.0566 19.3912C23.1509 19.2267 23.2005 19.0403 23.2005 18.8506C23.2005 18.661 23.1509 18.4746 23.0566 18.31C22.9623 18.1455 22.8266 18.0084 22.663 17.9125L21.4668 17.2107L13.2511 22.0276C12.7506 22.321 12.1811 22.4757 11.601 22.4757C11.0209 22.4757 10.4513 22.321 9.95087 22.0276L1.73517 17.2121Z" fill="url(#paint0_linear_4013_8178)"/>
|
||||
<path d="M1.73517 11.4121L0.538918 12.1124C0.3753 12.2084 0.239616 12.3454 0.145332 12.51C0.0510491 12.6746 0.00144577 12.8609 0.00144577 13.0506C0.00144577 13.2403 0.0510491 13.4266 0.145332 13.5912C0.239616 13.7558 0.3753 13.8928 0.538918 13.9887L11.0514 20.1512C11.2181 20.2489 11.4078 20.3003 11.601 20.3003C11.7941 20.3003 11.9838 20.2489 12.1505 20.1512L22.663 13.9887C22.8266 13.8928 22.9623 13.7558 23.0566 13.5912C23.1509 13.4266 23.2005 13.2403 23.2005 13.0506C23.2005 12.8609 23.1509 12.6746 23.0566 12.51C22.9623 12.3454 22.8266 12.2084 22.663 12.1124L21.4668 11.4106L13.2511 16.2275C12.7506 16.5209 12.1811 16.6756 11.601 16.6756C11.0209 16.6756 10.4513 16.5209 9.95087 16.2275L1.73517 11.4121Z" fill="url(#paint1_linear_4013_8178)"/>
|
||||
<path d="M12.152 0.149921C11.9849 0.0517579 11.7947 0 11.601 0C11.4072 0 11.217 0.0517579 11.05 0.149921L0.537472 6.31242C0.373854 6.40835 0.23817 6.5454 0.143887 6.70997C0.0496035 6.87454 0 7.06091 0 7.25057C0 7.44024 0.0496035 7.6266 0.143887 7.79117C0.23817 7.95574 0.373854 8.09279 0.537472 8.18872L11.05 14.3512C11.217 14.4494 11.4072 14.5011 11.601 14.5011C11.7947 14.5011 11.9849 14.4494 12.152 14.3512L22.6645 8.18872C22.8281 8.09279 22.9638 7.95574 23.0581 7.79117C23.1523 7.6266 23.2019 7.44024 23.2019 7.25057C23.2019 7.06091 23.1523 6.87454 23.0581 6.70997C22.9638 6.5454 22.8281 6.40835 22.6645 6.31242L12.152 0.149921Z" fill="url(#paint2_linear_4013_8178)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4013_8178" x1="0.00144577" y1="21.6555" x2="23.2005" y2="21.6555" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4013_8178" x1="0.00144577" y1="15.8555" x2="23.2005" y2="15.8555" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4013_8178" x1="0" y1="7.25057" x2="23.2019" y2="7.25057" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70FDF7"/>
|
||||
<stop offset="0.325" stop-color="#747696"/>
|
||||
<stop offset="0.68" stop-color="#BD5372"/>
|
||||
<stop offset="1" stop-color="#F5A06C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -91,14 +91,14 @@ function Dropdown({
|
||||
{selectedValue && 'label' in selectedValue
|
||||
? selectedValue.label
|
||||
: selectedValue && 'description' in selectedValue
|
||||
? `${
|
||||
selectedValue.value < 1e9
|
||||
? selectedValue.value + ` (${selectedValue.description})`
|
||||
: selectedValue.description
|
||||
}`
|
||||
: placeholder
|
||||
? placeholder
|
||||
: 'From URL'}
|
||||
? `${
|
||||
selectedValue.value < 1e9
|
||||
? selectedValue.value + ` (${selectedValue.description})`
|
||||
: selectedValue.description
|
||||
}`
|
||||
: placeholder
|
||||
? placeholder
|
||||
: 'From URL'}
|
||||
</span>
|
||||
)}
|
||||
<img
|
||||
@@ -128,14 +128,14 @@ function Dropdown({
|
||||
{typeof option === 'string'
|
||||
? option
|
||||
: option.name
|
||||
? option.name
|
||||
: option.label
|
||||
? option.label
|
||||
: `${
|
||||
option.value < 1e9
|
||||
? option.value + ` (${option.description})`
|
||||
: option.description
|
||||
}`}
|
||||
? option.name
|
||||
: option.label
|
||||
? option.label
|
||||
: `${
|
||||
option.value < 1e9
|
||||
? option.value + ` (${option.description})`
|
||||
: option.description
|
||||
}`}
|
||||
</span>
|
||||
{showEdit && onEdit && (
|
||||
<img
|
||||
|
||||
54
frontend/src/components/Sidebar.tsx
Normal file
54
frontend/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
import Exit from '../assets/exit.svg';
|
||||
|
||||
type SidebarProps = {
|
||||
isOpen: boolean;
|
||||
toggleState: (arg0: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function Sidebar({
|
||||
isOpen,
|
||||
toggleState,
|
||||
children,
|
||||
}: SidebarProps) {
|
||||
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
sidebarRef.current &&
|
||||
!sidebarRef.current.contains(event.target as Node)
|
||||
) {
|
||||
toggleState(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={sidebarRef} className="h-vh relative">
|
||||
<div
|
||||
className={`fixed right-0 top-0 z-50 h-full w-72 transform bg-white shadow-xl transition-all duration-300 dark:bg-chinese-black sm:w-96 ${
|
||||
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
|
||||
} border-l border-[#9ca3af]/10`}
|
||||
>
|
||||
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
|
||||
<button
|
||||
className="w-7 rounded-full p-2 hover:bg-gray-1000 hover:dark:bg-gun-metal"
|
||||
onClick={() => toggleState(!isOpen)}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center gap-2 py-4 px-6 text-center">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,8 +30,8 @@ export default function Conversation() {
|
||||
const status = useSelector(selectStatus);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const endMessageRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const conversationRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
||||
const fetchStream = useRef<any>(null);
|
||||
@@ -48,39 +48,15 @@ export default function Conversation() {
|
||||
}, [queries.length, queries[queries.length - 1]]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById('inputbox') as HTMLInputElement;
|
||||
const element = document.getElementById('inputbox') as HTMLTextAreaElement;
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (status !== 'idle') {
|
||||
fetchStream.current && fetchStream.current.abort(); //abort previous stream
|
||||
}
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
const observerCallback: IntersectionObserverCallback = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
setHasScrolledToLast(entry.isIntersecting);
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(observerCallback, {
|
||||
root: null,
|
||||
threshold: [1, 0.8],
|
||||
});
|
||||
if (endMessageRef.current) {
|
||||
observer.observe(endMessageRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [endMessageRef.current]);
|
||||
fetchStream.current && fetchStream.current.abort();
|
||||
}, [conversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
@@ -90,10 +66,16 @@ export default function Conversation() {
|
||||
}, [queries[queries.length - 1]]);
|
||||
|
||||
const scrollIntoView = () => {
|
||||
endMessageRef?.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
if (!conversationRef?.current || eventInterrupt) return;
|
||||
|
||||
if (status === 'idle' || !queries[queries.length - 1].response) {
|
||||
conversationRef.current.scrollTo({
|
||||
behavior: 'smooth',
|
||||
top: conversationRef.current.scrollHeight,
|
||||
});
|
||||
} else {
|
||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuestion = ({
|
||||
@@ -119,14 +101,14 @@ export default function Conversation() {
|
||||
};
|
||||
|
||||
const handleQuestionSubmission = () => {
|
||||
if (inputRef.current?.textContent && status !== 'loading') {
|
||||
if (inputRef.current?.value && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
query: {
|
||||
prompt: inputRef.current.textContent,
|
||||
prompt: inputRef.current.value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -135,9 +117,10 @@ export default function Conversation() {
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({ question: inputRef.current.textContent });
|
||||
handleQuestion({ question: inputRef.current.value });
|
||||
}
|
||||
inputRef.current.textContent = '';
|
||||
inputRef.current.value = '';
|
||||
handleInput();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,7 +129,6 @@ export default function Conversation() {
|
||||
if (query.response) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
||||
key={`${index}ANSWER`}
|
||||
message={query.response}
|
||||
@@ -179,7 +161,6 @@ export default function Conversation() {
|
||||
);
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
||||
key={`${index}ERROR`}
|
||||
message={query.error}
|
||||
@@ -191,14 +172,26 @@ export default function Conversation() {
|
||||
return responseView;
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
inputRef.current && (inputRef.current.innerText = text);
|
||||
const handleInput = () => {
|
||||
if (inputRef.current) {
|
||||
if (window.innerWidth < 350) inputRef.current.style.height = 'auto';
|
||||
else inputRef.current.style.height = '64px';
|
||||
inputRef.current.style.height = `${Math.min(
|
||||
inputRef.current.scrollHeight,
|
||||
96,
|
||||
)}px`;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleInput();
|
||||
window.addEventListener('resize', handleInput);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleInput);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="flex h-screen flex-col gap-7 pb-2">
|
||||
<div className="flex h-[90vh] flex-col gap-7 pb-2 sm:h-[85vh]">
|
||||
{conversationId && (
|
||||
<>
|
||||
<button
|
||||
@@ -225,6 +218,7 @@ export default function Conversation() {
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
ref={conversationRef}
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex h-[90%] w-full flex-1 justify-center overflow-y-auto p-4 md:h-[83vh]"
|
||||
@@ -266,23 +260,22 @@ export default function Conversation() {
|
||||
{queries.length === 0 && <Hero handleQuestion={handleQuestion} />}
|
||||
</div>
|
||||
|
||||
<div className="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-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<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-1/2">
|
||||
<div className="flex w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<textarea
|
||||
id="inputbox"
|
||||
ref={inputRef}
|
||||
tabIndex={1}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
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={`inputbox-style h-16 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
onInput={handleInput}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
></textarea>
|
||||
{status === 'loading' ? (
|
||||
<img
|
||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||
@@ -299,7 +292,7 @@ export default function Conversation() {
|
||||
)}
|
||||
</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">
|
||||
<p className="text-gray-595959 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-bright-gray md:inline md:w-full">
|
||||
{t('tagline')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { forwardRef, useState } from 'react';
|
||||
import Avatar from '../components/Avatar';
|
||||
import CopyButton 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 ReactMarkdown from 'react-markdown';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
import Alert from '../assets/alert.svg';
|
||||
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
||||
import Dislike from '../assets/dislike.svg?react';
|
||||
import Document from '../assets/document.svg';
|
||||
import Like from '../assets/like.svg?react';
|
||||
import Link from '../assets/link.svg';
|
||||
import Sources from '../assets/sources.svg';
|
||||
import Avatar from '../components/Avatar';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import {
|
||||
selectChunks,
|
||||
selectSelectedDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import classes from './ConversationBubble.module.css';
|
||||
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
|
||||
|
||||
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
|
||||
|
||||
const ConversationBubble = forwardRef<
|
||||
@@ -29,15 +39,16 @@ const ConversationBubble = forwardRef<
|
||||
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
|
||||
ref,
|
||||
) {
|
||||
const [openSource, setOpenSource] = useState<number | null>(null);
|
||||
|
||||
const chunks = useSelector(selectChunks);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const [isLikeHovered, setIsLikeHovered] = useState(false);
|
||||
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
|
||||
const [isLikeClicked, setIsLikeClicked] = useState(false);
|
||||
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
|
||||
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||
|
||||
let bubble;
|
||||
|
||||
if (type === 'QUESTION') {
|
||||
bubble = (
|
||||
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
|
||||
@@ -55,18 +66,147 @@ const ConversationBubble = forwardRef<
|
||||
ref={ref}
|
||||
className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`}
|
||||
>
|
||||
<div className="flex flex-wrap self-start lg:flex-nowrap">
|
||||
<Avatar
|
||||
className="mt-2 h-12 w-12 text-2xl"
|
||||
avatar={
|
||||
<img
|
||||
src={DocsGPT3}
|
||||
alt="DocsGPT"
|
||||
className="h-full w-full object-cover"
|
||||
{DisableSourceFE ||
|
||||
type === 'ERROR' ||
|
||||
sources?.length === 0 ||
|
||||
sources?.some((source) => source.source === 'None') ? null : !sources &&
|
||||
chunks !== '0' &&
|
||||
selectedDocs ? (
|
||||
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[26px] w-[30px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt="Sources"
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<p className="text-base font-semibold">Sources</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex h-28 cursor-pointer flex-col items-start gap-1 rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
>
|
||||
<span className="h-px w-10 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-16 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-32 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
<span className="h-px w-20 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
sources && (
|
||||
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[26px] w-[30px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt="Sources"
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="text-base font-semibold">Sources</p>
|
||||
</div>
|
||||
<div className="ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{sources?.slice(0, 3)?.map((source, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div
|
||||
className="h-28 cursor-pointer rounded-[20px] bg-gray-1000 p-4 hover:bg-[#F1F1F1] dark:bg-gun-metal dark:hover:bg-[#2C2E3C]"
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
<p className="ellipsis-text h-12 break-words text-xs">
|
||||
{source.text}
|
||||
</p>
|
||||
<div
|
||||
className={`mt-[14px] flex flex-row items-center gap-[6px] underline-offset-2 ${
|
||||
source.source && source.source !== 'local'
|
||||
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
source.source && source.source !== 'local'
|
||||
? window.open(
|
||||
source.source,
|
||||
'_blank',
|
||||
'noopener, noreferrer',
|
||||
)
|
||||
: null
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Document}
|
||||
alt="Document"
|
||||
className="h-[17px] w-[17px] object-fill"
|
||||
/>
|
||||
<p
|
||||
className="mt-[2px] truncate text-xs"
|
||||
title={
|
||||
source.source && source.source !== 'local'
|
||||
? source.source
|
||||
: source.title
|
||||
}
|
||||
>
|
||||
{source.source && source.source !== 'local'
|
||||
? source.source
|
||||
: source.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{activeTooltip === index && (
|
||||
<div
|
||||
className={`absolute left-1/2 z-30 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver sm:w-56`}
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(sources?.length ?? 0) > 3 && (
|
||||
<div
|
||||
className="flex h-24 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<p className="ellipsis-text h-22 text-xs">{`View ${
|
||||
sources?.length ? sources.length - 3 : 0
|
||||
} more`}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[34px] w-[34px] text-2xl"
|
||||
avatar={
|
||||
<img
|
||||
src={DocsGPT3}
|
||||
alt="DocsGPT"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="text-base font-semibold">Answer</p>
|
||||
</div>
|
||||
<div
|
||||
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
|
||||
type === 'ERROR'
|
||||
@@ -86,15 +226,16 @@ const ConversationBubble = forwardRef<
|
||||
className="whitespace-pre-wrap break-normal leading-normal"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
code(props) {
|
||||
const { children, className, node, ref, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
|
||||
return !inline && match ? (
|
||||
return match ? (
|
||||
<div className="group relative">
|
||||
<SyntaxHighlighter
|
||||
{...rest}
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
{...props}
|
||||
style={vscDarkPlus}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
@@ -109,7 +250,10 @@ const ConversationBubble = forwardRef<
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className ? className : ''} {...props}>
|
||||
<code
|
||||
className={className ? className : 'whitespace-pre-line'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -165,51 +309,9 @@ const ConversationBubble = forwardRef<
|
||||
>
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
{DisableSourceFE ||
|
||||
type === 'ERROR' ||
|
||||
!sources ||
|
||||
sources.length === 0 ? null : (
|
||||
<>
|
||||
<span className="mt-3 h-px w-full bg-[#DEDEDE]"></span>
|
||||
<div className="mt-3 flex w-full flex-row flex-wrap items-center justify-start gap-2">
|
||||
<div className="py-1 text-base font-semibold">Sources:</div>
|
||||
<div className="flex flex-row flex-wrap items-center justify-start gap-2">
|
||||
{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 ${
|
||||
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)
|
||||
}
|
||||
>
|
||||
<p
|
||||
className={`truncate text-center text-base font-medium ${
|
||||
openSource === index
|
||||
? 'text-white'
|
||||
: 'text-[#007DFF]'
|
||||
}`}
|
||||
>
|
||||
{index + 1}. {source.title.substring(0, 45)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 flex justify-start lg:ml-12">
|
||||
<div className="my-2 ml-2 flex justify-start">
|
||||
<div
|
||||
className={`relative mr-5 block items-center justify-center lg:invisible
|
||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
|
||||
@@ -292,19 +394,15 @@ const ConversationBubble = forwardRef<
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sources && openSource !== null && sources[openSource] && (
|
||||
<div className="ml-10 mt-12 max-w-[300px] break-words rounded-xl bg-blue-200 p-2 dark:bg-gun-metal sm:max-w-[800px] lg:mt-2">
|
||||
<p className="m-1 w-3/4 truncate text-xs text-gray-500 dark:text-bright-gray">
|
||||
Source: {sources[openSource].title}
|
||||
</p>
|
||||
|
||||
<div className="m-2 rounded-xl border-2 border-gray-200 bg-white p-2 dark:border-chinese-silver dark:bg-dark-charcoal">
|
||||
<p className="text-break text-black dark:text-bright-gray">
|
||||
{sources[openSource].text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{sources && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
toggleState={(state: boolean) => {
|
||||
setIsSidebarOpen(state);
|
||||
}}
|
||||
>
|
||||
<AllSources sources={sources} />
|
||||
</Sidebar>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -312,4 +410,49 @@ const ConversationBubble = forwardRef<
|
||||
return bubble;
|
||||
});
|
||||
|
||||
type AllSourcesProps = {
|
||||
sources: { title: string; text: string; source: string }[];
|
||||
};
|
||||
|
||||
function AllSources(sources: AllSourcesProps) {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="w-full">
|
||||
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
|
||||
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%] "></div>
|
||||
</div>
|
||||
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
|
||||
{sources.sources.map((source, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]"
|
||||
>
|
||||
<span className="flex flex-row">
|
||||
<p
|
||||
title={source.title}
|
||||
className="ellipsis-text break-words text-left text-sm font-semibold"
|
||||
>
|
||||
{`${index + 1}. ${source.title}`}
|
||||
</p>
|
||||
{source.source && source.source !== 'local' ? (
|
||||
<img
|
||||
src={Link}
|
||||
alt="Link"
|
||||
className="h-3 w-3 cursor-pointer object-fill"
|
||||
onClick={() =>
|
||||
window.open(source.source, '_blank', 'noopener, noreferrer')
|
||||
}
|
||||
></img>
|
||||
) : null}
|
||||
</span>
|
||||
<p className="mt-3 max-h-24 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConversationBubble;
|
||||
|
||||
@@ -48,7 +48,8 @@ export default function ConversationTile({
|
||||
setConversationsName(conversation.name);
|
||||
}, [conversation.name]);
|
||||
|
||||
function handleEditConversation() {
|
||||
function handleEditConversation(event: SyntheticEvent) {
|
||||
event.stopPropagation();
|
||||
setIsEdit(true);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export const SharedConversation = () => {
|
||||
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
||||
<div className="flex w-full justify-center overflow-auto">
|
||||
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
|
||||
<div className="mb-2 w-full border-b pb-2">
|
||||
<div className="mb-2 w-full border-b pb-2 dark:border-b-silver">
|
||||
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
||||
{title}
|
||||
</h1>
|
||||
@@ -279,10 +279,10 @@ export const SharedConversation = () => {
|
||||
{t('sharedConv.button')}
|
||||
</button>
|
||||
)}
|
||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,6 +116,7 @@ export function handleFetchAnswerSteaming(
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
},
|
||||
signal,
|
||||
)
|
||||
@@ -184,6 +185,7 @@ export function handleSearch(
|
||||
history,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
|
||||
@@ -62,7 +62,7 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
index: state.conversation.queries.length - 1,
|
||||
query: { sources },
|
||||
query: { sources: sources ?? [] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -70,35 +70,18 @@ export function useDarkTheme() {
|
||||
localStorage.getItem('selectedTheme') === 'Dark' || false,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if dark mode preference exists in local storage
|
||||
const savedMode: string | null = localStorage.getItem('selectedTheme');
|
||||
|
||||
// Set dark mode based on local storage preference
|
||||
if (savedMode === 'Dark') {
|
||||
setIsDarkTheme(true);
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
} else {
|
||||
// If no preference found, set to default (light mode)
|
||||
setIsDarkTheme(false);
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
|
||||
if (isDarkTheme) {
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
document.body?.classList.add('dark');
|
||||
} else {
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
document.body?.classList.remove('dark');
|
||||
}
|
||||
}, [isDarkTheme]);
|
||||
//method to toggle theme
|
||||
|
||||
const toggleTheme: any = () => {
|
||||
setIsDarkTheme(!isDarkTheme);
|
||||
};
|
||||
|
||||
return [isDarkTheme, toggleTheme];
|
||||
}
|
||||
|
||||
30
frontend/src/hooks/useDefaultDocument.ts
Normal file
30
frontend/src/hooks/useDefaultDocument.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Doc, getDocs } from '../preferences/preferenceApi';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
setSelectedDocs,
|
||||
setSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
|
||||
export default function useDefaultDocument() {
|
||||
const dispatch = useDispatch();
|
||||
const selectedDoc = useSelector(selectSelectedDocs);
|
||||
|
||||
const fetchDocs = () => {
|
||||
getDocs().then((data) => {
|
||||
dispatch(setSourceDocs(data));
|
||||
if (!selectedDoc)
|
||||
data?.forEach((doc: Doc) => {
|
||||
if (doc.model && doc.name === 'default') {
|
||||
dispatch(setSelectedDocs(doc));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchDocs();
|
||||
}, []);
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body.dark {
|
||||
background-color: #202124; /* raisin-black */
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@@ -421,7 +424,20 @@ template {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.inputbox-style[contenteditable] {
|
||||
.inputbox-style {
|
||||
resize: none;
|
||||
padding-left: 36px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.bottom-safe {
|
||||
bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.ellipsis-text {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"tokenUsage": "Token Usage"
|
||||
},
|
||||
"apiKeys": {
|
||||
"label": "API Keys",
|
||||
"label": "Chatbots",
|
||||
"name": "Name",
|
||||
"key": "API Key",
|
||||
"sourceDoc": "Source Document",
|
||||
@@ -71,7 +71,7 @@
|
||||
"remote": "Remote",
|
||||
"name": "Name",
|
||||
"choose": "Choose Files",
|
||||
"info": "Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb",
|
||||
"info": "Please upload .pdf, .txt, .rst, .csv, .docx, .md, .zip limited to 25mb",
|
||||
"uploadedFiles": "Uploaded Files",
|
||||
"cancel": "Cancel",
|
||||
"train": "Train",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"tokenUsage": "Uso de Tokens"
|
||||
},
|
||||
"apiKeys": {
|
||||
"label": "Claves API",
|
||||
"label": "Chatbots",
|
||||
"name": "Nombre",
|
||||
"key": "Clave de API",
|
||||
"sourceDoc": "Documento Fuente",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"tokenUsage": "トークン使用量"
|
||||
},
|
||||
"apiKeys": {
|
||||
"label": "APIキー",
|
||||
"label": "チャットボット",
|
||||
"name": "名前",
|
||||
"key": "APIキー",
|
||||
"sourceDoc": "ソースドキュメント",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"tokenUsage": "令牌使用"
|
||||
},
|
||||
"apiKeys": {
|
||||
"label": "API 密钥",
|
||||
"label": "聊天机器人",
|
||||
"name": "名称",
|
||||
"key": "API 密钥",
|
||||
"sourceDoc": "源文档",
|
||||
|
||||
@@ -19,15 +19,11 @@ export default function DeleteConvModal({
|
||||
const dispatch = useDispatch();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const { t } = useTranslation();
|
||||
useOutsideAlerter(
|
||||
modalRef,
|
||||
() => {
|
||||
if (isMobile && modalState === 'ACTIVE') {
|
||||
dispatch(setModalState('INACTIVE'));
|
||||
}
|
||||
},
|
||||
[modalState],
|
||||
);
|
||||
useOutsideAlerter(modalRef, () => {
|
||||
if (isMobile && modalState === 'ACTIVE') {
|
||||
dispatch(setModalState('INACTIVE'));
|
||||
}
|
||||
}, [modalState]);
|
||||
|
||||
function handleSubmit() {
|
||||
handleDeleteAllConv();
|
||||
|
||||
@@ -22,15 +22,11 @@ export default function APIKeyModal({
|
||||
const modalRef = useRef(null);
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
useOutsideAlerter(
|
||||
modalRef,
|
||||
() => {
|
||||
if (isMobile && modalState === 'ACTIVE') {
|
||||
setModalState('INACTIVE');
|
||||
}
|
||||
},
|
||||
[modalState],
|
||||
);
|
||||
useOutsideAlerter(modalRef, () => {
|
||||
if (isMobile && modalState === 'ACTIVE') {
|
||||
setModalState('INACTIVE');
|
||||
}
|
||||
}, [modalState]);
|
||||
|
||||
function handleSubmit() {
|
||||
if (key.length <= 1) {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Modal from '../modals';
|
||||
import {
|
||||
setSelectedDocs,
|
||||
setSourceDocs,
|
||||
selectSourceDocs,
|
||||
selectSelectedDocs,
|
||||
} from './preferenceSlice';
|
||||
import { getDocs, Doc } from './preferenceApi';
|
||||
|
||||
export default function APIKeyModal({
|
||||
modalState,
|
||||
setModalState,
|
||||
isCancellable = true,
|
||||
}: {
|
||||
modalState: ActiveState;
|
||||
setModalState: (val: ActiveState) => void;
|
||||
isCancellable?: boolean;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const docs = useSelector(selectSourceDocs);
|
||||
const selectedDoc = useSelector(selectSelectedDocs);
|
||||
const [localSelectedDocs, setLocalSelectedDocs] = useState<Doc | null>(
|
||||
selectedDoc,
|
||||
);
|
||||
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!localSelectedDocs) {
|
||||
setIsError(true);
|
||||
} else {
|
||||
dispatch(setSelectedDocs(localSelectedDocs));
|
||||
setModalState('INACTIVE');
|
||||
setIsError(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setIsError(false);
|
||||
setModalState('INACTIVE');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function requestDocs() {
|
||||
const data = await getDocs();
|
||||
dispatch(setSourceDocs(data));
|
||||
}
|
||||
|
||||
requestDocs();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
handleSubmit={handleSubmit}
|
||||
isCancellable={isCancellable}
|
||||
handleCancel={handleCancel}
|
||||
modalState={modalState}
|
||||
errorMessage="Please select Source Documentation"
|
||||
isError={isError}
|
||||
render={() => {
|
||||
return (
|
||||
<article className="mx-auto mt-24 flex w-[90vw] max-w-lg flex-col gap-4 rounded-t-lg bg-white p-6 shadow-lg">
|
||||
<p className="text-xl text-jet">Select Source Documentation</p>
|
||||
<p className="text-lg leading-5 text-gray-500">
|
||||
Please select the library of documentation that you would like to
|
||||
use with our app.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="h-10 w-full cursor-pointer border-b-2"
|
||||
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
|
||||
>
|
||||
{!localSelectedDocs ? (
|
||||
<p className="py-3 text-gray-500">Select</p>
|
||||
) : (
|
||||
<p className="py-3">
|
||||
{localSelectedDocs.name} {localSelectedDocs.version}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isDocsListOpen && (
|
||||
<div className="absolute top-10 left-0 max-h-52 w-full overflow-y-scroll bg-white">
|
||||
{docs ? (
|
||||
docs.map((doc, index) => {
|
||||
if (doc.model) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setLocalSelectedDocs(doc);
|
||||
setIsDocsListOpen(false);
|
||||
}}
|
||||
className="h-10 w-full cursor-pointer border-x-2 border-b-2 hover:bg-gray-100"
|
||||
>
|
||||
<p className="ml-5 py-3">
|
||||
{doc.name} {doc.version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<div className="h-10 w-full cursor-pointer border-x-2 border-b-2 hover:bg-gray-100">
|
||||
<p className="ml-5 py-3">No default documentation.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -76,17 +76,17 @@ export function setLocalPrompt(prompt: string): void {
|
||||
localStorage.setItem('DocsGPTPrompt', prompt);
|
||||
}
|
||||
|
||||
export function setLocalRecentDocs(doc: Doc): void {
|
||||
export function setLocalRecentDocs(doc: Doc | null): void {
|
||||
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(doc));
|
||||
let namePath = doc.name;
|
||||
if (doc.language === namePath) {
|
||||
let namePath = doc?.name;
|
||||
if (doc?.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
|
||||
let docPath = 'default';
|
||||
if (doc.location === 'local') {
|
||||
if (doc?.location === 'local') {
|
||||
docPath = 'local' + '/' + doc.name + '/';
|
||||
} else if (doc.location === 'remote') {
|
||||
} else if (doc?.location === 'remote') {
|
||||
docPath =
|
||||
doc.language + '/' + namePath + '/' + doc.version + '/' + doc.model + '/';
|
||||
}
|
||||
|
||||
@@ -95,8 +95,7 @@ prefListenerMiddleware.startListening({
|
||||
matcher: isAnyOf(setSelectedDocs),
|
||||
effect: (action, listenerApi) => {
|
||||
setLocalRecentDocs(
|
||||
(listenerApi.getState() as RootState).preference.selectedDocs ??
|
||||
([] as unknown as Doc),
|
||||
(listenerApi.getState() as RootState).preference.selectedDocs ?? null,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function APIKeys() {
|
||||
}, []);
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="flex w-full flex-col lg:w-max">
|
||||
<div className="flex flex-col max-w-[876px]">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => setCreateModal(true)}
|
||||
@@ -103,7 +103,7 @@ export default function APIKeys() {
|
||||
<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]">
|
||||
<th className="w-[244px] border-r p-4">
|
||||
{t('settings.apiKeys.name')}
|
||||
</th>
|
||||
<th className="w-[244px] border-r px-4 py-2">
|
||||
|
||||
@@ -30,7 +30,7 @@ function Upload({
|
||||
const [activeTab, setActiveTab] = useState<string>('file');
|
||||
const [files, setfiles] = useState<File[]>([]);
|
||||
const [progress, setProgress] = useState<{
|
||||
type: 'UPLOAD' | 'TRAINIING';
|
||||
type: 'UPLOAD' | 'TRAINING';
|
||||
percentage: number;
|
||||
taskId?: string;
|
||||
failed?: boolean;
|
||||
@@ -61,10 +61,10 @@ function Upload({
|
||||
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 `}
|
||||
className={`h-8 overflow-hidden rounded-xl border border-purple-30 text-xs text-bright-gray outline-none `}
|
||||
>
|
||||
<div
|
||||
className={`h-full border-none p-1 w-${
|
||||
className={`h-full border-none text-xl w-${
|
||||
progress || 0
|
||||
}% flex items-center justify-center bg-purple-30 outline-none transition-all`}
|
||||
style={{ width: `${progressPercent || 0}%` }}
|
||||
@@ -86,15 +86,13 @@ function Upload({
|
||||
isFailed?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-5 flex flex-col items-center gap-2">
|
||||
<p className="text-xl tracking-[0.15px]">{title}...</p>
|
||||
<p className="text-sm text-gray-2000">This may take several minutes</p>
|
||||
<div className="mt-5 flex flex-col items-center gap-2 text-gray-2000 dark:text-bright-gray">
|
||||
<p className="text-gra text-xl tracking-[0.15px]">{title}...</p>
|
||||
<p className="text-sm">This may take several minutes</p>
|
||||
<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> */}
|
||||
|
||||
{/* progress bar */}
|
||||
<ProgressBar progressPercent={progress?.percentage as number} />
|
||||
</div>
|
||||
);
|
||||
@@ -208,7 +206,7 @@ function Upload({
|
||||
xhr.onload = () => {
|
||||
const { task_id } = JSON.parse(xhr.responseText);
|
||||
setTimeoutRef.current = setTimeout(() => {
|
||||
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
|
||||
setProgress({ type: 'TRAINING', percentage: 0, taskId: task_id });
|
||||
}, 3000);
|
||||
};
|
||||
xhr.open('POST', `${apiHost + '/api/upload'}`);
|
||||
@@ -239,7 +237,7 @@ function Upload({
|
||||
xhr.onload = () => {
|
||||
const { task_id } = JSON.parse(xhr.responseText);
|
||||
setTimeoutRef.current = setTimeout(() => {
|
||||
setProgress({ type: 'TRAINIING', percentage: 0, taskId: task_id });
|
||||
setProgress({ type: 'TRAINING', percentage: 0, taskId: task_id });
|
||||
}, 3000);
|
||||
};
|
||||
xhr.open('POST', `${apiHost + '/api/remote'}`);
|
||||
@@ -261,6 +259,7 @@ function Upload({
|
||||
'application/zip': ['.zip'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
['.docx'],
|
||||
'text/csv': ['.csv'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -284,7 +283,7 @@ function Upload({
|
||||
|
||||
if (progress?.type === 'UPLOAD') {
|
||||
view = <UploadProgress></UploadProgress>;
|
||||
} else if (progress?.type === 'TRAINIING') {
|
||||
} else if (progress?.type === 'TRAINING') {
|
||||
view = <TrainingProgress></TrainingProgress>;
|
||||
} else {
|
||||
view = (
|
||||
|
||||
@@ -9,7 +9,7 @@ javalang==0.13.0
|
||||
langchain==0.1.4
|
||||
langchain_community==0.2.9
|
||||
langchain-openai==0.0.5
|
||||
nltk==3.8.1
|
||||
nltk==3.9
|
||||
openapi3_parser==1.1.16
|
||||
pandas==2.2.0
|
||||
PyPDF2==3.0.1
|
||||
|
||||
Reference in New Issue
Block a user