Merge branch 'arc53:main' into Jackson

This commit is contained in:
Jackson
2024-07-30 17:45:06 +08:00
committed by GitHub
46 changed files with 2095 additions and 702 deletions

2
.gitignore vendored
View File

@@ -172,5 +172,5 @@ application/vectors/
node_modules/
.vscode/settings.json
models/
/models/
model/

View File

@@ -189,13 +189,14 @@ def complete_stream(question, retriever, conversation_id, user_api_key):
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
if(user_api_key is None):
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:

View File

@@ -6,8 +6,9 @@ from urllib.parse import urlparse
import requests
from pymongo import MongoClient
from bson.objectid import ObjectId
from bson.binary import Binary, UuidRepresentation
from werkzeug.utils import secure_filename
from bson.dbref import DBRef
from application.api.user.tasks import ingest, ingest_remote
from application.core.settings import settings
@@ -20,6 +21,8 @@ vectors_collection = db["vectors"]
prompts_collection = db["prompts"]
feedback_collection = db["feedback"]
api_key_collection = db["api_keys"]
shared_conversations_collections = db["shared_conversations"]
user = Blueprint("user", __name__)
current_dir = os.path.dirname(
@@ -491,3 +494,140 @@ def delete_api_key():
}
)
return {"status": "ok"}
#route to share conversation
##isPromptable should be passed through queries
@user.route("/api/share",methods=["POST"])
def share_conversation():
try:
data = request.get_json()
user = "local" if "user" not in data else data["user"]
conversation_id = data["conversation_id"]
isPromptable = request.args.get("isPromptable").lower() == "true"
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
current_n_queries = len(conversation["queries"])
##generate binary representation of uuid
explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD)
if(isPromptable):
source = "default" if "source" not in data else data["source"]
prompt_id = "default" if "prompt_id" not in data else data["prompt_id"]
chunks = "2" if "chunks" not in data else data["chunks"]
name = conversation["name"]+"(shared)"
pre_existing_api_document = api_key_collection.find_one({
"prompt_id":prompt_id,
"chunks":chunks,
"source":source,
"user":user
})
api_uuid = str(uuid.uuid4())
if(pre_existing_api_document):
api_uuid = pre_existing_api_document["key"]
pre_existing = shared_conversations_collections.find_one({
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
"isPromptable":isPromptable,
"first_n_queries":current_n_queries,
"user":user,
"api_key":api_uuid
})
if(pre_existing is not None):
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
else:
shared_conversations_collections.insert_one({
"uuid":explicit_binary,
"conversation_id": {
"$ref":"conversations",
"$id":ObjectId(conversation_id)
} ,
"isPromptable":isPromptable,
"first_n_queries":current_n_queries,
"user":user,
"api_key":api_uuid
})
return jsonify({"success":True,"identifier":str(explicit_binary.as_uuid())})
else:
api_key_collection.insert_one(
{
"name": name,
"key": api_uuid,
"source": source,
"user": user,
"prompt_id": prompt_id,
"chunks": chunks,
}
)
shared_conversations_collections.insert_one({
"uuid":explicit_binary,
"conversation_id": {
"$ref":"conversations",
"$id":ObjectId(conversation_id)
} ,
"isPromptable":isPromptable,
"first_n_queries":current_n_queries,
"user":user,
"api_key":api_uuid
})
## Identifier as route parameter in frontend
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
##isPromptable = False
pre_existing = shared_conversations_collections.find_one({
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
"isPromptable":isPromptable,
"first_n_queries":current_n_queries,
"user":user
})
if(pre_existing is not None):
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
else:
shared_conversations_collections.insert_one({
"uuid":explicit_binary,
"conversation_id": {
"$ref":"conversations",
"$id":ObjectId(conversation_id)
} ,
"isPromptable":isPromptable,
"first_n_queries":current_n_queries,
"user":user
})
## Identifier as route parameter in frontend
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
except Exception as err:
print (err)
return jsonify({"success":False,"error":str(err)}),400
#route to get publicly shared conversations
@user.route("/api/shared_conversation/<string:identifier>",methods=["GET"])
def get_publicly_shared_conversations(identifier : str):
try:
query_uuid = Binary.from_uuid(uuid.UUID(identifier), UuidRepresentation.STANDARD)
shared = shared_conversations_collections.find_one({"uuid":query_uuid})
conversation_queries=[]
if shared and 'conversation_id' in shared and isinstance(shared['conversation_id'], DBRef):
# Resolve the DBRef
conversation_ref = shared['conversation_id']
conversation = db.dereference(conversation_ref)
if(conversation is None):
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
conversation_queries = conversation['queries'][:(shared["first_n_queries"])]
for query in conversation_queries:
query.pop("sources") ## avoid exposing sources
else:
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
date = conversation["_id"].generation_time.isoformat()
res = {
"success":True,
"queries":conversation_queries,
"title":conversation["name"],
"timestamp":date
}
if(shared["isPromptable"] and "api_key" in shared):
res["api_key"] = shared["api_key"]
return jsonify(res), 200
except Exception as err:
print (err)
return jsonify({"success":False,"error":str(err)}),400

View File

@@ -14,7 +14,9 @@ class EmbeddingsSingleton:
@staticmethod
def get_instance(embeddings_name, *args, **kwargs):
if embeddings_name not in EmbeddingsSingleton._instances:
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(embeddings_name, *args, **kwargs)
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(
embeddings_name, *args, **kwargs
)
return EmbeddingsSingleton._instances[embeddings_name]
@staticmethod

View File

@@ -11,6 +11,7 @@
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
@@ -1489,7 +1490,7 @@
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/react": "*"
}
@@ -4194,6 +4195,15 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",

View File

