Merge branch 'arc53:main' into main
@@ -61,5 +61,7 @@ Copy .env_sample and create .env with your openai api token
|
|||||||
|
|
||||||
## [How to use any other documentation](https://github.com/arc53/docsgpt/wiki/How-to-train-on-other-documentation)
|
## [How to use any other documentation](https://github.com/arc53/docsgpt/wiki/How-to-train-on-other-documentation)
|
||||||
|
|
||||||
|
## [How to host it locally (so all data will stay on-premises)](https://github.com/arc53/DocsGPT/wiki/How-to-use-different-LLM's#hosting-everything-locally)
|
||||||
|
|
||||||
Built with [🦜️🔗 LangChain](https://github.com/hwchase17/langchain)
|
Built with [🦜️🔗 LangChain](https://github.com/hwchase17/langchain)
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +1,88 @@
|
|||||||
import os
|
import os
|
||||||
import pickle
|
import json
|
||||||
|
|
||||||
import dotenv
|
import dotenv
|
||||||
import datetime
|
|
||||||
from flask import Flask, request, render_template
|
|
||||||
# os.environ["LANGCHAIN_HANDLER"] = "langchain"
|
|
||||||
import faiss
|
|
||||||
from langchain import OpenAI, VectorDBQA
|
|
||||||
from langchain.chains.question_answering import load_qa_chain
|
|
||||||
from langchain.prompts import PromptTemplate
|
|
||||||
import requests
|
import requests
|
||||||
|
from flask import Flask, request, render_template
|
||||||
|
from langchain import FAISS
|
||||||
|
from langchain import OpenAI, VectorDBQA, HuggingFaceHub, Cohere
|
||||||
|
from langchain.chains.question_answering import load_qa_chain
|
||||||
|
from langchain.embeddings import OpenAIEmbeddings, HuggingFaceHubEmbeddings, CohereEmbeddings, HuggingFaceInstructEmbeddings
|
||||||
|
from langchain.prompts import PromptTemplate
|
||||||
|
|
||||||
|
# os.environ["LANGCHAIN_HANDLER"] = "langchain"
|
||||||
|
|
||||||
|
if os.getenv("LLM_NAME") is not None:
|
||||||
|
llm_choice = os.getenv("LLM_NAME")
|
||||||
|
else:
|
||||||
|
llm_choice = "openai"
|
||||||
|
|
||||||
|
if os.getenv("EMBEDDINGS_NAME") is not None:
|
||||||
|
embeddings_choice = os.getenv("EMBEDDINGS_NAME")
|
||||||
|
else:
|
||||||
|
embeddings_choice = "openai_text-embedding-ada-002"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if llm_choice == "manifest":
|
||||||
|
from manifest import Manifest
|
||||||
|
from langchain.llms.manifest import ManifestWrapper
|
||||||
|
|
||||||
|
manifest = Manifest(
|
||||||
|
client_name="huggingface",
|
||||||
|
client_connection="http://127.0.0.1:5000"
|
||||||
|
)
|
||||||
|
|
||||||
# Redirect PosixPath to WindowsPath on Windows
|
# Redirect PosixPath to WindowsPath on Windows
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
temp = pathlib.PosixPath
|
temp = pathlib.PosixPath
|
||||||
pathlib.PosixPath = pathlib.WindowsPath
|
pathlib.PosixPath = pathlib.WindowsPath
|
||||||
|
|
||||||
# loading the .env file
|
# loading the .env file
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
with open("combine_prompt.txt", "r") as f:
|
with open("combine_prompt.txt", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
|
|
||||||
# check if OPENAI_API_KEY is set
|
with open("combine_prompt_hist.txt", "r") as f:
|
||||||
if os.getenv("OPENAI_API_KEY") is not None:
|
template_hist = f.read()
|
||||||
api_key_set = True
|
|
||||||
|
|
||||||
|
if os.getenv("API_KEY") is not None:
|
||||||
|
api_key_set = True
|
||||||
else:
|
else:
|
||||||
api_key_set = False
|
api_key_set = False
|
||||||
|
if os.getenv("EMBEDDINGS_KEY") is not None:
|
||||||
|
embeddings_key_set = True
|
||||||
|
else:
|
||||||
|
embeddings_key_set = False
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
return render_template("index.html", api_key_set=api_key_set)
|
return render_template("index.html", api_key_set=api_key_set, llm_choice=llm_choice,
|
||||||
|
embeddings_choice=embeddings_choice)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/answer", methods=["POST"])
|
@app.route("/api/answer", methods=["POST"])
|
||||||
def api_answer():
|
def api_answer():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
question = data["question"]
|
question = data["question"]
|
||||||
|
history = data["history"]
|
||||||
if not api_key_set:
|
if not api_key_set:
|
||||||
api_key = data["api_key"]
|
api_key = data["api_key"]
|
||||||
else:
|
else:
|
||||||
api_key = os.getenv("OPENAI_API_KEY")
|
api_key = os.getenv("API_KEY")
|
||||||
|
if not embeddings_key_set:
|
||||||
|
embeddings_key = data["embeddings_key"]
|
||||||
|
else:
|
||||||
|
embeddings_key = os.getenv("EMBEDDINGS_KEY")
|
||||||
|
|
||||||
|
|
||||||
# check if the vectorstore is set
|
# check if the vectorstore is set
|
||||||
if "active_docs" in data:
|
if "active_docs" in data:
|
||||||
@@ -59,25 +93,37 @@ def api_answer():
|
|||||||
vectorstore = ""
|
vectorstore = ""
|
||||||
|
|
||||||
# loading the index and the store and the prompt template
|
# loading the index and the store and the prompt template
|
||||||
index = faiss.read_index(f"{vectorstore}docs.index")
|
# Note if you have used other embeddings than OpenAI, you need to change the embeddings
|
||||||
|
if embeddings_choice == "openai_text-embedding-ada-002":
|
||||||
|
docsearch = FAISS.load_local(vectorstore, OpenAIEmbeddings(openai_api_key=embeddings_key))
|
||||||
|
elif embeddings_choice == "huggingface_sentence-transformers/all-mpnet-base-v2":
|
||||||
|
docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings())
|
||||||
|
elif embeddings_choice == "huggingface_hkunlp/instructor-large":
|
||||||
|
docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings())
|
||||||
|
elif embeddings_choice == "cohere_medium":
|
||||||
|
docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key))
|
||||||
|
|
||||||
with open(f"{vectorstore}faiss_store.pkl", "rb") as f:
|
|
||||||
store = pickle.load(f)
|
|
||||||
|
|
||||||
store.index = index
|
|
||||||
# create a prompt template
|
# create a prompt template
|
||||||
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template)
|
if history:
|
||||||
# create a chain with the prompt template and the store
|
history = json.loads(history)
|
||||||
|
template_temp = template_hist.replace("{historyquestion}", history[0]).replace("{historyanswer}", history[1])
|
||||||
|
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template_temp)
|
||||||
|
else:
|
||||||
|
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template)
|
||||||
|
|
||||||
#chain = VectorDBQA.from_llm(llm=OpenAI(openai_api_key=api_key, temperature=0), vectorstore=store, combine_prompt=c_prompt)
|
if llm_choice == "openai":
|
||||||
# chain = VectorDBQA.from_chain_type(llm=OpenAI(openai_api_key=api_key, temperature=0), chain_type='map_reduce',
|
llm = OpenAI(openai_api_key=api_key, temperature=0)
|
||||||
# vectorstore=store)
|
elif llm_choice == "manifest":
|
||||||
|
llm = ManifestWrapper(client=manifest, llm_kwargs={"temperature": 0.001, "max_tokens": 2048})
|
||||||
|
elif llm_choice == "huggingface":
|
||||||
|
llm = HuggingFaceHub(repo_id="bigscience/bloom", huggingfacehub_api_token=api_key)
|
||||||
|
elif llm_choice == "cohere":
|
||||||
|
llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key)
|
||||||
|
|
||||||
qa_chain = load_qa_chain(OpenAI(openai_api_key=api_key, temperature=0), chain_type="map_reduce",
|
qa_chain = load_qa_chain(llm=llm, chain_type="map_reduce",
|
||||||
combine_prompt=c_prompt)
|
combine_prompt=c_prompt)
|
||||||
chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=store)
|
|
||||||
|
|
||||||
|
|
||||||
|
chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=4)
|
||||||
|
|
||||||
# fetch the answer
|
# fetch the answer
|
||||||
result = chain({"query": question})
|
result = chain({"query": question})
|
||||||
@@ -94,6 +140,7 @@ def api_answer():
|
|||||||
# }
|
# }
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/docs_check", methods=["POST"])
|
@app.route("/api/docs_check", methods=["POST"])
|
||||||
def check_docs():
|
def check_docs():
|
||||||
# check if docs exist in a vectorstore folder
|
# check if docs exist in a vectorstore folder
|
||||||
@@ -104,20 +151,21 @@ def check_docs():
|
|||||||
if os.path.exists(vectorstore):
|
if os.path.exists(vectorstore):
|
||||||
return {"status": 'exists'}
|
return {"status": 'exists'}
|
||||||
else:
|
else:
|
||||||
r = requests.get(base_path + vectorstore + "docs.index")
|
r = requests.get(base_path + vectorstore + "index.faiss")
|
||||||
# save to vectors directory
|
# save to vectors directory
|
||||||
# check if the directory exists
|
# check if the directory exists
|
||||||
if not os.path.exists(vectorstore):
|
if not os.path.exists(vectorstore):
|
||||||
os.makedirs(vectorstore)
|
os.makedirs(vectorstore)
|
||||||
|
|
||||||
with open(vectorstore + "docs.index", "wb") as f:
|
with open(vectorstore + "index.faiss", "wb") as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
# download the store
|
# download the store
|
||||||
r = requests.get(base_path + vectorstore + "faiss_store.pkl")
|
r = requests.get(base_path + vectorstore + "index.pkl")
|
||||||
with open(vectorstore + "faiss_store.pkl", "wb") as f:
|
with open(vectorstore + "index.pkl", "wb") as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
|
|
||||||
return {"status": 'loaded'}
|
return {"status": 'loaded'}
|
||||||
|
|
||||||
|
|
||||||
# handling CORS
|
# handling CORS
|
||||||
@app.after_request
|
@app.after_request
|
||||||
|
|||||||
27
application/combine_prompt_hist.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
You are a DocsGPT bot assistant by Arc53 that provides help with programming libraries. You give thorough answers with code examples.
|
||||||
|
Given the following extracted parts of a long document and a question, create a final answer with references ("SOURCES").
|
||||||
|
ALWAYS return a "SOURCES" part in your answer. You can also remeber things from previous questions and use them in your answer.
|
||||||
|
|
||||||
|
QUESTION: How to merge tables in pandas?
|
||||||
|
=========
|
||||||
|
Content: pandas provides various facilities for easily combining together Series or DataFrame with various kinds of set logic for the indexes and relational algebra functionality in the case of join / merge-type operations.
|
||||||
|
Source: 28-pl
|
||||||
|
Content: pandas provides a single function, merge(), as the entry point for all standard database join operations between DataFrame or named Series objects: \n\npandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
|
||||||
|
Source: 30-pl
|
||||||
|
=========
|
||||||
|
FINAL ANSWER: To merge two tables in pandas, you can use the pd.merge() function. The basic syntax is: \n\npd.merge(left, right, on, how) \n\nwhere left and right are the two tables to merge, on is the column to merge on, and how is the type of merge to perform. \n\nFor example, to merge the two tables df1 and df2 on the column 'id', you can use: \n\npd.merge(df1, df2, on='id', how='inner')
|
||||||
|
SOURCES: 28-pl 30-pl
|
||||||
|
|
||||||
|
QUESTION: {historyquestion}
|
||||||
|
=========
|
||||||
|
CONTENT:
|
||||||
|
SOURCE:
|
||||||
|
=========
|
||||||
|
FINAL ANSWER: {historyanswer}
|
||||||
|
SOURCES:
|
||||||
|
|
||||||
|
QUESTION: {question}
|
||||||
|
=========
|
||||||
|
{summaries}
|
||||||
|
=========
|
||||||
|
FINAL ANSWER:
|
||||||
BIN
application/index.faiss
Normal file
BIN
application/index.pkl
Normal file
@@ -45,6 +45,7 @@ pytz==2022.7.1
|
|||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
regex==2022.10.31
|
regex==2022.10.31
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
|
retry==0.9.2
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
snowballstemmer==2.2.0
|
snowballstemmer==2.2.0
|
||||||
Sphinx==6.1.3
|
Sphinx==6.1.3
|
||||||
@@ -64,6 +65,6 @@ typer==0.7.0
|
|||||||
typing-inspect==0.8.0
|
typing-inspect==0.8.0
|
||||||
typing_extensions==4.4.0
|
typing_extensions==4.4.0
|
||||||
urllib3==1.26.14
|
urllib3==1.26.14
|
||||||
Werkzeug==2.2.2
|
Werkzeug==2.2.3
|
||||||
XlsxWriter==3.0.8
|
XlsxWriter==3.0.8
|
||||||
yarl==1.8.2
|
yarl==1.8.2
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ if (el) {
|
|||||||
|
|
||||||
body: JSON.stringify({question: message,
|
body: JSON.stringify({question: message,
|
||||||
api_key: localStorage.getItem('apiKey'),
|
api_key: localStorage.getItem('apiKey'),
|
||||||
|
embeddings_key: localStorage.getItem('apiKey'),
|
||||||
|
history: localStorage.getItem('chatHistory'),
|
||||||
active_docs: localStorage.getItem('activeDocs')}),
|
active_docs: localStorage.getItem('activeDocs')}),
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -38,9 +40,12 @@ if (el) {
|
|||||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||||
document.getElementById("button-submit").innerHTML = 'Send';
|
document.getElementById("button-submit").innerHTML = 'Send';
|
||||||
document.getElementById("button-submit").disabled = false;
|
document.getElementById("button-submit").disabled = false;
|
||||||
|
let chatHistory = [message, data.answer];
|
||||||
|
localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
console.log(error);
|
||||||
document.getElementById("button-submit").innerHTML = 'Send';
|
document.getElementById("button-submit").innerHTML = 'Send';
|
||||||
document.getElementById("button-submit").disabled = false;
|
document.getElementById("button-submit").disabled = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,13 +131,17 @@ This will return a new DataFrame with all the columns from both tables, and only
|
|||||||
var option = document.createElement("option");
|
var option = document.createElement("option");
|
||||||
if (docsIndex[key].name == docsIndex[key].language) {
|
if (docsIndex[key].name == docsIndex[key].language) {
|
||||||
option.text = docsIndex[key].name + " " + docsIndex[key].version;
|
option.text = docsIndex[key].name + " " + docsIndex[key].version;
|
||||||
option.value = docsIndex[key].name + "/" + ".project" + "/" + docsIndex[key].version + "/";
|
option.value = docsIndex[key].name + "/" + ".project" + "/" + docsIndex[key].version + "/{{ embeddings_choice }}/";
|
||||||
select.add(option);
|
if (docsIndex[key].model == "{{ embeddings_choice }}") {
|
||||||
|
select.add(option);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
option.text = docsIndex[key].name + " " + docsIndex[key].version;
|
option.text = docsIndex[key].name + " " + docsIndex[key].version;
|
||||||
option.value = docsIndex[key].language + "/" + docsIndex[key].name + "/" + docsIndex[key].version + "/";
|
option.value = docsIndex[key].language + "/" + docsIndex[key].name + "/" + docsIndex[key].version + "/{{ embeddings_choice }}/";
|
||||||
select.add(option);
|
if (docsIndex[key].model == "{{ embeddings_choice }}") {
|
||||||
|
select.add(option);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ module.exports = {
|
|||||||
plugins: ['react'],
|
plugins: ['react'],
|
||||||
rules: {
|
rules: {
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/parsers': {
|
'import/parsers': {
|
||||||
@@ -34,10 +40,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'prettier/prettier': [
|
};
|
||||||
'error',
|
|
||||||
{
|
|
||||||
endOfLine: 'auto',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
5893
frontend/package-lock.json
generated
@@ -1,28 +1,11 @@
|
|||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useMediaQuery } from '../hooks';
|
|
||||||
import { selectIsMenuOpen } from '../store';
|
|
||||||
|
|
||||||
//TODO - Add hyperlinks to text
|
//TODO - Add hyperlinks to text
|
||||||
//TODO - Styling
|
//TODO - Styling
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
||||||
const isMenuOpen = useSelector(selectIsMenuOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
//Parent div for all content shown through App.tsx routing needs to have this styling. Might change when state management is updated.
|
//Parent div for all content shown through App.tsx routing needs to have this styling. Might change when state management is updated.
|
||||||
<div
|
<div className="grid min-h-screen">
|
||||||
className={`${
|
<article className=" mx-auto my-auto flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-lg bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
|
||||||
isMobile
|
|
||||||
? isMenuOpen
|
|
||||||
? 'mt-80'
|
|
||||||
: 'mt-16'
|
|
||||||
: isMenuOpen
|
|
||||||
? 'md:ml-72 lg:ml-96'
|
|
||||||
: 'ml-16'
|
|
||||||
} h-full w-full p-6 transition-all`}
|
|
||||||
>
|
|
||||||
<article className="mx-auto my-auto flex w-full max-w-6xl flex-col gap-6 rounded-lg bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
|
|
||||||
<p className="text-3xl font-semibold">About DocsGPT 🦖</p>
|
<p className="text-3xl font-semibold">About DocsGPT 🦖</p>
|
||||||
<p className="mt-4 text-xl font-bold">
|
<p className="mt-4 text-xl font-bold">
|
||||||
Find the information in your documentation through AI-powered
|
Find the information in your documentation through AI-powered
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './Navigation';
|
||||||
import Conversation from './components/Conversation/Conversation';
|
import Conversation from './conversation/Conversation';
|
||||||
import APIKeyModal from './components/APIKeyModal';
|
import About from './About';
|
||||||
import About from './components/About';
|
import { useState } from 'react';
|
||||||
|
import { ActiveState } from './models/misc';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [navState, setNavState] = useState<ActiveState>('ACTIVE');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col transition-all md:flex-row">
|
<div className="min-h-full min-w-full">
|
||||||
<APIKeyModal />
|
<Navigation
|
||||||
<Navigation />
|
navState={navState}
|
||||||
<Routes>
|
setNavState={(val: ActiveState) => setNavState(val)}
|
||||||
<Route path="/" element={<Conversation />} />
|
/>
|
||||||
<Route path="/about" element={<About />} />
|
<div
|
||||||
</Routes>
|
className={`transition-all duration-200 ${
|
||||||
|
navState === 'ACTIVE' ? 'ml-0 md:ml-72 lg:ml-96' : ' ml-0 md:ml-16'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Conversation />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
frontend/src/Avatar.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default function Avatar({
|
||||||
|
avatar,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
avatar: string;
|
||||||
|
size?: 'SMALL' | 'MEDIUM' | 'LARGE';
|
||||||
|
}) {
|
||||||
|
return <div>{avatar}</div>;
|
||||||
|
}
|
||||||
21
frontend/src/Hero.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default function Hero({ className = '' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col ${className}`}>
|
||||||
|
<p className="mb-10 text-center text-4xl font-semibold">
|
||||||
|
DocsGPT <span className="text-3xl">🦖</span>
|
||||||
|
</p>
|
||||||
|
<p className="mb-3 text-center">
|
||||||
|
Welcome to DocsGPT, your technical documentation assistant!
|
||||||
|
</p>
|
||||||
|
<p className="mb-3 text-center">
|
||||||
|
Enter a query related to the information in the documentation you
|
||||||
|
selected to receive and we will provide you with the most relevant
|
||||||
|
answers.
|
||||||
|
</p>
|
||||||
|
<p className="text-center">
|
||||||
|
Start by entering your query in the input field below and we will do the
|
||||||
|
rest!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
frontend/src/Navigation.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import Arrow1 from './assets/arrow.svg';
|
||||||
|
import Hamburger from './assets/hamburger.svg';
|
||||||
|
import Key from './assets/key.svg';
|
||||||
|
import Info from './assets/info.svg';
|
||||||
|
import Link from './assets/link.svg';
|
||||||
|
import { ActiveState } from './models/misc';
|
||||||
|
import APIKeyModal from './preferences/APIKeyModal';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { selectApiKeyStatus } from './preferences/preferenceSlice';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
//TODO - Need to replace Chat button to open secondary nav with scrollable past chats option and new chat at top
|
||||||
|
//TODO - Need to add Discord and Github links
|
||||||
|
|
||||||
|
export default function Navigation({
|
||||||
|
navState,
|
||||||
|
setNavState,
|
||||||
|
}: {
|
||||||
|
navState: ActiveState;
|
||||||
|
setNavState: (val: ActiveState) => void;
|
||||||
|
}) {
|
||||||
|
const isApiKeySet = useSelector(selectApiKeyStatus);
|
||||||
|
const [apiKeyModalState, setApiKeyModalState] = useState<ActiveState>(
|
||||||
|
isApiKeySet ? 'INACTIVE' : 'ACTIVE',
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
navState === 'INACTIVE' && '-ml-96 md:-ml-60 lg:-ml-80'
|
||||||
|
} fixed z-10 flex h-full w-72 flex-col border-r-2 border-gray-100 bg-gray-50 transition-all duration-200 lg:w-96`}
|
||||||
|
>
|
||||||
|
<div className={'h-16 w-full border-b-2 border-gray-100'}>
|
||||||
|
<button
|
||||||
|
className="float-right mr-5 mt-5 h-5 w-5"
|
||||||
|
onClick={() =>
|
||||||
|
setNavState(navState === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={Arrow1}
|
||||||
|
alt="menu toggle"
|
||||||
|
className={`${
|
||||||
|
navState === 'INACTIVE' ? 'rotate-180' : 'rotate-0'
|
||||||
|
} m-auto w-3 transition-all duration-200`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow border-b-2 border-gray-100"></div>
|
||||||
|
|
||||||
|
<div className="flex h-16 flex-col border-b-2 border-gray-100">
|
||||||
|
<div
|
||||||
|
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
setApiKeyModalState('ACTIVE');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={Key} alt="key" className="ml-2 w-6" />
|
||||||
|
<p className="my-auto text-eerie-black">Reset Key</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-48 flex-col border-b-2 border-gray-100">
|
||||||
|
<NavLink
|
||||||
|
to="/about"
|
||||||
|
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<img src={Info} alt="info" className="ml-2 w-5" />
|
||||||
|
<p className="my-auto text-eerie-black">About</p>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
|
||||||
|
<img src={Link} alt="link" className="ml-2 w-5" />
|
||||||
|
<p className="my-auto text-eerie-black">Discord</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
|
||||||
|
<img src={Link} alt="link" className="ml-2 w-5" />
|
||||||
|
<p className="my-auto text-eerie-black">Github</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="fixed mt-5 ml-6 h-6 w-6 md:hidden"
|
||||||
|
onClick={() => setNavState('ACTIVE')}
|
||||||
|
>
|
||||||
|
<img src={Hamburger} alt="menu toggle" className="w-7" />
|
||||||
|
</button>
|
||||||
|
<APIKeyModal
|
||||||
|
modalState={apiKeyModalState}
|
||||||
|
setModalState={setApiKeyModalState}
|
||||||
|
isCancellable={isApiKeySet}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 200 B After Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
3
frontend/src/assets/send.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="21" height="18" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.00999999 18L21 9L0.00999999 0L0 7L15 9L0 11L0.00999999 18Z" fill="black" fill-opacity="0.54"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 210 B |
@@ -1,63 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import {
|
|
||||||
setApiKey,
|
|
||||||
toggleApiKeyModal,
|
|
||||||
selectIsApiKeyModalOpen,
|
|
||||||
} from '../store';
|
|
||||||
|
|
||||||
export default function APIKeyModal({}) {
|
|
||||||
//TODO - Add form validation?
|
|
||||||
//TODO - Connect to backend
|
|
||||||
//TODO - Add link to OpenAI API Key page
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const isApiModalOpen = useSelector(selectIsApiKeyModalOpen);
|
|
||||||
const [key, setKey] = useState('');
|
|
||||||
const [formError, setFormError] = useState(false);
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
if (key.length < 1) {
|
|
||||||
setFormError(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setApiKey(key));
|
|
||||||
dispatch(toggleApiKeyModal());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isApiModalOpen ? 'visible' : 'hidden'
|
|
||||||
} absolute 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">
|
|
||||||
<p className="text-xl text-jet">OpenAI API Key</p>
|
|
||||||
<p className="text-lg leading-5 text-gray-500">
|
|
||||||
Before you can start using DocsGPT we need you to provide an API key
|
|
||||||
for llm. Currently, we support only OpenAI but soon many more. You can
|
|
||||||
find it here.
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="h-10 w-full border-b-2 border-jet focus:outline-none"
|
|
||||||
value={key}
|
|
||||||
maxLength={100}
|
|
||||||
placeholder="API Key"
|
|
||||||
onChange={(e) => setKey(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
{formError && (
|
|
||||||
<p className="text-sm text-red-500">Please enter a valid API key</p>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => handleSubmit()}
|
|
||||||
className="ml-auto h-10 w-20 rounded-lg bg-violet-800 text-white transition-all hover:bg-violet-700"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useMediaQuery } from '../../hooks';
|
|
||||||
import { selectIsMenuOpen } from '../../store';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
export default function Conversation() {
|
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
||||||
const isMenuOpen = useSelector(selectIsMenuOpen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
//Parent div for all content shown through App.tsx routing needs to have this styling.
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isMobile
|
|
||||||
? isMenuOpen
|
|
||||||
? 'mt-80'
|
|
||||||
: 'mt-16'
|
|
||||||
: isMenuOpen
|
|
||||||
? 'md:ml-72 lg:ml-96'
|
|
||||||
: 'ml-16'
|
|
||||||
} h-full w-full p-6 transition-all`}
|
|
||||||
>
|
|
||||||
Docs GPT Chat Placeholder
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import { useMediaQuery } from '../hooks';
|
|
||||||
import {
|
|
||||||
toggleApiKeyModal,
|
|
||||||
selectIsMenuOpen,
|
|
||||||
toggleIsMenuOpen,
|
|
||||||
} from '../store';
|
|
||||||
import Arrow1 from '../imgs/arrow.svg';
|
|
||||||
import Hamburger from '../imgs/hamburger.svg';
|
|
||||||
import Key from '../imgs/key.svg';
|
|
||||||
import Info from '../imgs/info.svg';
|
|
||||||
import Link from '../imgs/link.svg';
|
|
||||||
import Exit from '../imgs/exit.svg';
|
|
||||||
|
|
||||||
//TODO - Need to replace Chat button to open secondary nav with scrollable past chats option and new chat at top
|
|
||||||
//TODO - Need to add Discord and Github links
|
|
||||||
|
|
||||||
function MobileNavigation({}) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const isMenuOpen = useSelector(selectIsMenuOpen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'border-b-2 border-gray-100' : 'h-16'
|
|
||||||
} fixed flex w-full flex-col bg-gray-50 transition-all`}
|
|
||||||
>
|
|
||||||
<div className="h-16 w-full border-b-2 border-gray-100">
|
|
||||||
{isMenuOpen ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="mt-5 ml-6 h-6 w-6"
|
|
||||||
onClick={() => dispatch(toggleIsMenuOpen())}
|
|
||||||
>
|
|
||||||
<img src={Exit} alt="menu toggle" className="w-5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="mt-5 ml-6 h-6 w-6"
|
|
||||||
onClick={() => dispatch(toggleIsMenuOpen())}
|
|
||||||
>
|
|
||||||
<img src={Hamburger} alt="menu toggle" className="w-7" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isMenuOpen && (
|
|
||||||
<nav className="my-4 flex flex-col">
|
|
||||||
<NavLink
|
|
||||||
to="/"
|
|
||||||
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<img src={Info} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Chat</p>
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/about"
|
|
||||||
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<img src={Info} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">About</p>
|
|
||||||
</NavLink>
|
|
||||||
<div className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100">
|
|
||||||
<img src={Link} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Discord</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100">
|
|
||||||
<img src={Link} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Github</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
|
|
||||||
onClick={() => dispatch(toggleApiKeyModal())}
|
|
||||||
>
|
|
||||||
<img src={Key} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Reset Key</p>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DesktopNavigation() {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const isMenuOpen = useSelector(selectIsMenuOpen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'w-72 lg:w-96' : 'w-16'
|
|
||||||
} fixed flex h-screen flex-col border-r-2 border-gray-100 bg-gray-50 transition-all`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'w-full' : 'w-16'
|
|
||||||
} ml-auto h-16 border-b-2 border-gray-100`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="float-right mr-5 mt-5 h-5 w-5"
|
|
||||||
onClick={() => dispatch(toggleIsMenuOpen())}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Arrow1}
|
|
||||||
alt="menu toggle"
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'rotate-0' : 'rotate-180'
|
|
||||||
} m-auto w-3 transition-all`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isMenuOpen && (
|
|
||||||
<>
|
|
||||||
<div className="flex-grow border-b-2 border-gray-100"></div>
|
|
||||||
|
|
||||||
<div className="flex h-16 flex-col border-b-2 border-gray-100">
|
|
||||||
<div
|
|
||||||
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
|
|
||||||
onClick={() => dispatch(toggleApiKeyModal())}
|
|
||||||
>
|
|
||||||
<img src={Key} alt="key" className="ml-2 w-6" />
|
|
||||||
<p className="my-auto text-eerie-black">Reset Key</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-48 flex-col border-b-2 border-gray-100">
|
|
||||||
<NavLink
|
|
||||||
to="/about"
|
|
||||||
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<img src={Info} alt="info" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">About</p>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
|
|
||||||
<img src={Link} alt="link" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Discord</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
|
|
||||||
<img src={Link} alt="link" className="ml-2 w-5" />
|
|
||||||
<p className="my-auto text-eerie-black">Github</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Navigation() {
|
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return <MobileNavigation />;
|
|
||||||
} else {
|
|
||||||
return <DesktopNavigation />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
frontend/src/conversation/Conversation.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Hero from '../Hero';
|
||||||
|
import ConversationBubble from './ConversationBubble';
|
||||||
|
import ConversationInput from './ConversationInput';
|
||||||
|
import { selectConversation } from './conversationSlice';
|
||||||
|
|
||||||
|
export default function Conversation() {
|
||||||
|
const messages = useSelector(selectConversation);
|
||||||
|
const endMessageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => endMessageRef?.current?.scrollIntoView());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-6">
|
||||||
|
<div className="w-10/12 transition-all md:w-1/2">
|
||||||
|
{messages.map((message, index) => {
|
||||||
|
return (
|
||||||
|
<ConversationBubble
|
||||||
|
ref={index === messages.length - 1 ? endMessageRef : null}
|
||||||
|
className="mb-7"
|
||||||
|
key={index}
|
||||||
|
message={message.text}
|
||||||
|
type={message.type}
|
||||||
|
></ConversationBubble>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{messages.length === 0 && <Hero className="mt-24"></Hero>}
|
||||||
|
</div>
|
||||||
|
<ConversationInput className="fixed bottom-2 w-10/12 md:w-[50%]"></ConversationInput>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
frontend/src/conversation/ConversationBubble.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
|
import Avatar from '../Avatar';
|
||||||
|
import { MESSAGE_TYPE } from './conversationModels';
|
||||||
|
|
||||||
|
const ConversationBubble = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
{
|
||||||
|
message: string;
|
||||||
|
type: MESSAGE_TYPE;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
>(function ConversationBubble({ message, type, className }, ref) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`flex rounded-3xl ${
|
||||||
|
type === 'QUESTION' ? '' : 'bg-gray-1000'
|
||||||
|
} py-7 px-5 ${className}`}
|
||||||
|
>
|
||||||
|
<Avatar avatar={type === 'QUESTION' ? '👤' : '🦖'}></Avatar>
|
||||||
|
<p className="ml-5">{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ConversationBubble;
|
||||||
21
frontend/src/conversation/ConversationInput.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Send from './../assets/send.svg';
|
||||||
|
|
||||||
|
export default function ConversationInput({
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`${className} flex`}>
|
||||||
|
<div
|
||||||
|
contentEditable
|
||||||
|
className={`min-h-5 border-000000 overflow-x-hidden; max-h-24 w-full overflow-y-auto rounded-xl border bg-white p-2 pr-9 opacity-100 focus:border-2 focus:outline-none`}
|
||||||
|
></div>
|
||||||
|
<img
|
||||||
|
onClick={() => console.log('here')}
|
||||||
|
src={Send}
|
||||||
|
className="relative right-9"
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
frontend/src/conversation/conversationModels.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER';
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
text: string;
|
||||||
|
type: MESSAGE_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationState {
|
||||||
|
conversation: Message[];
|
||||||
|
}
|
||||||
50
frontend/src/conversation/conversationSlice.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import store from '../store';
|
||||||
|
import { ConversationState, Message } from './conversationModels';
|
||||||
|
|
||||||
|
// harcoding the initial state just for demo
|
||||||
|
const initialState: ConversationState = {
|
||||||
|
conversation: [
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
|
||||||
|
{ text: 'what is ChatGPT', type: 'QUESTION' },
|
||||||
|
{
|
||||||
|
text: 'ChatGPT is large learning model',
|
||||||
|
type: 'ANSWER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const conversationSlice = createSlice({
|
||||||
|
name: 'conversation',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addMessage(state, action: PayloadAction<Message>) {
|
||||||
|
state.conversation.push(action.payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addMessage } = conversationSlice.actions;
|
||||||
|
|
||||||
|
type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export const selectConversation = (state: RootState) =>
|
||||||
|
state.conversation.conversation;
|
||||||
|
|
||||||
|
export default conversationSlice.reducer;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export function useMediaQuery(query: string): boolean {
|
|
||||||
const [matches, setMatches] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const media = window.matchMedia(query);
|
|
||||||
|
|
||||||
if (media.matches !== matches) {
|
|
||||||
setMatches(media.matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
const listener = () => {
|
|
||||||
setMatches(media.matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
media.addEventListener('resize', listener);
|
|
||||||
return () => media.removeEventListener('resize', listener);
|
|
||||||
}, [matches, query]);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
5
frontend/src/models/misc.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type ActiveState = 'ACTIVE' | 'INACTIVE';
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
83
frontend/src/preferences/APIKeyModal.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
|
import { setApiKey } from './preferenceSlice';
|
||||||
|
|
||||||
|
export default function APIKeyModal({
|
||||||
|
modalState,
|
||||||
|
setModalState,
|
||||||
|
isCancellable = true,
|
||||||
|
}: {
|
||||||
|
modalState: ActiveState;
|
||||||
|
setModalState: (val: ActiveState) => void;
|
||||||
|
isCancellable?: boolean;
|
||||||
|
}) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [key, setKey] = useState('');
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (key.length <= 1) {
|
||||||
|
setIsError(true);
|
||||||
|
} else {
|
||||||
|
dispatch(setApiKey(key));
|
||||||
|
setModalState('INACTIVE');
|
||||||
|
setKey('');
|
||||||
|
setIsError(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
setKey('');
|
||||||
|
setIsError(false);
|
||||||
|
setModalState('INACTIVE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
modalState === 'ACTIVE' ? 'visible' : 'hidden'
|
||||||
|
} absolute 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">
|
||||||
|
<p className="text-xl text-jet">OpenAI API Key</p>
|
||||||
|
<p className="text-lg leading-5 text-gray-500">
|
||||||
|
Before you can start using DocsGPT we need you to provide an API key
|
||||||
|
for llm. Currently, we support only OpenAI but soon many more. You can
|
||||||
|
find it here.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="h-10 w-full border-b-2 border-jet focus:outline-none"
|
||||||
|
value={key}
|
||||||
|
maxLength={100}
|
||||||
|
placeholder="API Key"
|
||||||
|
onChange={(e) => setKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row-reverse">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSubmit()}
|
||||||
|
className="ml-auto h-10 w-20 rounded-lg bg-violet-800 text-white transition-all hover:bg-violet-700"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{isCancellable && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCancel()}
|
||||||
|
className="ml-5 h-10 w-20 rounded-lg border border-violet-700 bg-white text-violet-800 transition-all hover:bg-violet-700 hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isError && (
|
||||||
|
<p className="mr-auto text-sm text-red-500">
|
||||||
|
Please enter a valid API key
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
frontend/src/preferences/preferenceSlice.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
interface Preference {
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: Preference = {
|
||||||
|
apiKey: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prefSlice = createSlice({
|
||||||
|
name: 'preference',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setApiKey: (state, action) => {
|
||||||
|
state.apiKey = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setApiKey } = prefSlice.actions;
|
||||||
|
export default prefSlice.reducer;
|
||||||
|
|
||||||
|
type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|
||||||
|
export const selectApiKey = (state: RootState) => state.preference.apiKey;
|
||||||
|
export const selectApiKeyStatus = (state: RootState) =>
|
||||||
|
!!state.preference.apiKey;
|
||||||
@@ -1,48 +1,12 @@
|
|||||||
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { conversationSlice } from './conversation/conversationSlice';
|
||||||
interface State {
|
import { prefSlice } from './preferences/preferenceSlice';
|
||||||
isApiKeyModalOpen: boolean;
|
|
||||||
apiKey: string;
|
|
||||||
isMenuOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: State = {
|
|
||||||
isApiKeyModalOpen: false,
|
|
||||||
apiKey: '',
|
|
||||||
isMenuOpen: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const slice = createSlice({
|
|
||||||
name: 'app',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
toggleApiKeyModal: (state) => {
|
|
||||||
state.isApiKeyModalOpen = !state.isApiKeyModalOpen;
|
|
||||||
console.log('showApiKeyModal', state.isApiKeyModalOpen);
|
|
||||||
},
|
|
||||||
setApiKey: (state, action: PayloadAction<string>) => {
|
|
||||||
state.apiKey = action.payload;
|
|
||||||
console.log('setApiKey', action.payload);
|
|
||||||
},
|
|
||||||
toggleIsMenuOpen: (state) => {
|
|
||||||
state.isMenuOpen = !state.isMenuOpen;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { toggleApiKeyModal, setApiKey, toggleIsMenuOpen } = slice.actions;
|
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
app: slice.reducer,
|
preference: prefSlice.reducer,
|
||||||
|
conversation: conversationSlice.reducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type RootState = ReturnType<typeof store.getState>;
|
|
||||||
|
|
||||||
export const selectIsApiKeyModalOpen = (state: RootState) =>
|
|
||||||
state.app.isApiKeyModalOpen;
|
|
||||||
export const selectApiKey = (state: RootState) => state.app.apiKey;
|
|
||||||
export const selectIsMenuOpen = (state: RootState) => state.app.isMenuOpen;
|
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module.exports = {
|
|||||||
'eerie-black': '#212121',
|
'eerie-black': '#212121',
|
||||||
jet: '#343541',
|
jet: '#343541',
|
||||||
'gray-alpha': 'rgba(0,0,0, .1)',
|
'gray-alpha': 'rgba(0,0,0, .1)',
|
||||||
|
'gray-1000': '#F6F6F6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import nltk
|
import nltk
|
||||||
import dotenv
|
import dotenv
|
||||||
@@ -18,13 +20,17 @@ app = typer.Typer(add_completion=False)
|
|||||||
nltk.download('punkt', quiet=True)
|
nltk.download('punkt', quiet=True)
|
||||||
nltk.download('averaged_perceptron_tagger', quiet=True)
|
nltk.download('averaged_perceptron_tagger', quiet=True)
|
||||||
|
|
||||||
|
|
||||||
#Splits all files in specified folder to documents
|
#Splits all files in specified folder to documents
|
||||||
@app.command()
|
@app.command()
|
||||||
def ingest(directory: Optional[str] = typer.Option("inputs",
|
def ingest(yes: bool = typer.Option(False, "-y", "--yes", prompt=False,
|
||||||
help="Path to the directory for index creation."),
|
help="Whether to skip price confirmation"),
|
||||||
files: Optional[List[str]] = typer.Option(None,
|
dir: Optional[List[str]] = typer.Option(["inputs"],
|
||||||
help="""File paths to use (Optional; overrides directory).
|
help="""List of paths to directory for index creation.
|
||||||
E.g. --files inputs/1.md --files inputs/2.md"""),
|
E.g. --dir inputs --dir inputs2"""),
|
||||||
|
file: Optional[List[str]] = typer.Option(None,
|
||||||
|
help="""File paths to use (Optional; overrides dir).
|
||||||
|
E.g. --file inputs/1.md --file inputs/2.md"""),
|
||||||
recursive: Optional[bool] = typer.Option(True,
|
recursive: Optional[bool] = typer.Option(True,
|
||||||
help="Whether to recursively search in subdirectories."),
|
help="Whether to recursively search in subdirectories."),
|
||||||
limit: Optional[int] = typer.Option(None,
|
limit: Optional[int] = typer.Option(None,
|
||||||
@@ -38,27 +44,39 @@ def ingest(directory: Optional[str] = typer.Option("inputs",
|
|||||||
Creates index from specified location or files.
|
Creates index from specified location or files.
|
||||||
By default /inputs folder is used, .rst and .md are parsed.
|
By default /inputs folder is used, .rst and .md are parsed.
|
||||||
"""
|
"""
|
||||||
raw_docs = SimpleDirectoryReader(input_dir=directory, input_files=files, recursive=recursive,
|
|
||||||
required_exts=formats, num_files_limit=limit,
|
|
||||||
exclude_hidden=exclude).load_data()
|
|
||||||
raw_docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
|
||||||
print(raw_docs)
|
|
||||||
# Here we split the documents, as needed, into smaller chunks.
|
|
||||||
# We do this due to the context limits of the LLMs.
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter()
|
|
||||||
docs = text_splitter.split_documents(raw_docs)
|
|
||||||
|
|
||||||
# Here we check for command line arguments for bot calls.
|
def process_one_docs(directory, folder_name):
|
||||||
# If no argument exists or the permission_bypass_flag argument is not '-y',
|
raw_docs = SimpleDirectoryReader(input_dir=directory, input_files=file, recursive=recursive,
|
||||||
# user permission is requested to call the API.
|
required_exts=formats, num_files_limit=limit,
|
||||||
if len(sys.argv) > 1:
|
exclude_hidden=exclude).load_data()
|
||||||
permission_bypass_flag = sys.argv[1]
|
raw_docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
||||||
if permission_bypass_flag == '-y':
|
# Here we split the documents, as needed, into smaller chunks.
|
||||||
call_openai_api(docs)
|
# We do this due to the context limits of the LLMs.
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter()
|
||||||
|
docs = text_splitter.split_documents(raw_docs)
|
||||||
|
|
||||||
|
# Here we check for command line arguments for bot calls.
|
||||||
|
# If no argument exists or the yes is not True, then the
|
||||||
|
# user permission is requested to call the API.
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if yes:
|
||||||
|
call_openai_api(docs, folder_name)
|
||||||
|
else:
|
||||||
|
get_user_permission(docs, folder_name)
|
||||||
else:
|
else:
|
||||||
get_user_permission(docs)
|
get_user_permission(docs, folder_name)
|
||||||
else:
|
|
||||||
get_user_permission(docs)
|
folder_counts = defaultdict(int)
|
||||||
|
folder_names = []
|
||||||
|
for dir_path in dir:
|
||||||
|
folder_name = os.path.basename(os.path.normpath(dir_path))
|
||||||
|
folder_counts[folder_name] += 1
|
||||||
|
if folder_counts[folder_name] > 1:
|
||||||
|
folder_name = f"{folder_name}_{folder_counts[folder_name]}"
|
||||||
|
folder_names.append(folder_name)
|
||||||
|
|
||||||
|
for directory, folder_name in zip(dir, folder_names):
|
||||||
|
process_one_docs(directory, folder_name)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import os
|
||||||
import faiss
|
import faiss
|
||||||
import pickle
|
import pickle
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from langchain.vectorstores import FAISS
|
from langchain.vectorstores import FAISS
|
||||||
from langchain.embeddings import OpenAIEmbeddings
|
from langchain.embeddings import OpenAIEmbeddings
|
||||||
|
|
||||||
|
#from langchain.embeddings import HuggingFaceEmbeddings
|
||||||
|
#from langchain.embeddings import HuggingFaceInstructEmbeddings
|
||||||
|
#from langchain.embeddings import CohereEmbeddings
|
||||||
|
|
||||||
|
from retry import retry
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def num_tokens_from_string(string: str, encoding_name: str) -> int:
|
def num_tokens_from_string(string: str, encoding_name: str) -> int:
|
||||||
# Function to convert string to tokens and estimate user cost.
|
# Function to convert string to tokens and estimate user cost.
|
||||||
@@ -12,8 +20,17 @@ def num_tokens_from_string(string: str, encoding_name: str) -> int:
|
|||||||
total_price = ((num_tokens/1000) * 0.0004)
|
total_price = ((num_tokens/1000) * 0.0004)
|
||||||
return num_tokens, total_price
|
return num_tokens, total_price
|
||||||
|
|
||||||
def call_openai_api(docs):
|
@retry(tries=10, delay=60)
|
||||||
|
def store_add_texts_with_retry(store, i):
|
||||||
|
store.add_texts([i.page_content], metadatas=[i.metadata])
|
||||||
|
|
||||||
|
def call_openai_api(docs, folder_name):
|
||||||
# Function to create a vector store from the documents and save it to disk.
|
# Function to create a vector store from the documents and save it to disk.
|
||||||
|
|
||||||
|
# create output folder if it doesn't exist
|
||||||
|
if not os.path.exists(f"outputs/{folder_name}"):
|
||||||
|
os.makedirs(f"outputs/{folder_name}")
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
docs_test = [docs[0]]
|
docs_test = [docs[0]]
|
||||||
# remove the first element from docs
|
# remove the first element from docs
|
||||||
@@ -22,34 +39,25 @@ def call_openai_api(docs):
|
|||||||
#docs = docs[:n]
|
#docs = docs[:n]
|
||||||
c1 = 0
|
c1 = 0
|
||||||
store = FAISS.from_documents(docs_test, OpenAIEmbeddings())
|
store = FAISS.from_documents(docs_test, OpenAIEmbeddings())
|
||||||
|
|
||||||
|
# Uncomment for MPNet embeddings
|
||||||
|
# model_name = "sentence-transformers/all-mpnet-base-v2"
|
||||||
|
# hf = HuggingFaceEmbeddings(model_name=model_name)
|
||||||
|
# store = FAISS.from_documents(docs_test, hf)
|
||||||
for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs), bar_format='{l_bar}{bar}| Time Left: {remaining}'):
|
for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs), bar_format='{l_bar}{bar}| Time Left: {remaining}'):
|
||||||
try:
|
try:
|
||||||
import time
|
store_add_texts_with_retry(store, i)
|
||||||
store.add_texts([i.page_content], metadatas=[i.metadata])
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
print("Error on ", i)
|
print("Error on ", i)
|
||||||
print("Saving progress")
|
print("Saving progress")
|
||||||
print(f"stopped at {c1} out of {len(docs)}")
|
print(f"stopped at {c1} out of {len(docs)}")
|
||||||
faiss.write_index(store.index, "docs.index")
|
store.save_local(f"outputs/{folder_name}")
|
||||||
store_index_bak = store.index
|
break
|
||||||
store.index = None
|
|
||||||
with open("faiss_store.pkl", "wb") as f:
|
|
||||||
pickle.dump(store, f)
|
|
||||||
print("Sleeping for 60 seconds and trying again")
|
|
||||||
time.sleep(60)
|
|
||||||
faiss.write_index(store_index_bak, "docs.index")
|
|
||||||
store.index = store_index_bak
|
|
||||||
store.add_texts([i.page_content], metadatas=[i.metadata])
|
|
||||||
c1 += 1
|
c1 += 1
|
||||||
|
store.save_local(f"outputs/{folder_name}")
|
||||||
|
|
||||||
|
def get_user_permission(docs, folder_name):
|
||||||
faiss.write_index(store.index, "docs.index")
|
|
||||||
store.index = None
|
|
||||||
with open("faiss_store.pkl", "wb") as f:
|
|
||||||
pickle.dump(store, f)
|
|
||||||
|
|
||||||
def get_user_permission(docs):
|
|
||||||
# Function to ask user permission to call the OpenAI api and spend their OpenAI funds.
|
# Function to ask user permission to call the OpenAI api and spend their OpenAI funds.
|
||||||
# Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents.
|
# Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents.
|
||||||
#docs_content = (" ".join(docs))
|
#docs_content = (" ".join(docs))
|
||||||
@@ -65,8 +73,8 @@ def get_user_permission(docs):
|
|||||||
#Here we check for user permission before calling the API.
|
#Here we check for user permission before calling the API.
|
||||||
user_input = input("Price Okay? (Y/N) \n").lower()
|
user_input = input("Price Okay? (Y/N) \n").lower()
|
||||||
if user_input == "y":
|
if user_input == "y":
|
||||||
call_openai_api(docs)
|
call_openai_api(docs, folder_name)
|
||||||
elif user_input == "":
|
elif user_input == "":
|
||||||
call_openai_api(docs)
|
call_openai_api(docs, folder_name)
|
||||||
else:
|
else:
|
||||||
print("The API was not called. No money was spent.")
|
print("The API was not called. No money was spent.")
|
||||||
|
|||||||