Compare commits

..

57 Commits
0.8.0 ... 0.8.1

Author SHA1 Message Date
Alex
e07df29ab9 Update FLASK_DEBUG_MODE setting to use value from settings module 2024-04-08 13:27:43 +01:00
Alex
abf24fe60f Update FLASK_DEBUG_MODE setting to use value from settings module 2024-04-08 13:15:58 +01:00
Alex
6911f8652a Fix vectorstore path in check_docs function 2024-04-08 13:06:05 +01:00
Alex
6658cec6a0 Merge pull request #897 from arc53/dependabot/npm_and_yarn/frontend/vite-5.0.13
Bump vite from 5.0.12 to 5.0.13 in /frontend
2024-04-08 13:03:20 +01:00
Alex
14011b9d84 Merge pull request #891 from arc53/dependabot/npm_and_yarn/mock-backend/express-4.19.2
Bump express from 4.18.2 to 4.19.2 in /mock-backend
2024-04-08 13:02:58 +01:00
Alex
bd2d0b6790 Merge pull request from GHSA-p5qc-vj2x-9rjp
advisory-fix
2024-04-08 12:58:36 +01:00
Alex
d36f58230a advisory-fix 2024-04-08 12:56:27 +01:00
Alex
018f950ca3 Merge pull request #908 from arc53/api-keys-documentation-guide
API keys guide
2024-04-08 10:36:35 +01:00
Alex
db8db9fae9 Add prompt_id and chunks fields in create_api_key function 2024-04-08 10:35:15 +01:00
Pavel
79ce8d6563 guide 2024-04-07 20:14:16 +04:00
Alex
13eaa9a35a Merge pull request #904 from arc53/fix/update-docs-widget
Update api key to new data
2024-04-06 11:39:32 +01:00
Alex
6e147b3ed2 Update api key to new data 2024-04-05 14:49:32 +01:00
Alex
c162f79daa Merge pull request #903 from arc53/feature/api-key-create
Feature/api key create
2024-04-05 13:18:11 +01:00
Alex
87585be687 Merge branch 'main' into feature/api-key-create 2024-04-05 13:01:42 +01:00
Alex
ea08d6413c Merge pull request #902 from ManishMadan2882/feature/api-key-create
Add Prompt, Chunks in Create Key
2024-04-04 12:45:33 +01:00
Alex
879905edf6 Refactor create_api_key function to include prompt_id and chunks in routes.py 2024-04-04 12:38:23 +01:00
Alex
6fd80a5582 Merge pull request #899 from siiddhantt/main
feat: added prompts section under general in settings
2024-04-04 10:25:08 +01:00
Siddhant Rai
0dc7333563 fix: added API Keys in tabs 2024-04-04 14:42:14 +05:30
Siddhant Rai
f61c3168d2 fix: issue with editing new prompts 2024-04-04 14:29:37 +05:30
Siddhant Rai
9cadd74a96 fix: minor ui changes 2024-04-04 13:42:32 +05:30
Siddhant Rai
729fa2352b feat: added prompts section under general in settings 2024-04-04 00:48:49 +05:30
dependabot[bot]
b673aaf9f0 Bump vite from 5.0.12 to 5.0.13 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.0.13.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.13/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:53:09 +00:00
Alex
3132cc6005 Merge pull request #895 from sarfarazsiddiquii/new_branch
added feature #887
2024-04-03 17:09:06 +01:00
ManishMadan2882
ac994d3077 add prompt,chunks in create key 2024-04-03 19:19:53 +05:30
sarfaraz siddiqui
02d4f7f2da functions can accept null 2024-04-03 18:08:46 +05:30
Alex
d99569f005 Merge pull request #896 from arc53/api-update
api update
2024-04-01 18:22:13 +01:00
Pavel
ec5166249a api update
A description of 3 more api methods in documentation.
2024-04-01 21:07:27 +04:00
Alex
dadd12adb3 Update API key in DocsGPTWidget.tsx 2024-04-01 11:25:59 +01:00
Alex
88b4fb8c2a Update API key in DocsGPTWidget.tsx 2024-04-01 11:25:31 +01:00
sarfaraz siddiqui
afecae3786 added feature #887 2024-03-31 03:50:11 +05:30
Alex
d18598bc33 Merge pull request #894 from arc53/feature/api-key-create
Feature/api key create
2024-03-29 20:04:26 +00:00
Alex
794fc05ada Merge branch 'main' into feature/api-key-create 2024-03-29 19:59:45 +00:00
Alex
5daeb7f876 Merge pull request #892 from ManishMadan2882/feature/api-key-create
Feature/api key create
2024-03-29 19:57:25 +00:00
ManishMadan2882
53e71c545e api key modal - enhancements 2024-03-29 19:11:40 +05:30
ManishMadan2882
959a55e36c adding dark mode - api key 2024-03-29 04:13:12 +05:30
ManishMadan2882
64572b0024 feat(settings): api key endpoints 2024-03-29 03:26:45 +05:30
Manish Madan
9a0c1caa43 Merge branch 'arc53:feature/api-key-create' into feature/api-key-create 2024-03-28 19:28:23 +05:30
ManishMadan2882
eed6723147 feat(settings): api keys tab 2024-03-28 19:25:35 +05:30
Alex
97fabf51b8 Refactor conversationSlice.ts and conversationApi.ts 2024-03-28 13:43:10 +00:00
Alex
5e5e2b8aee Merge pull request #877 from siiddhantt/main
Added reddit loader
2024-03-27 16:55:01 +00:00
Siddhant Rai
e01071426f feat: field to pass number of posts as a parameter 2024-03-27 19:20:55 +05:30
Siddhant Rai
eed1bfbe50 feat: fields to handle reddit loader + minor changes 2024-03-26 16:07:44 +05:30
Siddhant Rai
0c3970a266 Merge branch 'arc53:main' into main 2024-03-26 16:07:25 +05:30
dependabot[bot]
267cfb621e Bump express from 4.18.2 to 4.19.2 in /mock-backend
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 10:25:02 +00:00
Alex
0e90febab2 Merge pull request #890 from arc53/dependabot/npm_and_yarn/docs/katex-0.16.10
Bump katex from 0.16.9 to 0.16.10 in /docs
2024-03-26 10:24:19 +00:00
dependabot[bot]
31d947837f Bump katex from 0.16.9 to 0.16.10 in /docs
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.9 to 0.16.10.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.9...v0.16.10)

---
updated-dependencies:
- dependency-name: katex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 20:31:43 +00:00
Alex
017b11fbba Merge pull request #888 from arc53/fix/parsing-chunks-issue
Fix parsing issue with chunks in store.ts
2024-03-23 11:47:11 +00:00
Alex
3c492062a9 Fix parsing issue with chunks in store.ts 2024-03-23 11:42:50 +00:00
Alex
b26b49d0ca Merge pull request #883 from arc53/feat/chunks
Add support for setting the number of chunks processed per query
2024-03-22 15:34:09 +00:00
Alex
ed08123550 Add support for setting the number of chunks processed per query 2024-03-22 14:50:56 +00:00
Alex
add2db5b7a Merge pull request #881 from arc53/fix_model_selection_for_openai
Fix model selection at least for openAI LLM_NAME
2024-03-21 16:47:52 +00:00
Siddhant Rai
f272d7121a Merge branch 'arc53:main' into main 2024-03-21 19:38:44 +05:30
Anton Larin
577556678c Fix model selection at least for openAI LLM_NAME 2024-03-21 10:14:48 +01:00
Alex
e146922367 Merge pull request #880 from ManishMadan2882/main
Customised Scrollbar, fixed: Hero wasn't completely scrollable in Mobile
2024-03-20 10:08:22 +00:00
ManishMadan2882
6f1548b7f8 customised scrollbar 2024-03-19 21:40:00 +05:30
ManishMadan2882
9e6fe47b44 fix(hero): not fully scrollable in mobile 2024-03-19 21:39:16 +05:30
Siddhant Rai
60cfea1126 feat: added reddit loader 2024-03-16 20:22:05 +05:30
32 changed files with 1505 additions and 480 deletions

1
.gitignore vendored
View File