@@ -1,4 +1,5 @@
import { Routes, Route } from 'react-router-dom';
import { useEffect } from 'react';
import Navigation from './Navigation';
import Conversation from './conversation/Conversation';
import About from './About';
@@ -8,29 +9,53 @@ import { useMediaQuery } from './hooks';
import { useState } from 'react';
import Setting from './settings';
import './locale/i18n';
import { Outlet } from 'react-router-dom';
import { SharedConversation } from './conversation/SharedConversation';
import { useDarkTheme } from './hooks';
inject();
export default function App() {
function MainLayout() {
const { isMobile } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!isMobile);
return (
<div className="min-h-full min-w-full dark:bg-raisin-black">
<div className="dark:bg-raisin-black">
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
<div
className={`transition-all duration-200 ${
className={`min-h-screen ${
!isMobile
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
: 'ml-0 md:ml-16'
}`}
>
<Routes>
<Route path="/" element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<PageNotFound />} />
<Route path="/settings" element={<Setting />} />
</Routes>
<Outlet />
</div>
</div>
);
}
export default function App() {
const [isDarkTheme] = useDarkTheme();
useEffect(() => {
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
if (isDarkTheme) {
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
document.getElementById('root')?.classList.remove('dark');
}
}, [isDarkTheme]);
return (
<>
<Routes>
<Route element={<MainLayout />}>
<Route index element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="/settings" element={<Setting />} />
</Route>
<Route path="/share/:identifier" element={<SharedConversation />} />
<Route path="/*" element={<PageNotFound />} />
</Routes>
</>
);
}

View File

@@ -1,46 +1,48 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { NavLink, useNavigate } from 'react-router-dom';
import conversationService from './api/services/conversationService';
import userService from './api/services/userService';
import Add from './assets/add.svg';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
import Discord from './assets/discord.svg';
import Expand from './assets/expand.svg';
import Github from './assets/github.svg';
import Hamburger from './assets/hamburger.svg';
import HamburgerDark from './assets/hamburger-dark.svg';
import Hamburger from './assets/hamburger.svg';
import Info from './assets/info.svg';
import SettingGear from './assets/settingGear.svg';
import Twitter from './assets/TwitterX.svg';
import Add from './assets/add.svg';
import UploadIcon from './assets/upload.svg';
import { ActiveState } from './models/misc';
import APIKeyModal from './preferences/APIKeyModal';
import DeleteConvModal from './modals/DeleteConvModal';
import {
selectApiKeyStatus,
selectSelectedDocs,
selectSelectedDocsStatus,
selectSourceDocs,
setSelectedDocs,
selectConversations,
setConversations,
selectConversationId,
selectModalStateDeleteConv,
setModalStateDeleteConv,
setSourceDocs,
} from './preferences/preferenceSlice';
import SourceDropdown from './components/SourceDropdown';
import {
setConversation,
updateConversationId,
} from './conversation/conversationSlice';
import { useMediaQuery, useOutsideAlerter } from './hooks';
import Upload from './upload/Upload';
import { Doc, getConversations, getDocs } from './preferences/preferenceApi';
import SelectDocsModal from './preferences/SelectDocsModal';
import ConversationTile from './conversation/ConversationTile';
import { useDarkTheme } from './hooks';
import SourceDropdown from './components/SourceDropdown';
import { useTranslation } from 'react-i18next';
import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks';
import DeleteConvModal from './modals/DeleteConvModal';
import { ActiveState } from './models/misc';
import APIKeyModal from './preferences/APIKeyModal';
import { Doc, getConversations, getDocs } from './preferences/preferenceApi';
import {
selectApiKeyStatus,
selectConversationId,
selectConversations,
selectModalStateDeleteConv,
selectSelectedDocs,
selectSelectedDocsStatus,
selectSourceDocs,
setConversations,
setModalStateDeleteConv,
setSelectedDocs,
setSourceDocs,
} from './preferences/preferenceSlice';
import SelectDocsModal from './preferences/SelectDocsModal';
import Upload from './upload/Upload';
interface NavigationProps {
navOpen: boolean;
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
@@ -85,7 +87,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
useState<ActiveState>('INACTIVE');
const navRef = useRef(null);
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const navigate = useNavigate();
@@ -106,9 +107,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}
const handleDeleteAllConversations = () => {
fetch(`${apiHost}/api/delete_all_conversations`, {
method: 'POST',
})
conversationService
.deleteAll()
.then(() => {
fetchConversations();
})
@@ -116,9 +116,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
};
const handleDeleteConversation = (id: string) => {
fetch(`${apiHost}/api/delete_conversation?id=${id}`, {
method: 'POST',
})
conversationService
.delete(id, {})
.then(() => {
fetchConversations();
})
@@ -128,17 +127,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const handleDeleteClick = (doc: Doc) => {
const docPath = `indexes/local/${doc.name}`;
fetch(`${apiHost}/api/delete_old?path=${docPath}`, {
method: 'GET',
})
userService
.deletePath(docPath)
.then(() => {
// remove the image element from the DOM
// const imageElement = document.querySelector(
// `#img-${index}`,
// ) as HTMLElement;
// const parentElement = imageElement.parentNode as HTMLElement;
// parentElement.parentNode?.removeChild(parentElement);
return getDocs();
})
.then((updatedDocs) => {
@@ -153,10 +144,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
};
const handleConversationClick = (index: string) => {
// fetch the conversation from the server and setConversation in the store
fetch(`${apiHost}/api/get_single_conversation?id=${index}`, {
method: 'GET',
})
conversationService
.getConversation(index)
.then((response) => response.json())
.then((data) => {
navigate('/');
@@ -173,13 +162,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
name: string;
id: string;
}) {
await fetch(`${apiHost}/api/update_conversation_name`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConversation),
})
await conversationService
.update(updatedConversation)
.then((response) => response.json())
.then((data) => {
if (data) {

View File

@@ -2,11 +2,11 @@ import { Link } from 'react-router-dom';
export default function PageNotFound() {
return (
<div className="mx-5 grid min-h-screen md:mx-36">
<p className="mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
<div className="grid min-h-screen dark:bg-raisin-black">
<p className="mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 text-jet dark:bg-outer-space dark:text-gray-100 lg:p-10 xl:p-16">
<h1>404</h1>
<p>The page you are looking for does not exist.</p>
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 py-2 px-4 text-white hover:bg-blue-3000">
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 py-2 px-4 text-white transition-colors duration-100 hover:bg-blue-3000">
<Link to="/">Go Back Home</Link>
</button>
</p>

View File

@@ -0,0 +1,69 @@
const baseURL = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const defaultHeaders = {
'Content-Type': 'application/json',
};
const apiClient = {
get: (url: string, headers = {}, signal?: AbortSignal): Promise<any> =>
fetch(`${baseURL}${url}`, {
method: 'GET',
headers: {
...defaultHeaders,
...headers,
},
signal,
}).then((response) => {
return response;
}),
post: (
url: string,
data: any,
headers = {},
signal?: AbortSignal,
): Promise<any> =>
fetch(`${baseURL}${url}`, {
method: 'POST',
headers: {
...defaultHeaders,
...headers,
},
body: JSON.stringify(data),
signal,
}).then((response) => {
return response;
}),
put: (
url: string,
data: any,
headers = {},
signal?: AbortSignal,
): Promise<any> =>
fetch(`${baseURL}${url}`, {
method: 'PUT',
headers: {
...defaultHeaders,
...headers,
},
body: JSON.stringify(data),
signal,
}).then((response) => {
return response;
}),
delete: (url: string, headers = {}, signal?: AbortSignal): Promise<any> =>
fetch(`${baseURL}${url}`, {
method: 'DELETE',
headers: {
...defaultHeaders,
...headers,
},
signal,
}).then((response) => {
return response;
}),
};
export default apiClient;

View File

@@ -0,0 +1,33 @@
const endpoints = {
USER: {
DOCS: '/api/combine',
DOCS_CHECK: '/api/docs_check',
API_KEYS: '/api/get_api_keys',
CREATE_API_KEY: '/api/create_api_key',
DELETE_API_KEY: '/api/delete_api_key',
PROMPTS: '/api/get_prompts',
CREATE_PROMPT: '/api/create_prompt',
DELETE_PROMPT: '/api/delete_prompt',
UPDATE_PROMPT: '/api/update_prompt',
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
DELETE_PATH: (docPath: string) => `/api/delete_old?path=${docPath}`,
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
},
CONVERSATION: {
ANSWER: '/api/answer',
ANSWER_STREAMING: '/stream',
SEARCH: '/api/search',
FEEDBACK: '/api/feedback',
CONVERSATION: (id: string) => `/api/get_single_conversation?id=${id}`,
CONVERSATIONS: '/api/get_conversations',
SHARE_CONVERSATION: (isPromptable: boolean) =>
`/api/share?isPromptable=${isPromptable}`,
SHARED_CONVERSATION: (identifier: string) =>
`/api/shared_conversation/${identifier}`,
DELETE: (id: string) => `/api/delete_conversation?id=${id}`,
DELETE_ALL: '/api/delete_all_conversations',
UPDATE: '/api/update_conversation_name',
},
};
export default endpoints;

View File

@@ -0,0 +1,32 @@
import apiClient from '../client';
import endpoints from '../endpoints';
const conversationService = {
answer: (data: any, signal: AbortSignal): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.ANSWER, data, {}, signal),
answerStream: (data: any, signal: AbortSignal): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.ANSWER_STREAMING, data, {}, signal),
search: (data: any): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.SEARCH, data),
feedback: (data: any): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.FEEDBACK, data),
getConversation: (id: string): Promise<any> =>
apiClient.get(endpoints.CONVERSATION.CONVERSATION(id)),
getConversations: (): Promise<any> =>
apiClient.get(endpoints.CONVERSATION.CONVERSATIONS),
shareConversation: (isPromptable: boolean, data: any): Promise<any> =>
apiClient.post(
endpoints.CONVERSATION.SHARE_CONVERSATION(isPromptable),
data,
),
getSharedConversation: (identifier: string): Promise<any> =>
apiClient.get(endpoints.CONVERSATION.SHARED_CONVERSATION(identifier)),
delete: (id: string, data: any): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.DELETE(id), data),
deleteAll: (): Promise<any> =>
apiClient.get(endpoints.CONVERSATION.DELETE_ALL),
update: (data: any): Promise<any> =>
apiClient.post(endpoints.CONVERSATION.UPDATE, data),
};
export default conversationService;

View File

@@ -0,0 +1,28 @@
import apiClient from '../client';
import endpoints from '../endpoints';
const userService = {
getDocs: (): Promise<any> => apiClient.get(endpoints.USER.DOCS),
checkDocs: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.DOCS_CHECK, data),
getAPIKeys: (): Promise<any> => apiClient.get(endpoints.USER.API_KEYS),
createAPIKey: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_API_KEY, data),
deleteAPIKey: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.DELETE_API_KEY, data),
getPrompts: (): Promise<any> => apiClient.get(endpoints.USER.PROMPTS),
createPrompt: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_PROMPT, data),
deletePrompt: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.DELETE_PROMPT, data),
updatePrompt: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.UPDATE_PROMPT, data),
getSinglePrompt: (id: string): Promise<any> =>
apiClient.get(endpoints.USER.SINGLE_PROMPT(id)),
deletePath: (docPath: string): Promise<any> =>
apiClient.get(endpoints.USER.DELETE_PATH(docPath)),
getTaskStatus: (task_id: string): Promise<any> =>
apiClient.get(endpoints.USER.TASK_STATUS(task_id)),
};
export default userService;

View File

@@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66427 17.7747C6.66427 18.2167 6.84488 18.6406 7.16637 18.9532C7.48787 19.2658 7.9239 19.4413 8.37856 19.4413H15.2357C15.6904 19.4413 16.1264 19.2658 16.4479 18.9532C16.7694 18.6406 16.95 18.2167 16.95 17.7747V7.77468H6.66427V17.7747ZM8.37856 9.44135H15.2357V17.7747H8.37856V9.44135ZM14.8071 5.27468L13.95 4.44135H9.66427L8.80713 5.27468H5.80713V6.94135H17.8071V5.27468H14.8071Z" fill="#D30000"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.04167 7.2997C1.96431 7.2997 1.89013 7.32976 1.83543 7.38326C1.78073 7.43677 1.75 7.50934 1.75 7.585V15.0029C1.75 15.1604 1.88067 15.2882 2.04167 15.2882H11.9583C12.0357 15.2882 12.1099 15.2581 12.1646 15.2046C12.2193 15.1511 12.25 15.0785 12.25 15.0029V7.585C12.25 7.50934 12.2193 7.43677 12.1646 7.38326C12.1099 7.32976 12.0357 7.2997 11.9583 7.2997H10.7917C10.5596 7.2997 10.337 7.20952 10.1729 7.04901C10.0089 6.8885 9.91667 6.67079 9.91667 6.44379C9.91667 6.21679 10.0089 5.99909 10.1729 5.83857C10.337 5.67806 10.5596 5.58788 10.7917 5.58788H11.9583C13.0853 5.58788 14 6.48259 14 7.585V15.0029C14 15.5325 13.7849 16.0405 13.402 16.4151C13.0191 16.7896 12.4998 17 11.9583 17H2.04167C1.50018 17 0.980877 16.7896 0.59799 16.4151C0.215104 16.0405 0 15.5325 0 15.0029V7.585C0 6.48259 0.914667 5.58788 2.04167 5.58788H3.20833C3.4404 5.58788 3.66296 5.67806 3.82705 5.83857C3.99115 5.99909 4.08333 6.21679 4.08333 6.44379C4.08333 6.67079 3.99115 6.8885 3.82705 7.04901C3.66296 7.20952 3.4404 7.2997 3.20833 7.2997H2.04167ZM6.7935 0.0838185C6.82059 0.0572492 6.85278 0.0361694 6.88821 0.0217864C6.92365 0.0074035 6.96164 0 7 0C7.03836 0 7.07635 0.0074035 7.11179 0.0217864C7.14722 0.0361694 7.17941 0.0572492 7.2065 0.0838185L10.5852 3.38877C10.6261 3.42867 10.6539 3.47955 10.6652 3.53496C10.6765 3.59037 10.6707 3.64782 10.6486 3.70001C10.6265 3.75221 10.589 3.7968 10.541 3.82815C10.4929 3.85949 10.4364 3.87617 10.3787 3.87607H7.875V10.438C7.875 10.665 7.78281 10.8827 7.61872 11.0433C7.45462 11.2038 7.23206 11.2939 7 11.2939C6.76794 11.2939 6.54538 11.2038 6.38128 11.0433C6.21719 10.8827 6.125 10.665 6.125 10.438V3.87607H3.62133C3.56357 3.87617 3.50708 3.85949 3.45902 3.82815C3.41096 3.7968 3.37349 3.75221 3.35138 3.70001C3.32926 3.64782 3.32348 3.59037 3.33478 3.53496C3.34607 3.47955 3.37394 3.42867 3.41483 3.38877L6.7935 0.0838185Z" fill="#747474"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="10" height="25" viewBox="0 0 10 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3.5C3.9 3.5 3 4.4 3 5.5C3 6.6 3.9 7.5 5 7.5C6.1 7.5 7 6.6 7 5.5C7 4.4 6.1 3.5 5 3.5ZM5 17.5C3.9 17.5 3 18.4 3 19.5C3 20.6 3.9 21.5 5 21.5C6.1 21.5 7 20.6 7 19.5C7 18.4 6.1 17.5 5 17.5ZM5 10.5C3.9 10.5 3 11.4 3 12.5C3 13.6 3.9 14.5 5 14.5C6.1 14.5 7 13.6 7 12.5C7 11.4 6.1 10.5 5 10.5Z" fill="#747474"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,43 @@
import { InputProps } from './types';
const Input = ({
id,
name,
type,
value,
isAutoFocused = false,
placeholder,
maxLength,
className,
colorVariant = 'silver',
children,
onChange,
onPaste,
onKeyDown,
}: InputProps) => {
const colorStyles = {
silver: 'border-silver dark:border-silver/40',
jet: 'border-jet',
gray: 'border-gray-5000 dark:text-silver',
};
return (
<input
className={`h-[42px] w-full rounded-full border-2 px-3 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]}`}
type={type}
id={id}
name={name}
autoFocus={isAutoFocused}
placeholder={placeholder}
maxLength={maxLength}
value={value}
onChange={onChange}
onPaste={onPaste}
onKeyDown={onKeyDown}
>
{children}
</input>
);
};
export default Input;