@@ -75,6 +75,7 @@ target/
# Jupyter Notebook
.ipynb_checkpoints
**/*.ipynb
# IPython
profile_default/

View File

@@ -26,14 +26,18 @@ db = mongo["docsgpt"]
conversations_collection = db["conversations"]
vectors_collection = db["vectors"]
prompts_collection = db["prompts"]
api_key_collection = db["api_keys"]
answer = Blueprint('answer', __name__)
if settings.LLM_NAME == "gpt4":
gpt_model = 'gpt-4'
gpt_model = ""
# to have some kind of default behaviour
if settings.LLM_NAME == "openai":
gpt_model = 'gpt-3.5-turbo'
elif settings.LLM_NAME == "anthropic":
gpt_model = 'claude-2'
else:
gpt_model = 'gpt-3.5-turbo'
if settings.MODEL_NAME: # in case there is particular model name configured
gpt_model = settings.MODEL_NAME
# load the prompts
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -74,6 +78,12 @@ def run_async_chain(chain, question, chat_history):
result["answer"] = answer
return result
def get_data_from_api_key(api_key):
data = api_key_collection.find_one({"key": api_key})
if data is None:
return bad_request(401, "Invalid API key")
return data
def get_vectorstore(data):
if "active_docs" in data:
@@ -95,9 +105,8 @@ def is_azure_configured():
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME
def complete_stream(question, docsearch, chat_history, api_key, prompt_id, conversation_id):
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=api_key)
def complete_stream(question, docsearch, chat_history, prompt_id, conversation_id, chunks=2):
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=settings.API_KEY)
if prompt_id == 'default':
prompt = chat_combine_template
elif prompt_id == 'creative':
@@ -106,8 +115,11 @@ def complete_stream(question, docsearch, chat_history, api_key, prompt_id, conve
prompt = chat_combine_strict
else:
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
docs = docsearch.search(question, k=2)
if chunks == 0:
docs = []
else:
docs = docsearch.search(question, k=chunks)
if settings.LLM_NAME == "llama.cpp":
docs = [docs[0]]
# join all page_content together with a newline
@@ -182,36 +194,43 @@ def stream():
data = request.get_json()
# get parameter from url question
question = data["question"]
history = data["history"]
# history to json object from string
history = json.loads(history)
conversation_id = data["conversation_id"]
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
# check if active_docs is set
if not api_key_set:
api_key = data["api_key"]
else:
api_key = settings.API_KEY
if not embeddings_key_set:
embeddings_key = data["embeddings_key"]
else:
embeddings_key = settings.EMBEDDINGS_KEY
if "active_docs" in data:
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
vectorstore = get_vectorstore({"active_docs": data_key["source"]})
elif "active_docs" in data:
vectorstore = get_vectorstore({"active_docs": data["active_docs"]})
else:
vectorstore = ""
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, settings.EMBEDDINGS_KEY)
return Response(
complete_stream(question, docsearch,
chat_history=history, api_key=api_key,
chat_history=history,
prompt_id=prompt_id,
conversation_id=conversation_id), mimetype="text/event-stream"
conversation_id=conversation_id,
chunks=chunks), mimetype="text/event-stream"
)
@@ -219,24 +238,23 @@ def stream():
def api_answer():
data = request.get_json()
question = data["question"]
history = data["history"]
if "history" not in data:
history = []
else:
history = data["history"]
if "conversation_id" not in data:
conversation_id = None
else:
conversation_id = data["conversation_id"]
print("-" * 5)
if not api_key_set:
api_key = data["api_key"]
else:
api_key = settings.API_KEY
if not embeddings_key_set:
embeddings_key = data["embeddings_key"]
else:
embeddings_key = settings.EMBEDDINGS_KEY
if 'prompt_id' in data:
prompt_id = data["prompt_id"]
else:
prompt_id = 'default'
if 'chunks' in data:
chunks = int(data["chunks"])
else:
chunks = 2
if prompt_id == 'default':
prompt = chat_combine_template
@@ -250,17 +268,24 @@ def api_answer():
# use try and except to check for exception
try:
# check if the vectorstore is set
vectorstore = get_vectorstore(data)
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
vectorstore = get_vectorstore({"active_docs": data_key["source"]})
else:
vectorstore = get_vectorstore(data)
# loading the index and the store and the prompt template
# Note if you have used other embeddings than OpenAI, you need to change the embeddings
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, settings.EMBEDDINGS_KEY)
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=api_key)
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=settings.API_KEY)
docs = docsearch.search(question, k=2)
if chunks == 0:
docs = []
else:
docs = docsearch.search(question, k=chunks)
# join all page_content together with a newline
docs_together = "\n".join([doc.page_content for doc in docs])
p_chat_combine = prompt.replace("{summaries}", docs_together)
@@ -348,20 +373,22 @@ def api_search():
# get parameter from url question
question = data["question"]
if not embeddings_key_set:
if "embeddings_key" in data:
embeddings_key = data["embeddings_key"]
else:
embeddings_key = settings.EMBEDDINGS_KEY
else:
embeddings_key = settings.EMBEDDINGS_KEY
if "active_docs" in data:
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
vectorstore = data_key["source"]
elif "active_docs" in data:
vectorstore = get_vectorstore({"active_docs": data["active_docs"]})
else:
vectorstore = ""
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
docs = docsearch.search(question, k=2)
if 'chunks' in data:
chunks = int(data["chunks"])
else:
chunks = 2
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, settings.EMBEDDINGS_KEY)
if chunks == 0:
docs = []
else:
docs = docsearch.search(question, k=chunks)
source_log_docs = []
for doc in docs:

View File

@@ -1,5 +1,7 @@
import os
import uuid
from flask import Blueprint, request, jsonify
from urllib.parse import urlparse
import requests
from pymongo import MongoClient
from bson.objectid import ObjectId
@@ -16,6 +18,7 @@ conversations_collection = db["conversations"]
vectors_collection = db["vectors"]
prompts_collection = db["prompts"]
feedback_collection = db["feedback"]
api_key_collection = db["api_keys"]
user = Blueprint('user', __name__)
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -245,25 +248,31 @@ def check_docs():
# split docs on / and take first part
if data["docs"].split("/")[0] == "local":
return {"status": "exists"}
vectorstore = "vectors/" + data["docs"]
vectorstore = "vectors/" + secure_filename(data["docs"])
base_path = "https://raw.githubusercontent.com/arc53/DocsHUB/main/"
if os.path.exists(vectorstore) or data["docs"] == "default":
return {"status": "exists"}
else:
r = requests.get(base_path + vectorstore + "index.faiss")
file_url = urlparse(base_path + vectorstore + "index.faiss")
if file_url.scheme in ['https'] and file_url.netloc == 'raw.githubusercontent.com' and file_url.path.startswith('/arc53/DocsHUB/main/'):
r = requests.get(file_url.geturl())
if r.status_code != 200:
return {"status": "null"}
if r.status_code != 200:
return {"status": "null"}
else:
if not os.path.exists(vectorstore):
os.makedirs(vectorstore)
with open(vectorstore + "index.faiss", "wb") as f:
f.write(r.content)
# download the store
r = requests.get(base_path + vectorstore + "index.pkl")
with open(vectorstore + "index.pkl", "wb") as f:
f.write(r.content)
else:
if not os.path.exists(vectorstore):
os.makedirs(vectorstore)
with open(vectorstore + "index.faiss", "wb") as f:
f.write(r.content)
# download the store
r = requests.get(base_path + vectorstore + "index.pkl")
with open(vectorstore + "index.pkl", "wb") as f:
f.write(r.content)
return {"status": "null"}
return {"status": "loaded"}
@@ -343,5 +352,52 @@ def update_prompt_name():
@user.route("/api/get_api_keys", methods=["GET"])
def get_api_keys():
user = "local"
keys = api_key_collection.find({"user": user})
list_keys = []
for key in keys:
list_keys.append({
"id": str(key["_id"]),
"name": key["name"],
"key": key["key"][:4] + "..." + key["key"][-4:],
"source": key["source"],
"prompt_id": key["prompt_id"],
"chunks": key["chunks"]
})
return jsonify(list_keys)
@user.route("/api/create_api_key", methods=["POST"])
def create_api_key():
data = request.get_json()
name = data["name"]
source = data["source"]
prompt_id = data["prompt_id"]
chunks = data["chunks"]
key = str(uuid.uuid4())
user = "local"
resp = api_key_collection.insert_one(
{
"name": name,
"key": key,
"source": source,
"user": user,
"prompt_id": prompt_id,
"chunks": chunks
}
)
new_id = str(resp.inserted_id)
return {"id": new_id, "key": key}
@user.route("/api/delete_api_key", methods=["POST"])
def delete_api_key():
data = request.get_json()
id = data["id"]
api_key_collection.delete_one(
{
"_id": ObjectId(id),
}
)
return {"status": "ok"}

View File

@@ -40,5 +40,5 @@ def after_request(response):
return response
if __name__ == "__main__":
app.run(debug=True, port=7091)
app.run(debug=settings.FLASK_DEBUG_MODE, port=7091)

View File

@@ -9,6 +9,7 @@ current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__
class Settings(BaseSettings):
LLM_NAME: str = "docsgpt"
MODEL_NAME: Optional[str] = None # when LLM_NAME is openai, MODEL_NAME can be e.g. gpt-4-turbo-preview or gpt-3.5-turbo
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
@@ -58,6 +59,8 @@ class Settings(BaseSettings):
QDRANT_PATH: Optional[str] = None
QDRANT_DISTANCE_FUNC: str = "Cosine"
FLASK_DEBUG_MODE: bool = False
path = Path(__file__).parent.parent.absolute()
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")

View File

@@ -0,0 +1,26 @@
from application.parser.remote.base import BaseRemote
from langchain_community.document_loaders import RedditPostsLoader
class RedditPostsLoaderRemote(BaseRemote):
def load_data(self, inputs):
data = eval(inputs)
client_id = data.get("client_id")
client_secret = data.get("client_secret")
user_agent = data.get("user_agent")
categories = data.get("categories", ["new", "hot"])
mode = data.get("mode", "subreddit")
search_queries = data.get("search_queries")
number_posts = data.get("number_posts", 10)
self.loader = RedditPostsLoader(
client_id=client_id,
client_secret=client_secret,
user_agent=user_agent,
categories=categories,
mode=mode,
search_queries=search_queries,
number_posts=number_posts,
)
documents = self.loader.load()
print(f"Loaded {len(documents)} documents from Reddit")
return documents

View File

@@ -1,13 +1,15 @@
from application.parser.remote.sitemap_loader import SitemapLoader
from application.parser.remote.crawler_loader import CrawlerLoader
from application.parser.remote.web_loader import WebLoader
from application.parser.remote.reddit_loader import RedditPostsLoaderRemote
class RemoteCreator:
loaders = {
'url': WebLoader,
'sitemap': SitemapLoader,
'crawler': CrawlerLoader
"url": WebLoader,
"sitemap": SitemapLoader,
"crawler": CrawlerLoader,
"reddit": RedditPostsLoaderRemote,
}
@classmethod
@@ -15,4 +17,4 @@ class RemoteCreator:
loader_class = cls.loaders.get(type.lower())
if not loader_class:
raise ValueError(f"No LLM class found for type {type}")
return loader_class(*args, **kwargs)
return loader_class(*args, **kwargs)

View File

@@ -15,23 +15,27 @@ from application.parser.schema.base import Document
from application.parser.token_func import group_split
try:
nltk.download('punkt', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
nltk.download("punkt", quiet=True)
nltk.download("averaged_perceptron_tagger", quiet=True)
except FileExistsError:
pass
# Define a function to extract metadata from a given filename.
def metadata_from_filename(title):
store = '/'.join(title.split('/')[1:3])
return {'title': title, 'store': store}
store = "/".join(title.split("/")[1:3])
return {"title": title, "store": store}
# Define a function to generate a random string of a given length.
def generate_random_string(length):
return ''.join([string.ascii_letters[i % 52] for i in range(length)])
return "".join([string.ascii_letters[i % 52] for i in range(length)])
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Define the main function for ingesting and processing documents.
def ingest_worker(self, directory, formats, name_job, filename, user):
@@ -62,38 +66,52 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
token_check = True
min_tokens = 150
max_tokens = 1250
full_path = directory + '/' + user + '/' + name_job
full_path = directory + "/" + user + "/" + name_job
import sys
print(full_path, file=sys.stderr)
# check if API_URL env variable is set
file_data = {'name': name_job, 'file': filename, 'user': user}
response = requests.get(urljoin(settings.API_URL, "/api/download"), params=file_data)
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):
os.makedirs(full_path)
with open(full_path + '/' + filename, 'wb') as f:
with open(full_path + "/" + filename, "wb") as f:
f.write(file)
# check if file is .zip and extract it
if filename.endswith('.zip'):
with zipfile.ZipFile(full_path + '/' + filename, 'r') as zip_ref:
if filename.endswith(".zip"):
with zipfile.ZipFile(full_path + "/" + filename, "r") as zip_ref:
zip_ref.extractall(full_path)
os.remove(full_path + '/' + filename)
os.remove(full_path + "/" + filename)
self.update_state(state='PROGRESS', meta={'current': 1})
self.update_state(state="PROGRESS", meta={"current": 1})
raw_docs = SimpleDirectoryReader(input_dir=full_path, input_files=input_files, recursive=recursive,
required_exts=formats, num_files_limit=limit,
exclude_hidden=exclude, file_metadata=metadata_from_filename).load_data()
raw_docs = group_split(documents=raw_docs, min_tokens=min_tokens, max_tokens=max_tokens, token_check=token_check)
raw_docs = SimpleDirectoryReader(
input_dir=full_path,
input_files=input_files,
recursive=recursive,
required_exts=formats,
num_files_limit=limit,
exclude_hidden=exclude,
file_metadata=metadata_from_filename,
).load_data()
raw_docs = group_split(
documents=raw_docs,
min_tokens=min_tokens,
max_tokens=max_tokens,
token_check=token_check,
)
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
call_openai_api(docs, full_path, self)
self.update_state(state='PROGRESS', meta={'current': 100})
self.update_state(state="PROGRESS", meta={"current": 100})
if sample:
for i in range(min(5, len(raw_docs))):
@@ -101,70 +119,80 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
# get files from outputs/inputs/index.faiss and outputs/inputs/index.pkl
# and send them to the server (provide user and name in form)
file_data = {'name': name_job, 'user': user}
file_data = {"name": name_job, "user": user}
if settings.VECTOR_STORE == "faiss":
files = {'file_faiss': open(full_path + '/index.faiss', 'rb'),
'file_pkl': open(full_path + '/index.pkl', 'rb')}
response = requests.post(urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data)
response = requests.get(urljoin(settings.API_URL, "/api/delete_old?path=" + full_path))
files = {
"file_faiss": open(full_path + "/index.faiss", "rb"),
"file_pkl": open(full_path + "/index.pkl", "rb"),
}
response = requests.post(
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
)
response = requests.get(
urljoin(settings.API_URL, "/api/delete_old?path=" + full_path)
)
else:
response = requests.post(urljoin(settings.API_URL, "/api/upload_index"), data=file_data)
response = requests.post(
urljoin(settings.API_URL, "/api/upload_index"), data=file_data
)
# delete local
shutil.rmtree(full_path)
return {
'directory': directory,
'formats': formats,
'name_job': name_job,
'filename': filename,
'user': user,
'limited': False
"directory": directory,
"formats": formats,
"name_job": name_job,
"filename": filename,
"user": user,
"limited": False,
}
def remote_worker(self, source_data, name_job, user, directory = 'temp', loader = 'url'):
def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
# sample = False
token_check = True
min_tokens = 150
max_tokens = 1250
full_path = directory + '/' + user + '/' + name_job
full_path = directory + "/" + user + "/" + name_job
if not os.path.exists(full_path):
os.makedirs(full_path)
self.update_state(state='PROGRESS', meta={'current': 1})
self.update_state(state="PROGRESS", meta={"current": 1})
# source_data {"data": [url]} for url type task just urls
# Use RemoteCreator to load data from URL
remote_loader = RemoteCreator.create_loader(loader)
raw_docs = remote_loader.load_data(source_data)
docs = group_split(documents=raw_docs, min_tokens=min_tokens, max_tokens=max_tokens, token_check=token_check)
docs = group_split(
documents=raw_docs,
min_tokens=min_tokens,
max_tokens=max_tokens,
token_check=token_check,
)
#docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
# docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
call_openai_api(docs, full_path, self)
self.update_state(state='PROGRESS', meta={'current': 100})
self.update_state(state="PROGRESS", meta={"current": 100})
# Proceed with uploading and cleaning as in the original function
file_data = {'name': name_job, 'user': user}
file_data = {"name": name_job, "user": user}
if settings.VECTOR_STORE == "faiss":
files = {'file_faiss': open(full_path + '/index.faiss', 'rb'),
'file_pkl': open(full_path + '/index.pkl', 'rb')}
requests.post(urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data)
files = {
"file_faiss": open(full_path + "/index.faiss", "rb"),
"file_pkl": open(full_path + "/index.pkl", "rb"),
}
requests.post(
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
)
requests.get(urljoin(settings.API_URL, "/api/delete_old?path=" + full_path))
else:
requests.post(urljoin(settings.API_URL, "/api/upload_index"), data=file_data)
shutil.rmtree(full_path)
return {
'urls': source_data,
'name_job': name_job,
'user': user,
'limited': False
}
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}

View File

@@ -1,4 +1,5 @@
from application.app import app
from application.core.settings import settings
if __name__ == "__main__":
app.run(debug=True, port=7091)
app.run(debug=settings.FLASK_DEBUG_MODE, port=7091)

27
docs/package-lock.json generated
View File

@@ -7,7 +7,7 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.3.0",
"docsgpt": "^0.3.7",
"next": "^14.0.4",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
@@ -422,6 +422,11 @@
"node": ">=6.9.0"
}
},
"node_modules/@bpmn-io/snarkdown": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@bpmn-io/snarkdown/-/snarkdown-2.2.0.tgz",
"integrity": "sha512-bVD7FIoaBDZeCJkMRgnBPDeptPlto87wt2qaCjf5t8iLaevDmTPaREd6FpBEGsHlUdHFFZWRk4qAoEC5So2M0Q=="
},
"node_modules/@braintree/sanitize-url": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
@@ -4958,11 +4963,12 @@
}
},
"node_modules/docsgpt": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.3.0.tgz",
"integrity": "sha512-0yT2m+HAlJ+289p278c3Zi07bu2wr6zULOT/bYXtJ/nb59V2Vpfdj2xFB49+lYLSeVe8H+Ij5fFSNZ6RkVRfMQ==",
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.3.7.tgz",
"integrity": "sha512-VHrXXOEFtjNTcpA8Blf3IzpLlJxOMhm/S5CM4FDjQEkdK9WWhI8yXd/0Rs/FS8oz7YbFrNxO758mlP7OtQtBBw==",
"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",
@@ -4972,6 +4978,7 @@
"@types/react-dom": "^18.2.19",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dompurify": "^3.0.9",
"flow-bin": "^0.229.2",
"i": "^0.3.7",
"install": "^0.13.0",
@@ -5029,9 +5036,9 @@
}
},
"node_modules/dompurify": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.7.tgz",
"integrity": "sha512-BViYTZoqP3ak/ULKOc101y+CtHDUvBsVgSxIF1ku0HmK6BRf+C03MC+tArMvOPtVtZp83DDh5puywKDu4sbVjQ=="
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
"integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg=="
},
"node_modules/domutils": {
"version": "2.8.0",
@@ -6206,9 +6213,9 @@
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
},
"node_modules/katex": {
"version": "0.16.9",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
"version": "0.16.10",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"

View File

@@ -227,3 +227,124 @@ JSON response indicating the status of the operation:
```json
{ "status": "ok" }
```
### 7. /api/get_api_keys
**Description:**
The endpoint retrieves a list of API keys for the user.
**Request:**
**Method**: `GET`
**Sample JavaScript Fetch Request:**
```js
// get_api_keys (GET http://127.0.0.1:5000/api/get_api_keys)
fetch("http://localhost:5001/api/get_api_keys", {
"method": "GET",
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
})
.then((res) => res.text())
.then(console.log.bind(console))
```
**Response:**
JSON response with a list of created API keys:
```json
[
{
"id": "string",
"name": "string",
"key": "string",
"source": "string"
},
...
]
```
### 8. /api/create_api_key
**Description:**
Create a new API key for the user.
**Request:**
**Method**: `POST`
**Headers**: Content-Type should be set to `application/json; charset=utf-8`
**Request Body**: JSON object with the following fields:
* `name` — A name for the API key.
* `source` — The source documents that will be used.
* `prompt_id` — The prompt ID.
* `chunks` — The number of chunks used to process an answer.
Here is a JavaScript Fetch Request example:
```js
// create_api_key (POST http://127.0.0.1:5000/api/create_api_key)
fetch("http://127.0.0.1:5000/api/create_api_key", {
"method": "POST",
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": JSON.stringify({"name":"Example Key Name",
"source":"Example Source",
"prompt_id":"creative",
"chunks":"2"})
})
.then((res) => res.json())
.then(console.log.bind(console))
```
**Response**
In response, you will get a JSON document containing the `id` and `key`:
```json
{
"id": "string",
"key": "string"
}
```
### 9. /api/delete_api_key
**Description:**
Delete an API key for the user.
**Request:**
**Method**: `POST`
**Headers**: Content-Type should be set to `application/json; charset=utf-8`
**Request Body**: JSON object with the field:
* `id` — The unique identifier of the API key to be deleted.
Here is a JavaScript Fetch Request example:
```js
// delete_api_key (POST http://127.0.0.1:5000/api/delete_api_key)
fetch("http://127.0.0.1:5000/api/delete_api_key", {
"method": "POST",
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": JSON.stringify({"id":"API_KEY_ID"})
})
.then((res) => res.json())
.then(console.log.bind(console))
```
**Response:**
In response, you will get a JSON document indicating the status of the operation:
```json
{
"status": "ok"
}
```

View File

@@ -4,7 +4,11 @@
"href": "/Extensions/Chatwoot-extension"
},
"react-widget": {
"title": "🏗️ Widget setup",
"href": "/Extensions/react-widget"
}
"title": "🏗️ Widget setup",
"href": "/Extensions/react-widget"
},
"api-key-guide": {
"title": "🔐 API Keys guide",
"href": "/Extensions/api-key-guide"
}
}

View File

@@ -0,0 +1,30 @@
## Guide to DocsGPT API Keys
DocsGPT API keys are essential for developers and users who wish to integrate the DocsGPT models into external applications, such as the our widget. This guide will walk you through the steps of obtaining an API key, starting from uploading your document to understanding the key variables associated with API keys.
### Uploading Your Document
Before creating your first API key, you must upload the document that will be linked to this key. You can upload your document through two methods:
- **GUI Web App Upload:** A user-friendly graphical interface that allows for easy upload and management of documents.
- **Using `/api/upload` Method:** For users comfortable with API calls, this method provides a direct way to upload documents.
### Obtaining Your API Key
After uploading your document, you can obtain an API key either through the graphical user interface or via an API call:
- **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the API Keys option, and press 'Create New' to generate your key.
- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://docs.docsgpt.co.uk/Developing/API-docs#8-apicreate_api_key).
### Understanding Key Variables
Upon creating your API key, you will encounter several key variables. Each serves a specific purpose:
- **Name:** Assign a name to your API key for easy identification.
- **Source:** Indicates the source document(s) linked to your API key, which DocsGPT will use to generate responses.
- **ID:** A unique identifier for your API key. You can view this by making a call to `/api/get_api_keys`.
- **Key:** The API key itself, which will be used in your application to authenticate API requests.
With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the API key, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation.
Congratulations on taking the first step towards enhancing your applications with DocsGPT! With this guide, you're now equipped to navigate the process of obtaining and understanding DocsGPT API keys.

View File

@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<DocsGPTWidget selectDocs="local/docsgpt-sep.zip/"/>
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" />
</>
)
}

View File

@@ -285,7 +285,7 @@ const Hero = ({ title, description }: { title: string, description: string }) =>
export const DocsGPTWidget = ({
apiHost = 'https://gptcloud.arc53.com',
selectDocs = 'default',
apiKey = 'docsgpt-public',
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',

View File

@@ -44,7 +44,7 @@
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.5",
"vite": "^5.0.12",
"vite": "^5.0.13",
"vite-plugin-svgr": "^4.2.0"
}
},
@@ -7855,9 +7855,9 @@
}
},
"node_modules/vite": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
"integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
"version": "5.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz",
"integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",

View File

@@ -55,7 +55,7 @@
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.5",
"vite": "^5.0.12",
"vite": "^5.0.13",
"vite-plugin-svgr": "^4.2.0"
}
}

View File

@@ -7,9 +7,7 @@ export default function Hero({ className = '' }: { className?: string }) {
const [isDarkTheme] = useDarkTheme();
return (
<div
className={`mt-14 ${
isMobile ? 'mb-2' : 'mb-12'
} flex flex-col text-black-1000 dark:text-bright-gray`}
className={`mt-14 mb-32 flex flex-col text-black-1000 dark:text-bright-gray lg:mt-6`}
>
<div className=" mb-2 flex items-center justify-center sm:mb-10">
<p className="mr-2 text-4xl font-semibold">DocsGPT</p>
@@ -39,16 +37,14 @@ export default function Hero({ className = '' }: { className?: string }) {
</>
)}
<div
className={`sections ${
isMobile ? '' : 'mt-1'
} flex flex-wrap items-center justify-center gap-2 sm:gap-1 md:gap-0`}
className={`mt-0 flex flex-wrap items-center justify-center gap-2 sm:mt-1 sm:gap-4 md:gap-4 lg:gap-0`}
>
{/* first */}
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-tr-none md:rounded-br-none">
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-tr-none lg:rounded-br-none">
<div
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 py-8'
} md:rounded-tr-none md:rounded-br-none`}
} lg:rounded-tr-none lg:rounded-br-none`}
>
{/* Add Mobile check here */}
{isMobile ? (
@@ -93,11 +89,11 @@ export default function Hero({ className = '' }: { className?: string }) {
</div>
</div>
{/* second */}
<div className="h-auto rounded-[50px] bg-gradient-to-r from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-none md:py-1 md:px-0">
<div className="h-auto rounded-[50px] bg-gradient-to-r from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-none lg:py-1 lg:px-0">
<div
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 py-6'
} md:rounded-none`}
} lg:rounded-none`}
>
{/* Add Mobile check here */}
{isMobile ? (
@@ -138,7 +134,7 @@ export default function Hero({ className = '' }: { className?: string }) {
</div>
</div>
{/* third */}
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-tl-none md:rounded-bl-none ">
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] lg:h-60 lg:rounded-tl-none lg:rounded-bl-none ">
<div
className={`firefox h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
isMobile ? '3.5' : '6 px-6 '

View File

@@ -257,7 +257,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
New Chat
</p>
</NavLink>
<div className="mb-auto h-[56vh] overflow-x-hidden overflow-y-scroll dark:text-white">
<div className="mb-auto h-[56vh] overflow-y-auto overflow-x-hidden dark:text-white">
{conversations && (
<div>
<p className="ml-6 mt-3 text-sm font-semibold">Chats</p>

View File

@@ -2,16 +2,21 @@ import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ArrowLeft from './assets/arrow-left.svg';
import ArrowRight from './assets/arrow-right.svg';
import Exit from './assets/exit.svg';
import Trash from './assets/trash.svg';
import {
selectPrompt,
setPrompt,
selectSourceDocs,
setSourceDocs,
setChunks,
selectChunks,
} from './preferences/preferenceSlice';
import { Doc } from './preferences/preferenceApi';
import { useDarkTheme } from './hooks';
import Dropdown from './components/Dropdown';
import { ActiveState } from './models/misc';
import PromptsModal from './preferences/PromptsModal';
type PromptProps = {
prompts: { name: string; id: string; type: string }[];
selectedPrompt: { name: string; id: string; type: string };
@@ -19,67 +24,24 @@ type PromptProps = {
setPrompts: (prompts: { name: string; id: string; type: string }[]) => void;
apiHost: string;
};
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const Setting: React.FC = () => {
const tabs = ['General', 'Prompts', 'Documents'];
//const tabs = ['General', 'Prompts', 'Documents', 'Widgets'];
const tabs = ['General', 'Documents', 'API Keys'];
const [activeTab, setActiveTab] = useState('General');
const [prompts, setPrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
const selectedPrompt = useSelector(selectPrompt);
const [isAddPromptModalOpen, setAddPromptModalOpen] = useState(false);
const documents = useSelector(selectSourceDocs);
const [isAddDocumentModalOpen, setAddDocumentModalOpen] = useState(false);
const dispatch = useDispatch();
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const [widgetScreenshot, setWidgetScreenshot] = useState<File | null>(null);
const updateWidgetScreenshot = (screenshot: File | null) => {
setWidgetScreenshot(screenshot);
};
useEffect(() => {
const fetchPrompts = async () => {
try {
const response = await fetch(`${apiHost}/api/get_prompts`);
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const promptsData = await response.json();
setPrompts(promptsData);
} catch (error) {
console.error(error);
}
};
fetchPrompts();
}, []);
const onDeletePrompt = (name: string, id: string) => {
setPrompts(prompts.filter((prompt) => prompt.id !== id));
fetch(`${apiHost}/api/delete_prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// send id in body only
body: JSON.stringify({ id: id }),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete prompt');
}
})
.catch((error) => {
console.error(error);
});
};
const handleDeleteClick = (index: number, doc: Doc) => {
const docPath = 'indexes/' + 'local' + '/' + doc.name;
@@ -158,18 +120,6 @@ const Setting: React.FC = () => {
switch (activeTab) {
case 'General':
return <General />;
case 'Prompts':
return (
<Prompts
prompts={prompts}
selectedPrompt={selectedPrompt}
onSelectPrompt={(name, id, type) =>
dispatch(setPrompt({ name: name, id: id, type: type }))
}
setPrompts={setPrompts}
apiHost={apiHost}
/>
);
case 'Documents':
return (
<Documents
@@ -184,6 +134,8 @@ const Setting: React.FC = () => {
onWidgetScreenshotChange={updateWidgetScreenshot} // Add this line
/>
);
case 'API Keys':
return <APIKeys />;
default:
return null;
}
@@ -193,32 +145,86 @@ const Setting: React.FC = () => {
const General: React.FC = () => {
const themes = ['Light', 'Dark'];
const languages = ['English'];
const chunks = ['0', '2', '4', '6', '8', '10'];
const [prompts, setPrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
const selectedChunks = useSelector(selectChunks);
const [isDarkTheme, toggleTheme] = useDarkTheme();
const [selectedTheme, setSelectedTheme] = useState(
isDarkTheme ? 'Dark' : 'Light',
);
const dispatch = useDispatch();
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
const selectedPrompt = useSelector(selectPrompt);
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
useEffect(() => {
const fetchPrompts = async () => {
try {
const response = await fetch(`${apiHost}/api/get_prompts`);
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const promptsData = await response.json();
setPrompts(promptsData);
} catch (error) {
console.error(error);
}
};
fetchPrompts();
}, []);
return (
<div className="mt-[59px]">
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">Select Theme</p>
<Dropdown
alignMidddle
options={themes}
selectedValue={selectedTheme}
onSelect={(option: string) => {
setSelectedTheme(option);
option !== selectedTheme && toggleTheme();
}}
size="w-56"
rounded="3xl"
/>
</div>
<div>
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">
Select Language
</p>
<Dropdown
alignMidddle
options={languages}
selectedValue={selectedLanguage}
onSelect={setSelectedLanguage}
size="w-56"
rounded="3xl"
/>
</div>
<div className="mb-4">
<p className="font-bold text-jet dark:text-bright-gray">
Chunks processed per query
</p>
<Dropdown
alignMidddle
options={chunks}
selectedValue={selectedChunks}
onSelect={(value: string) => dispatch(setChunks(value))}
size="w-56"
rounded="3xl"
/>
</div>
<div>
<Prompts
prompts={prompts}
selectedPrompt={selectedPrompt}
onSelectPrompt={(name, id, type) =>
dispatch(setPrompt({ name: name, id: id, type: type }))
}
setPrompts={setPrompts}
apiHost={apiHost}
/>
</div>
</div>
@@ -232,7 +238,6 @@ const Prompts: React.FC<PromptProps> = ({
selectedPrompt,
onSelectPrompt,
setPrompts,
apiHost,
}) => {
const handleSelectPrompt = ({
name,
@@ -243,11 +248,20 @@ const Prompts: React.FC<PromptProps> = ({
id: string;
type: string;
}) => {
setNewPromptName(name);
setEditPromptName(name);
onSelectPrompt(name, id, type);
};
const [newPromptName, setNewPromptName] = useState(selectedPrompt.name);
const [newPromptName, setNewPromptName] = useState('');
const [newPromptContent, setNewPromptContent] = useState('');
const [editPromptName, setEditPromptName] = useState('');
const [editPromptContent, setEditPromptContent] = useState('');
const [currentPromptEdit, setCurrentPromptEdit] = useState({
id: '',
name: '',
type: '',
});
const [modalType, setModalType] = useState<'ADD' | 'EDIT'>('ADD');
const [modalState, setModalState] = useState<ActiveState>('INACTIVE');
const handleAddPrompt = async () => {
try {
@@ -271,6 +285,7 @@ const Prompts: React.FC<PromptProps> = ({
{ name: newPromptName, id: newPrompt.id, type: 'private' },
]);
}
setModalState('INACTIVE');
onSelectPrompt(newPromptName, newPrompt.id, newPromptContent);
setNewPromptName(newPromptName);
} catch (error) {
@@ -278,16 +293,14 @@ const Prompts: React.FC<PromptProps> = ({
}
};
const handleDeletePrompt = () => {
setPrompts(prompts.filter((prompt) => prompt.id !== selectedPrompt.id));
console.log('selectedPrompt.id', selectedPrompt.id);
const handleDeletePrompt = (id: string) => {
setPrompts(prompts.filter((prompt) => prompt.id !== id));
fetch(`${apiHost}/api/delete_prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: selectedPrompt.id }),
body: JSON.stringify({ id: id }),
})
.then((response) => {
if (!response.ok) {
@@ -296,7 +309,6 @@ const Prompts: React.FC<PromptProps> = ({
// get 1st prompt and set it as selected
if (prompts.length > 0) {
onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type);
setNewPromptName(prompts[0].name);
}
})
.catch((error) => {
@@ -304,50 +316,65 @@ const Prompts: React.FC<PromptProps> = ({
});
};
useEffect(() => {
const fetchPromptContent = async () => {
console.log('fetching prompt content');
try {
const response = await fetch(
`${apiHost}/api/get_single_prompt?id=${selectedPrompt.id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
const fetchPromptContent = async (id: string) => {
console.log('fetching prompt content');
try {
const response = await fetch(
`${apiHost}/api/get_single_prompt?id=${id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
);
if (!response.ok) {
throw new Error('Failed to fetch prompt content');
}
const promptContent = await response.json();
setNewPromptContent(promptContent.content);
} catch (error) {
console.error(error);
},
);
if (!response.ok) {
throw new Error('Failed to fetch prompt content');
}
};
const promptContent = await response.json();
setEditPromptContent(promptContent.content);
} catch (error) {
console.error(error);
}
};
fetchPromptContent();
}, [selectedPrompt]);
const handleSaveChanges = () => {
const handleSaveChanges = (id: string, type: string) => {
fetch(`${apiHost}/api/update_prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedPrompt.id,
name: newPromptName,
content: newPromptContent,
id: id,
name: editPromptName,
content: editPromptContent,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to update prompt');
}
onSelectPrompt(newPromptName, selectedPrompt.id, selectedPrompt.type);
setNewPromptName(newPromptName);
if (setPrompts) {
const existingPromptIndex = prompts.findIndex(
(prompt) => prompt.id === id,
);
if (existingPromptIndex === -1) {
setPrompts([
...prompts,
{ name: editPromptName, id: id, type: type },
]);
} else {
const updatedPrompts = [...prompts];
updatedPrompts[existingPromptIndex] = {
name: editPromptName,
id: id,
type: type,
};
setPrompts(updatedPrompts);
}
}
setModalState('INACTIVE');
onSelectPrompt(editPromptName, id, type);
})
.catch((error) => {
console.error(error);
@@ -355,76 +382,65 @@ const Prompts: React.FC<PromptProps> = ({
};
return (
<div className="mt-[59px]">
<div className="mb-4">
<p className="font-semibold dark:text-bright-gray">Active Prompt</p>
<Dropdown
options={prompts}
selectedValue={selectedPrompt.name}
onSelect={handleSelectPrompt}
/>
<>
<div>
<div className="mb-4 flex flex-row items-center gap-8">
<div>
<p className="font-semibold dark:text-bright-gray">Active Prompt</p>
<Dropdown
options={prompts}
selectedValue={selectedPrompt.name}
onSelect={handleSelectPrompt}
size="w-56"
rounded="3xl"
showEdit
showDelete
onEdit={({
id,
name,
type,
}: {
id: string;
name: string;
type: string;
}) => {
setModalType('EDIT');
setEditPromptName(name);
fetchPromptContent(id);
setCurrentPromptEdit({ id: id, name: name, type: type });
setModalState('ACTIVE');
}}
onDelete={handleDeletePrompt}
/>
</div>
<button
className="mt-[24px] rounded-3xl border-2 border-solid border-purple-30 px-5 py-3 text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => {
setModalType('ADD');
setModalState('ACTIVE');
}}
>
Add new
</button>
</div>
</div>
<div className="mb-4">
<p className="dark:text-bright-gray">Prompt name </p>{' '}
<p className="mb-2 text-xs italic text-eerie-black dark:text-bright-gray">
start by editing name
</p>
<input
type="text"
value={newPromptName}
placeholder="Active Prompt Name"
className="w-full rounded-lg border-2 p-2 dark:border-chinese-silver dark:bg-transparent dark:text-white"
onChange={(e) => setNewPromptName(e.target.value)}
/>
</div>
<div className="mb-4">
<p className="mb-2 dark:text-bright-gray">Prompt content</p>
<textarea
className="h-32 w-full rounded-lg border-2 p-2 dark:border-chinese-silver dark:bg-transparent dark:text-white"
value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)}
placeholder="Active prompt contents"
/>
</div>
<div className="flex justify-between">
<button
className={`rounded-lg bg-green-500 px-4 py-2 font-bold text-white transition-all hover:bg-green-700 ${
newPromptName === selectedPrompt.name
? 'cursor-not-allowed opacity-50'
: ''
}`}
onClick={handleAddPrompt}
disabled={newPromptName === selectedPrompt.name}
>
Add New Prompt
</button>
<button
className={`rounded-lg bg-red-500 px-4 py-2 font-bold text-white transition-all hover:bg-red-700 ${
selectedPrompt.type === 'public'
? 'cursor-not-allowed opacity-50'
: ''
}`}
onClick={handleDeletePrompt}
disabled={selectedPrompt.type === 'public'}
>
Delete Prompt
</button>
<button
className={`rounded-lg bg-blue-500 px-4 py-2 font-bold text-white transition-all hover:bg-blue-700 ${
selectedPrompt.type === 'public'
? 'cursor-not-allowed opacity-50'
: ''
}`}
onClick={handleSaveChanges}
disabled={selectedPrompt.type === 'public'}
>
Save Changes
</button>
</div>
</div>
<PromptsModal
type={modalType}
modalState={modalState}
setModalState={setModalState}
newPromptName={newPromptName}
setNewPromptName={setNewPromptName}
newPromptContent={newPromptContent}
setNewPromptContent={setNewPromptContent}
editPromptName={editPromptName}
setEditPromptName={setEditPromptName}
editPromptContent={editPromptContent}
setEditPromptContent={setEditPromptContent}
currentPromptEdit={currentPromptEdit}
handleAddPrompt={handleAddPrompt}
handleEditPrompt={handleSaveChanges}
/>
</>
);
};
@@ -468,7 +484,6 @@ const AddPromptModal: React.FC<AddPromptModalProps> = ({
</div>
);
};
type DocumentsProps = {
documents: Doc[] | null;
handleDeleteDocument: (index: number, document: Doc) => void;
@@ -481,8 +496,6 @@ const Documents: React.FC<DocumentsProps> = ({
return (
<div className="mt-8">
<div className="flex flex-col">
{/* <h2 className="text-xl font-semibold">Documents</h2> */}
<div className="mt-[27px] w-max overflow-x-auto rounded-xl border dark:border-chinese-silver">
<table className="block w-full table-auto content-center justify-center text-center dark:text-bright-gray">
<thead>
@@ -617,7 +630,335 @@ const AddDocumentModal: React.FC<AddDocumentModalProps> = ({
</div>
);
};
const APIKeys: React.FC = () => {
const [isCreateModalOpen, setCreateModal] = useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false);
const [newKey, setNewKey] = useState('');
const [apiKeys, setApiKeys] = useState<
{ name: string; key: string; source: string; id: string }[]
>([]);
const handleDeleteKey = (id: string) => {
fetch(`${apiHost}/api/delete_api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id }),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete API Key');
}
return response.json();
})
.then((data) => {
data.status === 'ok' &&
setApiKeys((previous) => previous.filter((elem) => elem.id !== id));
})
.catch((error) => {
console.error(error);
});
};
useEffect(() => {
fetchAPIKeys();
}, []);
const fetchAPIKeys = async () => {
try {
const response = await fetch(`${apiHost}/api/get_api_keys`);
if (!response.ok) {
throw new Error('Failed to fetch API Keys');
}
const apiKeys = await response.json();
setApiKeys(apiKeys);
} catch (error) {
console.log(error);
}
};
const createAPIKey = (payload: {
name: string;
source: string;
prompt_id: string;
chunks: string;
}) => {
fetch(`${apiHost}/api/create_api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to create API Key');
}
return response.json();
})
.then((data) => {
setApiKeys([...apiKeys, data]);
setCreateModal(false); //close the create key modal
setNewKey(data.key);
setSaveKeyModal(true); // render the newly created key
fetchAPIKeys();
})
.catch((error) => {
console.error(error);
});
};
return (
<div className="mt-8">
<div className="flex w-full flex-col lg:w-max">
<div className="flex justify-end">
<button
onClick={() => setCreateModal(true)}
className="rounded-full bg-purple-30 px-4 py-3 text-sm text-white hover:bg-[#7E66B1]"
>
Create New
</button>
</div>
{isCreateModalOpen && (
<CreateAPIKeyModal
close={() => setCreateModal(false)}
createAPIKey={createAPIKey}
/>
)}
{isSaveKeyModalOpen && (
<SaveAPIKeyModal
apiKey={newKey}
close={() => setSaveKeyModal(false)}
/>
)}
<div className="mt-[27px] w-full">
<div className="w-full overflow-x-auto">
<table className="block w-max table-auto content-center justify-center rounded-xl border text-center dark:border-chinese-silver dark:text-bright-gray">
<thead>
<tr>
<th className="border-r p-4 md:w-[244px]">Name</th>
<th className="w-[244px] border-r px-4 py-2">
Source document
</th>
<th className="w-[244px] border-r px-4 py-2">API Key</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{apiKeys?.map((element, index) => (
<tr key={index}>
<td className="border-r border-t p-4">{element.name}</td>
<td className="border-r border-t p-4">{element.source}</td>
<td className="border-r border-t p-4">{element.key}</td>
<td className="border-t p-4">
<img
src={Trash}
alt="Delete"
className="h-4 w-4 cursor-pointer hover:opacity-50"
id={`img-${index}`}
onClick={() => handleDeleteKey(element.id)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
type SaveAPIKeyModalProps = {
apiKey: string;
close: () => void;
};
const SaveAPIKeyModal: React.FC<SaveAPIKeyModalProps> = ({ apiKey, close }) => {
const [isCopied, setIsCopied] = useState(false);
const handleCopyKey = () => {
navigator.clipboard.writeText(apiKey);
setIsCopied(true);
};
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div className="relative w-11/12 rounded-md bg-white p-5 dark:bg-outer-space dark:text-bright-gray sm:w-[512px]">
<button className="absolute top-4 right-4 w-4" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<h1 className="my-0 text-xl font-medium">Please save your Key</h1>
<h3 className="text-sm font-normal text-outer-space">
This is the only time your key will be shown.
</h3>
<div className="flex justify-between py-2">
<div>
<h2 className="text-base font-semibold">API Key</h2>
<span className="text-sm font-normal leading-7 ">{apiKey}</span>
</div>
<button
className="my-1 h-10 w-20 rounded-full border border-purple-30 p-2 text-sm text-purple-30 dark:border-purple-500 dark:text-purple-500"
onClick={handleCopyKey}
>
{isCopied ? 'Copied' : 'Copy'}
</button>
</div>
<button
onClick={close}
className="rounded-full bg-philippine-yellow px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
>
I saved the Key
</button>
</div>
</div>
);
};
type CreateAPIKeyModalProps = {
close: () => void;
createAPIKey: (payload: {
name: string;
source: string;
prompt_id: string;
chunks: string;
}) => void;
};
const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
close,
createAPIKey,
}) => {
const [APIKeyName, setAPIKeyName] = useState<string>('');
const [sourcePath, setSourcePath] = useState<{
label: string;
value: string;
} | null>(null);
const chunkOptions = ['0', '2', '4', '6', '8', '10'];
const [chunk, setChunk] = useState<string>('2');
const [activePrompts, setActivePrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
const [prompt, setPrompt] = useState<{
name: string;
id: string;
type: string;
} | null>(null);
const docs = useSelector(selectSourceDocs);
useEffect(() => {
const fetchPrompts = async () => {
try {
const response = await fetch(`${apiHost}/api/get_prompts`);
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const promptsData = await response.json();
setActivePrompts(promptsData);
} catch (error) {
console.error(error);
}
};
fetchPrompts();
}, []);
const extractDocPaths = () =>
docs
? docs
.filter((doc) => doc.model === embeddingsName)
.map((doc: Doc) => {
let namePath = doc.name;
if (doc.language === namePath) {
namePath = '.project';
}
let docPath = 'default';
if (doc.location === 'local') {
docPath = 'local' + '/' + doc.name + '/';
} else if (doc.location === 'remote') {
docPath =
doc.language +
'/' +
namePath +
'/' +
doc.version +
'/' +
doc.model +
'/';
}
return {
label: doc.name,
value: docPath,
};
})
: [];
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div className="relative w-11/12 rounded-lg bg-white p-5 dark:bg-outer-space sm:w-[512px]">
<button className="absolute top-2 right-2 m-2 w-4" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<span className="mb-4 text-xl font-bold text-jet dark:text-bright-gray">
Create New API Key
</span>
<div className="relative my-4">
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
API Key Name
</span>
<input
type="text"
className="h-10 w-full rounded-md border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={APIKeyName}
onChange={(e) => setAPIKeyName(e.target.value)}
/>
</div>
<div className="my-4">
<Dropdown
fullWidth
placeholder="Select the source doc"
selectedValue={sourcePath}
onSelect={(selection: { label: string; value: string }) =>
setSourcePath(selection)
}
options={extractDocPaths()}
/>
</div>
<div className="my-4">
<Dropdown
fullWidth
options={activePrompts}
selectedValue={prompt ? prompt.name : null}
placeholder="Select Active Prompt"
onSelect={(value: { name: string; id: string; type: string }) =>
setPrompt(value)
}
/>
</div>
<div className="my-4">
<p className="mb-2 ml-2 font-bold text-jet dark:text-bright-gray">
Chunks processed per query
</p>
<Dropdown
fullWidth
alignMidddle
options={chunkOptions}
selectedValue={chunk}
onSelect={(value: string) => setChunk(value)}
/>
</div>
<button
disabled={!sourcePath || APIKeyName.length === 0 || !prompt}
onClick={() =>
sourcePath &&
prompt &&
createAPIKey({
name: APIKeyName,
source: sourcePath.value,
prompt_id: prompt.id,
chunks: chunk,
})
}
className="float-right my-4 rounded-full bg-purple-30 px-4 py-3 text-white disabled:opacity-50"
>
Create
</button>
</div>
</div>
);
};
const Widgets: React.FC<{
widgetScreenshot: File | null;
onWidgetScreenshotChange: (screenshot: File | null) => void;

View File

@@ -1,57 +1,74 @@
import { useState } from 'react';
import Arrow2 from '../assets/dropdown-arrow.svg';
import Edit from '../assets/edit.svg';
import Trash from '../assets/trash.svg';
function Dropdown({
options,
selectedValue,
onSelect,
size = 'w-32',
rounded = 'xl',
showEdit,
onEdit,
showDelete,
onDelete,
placeholder,
fullWidth,
alignMidddle,
}: {
options:
| string[]
| { name: string; id: string; type: string }[]
| { label: string; value: string }[];
selectedValue: string | { label: string; value: string };
selectedValue: string | { label: string; value: string } | null;
onSelect:
| ((value: string) => void)
| ((value: { name: string; id: string; type: string }) => void)
| ((value: { label: string; value: string }) => void);
size?: string;
rounded?: 'xl' | '3xl';
showEdit?: boolean;
onEdit?: (value: { name: string; id: string; type: string }) => void;
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
fullWidth?: boolean;
alignMidddle?: boolean;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={
className={[
typeof selectedValue === 'string'
? 'relative mt-2 w-32'
: 'relative w-full align-middle'
}
? 'relative mt-2'
: 'relative align-middle',
size,
].join(' ')}
>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex w-full cursor-pointer items-center justify-between border-2 bg-white p-3 dark:border-chinese-silver dark:bg-transparent ${
isOpen
? typeof selectedValue === 'string'
? 'rounded-t-xl'
: 'rounded-t-2xl'
: typeof selectedValue === 'string'
? 'rounded-xl'
: 'rounded-full'
className={`flex w-full cursor-pointer items-center justify-between border-2 border-silver bg-white px-5 py-3 dark:border-chinese-silver dark:bg-transparent ${
isOpen ? `rounded-t-${rounded}` : `rounded-${rounded}`
}`}
>
{typeof selectedValue === 'string' ? (
<span className="flex-1 overflow-hidden text-ellipsis dark:text-bright-gray">
<span className="overflow-hidden text-ellipsis dark:text-bright-gray">
{selectedValue}
</span>
) : (
<span
className={`overflow-hidden text-ellipsis dark:text-bright-gray ${
!selectedValue && 'text-silver'
className={`${
alignMidddle && 'flex-1'
} overflow-hidden text-ellipsis dark:text-bright-gray ${
!selectedValue && 'text-silver dark:text-gray-400'
}`}
>
{selectedValue ? selectedValue.label : 'From URL'}
{selectedValue
? selectedValue.label
: placeholder
? placeholder
: 'From URL'}
</span>
)}
<img
@@ -63,7 +80,7 @@ function Dropdown({
/>
</button>
{isOpen && (
<div className="absolute left-0 right-0 z-50 -mt-1 overflow-y-auto rounded-b-xl border-2 bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
<div className="absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border-2 bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
{options.map((option: any, index) => (
<div
key={index}
@@ -82,9 +99,35 @@ function Dropdown({
? option.name
: option.label}
</span>
{showEdit && onEdit && (
<img
src={Edit}
alt="Edit"
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
onClick={() => {
onEdit({
id: option.id,
name: option.name,
type: option.type,
});
setIsOpen(false);
}}
/>
)}
{showDelete && onDelete && (
<button onClick={() => onDelete(option)} className="p-2">
Delete
<button
onClick={() => onDelete(option.id)}
disabled={option.type === 'public'}
>
<img
src={Trash}
alt="Delete"
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
option.type === 'public'
? 'cursor-not-allowed opacity-50'
: ''
}`}
/>
</button>
)}
</div>

View File

@@ -24,6 +24,12 @@ function SourceDropdown({
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const handleEmptyDocumentSelect = () => {
dispatch(setSelectedDocs(null));
setIsDocsListOpen(false);
};
return (
<div className="relative w-5/6 rounded-3xl">
<button
@@ -35,7 +41,7 @@ function SourceDropdown({
<span className="ml-1 mr-2 flex-1 overflow-hidden text-ellipsis text-left dark:text-bright-gray">
<div className="flex flex-row gap-2">
<p className="max-w-3/4 truncate whitespace-nowrap">
{selectedDocs?.name}
{selectedDocs?.name || ''}
</p>
<p className="flex flex-col items-center justify-center">
{selectedDocs?.version}
@@ -93,6 +99,14 @@ function SourceDropdown({
<p className="ml-5 py-3">No default documentation.</p>
</div>
)}
<div
className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe"
onClick={handleEmptyDocumentSelect}
>
<span className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3">
Empty
</span>
</div>
</div>
)}
</div>

View File

@@ -160,7 +160,10 @@ const ConversationBubble = forwardRef<
>
{message}
</ReactMarkdown>
{DisableSourceFE || type === 'ERROR' ? null : (
{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">

View File

@@ -6,11 +6,11 @@ const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export function fetchAnswerApi(
question: string,
signal: AbortSignal,
apiKey: string,
selectedDocs: Doc,
selectedDocs: Doc | null,
history: Array<any> = [],
conversationId: string | null,
promptId: string | null,
chunks: string,
): Promise<
| {
result: any;
@@ -28,24 +28,26 @@ export function fetchAnswerApi(
title: any;
}
> {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
let docPath = 'default';
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
if (selectedDocs) {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
}
}
//in history array remove all keys except prompt and response
history = history.map((item) => {
@@ -59,12 +61,11 @@ export function fetchAnswerApi(
},
body: JSON.stringify({
question: question,
api_key: apiKey,
embeddings_key: apiKey,
history: history,
active_docs: docPath,
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
}),
signal,
})
@@ -90,31 +91,33 @@ export function fetchAnswerApi(
export function fetchAnswerSteaming(
question: string,
signal: AbortSignal,
apiKey: string,
selectedDocs: Doc,
selectedDocs: Doc | null,
history: Array<any> = [],
conversationId: string | null,
promptId: string | null,
chunks: string,
onEvent: (event: MessageEvent) => void,
): Promise<Answer> {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
let docPath = 'default';
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
if (selectedDocs) {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
}
}
history = history.map((item) => {
@@ -124,12 +127,11 @@ export function fetchAnswerSteaming(
return new Promise<Answer>((resolve, reject) => {
const body = {
question: question,
api_key: apiKey,
embeddings_key: apiKey,
active_docs: docPath,
history: JSON.stringify(history),
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
};
fetch(apiHost + '/stream', {
method: 'POST',
@@ -188,34 +190,35 @@ export function fetchAnswerSteaming(
}
export function searchEndpoint(
question: string,
apiKey: string,
selectedDocs: Doc,
selectedDocs: Doc | null,
conversation_id: string | null,
history: Array<any> = [],
chunks: string,
) {
/*
"active_docs": "default",
"question": "Summarise",
"conversation_id": null,
"history": "[]" */
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
let docPath = 'default';
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
if (selectedDocs) {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
namePath = '.project';
}
if (selectedDocs.location === 'local') {
docPath = 'local' + '/' + selectedDocs.name + '/';
} else if (selectedDocs.location === 'remote') {
docPath =
selectedDocs.language +
'/' +
namePath +
'/' +
selectedDocs.version +
'/' +
selectedDocs.model +
'/';
}
}
const body = {
@@ -223,6 +226,7 @@ export function searchEndpoint(
active_docs: docPath,
conversation_id,
history,
chunks: chunks,
};
return fetch(`${apiHost}/api/search`, {
method: 'POST',

View File

@@ -23,11 +23,11 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
await fetchAnswerSteaming(
question,
signal,
state.preference.apiKey,
state.preference.selectedDocs!,
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
(event) => {
const data = JSON.parse(event.data);
@@ -47,10 +47,10 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
searchEndpoint(
//search for sources post streaming
question,
state.preference.apiKey,
state.preference.selectedDocs!,
state.conversation.conversationId,
state.conversation.queries,
state.preference.chunks,
).then((sources) => {
//dispatch streaming sources
dispatch(
@@ -81,11 +81,11 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
const answer = await fetchAnswerApi(
question,
signal,
state.preference.apiKey,
state.preference.selectedDocs!,
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
);
if (answer) {
let sourcesPrepped = [];

View File

@@ -2,6 +2,26 @@
@tailwind components;
@tailwind utilities;
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
.dark ::-webkit-scrollbar-track {
background: #2f3036;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 40px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #b1afaf;
}
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document

View File

@@ -0,0 +1,213 @@
import { ActiveState } from '../models/misc';
function AddPrompt({
setModalState,
handleAddPrompt,
newPromptName,
setNewPromptName,
newPromptContent,
setNewPromptContent,
}: {
setModalState: (state: ActiveState) => void;
handleAddPrompt?: () => void;
newPromptName: string;
setNewPromptName: (name: string) => void;
newPromptContent: string;
setNewPromptContent: (content: string) => void;
}) {
return (
<div className="rounded-3xl px-4 py-2">
<p className="mb-1 text-xl text-jet dark:text-bright-gray">Add Prompt</p>
<p className="mb-7 text-xs text-[#747474] dark:text-[#7F7F82]">
Add your custom prompt and save it to DocsGPT
</p>
<div>
<input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={newPromptName}
onChange={(e) => setNewPromptName(e.target.value)}
></input>
<div className="relative bottom-12 left-3 mt-[-3.00px]">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Name
</span>
</div>
<div className="relative top-[7px] left-3">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Text
</span>
</div>
<textarea
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:bg-transparent dark:text-silver"
value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)}
></textarea>
</div>
<div className="mt-6 flex flex-row-reverse gap-4">
<button
onClick={handleAddPrompt}
className="rounded-3xl bg-purple-30 px-5 py-2 text-white transition-all hover:opacity-90"
>
Save
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer font-medium dark:text-light-gray"
>
Cancel
</button>
</div>
</div>
);
}
function EditPrompt({
setModalState,
handleEditPrompt,
editPromptName,
setEditPromptName,
editPromptContent,
setEditPromptContent,
currentPromptEdit,
}: {
setModalState: (state: ActiveState) => void;
handleEditPrompt?: (id: string, type: string) => void;
editPromptName: string;
setEditPromptName: (name: string) => void;
editPromptContent: string;
setEditPromptContent: (content: string) => void;
currentPromptEdit: { name: string; id: string; type: string };
}) {
return (
<div className="rounded-3xl px-4 py-2">
<p className="mb-1 text-xl text-jet dark:text-bright-gray">Edit Prompt</p>
<p className="mb-7 text-xs text-[#747474] dark:text-[#7F7F82]">
Edit your custom prompt and save it to DocsGPT
</p>
<div>
<input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={editPromptName}
onChange={(e) => setEditPromptName(e.target.value)}
></input>
<div className="relative bottom-12 left-3 mt-[-3.00px]">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Name
</span>
</div>
<div className="relative top-[7px] left-3">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Text
</span>
</div>
<textarea
className="h-56 w-full rounded-lg border-2 border-silver px-3 py-2 outline-none dark:bg-transparent dark:text-silver"
value={editPromptContent}
onChange={(e) => setEditPromptContent(e.target.value)}
></textarea>
</div>
<div className="mt-6 flex flex-row-reverse gap-4">
<button
className={`rounded-3xl bg-purple-30 px-5 py-2 text-white transition-all ${
currentPromptEdit.type === 'public'
? 'cursor-not-allowed opacity-50'
: 'hover:opacity-90'
}`}
onClick={() => {
handleEditPrompt &&
handleEditPrompt(currentPromptEdit.id, currentPromptEdit.type);
}}
disabled={currentPromptEdit.type === 'public'}
>
Save
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer font-medium dark:text-light-gray"
>
Cancel
</button>
</div>
</div>
);
}
export default function PromptsModal({
modalState,
setModalState,
type,
newPromptName,
setNewPromptName,
newPromptContent,
setNewPromptContent,
editPromptName,
setEditPromptName,
editPromptContent,
setEditPromptContent,
currentPromptEdit,
handleAddPrompt,
handleEditPrompt,
}: {
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
type: 'ADD' | 'EDIT';
newPromptName: string;
setNewPromptName: (name: string) => void;
newPromptContent: string;
setNewPromptContent: (content: string) => void;
editPromptName: string;
setEditPromptName: (name: string) => void;
editPromptContent: string;
setEditPromptContent: (content: string) => void;
currentPromptEdit: { name: string; id: string; type: string };
handleAddPrompt?: () => void;
handleEditPrompt?: (id: string, type: string) => void;
}) {
let view;
if (type === 'ADD') {
view = (
<AddPrompt
setModalState={setModalState}
handleAddPrompt={handleAddPrompt}
newPromptName={newPromptName}
setNewPromptName={setNewPromptName}
newPromptContent={newPromptContent}
setNewPromptContent={setNewPromptContent}
/>
);
} else if (type === 'EDIT') {
view = (
<EditPrompt
setModalState={setModalState}
handleEditPrompt={handleEditPrompt}
editPromptName={editPromptName}
setEditPromptName={setEditPromptName}
editPromptContent={editPromptContent}
setEditPromptContent={setEditPromptContent}
currentPromptEdit={currentPromptEdit}
/>
);
} else {
view = <></>;
}
return (
<article
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha`}
>
<article className="mx-auto mt-24 flex w-[90vw] max-w-lg flex-col gap-4 rounded-lg bg-white p-6 shadow-lg dark:bg-outer-space">
{view}
</article>
</article>
);
}

View File

@@ -10,6 +10,7 @@ interface Preference {
apiKey: string;
prompt: { name: string; id: string; type: string };
selectedDocs: Doc | null;
chunks: string;
sourceDocs: Doc[] | null;
conversations: { name: string; id: string }[] | null;
}
@@ -17,6 +18,7 @@ interface Preference {
const initialState: Preference = {
apiKey: 'xxx',
prompt: { name: 'default', id: 'default', type: 'public' },
chunks: '2',
selectedDocs: {
name: 'default',
language: 'default',
@@ -51,6 +53,9 @@ export const prefSlice = createSlice({
setPrompt: (state, action) => {
state.prompt = action.payload;
},
setChunks: (state, action) => {
state.chunks = action.payload;
},
},
});
@@ -60,6 +65,7 @@ export const {
setSourceDocs,
setConversations,
setPrompt,
setChunks,
} = prefSlice.actions;
export default prefSlice.reducer;
@@ -91,6 +97,16 @@ prefListenerMiddleware.startListening({
},
});
prefListenerMiddleware.startListening({
matcher: isAnyOf(setChunks),
effect: (action, listenerApi) => {
localStorage.setItem(
'DocsGPTChunks',
JSON.stringify((listenerApi.getState() as RootState).preference.chunks),
);
},
});
export const selectApiKey = (state: RootState) => state.preference.apiKey;
export const selectApiKeyStatus = (state: RootState) =>
!!state.preference.apiKey;
@@ -105,3 +121,4 @@ export const selectConversations = (state: RootState) =>
export const selectConversationId = (state: RootState) =>
state.conversation.conversationId;
export const selectPrompt = (state: RootState) => state.preference.prompt;
export const selectChunks = (state: RootState) => state.preference.chunks;

View File

@@ -8,11 +8,13 @@ import {
const key = localStorage.getItem('DocsGPTApiKey');
const prompt = localStorage.getItem('DocsGPTPrompt');
const doc = localStorage.getItem('DocsGPTRecentDocs');
const chunks = localStorage.getItem('DocsGPTChunks');
const store = configureStore({
preloadedState: {
preference: {
apiKey: key ?? '',
chunks: JSON.parse(chunks ?? '2').toString(),
selectedDocs: doc !== null ? JSON.parse(doc) : null,
prompt:
prompt !== null

View File

@@ -17,10 +17,18 @@ export default function Upload({
const [docName, setDocName] = useState('');
const [urlName, setUrlName] = useState('');
const [url, setUrl] = useState('');
const [redditData, setRedditData] = useState({
client_id: '',
client_secret: '',
user_agent: '',
search_queries: [''],
number_posts: 10,
});
const urlOptions: { label: string; value: string }[] = [
{ label: 'Crawler', value: 'crawler' },
// { label: 'Sitemap', value: 'sitemap' },
{ label: 'Link', value: 'url' },
{ label: 'Reddit', value: 'reddit' },
];
const [urlType, setUrlType] = useState<{ label: string; value: string }>({
label: 'Link',
@@ -163,7 +171,6 @@ export default function Upload({
};
const uploadRemote = () => {
console.log('here');
const formData = new FormData();
formData.append('name', urlName);
formData.append('user', 'local');
@@ -171,6 +178,13 @@ export default function Upload({
formData.append('source', urlType?.value);
}
formData.append('data', url);
if (
redditData.client_id.length > 0 &&
redditData.client_secret.length > 0
) {
formData.set('name', 'other');
formData.set('data', JSON.stringify(redditData));
}
const apiHost = import.meta.env.VITE_API_HOST;
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
@@ -202,6 +216,19 @@ export default function Upload({
['.docx'],
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
if (name === 'search_queries' && value.length > 0) {
setRedditData({
...redditData,
[name]: value.split(',').map((item) => item.trim()),
});
} else
setRedditData({
...redditData,
[name]: value,
});
};
let view;
if (progress?.type === 'UPLOAD') {
view = <UploadProgress></UploadProgress>;
@@ -275,36 +302,111 @@ export default function Upload({
{activeTab === 'remote' && (
<>
<Dropdown
fullWidth
options={urlOptions}
selectedValue={urlType}
onSelect={(value: { label: string; value: string }) =>
setUrlType(value)
}
size="w-full"
rounded="3xl"
/>
<input
placeholder="Enter name"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Name
</span>
</div>
<input
placeholder="URL Link"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={url}
onChange={(e) => setUrl(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Link
</span>
</div>
{urlType.label !== 'Reddit' ? (
<>
<input
placeholder="Enter name"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Name
</span>
</div>
<input
placeholder="URL Link"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
value={url}
onChange={(e) => setUrl(e.target.value)}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Link
</span>
</div>
</>
) : (
<>
<input
placeholder="Enter client ID"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Client ID
</span>
</div>
<input
placeholder="Enter client secret"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Client secret
</span>
</div>
<input
placeholder="Enter user agent"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
User agent
</span>
</div>
<input
placeholder="Enter search queries"
type="text"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Search queries
</span>
</div>
<input
placeholder="Enter number of posts"
type="number"
className="h-10 w-full rounded-full border-2 border-silver px-3 outline-none dark:bg-transparent dark:text-silver"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
></input>
<div className="relative bottom-12 left-2 mt-[-18.39px]">
<span className="bg-white px-2 text-xs text-silver dark:bg-outer-space dark:text-silver">
Number of posts
</span>
</div>
</>
)}
</>
)}
<div className="flex flex-row-reverse">

View File

@@ -46,7 +46,8 @@ module.exports = {
'gun-metal':'#2E303E',
'sonic-silver':'#747474',
'soap':'#D8CCF1',
'independence':'#54546D'
'independence':'#54546D',
'philippine-yellow':'#FFC700',
},
},
},

View File

@@ -357,9 +357,9 @@
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
@@ -458,16 +458,16 @@
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -515,43 +515,6 @@
"isarray": "0.0.1"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/express/node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",