View File

@@ -77,7 +77,7 @@ function SourceDropdown({
/>
</button>
{isDocsListOpen && (
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
{options ? (
options.map((option: any, index: number) => {
if (option.model === embeddingsName) {

View File

@@ -0,0 +1,21 @@
export type InputProps = {
type: 'text' | 'number';
value: string | string[] | number;
colorVariant?: 'silver' | 'jet' | 'gray';
isAutoFocused?: boolean;
id?: string;
maxLength?: number;
name?: string;
placeholder?: string;
className?: string;
children?: React.ReactElement;
onChange: (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
onPaste?: (
e: React.ClipboardEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
onKeyDown?: (
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
};

View File

@@ -1,9 +1,22 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useDarkTheme } from '../hooks';
import ArrowDown from '../assets/arrow-down.svg';
import Send from '../assets/send.svg';
import SendDark from '../assets/send_dark.svg';
import ShareIcon from '../assets/share.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
import RetryIcon from '../components/RetryIcon';
import Hero from '../Hero';
import { useDarkTheme } from '../hooks';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { selectConversationId } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import ConversationBubble from './ConversationBubble';
import { handleSendFeedback } from './conversationHandlers';
import { FEEDBACK, Query } from './conversationModels';
import {
addQuery,
fetchAnswer,
@@ -11,18 +24,11 @@ import {
selectStatus,
updateQuery,
} from './conversationSlice';
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import Spinner from './../assets/spinner.svg';
import SpinnerDark from './../assets/spinner-dark.svg';
import { FEEDBACK, Query } from './conversationModels';
import { sendFeedback } from './conversationApi';
import { useTranslation } from 'react-i18next';
import ArrowDown from './../assets/arrow-down.svg';
import RetryIcon from '../components/RetryIcon';
export default function Conversation() {
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const conversationId = useSelector(selectConversationId);
const dispatch = useDispatch<AppDispatch>();
const endMessageRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
@@ -31,6 +37,7 @@ export default function Conversation() {
const fetchStream = useRef<any>(null);
const [eventInterrupt, setEventInterrupt] = useState(false);
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const { t } = useTranslation();
const handleUserInterruption = () => {
@@ -106,7 +113,7 @@ export default function Conversation() {
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
const prevFeedback = query.feedback;
dispatch(updateQuery({ index, query: { feedback } }));
sendFeedback(query.prompt, query.response!, feedback).catch(() =>
handleSendFeedback(query.prompt, query.response!, feedback).catch(() =>
dispatch(updateQuery({ index, query: { feedback: prevFeedback } })),
);
};
@@ -187,11 +194,36 @@ export default function Conversation() {
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
inputRef.current && (inputRef.current.innerText = text);
};
return (
<div className="flex h-screen flex-col gap-7 pb-2">
{conversationId && (
<>
<button
title="Share"
onClick={() => {
setShareModalState(true);
}}
className="fixed top-4 right-20 z-0 rounded-full hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="m-2 h-5 w-5 filter dark:invert"
alt="share"
src={ShareIcon}
/>
</button>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</>
)}
<div
onWheel={handleUserInterruption}
onTouchMove={handleUserInterruption}
@@ -257,9 +289,9 @@ export default function Conversation() {
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
></img>
) : (
<div className="mx-1 cursor-pointer rounded-full p-4 text-center hover:bg-gray-3000">
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
<img
className="w-6 text-white "
className="ml-[4px] h-6 w-6 text-white "
onClick={handleQuestionSubmission}
src={isDarkTheme ? SendDark : Send}
></img>

View File

@@ -1,6 +1,6 @@
import { forwardRef, useState } from 'react';
import Avatar from '../components/Avatar';
import CoppyButton from '../components/CopyButton';
import CopyButton from '../components/CopyButton';
import remarkGfm from 'remark-gfm';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import classes from './ConversationBubble.module.css';
@@ -103,7 +103,7 @@ const ConversationBubble = forwardRef<
className={`absolute right-3 top-3 lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
>
<CoppyButton
<CopyButton
text={String(children).replace(/\n$/, '')}
/>
</div>
@@ -215,78 +215,82 @@ const ConversationBubble = forwardRef<
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
>
<div>
<CoppyButton text={message} />
<CopyButton text={message} />
</div>
</div>
<div
className={`relative mr-5 flex items-center justify-center ${
!isLikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'LIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div>
{handleFeedback && (
<>
<div
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
isLikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
className={`relative mr-5 flex items-center justify-center ${
!isLikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'LIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<Like
className={`cursor-pointer
<div>
<div
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
isLikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
<Like
className={`cursor-pointer
${
isLikeClicked || feedback === 'LIKE'
? 'fill-white-3000 stroke-purple-30 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
}`}
onClick={() => {
handleFeedback?.('LIKE');
setIsLikeClicked(true);
setIsDislikeClicked(false);
}}
onMouseEnter={() => setIsLikeHovered(true)}
onMouseLeave={() => setIsLikeHovered(false)}
></Like>
onClick={() => {
handleFeedback?.('LIKE');
setIsLikeClicked(true);
setIsDislikeClicked(false);
}}
onMouseEnter={() => setIsLikeHovered(true)}
onMouseLeave={() => setIsLikeHovered(false)}
></Like>
</div>
</div>
</div>
</div>
</div>
<div
className={`mr-13 relative flex items-center justify-center ${
!isDislikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'DISLIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isDislikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
className={`mr-13 relative flex items-center justify-center ${
!isDislikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'DISLIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<Dislike
className={`cursor-pointer ${
isDislikeClicked || feedback === 'DISLIKE'
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
}`}
onClick={() => {
handleFeedback?.('DISLIKE');
setIsDislikeClicked(true);
setIsLikeClicked(false);
}}
onMouseEnter={() => setIsDislikeHovered(true)}
onMouseLeave={() => setIsDislikeHovered(false)}
></Dislike>
<div>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isDislikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
<Dislike
className={`cursor-pointer ${
isDislikeClicked || feedback === 'DISLIKE'
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
}`}
onClick={() => {
handleFeedback?.('DISLIKE');
setIsDislikeClicked(true);
setIsLikeClicked(false);
}}
onMouseEnter={() => setIsDislikeHovered(true)}
onMouseLeave={() => setIsDislikeHovered(false)}
></Dislike>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div>
{sources && openSource !== null && sources[openSource] && (

View File

@@ -1,14 +1,19 @@
import { useEffect, useRef, useState } from 'react';
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import Edit from '../assets/edit.svg';
import Exit from '../assets/exit.svg';
import Message from '../assets/message.svg';
import MessageDark from '../assets/message-dark.svg';
import { useDarkTheme } from '../hooks';
import ConfirmationModal from '../modals/ConfirmationModal';
import CheckMark2 from '../assets/checkMark2.svg';
import Trash from '../assets/trash.svg';
import Trash from '../assets/red-trash.svg';
import Share from '../assets/share.svg';
import threeDots from '../assets/three-dots.svg';
import { selectConversationId } from '../preferences/preferenceSlice';
import { ActiveState } from '../models/misc';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { useTranslation } from 'react-i18next';
interface ConversationProps {
name: string;
@@ -32,13 +37,20 @@ export default function ConversationTile({
const [isDarkTheme] = useDarkTheme();
const [isEdit, setIsEdit] = useState(false);
const [conversationName, setConversationsName] = useState('');
const [isOpen, setOpen] = useState<boolean>(false);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const [isHovered, setIsHovered] = useState(false);
const [deleteModalState, setDeleteModalState] =
useState<ActiveState>('INACTIVE');
const menuRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
useEffect(() => {
setConversationsName(conversation.name);
}, [conversation.name]);
function handleEditConversation() {
setIsEdit(true);
setOpen(false);
}
function handleSaveConversation(changedConversation: ConversationProps) {
@@ -50,76 +62,172 @@ export default function ConversationTile({
}
}
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
function onClear() {
setConversationsName(conversation.name);
setIsEdit(false);
}
return (
<div
ref={tileRef}
onClick={() => {
selectConversation(conversation.id);
}}
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
conversationId === conversation.id
? 'bg-gray-100 dark:bg-[#28292E]'
: ''
}`}
>
<>
<div
className={`flex ${
conversationId === conversation.id ? 'w-[75%]' : 'w-[95%]'
} gap-4`}
ref={tileRef}
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
onClick={() => {
conversationId !== conversation.id &&
selectConversation(conversation.id);
}}
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
conversationId === conversation.id || isOpen || isHovered
? 'bg-gray-100 dark:bg-[#28292E]'
: ''
}`}
>
<img
src={isDarkTheme ? MessageDark : Message}
className="ml-4 w-5 dark:text-white"
/>
{isEdit ? (
<input
autoFocus
type="text"
className="h-6 w-full px-1 text-sm font-normal leading-6 outline-[#0075FF] focus:outline-1"
value={conversationName}
onChange={(e) => setConversationsName(e.target.value)}
<div className={`flex w-10/12 gap-4`}>
<img
src={isDarkTheme ? MessageDark : Message}
className="ml-4 w-5 dark:text-white"
/>
) : (
<p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white">
{conversationName}
</p>
{isEdit ? (
<input
autoFocus
type="text"
className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]"
value={conversationName}
onChange={(e) => setConversationsName(e.target.value)}
/>
) : (
<p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white">
{conversationName}
</p>
)}
</div>
{(conversationId === conversation.id || isHovered || isOpen) && (
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
{isEdit ? (
<div className="flex gap-1">
<img
src={CheckMark2}
alt="Edit"
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
id={`img-${conversation.id}`}
onClick={(event: SyntheticEvent) => {
event.stopPropagation();
handleSaveConversation({
id: conversation.id,
name: conversationName,
});
}}
/>
<img
src={Exit}
alt="Exit"
className={`mr-4 mt-px h-3 w-3 cursor-pointer filter hover:opacity-50 dark:invert`}
id={`img-${conversation.id}`}
onClick={(event: SyntheticEvent) => {
event.stopPropagation();
onClear();
}}
/>
</div>
) : (
<button
onClick={(event: SyntheticEvent) => {
event.stopPropagation();
setOpen(true);
}}
className="mr-2 flex w-4 justify-center"
>
<img src={threeDots} width={8} />
</button>
)}
{isOpen && (
<div className="flex-start absolute z-30 flex w-32 translate-x-1 translate-y-5 flex-col rounded-xl bg-stone-100 text-sm text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver md:w-36">
<button
onClick={(event: SyntheticEvent) => {
event.stopPropagation();
setShareModalState(true);
setOpen(false);
}}
className="flex-start flex items-center gap-4 rounded-t-xl p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Share}
alt="Share"
width={14}
height={14}
className="cursor-pointer hover:opacity-50"
id={`img-${conversation.id}`}
/>
<span>{t('convTile.share')}</span>
</button>
<button
onClick={handleEditConversation}
className="flex-start flex items-center gap-4 p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Edit}
alt="Edit"
width={16}
height={16}
className="cursor-pointer hover:opacity-50"
id={`img-${conversation.id}`}
/>
<span>{t('convTile.rename')}</span>
</button>
<button
onClick={(event: SyntheticEvent) => {
event.stopPropagation();
setDeleteModalState('ACTIVE');
setOpen(false);
}}
className="flex-start flex items-center gap-3 rounded-b-xl p-2 text-red-700 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Trash}
alt="Edit"
width={24}
height={24}
className="cursor-pointer hover:opacity-50"
/>
<span>{t('convTile.delete')}</span>
</button>
</div>
)}
</div>
)}
</div>
{conversationId === conversation.id && (
<div className="flex text-white dark:text-[#949494]">
<img
src={isEdit ? CheckMark2 : Edit}
alt="Edit"
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
isEdit
? handleSaveConversation({
id: conversationId,
name: conversationName,
})
: handleEditConversation();
}}
/>
<img
src={isEdit ? Exit : Trash}
alt="Exit"
className={`mr-4 ${
isEdit ? 'h-3 w-3' : 'h-4 w-4'
}mt-px cursor-pointer hover:opacity-50`}
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
isEdit ? onClear() : onDeleteConversation(conversation.id);
}}
/>
</div>
<ConfirmationModal
message={t('convTile.deleteWarning')}
modalState={deleteModalState}
setModalState={setDeleteModalState}
handleSubmit={() => onDeleteConversation(conversation.id)}
submitLabel={t('convTile.delete')}
/>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
isHovered && setIsHovered(false);
}}
conversationId={conversation.id}
/>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,288 @@
import { Query } from './conversationModels';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import conversationService from '../api/services/conversationService';
import ConversationBubble from './ConversationBubble';
import Send from '../assets/send.svg';
import Spinner from '../assets/spinner.svg';
import {
selectClientAPIKey,
setClientApiKey,
updateQuery,
addQuery,
fetchSharedAnswer,
selectStatus,
} from './sharedConversationSlice';
import { setIdentifier, setFetchedData } from './sharedConversationSlice';
import { useDispatch } from 'react-redux';
import { AppDispatch } from '../store';
import {
selectDate,
selectTitle,
selectQueries,
} from './sharedConversationSlice';
import { useSelector } from 'react-redux';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export const SharedConversation = () => {
const navigate = useNavigate();
const { identifier } = useParams(); //identifier is a uuid, not conversationId
const queries = useSelector(selectQueries);
const title = useSelector(selectTitle);
const date = useSelector(selectDate);
const apiKey = useSelector(selectClientAPIKey);
const status = useSelector(selectStatus);
const inputRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const [eventInterrupt, setEventInterrupt] = useState(false);
const endMessageRef = useRef<HTMLDivElement>(null);
const handleUserInterruption = () => {
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
};
useEffect(() => {
!eventInterrupt && scrollIntoView();
}, [queries.length, queries[queries.length - 1]]);
useEffect(() => {
identifier && dispatch(setIdentifier(identifier));
const element = document.getElementById('inputbox') as HTMLInputElement;
if (element) {
element.focus();
}
}, []);
function formatISODate(isoDateStr: string) {
const date = new Date(isoDateStr);
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'June',
'July',
'Aug',
'Sept',
'Oct',
'Nov',
'Dec',
];
const month = monthNames[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12;
const minutesStr = minutes < 10 ? '0' + minutes : minutes;
const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`;
return formattedDate;
}
useEffect(() => {
if (queries.length) {
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
}
}, [queries[queries.length - 1]]);
const scrollIntoView = () => {
endMessageRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
};
const fetchQueries = () => {
identifier &&
conversationService
.getSharedConversation(identifier || '')
.then((res) => {
if (res.status === 404 || res.status === 400)
navigate('/pagenotfound');
return res.json();
})
.then((data) => {
if (data.success) {
dispatch(
setFetchedData({
queries: data.queries,
title: data.title,
date: data.date,
identifier,
}),
);
data.api_key && dispatch(setClientApiKey(data.api_key));
}
});
};
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
inputRef.current && (inputRef.current.innerText = text);
};
const prepResponseView = (query: Query, index: number) => {
let responseView;
if (query.response) {
responseView = (
<ConversationBubble
ref={endMessageRef}
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
key={`${index}ANSWER`}
message={query.response}
type={'ANSWER'}
></ConversationBubble>
);
} else if (query.error) {
responseView = (
<ConversationBubble
ref={endMessageRef}
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
key={`${index}ERROR`}
message={query.error}
type="ERROR"
></ConversationBubble>
);
}
return responseView;
};
const handleQuestionSubmission = () => {
if (inputRef.current?.textContent && status !== 'loading') {
if (lastQueryReturnedErr) {
// update last failed query with new prompt
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: inputRef.current.textContent,
},
}),
);
handleQuestion({
question: queries[queries.length - 1].prompt,
isRetry: true,
});
} else {
handleQuestion({ question: inputRef.current.textContent });
}
inputRef.current.textContent = '';
}
};
const handleQuestion = ({
question,
isRetry = false,
}: {
question: string;
isRetry?: boolean;
}) => {
question = question.trim();
if (question === '') return;
setEventInterrupt(false);
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
dispatch(fetchSharedAnswer({ question }));
};
useEffect(() => {
fetchQueries();
}, []);
return (
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
<div className="flex w-full justify-center overflow-auto">
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
<div className="mb-2 w-full border-b pb-2">
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
{title}
</h1>
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
{t('sharedConv.subtitle')}{' '}
<a href="/" className="text-[#007DFF]">
DocsGPT
</a>
</h2>
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
{date}
</h2>
</div>
<div className="">
{queries?.map((query, index) => {
return (
<Fragment key={index}>
<ConversationBubble
ref={endMessageRef}
className={'mb-1 last:mb-28 md:mb-7'}
key={`${index}QUESTION`}
message={query.prompt}
type="QUESTION"
sources={query.sources}
></ConversationBubble>
{prepResponseView(query, index)}
</Fragment>
);
})}
</div>
</div>
</div>
<div className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
{apiKey ? (
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
<div
id="inputbox"
ref={inputRef}
tabIndex={1}
onPaste={handlePaste}
placeholder={t('inputPlaceholder')}
contentEditable
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleQuestionSubmission();
}
}}
></div>
{status === 'loading' ? (
<img
src={Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert"
></img>
) : (
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
<img
onClick={handleQuestionSubmission}
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
src={Send}
></img>
</div>
)}
</div>
) : (
<button
onClick={() => navigate('/')}
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe"
>
{t('sharedConv.button')}
</button>
)}
</div>
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
{t('sharedConv.meta')}
</span>
</div>
);
};

View File

@@ -1,11 +1,9 @@
import { Answer, FEEDBACK } from './conversationModels';
import conversationService from '../api/services/conversationService';
import { Doc } from '../preferences/preferenceApi';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
import { Answer, FEEDBACK } from './conversationModels';
function getDocPath(selectedDocs: Doc | null): string {
let docPath = 'default';
if (selectedDocs) {
let namePath = selectedDocs.name;
if (selectedDocs.language === namePath) {
@@ -27,10 +25,10 @@ function getDocPath(selectedDocs: Doc | null): string {
docPath = selectedDocs.docLink;
}
}
return docPath;
}
export function fetchAnswerApi(
export function handleFetchAnswer(
question: string,
signal: AbortSignal,
selectedDocs: Doc | null,
@@ -57,27 +55,22 @@ export function fetchAnswerApi(
}
> {
const docPath = getDocPath(selectedDocs);
//in history array remove all keys except prompt and response
history = history.map((item) => {
return { prompt: item.prompt, response: item.response };
});
return fetch(apiHost + '/api/answer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: question,
history: history,
active_docs: docPath,
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
}),
signal,
})
return conversationService
.answer(
{
question: question,
history: history,
active_docs: docPath,
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
},
signal,
)
.then((response) => {
if (response.ok) {
return response.json();
@@ -97,7 +90,7 @@ export function fetchAnswerApi(
});
}
export function fetchAnswerSteaming(
export function handleFetchAnswerSteaming(
question: string,
signal: AbortSignal,
selectedDocs: Doc | null,
@@ -109,29 +102,23 @@ export function fetchAnswerSteaming(
onEvent: (event: MessageEvent) => void,
): Promise<Answer> {
const docPath = getDocPath(selectedDocs);
history = history.map((item) => {
return { prompt: item.prompt, response: item.response };
});
return new Promise<Answer>((resolve, reject) => {
const body = {
question: question,
active_docs: docPath,
history: JSON.stringify(history),
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
};
fetch(apiHost + '/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal,
})
conversationService
.answerStream(
{
question: question,
active_docs: docPath,
history: JSON.stringify(history),
conversation_id: conversationId,
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
},
signal,
)
.then((response) => {
if (!response.body) throw Error('No response body');
@@ -179,7 +166,8 @@ export function fetchAnswerSteaming(
});
});
}
export function searchEndpoint(
export function handleSearch(
question: string,
selectedDocs: Doc | null,
conversation_id: string | null,
@@ -188,48 +176,150 @@ export function searchEndpoint(
token_limit: number,
) {
const docPath = getDocPath(selectedDocs);
const body = {
question: question,
active_docs: docPath,
conversation_id,
history,
chunks: chunks,
token_limit: token_limit,
};
return fetch(`${apiHost}/api/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
return conversationService
.search({
question: question,
active_docs: docPath,
conversation_id,
history,
chunks: chunks,
token_limit: token_limit,
})
.then((response) => response.json())
.then((data) => {
return data;
})
.catch((err) => console.log(err));
}
export function sendFeedback(
export function handleSendFeedback(
prompt: string,
response: string,
feedback: FEEDBACK,
) {
return fetch(`${apiHost}/api/feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
return conversationService
.feedback({
question: prompt,
answer: response,
feedback: feedback,
}),
}).then((response) => {
if (response.ok) {
return Promise.resolve();
} else {
return Promise.reject();
}
})
.then((response) => {
if (response.ok) {
return Promise.resolve();
} else {
return Promise.reject();
}
});
}
export function handleFetchSharedAnswerStreaming( //for shared conversations
question: string,
signal: AbortSignal,
apiKey: string,
history: Array<any> = [],
onEvent: (event: MessageEvent) => void,
): Promise<Answer> {
history = history.map((item) => {
return { prompt: item.prompt, response: item.response };
});
return new Promise<Answer>((resolve, reject) => {
const payload = {
question: question,
history: JSON.stringify(history),
api_key: apiKey,
};
conversationService
.answerStream(payload, signal)
.then((response) => {
if (!response.body) throw Error('No response body');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let counterrr = 0;
const processStream = ({
done,
value,
}: ReadableStreamReadResult<Uint8Array>) => {
if (done) {
console.log(counterrr);
return;
}
counterrr += 1;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (let line of lines) {
if (line.trim() == '') {
continue;
}
if (line.startsWith('data:')) {
line = line.substring(5);
}
const messageEvent: MessageEvent = new MessageEvent('message', {
data: line,
});
onEvent(messageEvent); // handle each message
}
reader.read().then(processStream).catch(reject);
};
reader.read().then(processStream).catch(reject);
})
.catch((error) => {
console.error('Connection failed:', error);
reject(error);
});
});
}
export function handleFetchSharedAnswer(
question: string,
signal: AbortSignal,
apiKey: string,
): Promise<
| {
result: any;
answer: any;
sources: any;
query: string;
}
| {
result: any;
answer: any;
sources: any;
query: string;
title: any;
}
> {
return conversationService
.answer(
{
question: question,
api_key: apiKey,
},
signal,
)
.then((response) => {
if (response.ok) {
return response.json();
} else {
return Promise.reject(new Error(response.statusText));
}
})
.then((data) => {
const result = data.answer;
return {
answer: result,
query: question,
result,
sources: data.sources,
};
});
}

View File

@@ -1,10 +1,14 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import store from '../store';
import { fetchAnswerApi, fetchAnswerSteaming } from './conversationApi';
import { searchEndpoint } from './conversationApi';
import { Answer, ConversationState, Query, Status } from './conversationModels';
import { getConversations } from '../preferences/preferenceApi';
import { setConversations } from '../preferences/preferenceSlice';
import store from '../store';
import {
handleFetchAnswer,
handleFetchAnswerSteaming,
handleSearch,
} from './conversationHandlers';
import { Answer, ConversationState, Query, Status } from './conversationModels';
const initialState: ConversationState = {
queries: [],
@@ -20,7 +24,7 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
const state = getState() as RootState;
if (state.preference) {
if (API_STREAMING) {
await fetchAnswerSteaming(
await handleFetchAnswerSteaming(
question,
signal,
state.preference.selectedDocs!,
@@ -45,7 +49,7 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
console.error('Failed to fetch conversations: ', error);
});
searchEndpoint(
handleSearch(
//search for sources post streaming
question,
state.preference.selectedDocs!,
@@ -89,7 +93,7 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
},
);
} else {
const answer = await fetchAnswerApi(
const answer = await handleFetchAnswer(
question,
signal,
state.preference.selectedDocs!,

View File

@@ -0,0 +1,229 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import store from '../store';
import { Query, Status, Answer } from '../conversation/conversationModels';
import { createAsyncThunk } from '@reduxjs/toolkit';
import {
handleFetchSharedAnswer,
handleFetchSharedAnswerStreaming,
} from './conversationHandlers';
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
interface SharedConversationsType {
queries: Query[];
apiKey?: string;
identifier: string;
status: Status;
date?: string;
title?: string;
}
const initialState: SharedConversationsType = {
queries: [],
identifier: '',
status: 'idle',
};
export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
'shared/fetchAnswer',
async ({ question }, { dispatch, getState, signal }) => {
const state = getState() as RootState;
if (state.preference && state.sharedConversation.apiKey) {
if (API_STREAMING) {
await handleFetchSharedAnswerStreaming(
question,
signal,
state.sharedConversation.apiKey,
state.sharedConversation.queries,
(event) => {
const data = JSON.parse(event.data);
// check if the 'end' event has been received
if (data.type === 'end') {
// set status to 'idle'
dispatch(sharedConversationSlice.actions.setStatus('idle'));
dispatch(saveToLocalStorage());
} else if (data.type === 'error') {
// set status to 'failed'
dispatch(sharedConversationSlice.actions.setStatus('failed'));
dispatch(
sharedConversationSlice.actions.raiseError({
index: state.conversation.queries.length - 1,
message: data.error,
}),
);
} else {
const result = data.answer;
dispatch(
updateStreamingQuery({
index: state.sharedConversation.queries.length - 1,
query: { response: result },
}),
);
}
},
);
} else {
const answer = await handleFetchSharedAnswer(
question,
signal,
state.sharedConversation.apiKey,
);
if (answer) {
let sourcesPrepped = [];
sourcesPrepped = answer.sources.map((source: { title: string }) => {
if (source && source.title) {
const titleParts = source.title.split('/');
return {
...source,
title: titleParts[titleParts.length - 1],
};
}
return source;
});
dispatch(
updateQuery({
index: state.sharedConversation.queries.length - 1,
query: { response: answer.answer, sources: sourcesPrepped },
}),
);
dispatch(sharedConversationSlice.actions.setStatus('idle'));
}
}
}
return {
conversationId: null,
title: null,
answer: '',
query: question,
result: '',
sources: [],
};
},
);
export const sharedConversationSlice = createSlice({
name: 'sharedConversation',
initialState,
reducers: {
setStatus(state, action: PayloadAction<Status>) {
state.status = action.payload;
},
setIdentifier(state, action: PayloadAction<string>) {
state.identifier = action.payload;
},
setFetchedData(
state,
action: PayloadAction<{
queries: Query[];
title: string;
date: string;
identifier: string;
}>,
) {
const { queries, title, identifier, date } = action.payload;
const previousQueriesStr = localStorage.getItem(identifier);
const localySavedQueries: Query[] = previousQueriesStr
? JSON.parse(previousQueriesStr)
: [];
state.queries = [...queries, ...localySavedQueries];
state.title = title;
state.date = date;
state.identifier = identifier;
},
setClientApiKey(state, action: PayloadAction<string>) {
state.apiKey = action.payload;
},
addQuery(state, action: PayloadAction<Query>) {
state.queries.push(action.payload);
},
updateStreamingQuery(
state,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { index, query } = action.payload;
if (query.response != undefined) {
state.queries[index].response =
(state.queries[index].response || '') + query.response;
} else {
state.queries[index] = {
...state.queries[index],
...query,
};
}
},
updateQuery(
state,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { index, query } = action.payload;
state.queries[index] = {
...state.queries[index],
...query,
};
},
raiseError(
state,
action: PayloadAction<{ index: number; message: string }>,
) {
const { index, message } = action.payload;
state.queries[index].error = message;
},
saveToLocalStorage(state) {
const previousQueriesStr = localStorage.getItem(state.identifier);
previousQueriesStr
? localStorage.setItem(
state.identifier,
JSON.stringify([
...JSON.parse(previousQueriesStr),
state.queries[state.queries.length - 1],
]),
)
: localStorage.setItem(
state.identifier,
JSON.stringify([state.queries[state.queries.length - 1]]),
);
},
},
extraReducers(builder) {
builder
.addCase(fetchSharedAnswer.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchSharedAnswer.rejected, (state, action) => {
if (action.meta.aborted) {
state.status = 'idle';
return state;
}
state.status = 'failed';
state.queries[state.queries.length - 1].error =
'Something went wrong. Please check your internet connection.';
});
},
});
export const {
setStatus,
setIdentifier,
setFetchedData,
setClientApiKey,
updateQuery,
updateStreamingQuery,
addQuery,
saveToLocalStorage,
} = sharedConversationSlice.actions;
export const selectStatus = (state: RootState) => state.conversation.status;
export const selectClientAPIKey = (state: RootState) =>
state.sharedConversation.apiKey;
export const selectQueries = (state: RootState) =>
state.sharedConversation.queries;
export const selectTitle = (state: RootState) => state.sharedConversation.title;
export const selectDate = (state: RootState) => state.sharedConversation.date;
type RootState = ReturnType<typeof store.getState>;
sharedConversationSlice;
export default sharedConversationSlice.reducer;

View File

@@ -77,21 +77,23 @@ export function useDarkTheme() {
// Set dark mode based on local storage preference
if (savedMode === 'Dark') {
setIsDarkTheme(true);
document.documentElement.classList.add('dark');
document.documentElement.classList.add('dark:bg-raisin-black');
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
// If no preference found, set to default (light mode)
setIsDarkTheme(false);
document.documentElement.classList.remove('dark');
document.getElementById('root')?.classList.remove('dark');
}
}, []);
useEffect(() => {
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
if (isDarkTheme) {
document.documentElement.classList.add('dark');
document.documentElement.classList.add('dark:bg-raisin-black');
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
document.documentElement.classList.remove('dark');
document.getElementById('root')?.classList.remove('dark');
}
}, [isDarkTheme]);
//method to toggle theme

View File

@@ -22,6 +22,18 @@
background: #b1afaf;
}
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
@@ -409,10 +421,6 @@ template {
width: 0;
}
/* .bottom-safe {
bottom: env(safe-area-inset-bottom, 0);
} */
.inputbox-style[contenteditable] {
padding-left: 36px;
padding-right: 36px;

View File

@@ -103,6 +103,23 @@
"deleteConv": {
"confirm": "Are you sure you want to delete all the conversations?",
"delete": "Delete"
},
"shareConv": {
"label": "Create a public page to share",
"note": "Source document, personal information and further conversation will remain private",
"create": "Create",
"option": "Allow users to prompt further"
}
},
"sharedConv": {
"subtitle": "Created with",
"button": "Get Started with DocsGPT",
"meta": "DocsGPT uses GenAI, please review critical information using sources."
},
"convTile": {
"share": "Share",
"delete": "Delete",
"rename": "Rename",
"deleteWarning": "Are you sure you want to delete this conversation?"
}
}

View File

@@ -103,6 +103,23 @@
"deleteConv": {
"confirm": "¿Está seguro de que desea eliminar todas las conversaciones?",
"delete": "Eliminar"
},
"shareConv": {
"label": "Crear una página pública para compartir",
"note": "El documento original, la información personal y las conversaciones posteriores permanecerán privadas",
"create": "Crear",
"option": "Permitir a los usuarios realizar más consultas."
}
},
"sharedConv": {
"subtitle": "Creado con",
"button": "Comienza con DocsGPT",
"meta": "DocsGPT utiliza GenAI, por favor revise la información crítica utilizando fuentes."
},
"convTile": {
"share": "Compartir",
"delete": "Eliminar",
"rename": "Renombrar",
"deleteWarning": "¿Está seguro de que desea eliminar esta conversación?"
}
}

View File

@@ -103,6 +103,23 @@
"deleteConv": {
"confirm": "すべての会話を削除してもよろしいですか?",
"delete": "削除"
},
"shareConv": {
"label": "共有ページを作成して共有する",
"note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります",
"create": "作成",
"option": "ユーザーがより多くのクエリを実行できるようにします。"
}
},
"sharedConv": {
"subtitle": "作成者",
"button": "DocsGPT を始める",
"meta": "DocsGPT は GenAI を使用しています、情報源を使用して重要情報を確認してください。"
},
"convTile": {
"share": "共有",
"delete": "削除",
"rename": "名前変更",
"deleteWarning": "この会話を削除してもよろしいですか?"
}
}

View File

@@ -103,6 +103,23 @@
"deleteConv": {
"confirm": "您确定要删除所有对话吗?",
"delete": "删除"
},
"shareConv": {
"label": "创建用于分享的公共页面",
"note": "源文档、个人信息和后续对话将保持私密",
"create": "创建",
"option": "允许用户进行更多查询。"
}
},
"sharedConv": {
"subtitle": "使用创建",
"button": "开始使用 DocsGPT",
"meta": "DocsGPT 使用 GenAI请使用资源查看关键信息。"
},
"convTile": {
"share": "分享",
"delete": "删除",
"rename": "重命名",
"deleteWarning": "您确定要删除此对话吗?"
}
}

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Exit from '../assets/exit.svg';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import { CreateAPIKeyModalProps, Doc } from '../models/misc';
import { selectSourceDocs } from '../preferences/preferenceSlice';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
export default function CreateAPIKeyModal({
close,
createAPIKey,
}: CreateAPIKeyModalProps) {
const { t } = useTranslation();
const docs = useSelector(selectSourceDocs);
const [APIKeyName, setAPIKeyName] = React.useState<string>('');
const [sourcePath, setSourcePath] = React.useState<{
label: string;
value: string;
} | null>(null);
const [prompt, setPrompt] = React.useState<{
name: string;
id: string;
type: string;
} | null>(null);
const [activePrompts, setActivePrompts] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const [chunk, setChunk] = React.useState<string>('2');
const chunkOptions = ['0', '2', '4', '6', '8', '10'];
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,
};
})
: [];
React.useEffect(() => {
const handleFetchPrompts = async () => {
try {
const response = await userService.getPrompts();
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const promptsData = await response.json();
setActivePrompts(promptsData);
} catch (error) {
console.error(error);
}
};
handleFetchPrompts();
}, []);
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="mb-6">
<span className="text-xl text-jet dark:text-bright-gray">
{t('modals.createAPIKey.label')}
</span>
</div>
<div className="relative mt-5 mb-4">
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.createAPIKey.apiKeyName')}
</span>
<Input
type="text"
className="rounded-md"
value={APIKeyName}
onChange={(e) => setAPIKeyName(e.target.value)}
></Input>
</div>
<div className="my-4">
<Dropdown
placeholder={t('modals.createAPIKey.sourceDoc')}
selectedValue={sourcePath}
onSelect={(selection: { label: string; value: string }) =>
setSourcePath(selection)
}
options={extractDocPaths()}
size="w-full"
rounded="xl"
border="border"
/>
</div>
<div className="my-4">
<Dropdown
options={activePrompts}
selectedValue={prompt ? prompt.name : null}
placeholder={t('modals.createAPIKey.prompt')}
onSelect={(value: { name: string; id: string; type: string }) =>
setPrompt(value)
}
size="w-full"
border="border"
/>
</div>
<div className="my-4">
<p className="mb-2 ml-2 font-semibold text-jet dark:text-bright-gray">
{t('modals.createAPIKey.chunks')}
</p>
<Dropdown
options={chunkOptions}
selectedValue={chunk}
onSelect={(value: string) => setChunk(value)}
size="w-full"
border="border"
/>
</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 mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
>
{t('modals.createAPIKey.create')}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Exit from '../assets/exit.svg';
import { SaveAPIKeyModalProps } from '../models/misc';
export default function SaveAPIKeyModal({
apiKey,
close,
}: SaveAPIKeyModalProps) {
const { t } = useTranslation();
const [isCopied, setIsCopied] = React.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-3xl bg-white px-6 py-8 dark:bg-outer-space dark:text-bright-gray sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<h1 className="my-0 text-xl font-medium">
{' '}
{t('modals.saveKey.note')}
</h1>
<h3 className="text-sm font-normal text-outer-space">
{t('modals.saveKey.disclaimer')}
</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-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={handleCopyKey}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
</button>
</div>
<button
onClick={close}
className="rounded-full bg-philippine-yellow px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
>
{t('modals.saveKey.confirm')}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,201 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import {
selectSourceDocs,
selectSelectedDocs,
selectChunks,
selectPrompt,
} from '../preferences/preferenceSlice';
import Dropdown from '../components/Dropdown';
import { Doc } from '../models/misc';
import Spinner from '../assets/spinner.svg';
import Exit from '../assets/exit.svg';
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';
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
import conversationService from '../api/services/conversationService';
export const ShareConversationModal = ({
close,
conversationId,
}: {
close: () => void;
conversationId: string;
}) => {
const { t } = useTranslation();
const domain = window.location.origin;
const [identifier, setIdentifier] = useState<null | string>(null);
const [isCopied, setIsCopied] = useState(false);
const [status, setStatus] = useState<StatusType>('idle');
const [allowPrompt, setAllowPrompt] = useState<boolean>(false);
const sourceDocs = useSelector(selectSourceDocs);
const preSelectedDoc = useSelector(selectSelectedDocs);
const selectedPrompt = useSelector(selectPrompt);
const selectedChunk = useSelector(selectChunks);
const extractDocPaths = (docs: Doc[]) =>
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,
};
})
: [];
const [sourcePath, setSourcePath] = useState<{
label: string;
value: string;
} | null>(preSelectedDoc ? extractDocPaths([preSelectedDoc])[0] : null);
const handleCopyKey = (url: string) => {
navigator.clipboard.writeText(url);
setIsCopied(true);
};
const togglePromptPermission = () => {
setAllowPrompt(!allowPrompt);
setStatus('idle');
setIdentifier(null);
};
const shareCoversationPublicly: (isPromptable: boolean) => void = (
isPromptable = false,
) => {
setStatus('loading');
const payload: {
conversation_id: string;
chunks?: string;
prompt_id?: string;
source?: string;
} = { conversation_id: conversationId };
if (isPromptable) {
payload.chunks = selectedChunk;
payload.prompt_id = selectedPrompt.id;
sourcePath && (payload.source = sourcePath.value);
}
conversationService
.shareConversation(isPromptable, payload)
.then((res) => {
return res.json();
})
.then((data) => {
if (data.success && data.identifier) {
setIdentifier(data.identifier);
setStatus('fetched');
} else setStatus('failed');
})
.catch((err) => setStatus('failed'));
};
return (
<div className="fixed top-0 left-0 z-40 flex h-screen w-screen cursor-default items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="flex flex-col gap-2">
<h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2>
<p className="text-sm">{t('modals.shareConv.note')}</p>
<div className="flex items-center justify-between">
<span className="text-lg">{t('modals.shareConv.option')}</span>
<label className=" cursor-pointer select-none items-center">
<div className="relative">
<input
type="checkbox"
checked={allowPrompt}
onChange={togglePromptPermission}
className="sr-only"
/>
<div
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
allowPrompt
? 'bg-purple-30 dark:bg-purple-30'
: 'dark:bg-transparent'
}`}
></div>
<div
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
allowPrompt ? 'translate-x-full bg-silver' : 'bg-purple-30'
}`}
></div>
</div>
</label>
</div>
{allowPrompt && (
<div className="my-4">
<Dropdown
placeholder={t('modals.createAPIKey.sourceDoc')}
selectedValue={sourcePath}
onSelect={(selection: { label: string; value: string }) =>
setSourcePath(selection)
}
options={extractDocPaths(sourceDocs ?? [])}
size="w-full"
rounded="xl"
/>
</div>
)}
<div className="flex items-baseline justify-between gap-2">
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4">
{`${domain}/share/${identifier ?? '....'}`}
</span>
{status === 'fetched' ? (
<button
className="my-1 h-10 w-28 rounded-full border border-solid bg-purple-30 p-2 text-sm text-white hover:bg-[#6F3FD1]"
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
>
{isCopied
? t('modals.saveKey.copied')
: t('modals.saveKey.copy')}
</button>
) : (
<button
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full border border-solid bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-[#6F3FD1]"
onClick={() => {
shareCoversationPublicly(allowPrompt);
}}
>
{t('modals.shareConv.create')}
{status === 'loading' && (
<img
src={Spinner}
className="inline animate-spin cursor-pointer bg-transparent filter dark:invert"
></img>
)}
</button>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -21,7 +21,6 @@ export type PromptProps = {
selectedPrompt: { name: string; id: string; type: string };
onSelectPrompt: (name: string, id: string, type: string) => void;
setPrompts: (prompts: { name: string; id: string; type: string }[]) => void;
apiHost: string;
};
export type DocumentsProps = {

View File

@@ -4,6 +4,7 @@ import { ActiveState } from '../models/misc';
import { selectApiKey, setApiKey } from './preferenceSlice';
import { useMediaQuery, useOutsideAlerter } from './../hooks';
import Modal from '../modals';
import Input from '../components/Input';
export default function APIKeyModal({
modalState,
@@ -66,14 +67,15 @@ export default function APIKeyModal({
key for llm. Currently, we support only OpenAI but soon many more.
You can find it here.
</p>
<input
<Input
type="text"
className="h-10 w-full border-b-2 border-jet focus:outline-none"
colorVariant="jet"
className="h-10 border-b-2 focus:outline-none"
value={key}
maxLength={100}
placeholder="API Key"
onChange={(e) => setKey(e.target.value)}
/>
></Input>
</article>
);
}}

View File

@@ -1,5 +1,6 @@
import { ActiveState } from '../models/misc';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
function AddPrompt({
setModalState,
@@ -34,13 +35,13 @@ function AddPrompt({
Add your custom prompt and save it to DocsGPT
</p>
<div>
<input
<Input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 rounded-lg"
value={newPromptName}
onChange={(e) => setNewPromptName(e.target.value)}
></input>
></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
@@ -105,13 +106,13 @@ function EditPrompt({
Edit your custom prompt and save it to DocsGPT
</p>
<div>
<input
<Input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 rounded-lg"
value={editPromptName}
onChange={(e) => setEditPromptName(e.target.value)}
></input>
></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

View File

@@ -1,3 +1,6 @@
import conversationService from '../api/services/conversationService';
import userService from '../api/services/userService';
// not all properties in Doc are going to be present. Make some optional
export type Doc = {
location: string;
@@ -14,10 +17,7 @@ export type Doc = {
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
export async function getDocs(): Promise<Doc[] | null> {
try {
const apiHost =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const response = await fetch(apiHost + '/api/combine');
const response = await userService.getDocs();
const data = await response.json();
const docs: Doc[] = [];
@@ -37,10 +37,7 @@ export async function getConversations(): Promise<
{ name: string; id: string }[] | null
> {
try {
const apiHost =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const response = await fetch(apiHost + '/api/get_conversations');
const response = await conversationService.getConversations();
const data = await response.json();
const conversations: { name: string; id: string }[] = [];
@@ -93,14 +90,9 @@ export function setLocalRecentDocs(doc: Doc): void {
docPath =
doc.language + '/' + namePath + '/' + doc.version + '/' + doc.model + '/';
}
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
fetch(apiHost + '/api/docs_check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userService
.checkDocs({
docs: docPath,
}),
}).then((response) => response.json());
})
.then((response) => response.json());
}

View File

@@ -1,21 +1,12 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Dropdown from '../components/Dropdown';
import {
Doc,
CreateAPIKeyModalProps,
SaveAPIKeyModalProps,
} from '../models/misc';
import { selectSourceDocs } from '../preferences/preferenceSlice';
import Exit from '../assets/exit.svg';
import Trash from '../assets/trash.svg';
import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const APIKeys: React.FC = () => {
import userService from '../api/services/userService';
import Trash from '../assets/trash.svg';
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
export default function APIKeys() {
const { t } = useTranslation();
const [isCreateModalOpen, setCreateModal] = React.useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
@@ -23,14 +14,23 @@ const APIKeys: React.FC = () => {
const [apiKeys, setApiKeys] = React.useState<
{ name: string; key: string; source: string; id: string }[]
>([]);
const handleFetchKeys = async () => {
try {
const response = await userService.getAPIKeys();
if (!response.ok) {
throw new Error('Failed to fetch API Keys');
}
const apiKeys = await response.json();
setApiKeys(apiKeys);
} catch (error) {
console.log(error);
}
};
const handleDeleteKey = (id: string) => {
fetch(`${apiHost}/api/delete_api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id }),
})
userService
.deleteAPIKey({ id })
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete API Key');
@@ -45,34 +45,15 @@ const APIKeys: React.FC = () => {
console.error(error);
});
};
React.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: {
const handleCreateKey = (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),
})
userService
.createAPIKey(payload)
.then((response) => {
if (!response.ok) {
throw new Error('Failed to create API Key');
@@ -84,12 +65,16 @@ const APIKeys: React.FC = () => {
setCreateModal(false);
setNewKey(data.key);
setSaveKeyModal(true);
fetchAPIKeys();
handleFetchKeys();
})
.catch((error) => {
console.error(error);
});
};
React.useEffect(() => {
handleFetchKeys();
}, []);
return (
<div className="mt-8">
<div className="flex w-full flex-col lg:w-max">
@@ -103,8 +88,8 @@ const APIKeys: React.FC = () => {
</div>
{isCreateModalOpen && (
<CreateAPIKeyModal
createAPIKey={handleCreateKey}
close={() => setCreateModal(false)}
createAPIKey={createAPIKey}
/>
)}
{isSaveKeyModalOpen && (
@@ -154,192 +139,4 @@ const APIKeys: React.FC = () => {
</div>
</div>
);
};
const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
close,
createAPIKey,
}) => {
const [APIKeyName, setAPIKeyName] = React.useState<string>('');
const [sourcePath, setSourcePath] = React.useState<{
label: string;
value: string;
} | null>(null);
const chunkOptions = ['0', '2', '4', '6', '8', '10'];
const [chunk, setChunk] = React.useState<string>('2');
const [activePrompts, setActivePrompts] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const [prompt, setPrompt] = React.useState<{
name: string;
id: string;
type: string;
} | null>(null);
const docs = useSelector(selectSourceDocs);
React.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,
};
})
: [];
const { t } = useTranslation();
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="mb-6">
<span className="text-xl text-jet dark:text-bright-gray">
{t('modals.createAPIKey.label')}
</span>
</div>
<div className="relative mt-5 mb-4">
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.createAPIKey.apiKeyName')}
</span>
<input
type="text"
className="h-[42px] w-full rounded-md border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={APIKeyName}
onChange={(e) => setAPIKeyName(e.target.value)}
/>
</div>
<div className="my-4">
<Dropdown
placeholder={t('modals.createAPIKey.sourceDoc')}
selectedValue={sourcePath}
onSelect={(selection: { label: string; value: string }) =>
setSourcePath(selection)
}
options={extractDocPaths()}
size="w-full"
rounded="xl"
/>
</div>
<div className="my-4">
<Dropdown
options={activePrompts}
selectedValue={prompt ? prompt.name : null}
placeholder={t('modals.createAPIKey.prompt')}
onSelect={(value: { name: string; id: string; type: string }) =>
setPrompt(value)
}
size="w-full"
/>
</div>
<div className="my-4">
<p className="mb-2 ml-2 font-bold text-jet dark:text-bright-gray">
{t('modals.createAPIKey.chunks')}
</p>
<Dropdown
options={chunkOptions}
selectedValue={chunk}
onSelect={(value: string) => setChunk(value)}
size="w-full"
/>
</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 mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
>
{t('modals.createAPIKey.create')}
</button>
</div>
</div>
);
};
const SaveAPIKeyModal: React.FC<SaveAPIKeyModalProps> = ({ apiKey, close }) => {
const [isCopied, setIsCopied] = React.useState(false);
const { t } = useTranslation();
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-3xl bg-white px-6 py-8 dark:bg-outer-space dark:text-bright-gray sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<h1 className="my-0 text-xl font-medium">
{' '}
{t('modals.saveKey.note')}
</h1>
<h3 className="text-sm font-normal text-outer-space">
{t('modals.saveKey.disclaimer')}
</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-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={handleCopyKey}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
</button>
</div>
<button
onClick={close}
className="rounded-full bg-philippine-yellow px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
>
{t('modals.saveKey.confirm')}
</button>
</div>
</div>
);
};
export default APIKeys;
}

View File

@@ -1,22 +1,22 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Prompts from './Prompts';
import { useDarkTheme } from '../hooks';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import { useDarkTheme } from '../hooks';
import {
selectPrompt,
setPrompt,
setChunks,
selectChunks,
setTokenLimit,
selectPrompt,
selectTokenLimit,
setChunks,
setModalStateDeleteConv,
setPrompt,
setTokenLimit,
} from '../preferences/preferenceSlice';
import Prompts from './Prompts';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const General: React.FC = () => {
export default function General() {
const {
t,
i18n: { changeLanguage, language },
@@ -69,9 +69,9 @@ const General: React.FC = () => {
const selectedPrompt = useSelector(selectPrompt);
React.useEffect(() => {
const fetchPrompts = async () => {
const handleFetchPrompts = async () => {
try {
const response = await fetch(`${apiHost}/api/get_prompts`);
const response = await userService.getPrompts();
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
@@ -81,14 +81,13 @@ const General: React.FC = () => {
console.error(error);
}
};
fetchPrompts();
handleFetchPrompts();
}, []);
React.useEffect(() => {
localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string);
changeLanguage(selectedLanguage?.value);
}, [selectedLanguage, changeLanguage]);
return (
<div className="mt-[59px]">
<div className="mb-5">
@@ -171,7 +170,6 @@ const General: React.FC = () => {
dispatch(setPrompt({ name: name, id: id, type: type }))
}
setPrompts={setPrompts}
apiHost={apiHost}
/>
</div>
<div className="w-56">
@@ -189,6 +187,4 @@ const General: React.FC = () => {
</div>
</div>
);
};
export default General;
}

View File

@@ -1,15 +1,17 @@
import React from 'react';
import { PromptProps, ActiveState } from '../models/misc';
import Dropdown from '../components/Dropdown';
import PromptsModal from '../preferences/PromptsModal';
import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const Prompts: React.FC<PromptProps> = ({
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import { ActiveState, PromptProps } from '../models/misc';
import PromptsModal from '../preferences/PromptsModal';
export default function Prompts({
prompts,
selectedPrompt,
onSelectPrompt,
setPrompts,
}) => {
}: PromptProps) {
const handleSelectPrompt = ({
name,
id,
@@ -37,17 +39,12 @@ const Prompts: React.FC<PromptProps> = ({
t,
i18n: { changeLanguage, language },
} = useTranslation();
const handleAddPrompt = async () => {
try {
const response = await fetch(`${apiHost}/api/create_prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newPromptName,
content: newPromptContent,
}),
const response = await userService.createPrompt({
name: newPromptName,
content: newPromptContent,
});
if (!response.ok) {
throw new Error('Failed to add prompt');
@@ -69,18 +66,12 @@ const Prompts: React.FC<PromptProps> = ({
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: id }),
})
userService
.deletePrompt({ id })
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete prompt');
}
// get 1st prompt and set it as selected
if (prompts.length > 0) {
onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type);
}
@@ -90,18 +81,9 @@ const Prompts: React.FC<PromptProps> = ({
});
};
const fetchPromptContent = async (id: string) => {
console.log('fetching prompt content');
const handleFetchPromptContent = async (id: string) => {
try {
const response = await fetch(
`${apiHost}/api/get_single_prompt?id=${id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
const response = await userService.getSinglePrompt(id);
if (!response.ok) {
throw new Error('Failed to fetch prompt content');
}
@@ -113,17 +95,12 @@ const Prompts: React.FC<PromptProps> = ({
};
const handleSaveChanges = (id: string, type: string) => {
fetch(`${apiHost}/api/update_prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userService
.updatePrompt({
id: id,
name: editPromptName,
content: editPromptContent,
}),
})
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to update prompt');
@@ -154,7 +131,6 @@ const Prompts: React.FC<PromptProps> = ({
console.error(error);
});
};
return (
<>
<div>
@@ -183,7 +159,7 @@ const Prompts: React.FC<PromptProps> = ({
}) => {
setModalType('EDIT');
setEditPromptName(name);
fetchPromptContent(id);
handleFetchPromptContent(id);
setCurrentPromptEdit({ id: id, name: name, type: type });
setModalState('ACTIVE');
}}
@@ -219,6 +195,4 @@ const Prompts: React.FC<PromptProps> = ({
/>
</>
);
};
export default Prompts;
}

View File

@@ -1,22 +1,22 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import General from './General';
import Documents from './Documents';
import APIKeys from './APIKeys';
import Widgets from './Widgets';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
import i18n from '../locale/i18n';
import { Doc } from '../preferences/preferenceApi';
import {
selectSourceDocs,
setSourceDocs,
} from '../preferences/preferenceSlice';
import { Doc } from '../preferences/preferenceApi';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
import { useTranslation } from 'react-i18next';
import i18n from '../locale/i18n';
import APIKeys from './APIKeys';
import Documents from './Documents';
import General from './General';
import Widgets from './Widgets';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const Settings: React.FC = () => {
export default function Settings() {
const dispatch = useDispatch();
const { t } = useTranslation();
const tabs = [
@@ -33,11 +33,11 @@ const Settings: React.FC = () => {
const updateWidgetScreenshot = (screenshot: File | null) => {
setWidgetScreenshot(screenshot);
};
const handleDeleteClick = (index: number, doc: Doc) => {
const docPath = 'indexes/' + 'local' + '/' + doc.name;
fetch(`${apiHost}/api/delete_old?path=${docPath}`, {
method: 'GET',
})
userService
.deletePath(docPath)
.then((response) => {
if (response.ok && documents) {
const updatedDocuments = [
@@ -50,7 +50,6 @@ const Settings: React.FC = () => {
.catch((error) => console.error(error));
};
// persist active tab as the translated version of 'general' per language change
React.useEffect(() => {
setActiveTab(t('settings.general.label'));
}, [i18n.language]);
@@ -134,6 +133,4 @@ const Settings: React.FC = () => {
return null;
}
}
};
export default Settings;
}

View File

@@ -1,5 +1,6 @@
import { configureStore } from '@reduxjs/toolkit';
import { conversationSlice } from './conversation/conversationSlice';
import { sharedConversationSlice } from './conversation/sharedConversationSlice';
import {
prefListenerMiddleware,
prefSlice,
@@ -42,6 +43,7 @@ const store = configureStore({
reducer: {
preference: prefSlice.reducer,
conversation: conversationSlice.reducer,
sharedConversation: sharedConversationSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),

View File

@@ -1,12 +1,14 @@
import React, { useRef } from 'react';
import { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
import { getDocs } from '../preferences/preferenceApi';
import { setSelectedDocs, setSourceDocs } from '../preferences/preferenceSlice';
import Dropdown from '../components/Dropdown';
import { useTranslation } from 'react-i18next';
function Upload({
modalState,
@@ -94,20 +96,6 @@ function Upload({
{/* progress bar */}
<ProgressBar progressPercent={progress?.percentage as number} />
<button
onClick={() => {
setDocName('');
setfiles([]);
setProgress(undefined);
setModalState('INACTIVE');
}}
className={`rounded-3xl bg-purple-30 px-4 py-2 text-sm font-medium text-white ${
isCancellable ? '' : 'hidden'
}`}
>
Finish
</button>
</div>
);
}
@@ -124,8 +112,8 @@ function Upload({
if ((progress?.percentage ?? 0) < 100) {
timeoutID = setTimeout(() => {
const apiHost = import.meta.env.VITE_API_HOST;
fetch(`${apiHost}/api/task_status?task_id=${progress?.taskId}`)
userService
.getTaskStatus(progress?.taskId as string)
.then((data) => data.json())
.then((data) => {
if (data.status == 'SUCCESS') {
@@ -163,6 +151,10 @@ function Upload({
failed: false,
},
);
setDocName('');
setfiles([]);
setProgress(undefined);
setModalState('INACTIVE');
}
} else if (data.status == 'PROGRESS') {
setProgress(
@@ -272,7 +264,9 @@ function Upload({
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
if (name === 'search_queries' && value.length > 0) {
setRedditData({
@@ -323,12 +317,12 @@ function Upload({
{activeTab === 'file' && (
<>
<input
<Input
type="text"
className="h-[42px] w-full rounded-full border-2 border-gray-5000 px-3 outline-none dark:bg-transparent dark:text-silver"
colorVariant="gray"
value={docName}
onChange={(e) => setDocName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
@@ -373,25 +367,23 @@ function Upload({
/>
{urlType.label !== 'Reddit' ? (
<>
<input
<Input
placeholder={`Enter ${t('modals.uploadDoc.name')}`}
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
</span>
</div>
<input
<Input
placeholder={t('modals.uploadDoc.urlLink')}
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={url}
onChange={(e) => setUrl(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.link')}
@@ -400,66 +392,61 @@ function Upload({
</>
) : (
<>
<input
<Input
placeholder="Enter client ID"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.id')}
</span>
</div>
<input
<Input
placeholder="Enter client secret"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.secret')}
</span>
</div>
<input
<Input
placeholder="Enter user agent"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.agent')}
</span>
</div>
<input
<Input
placeholder="Enter search queries"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.searchQueries')}
</span>
</div>
<input
<Input
placeholder="Enter number of posts"
type="number"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.numberOfPosts')}

View File

@@ -48,6 +48,7 @@ module.exports = {
'soap':'#D8CCF1',
'independence':'#54546D',
'philippine-yellow':'#FFC700',
'bright-gray':'#EBEBEB'
},
},
},

View File

@@ -7,7 +7,7 @@ faiss_cpu==1.7.4
html2text==2020.1.16
javalang==0.13.0
langchain==0.1.4
langchain_community==0.0.16
langchain_community==0.2.9
langchain-openai==0.0.5
nltk==3.8.1
openapi3_parser==1.1.16