mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
resolved merge conflicts
This commit is contained in:
@@ -35,7 +35,8 @@ We're eager to provide personalized assistance when deploying your DocsGPT to a
|
||||
|
||||
[Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions)
|
||||
|
||||

|
||||
|
||||
<img src="https://github.com/user-attachments/assets/9a1f21de-7a15-4e42-9424-70d22ba5a913" alt="video-example-of-docs-gpt" width="1000" height="500">
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -292,6 +292,7 @@ class Stream(Resource):
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["question"]
|
||||
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
@@ -422,7 +423,7 @@ class Answer(Resource):
|
||||
@api.doc(description="Provide an answer based on the question and retriever")
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["question"]
|
||||
required_fields = ["question"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
@@ -7,7 +7,7 @@ from bson.binary import Binary, UuidRepresentation
|
||||
from bson.dbref import DBRef
|
||||
from bson.objectid import ObjectId
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
from flask_restx import inputs, fields, Namespace, Resource
|
||||
from pymongo import MongoClient
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -802,7 +802,7 @@ class ShareConversation(Resource):
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
|
||||
is_promptable = request.args.get("isPromptable")
|
||||
is_promptable = request.args.get("isPromptable", type=inputs.boolean)
|
||||
if is_promptable is None:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "isPromptable is required"}), 400
|
||||
@@ -831,7 +831,7 @@ class ShareConversation(Resource):
|
||||
uuid.uuid4(), UuidRepresentation.STANDARD
|
||||
)
|
||||
|
||||
if is_promptable.lower() == "true":
|
||||
if is_promptable:
|
||||
prompt_id = data.get("prompt_id", "default")
|
||||
chunks = data.get("chunks", "2")
|
||||
|
||||
@@ -859,7 +859,7 @@ class ShareConversation(Resource):
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"isPromptable": is_promptable.lower() == "true",
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
@@ -883,7 +883,7 @@ class ShareConversation(Resource):
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": is_promptable.lower() == "true",
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
@@ -918,7 +918,7 @@ class ShareConversation(Resource):
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": is_promptable.lower() == "true",
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
"api_key": api_uuid,
|
||||
@@ -939,7 +939,7 @@ class ShareConversation(Resource):
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"isPromptable": is_promptable.lower() == "false",
|
||||
"isPromptable": not is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
}
|
||||
@@ -962,7 +962,7 @@ class ShareConversation(Resource):
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"isPromptable": is_promptable.lower() == "false",
|
||||
"isPromptable": not is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
}
|
||||
|
||||
93
application/cache.py
Normal file
93
application/cache.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import redis
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from threading import Lock
|
||||
from application.core.settings import settings
|
||||
from application.utils import get_hash
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_redis_instance = None
|
||||
_instance_lock = Lock()
|
||||
|
||||
def get_redis_instance():
|
||||
global _redis_instance
|
||||
if _redis_instance is None:
|
||||
with _instance_lock:
|
||||
if _redis_instance is None:
|
||||
try:
|
||||
_redis_instance = redis.Redis.from_url(settings.CACHE_REDIS_URL, socket_connect_timeout=2)
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
_redis_instance = None
|
||||
return _redis_instance
|
||||
|
||||
def gen_cache_key(*messages, model="docgpt"):
|
||||
if not all(isinstance(msg, dict) for msg in messages):
|
||||
raise ValueError("All messages must be dictionaries.")
|
||||
messages_str = json.dumps(list(messages), sort_keys=True)
|
||||
combined = f"{model}_{messages_str}"
|
||||
cache_key = get_hash(combined)
|
||||
return cache_key
|
||||
|
||||
def gen_cache(func):
|
||||
def wrapper(self, model, messages, *args, **kwargs):
|
||||
try:
|
||||
cache_key = gen_cache_key(*messages)
|
||||
redis_client = get_redis_instance()
|
||||
if redis_client:
|
||||
try:
|
||||
cached_response = redis_client.get(cache_key)
|
||||
if cached_response:
|
||||
return cached_response.decode('utf-8')
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
result = func(self, model, messages, *args, **kwargs)
|
||||
if redis_client:
|
||||
try:
|
||||
redis_client.set(cache_key, result, ex=1800)
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
return result
|
||||
except ValueError as e:
|
||||
logger.error(e)
|
||||
return "Error: No user message found in the conversation to generate a cache key."
|
||||
return wrapper
|
||||
|
||||
def stream_cache(func):
|
||||
def wrapper(self, model, messages, stream, *args, **kwargs):
|
||||
cache_key = gen_cache_key(*messages)
|
||||
logger.info(f"Stream cache key: {cache_key}")
|
||||
|
||||
redis_client = get_redis_instance()
|
||||
if redis_client:
|
||||
try:
|
||||
cached_response = redis_client.get(cache_key)
|
||||
if cached_response:
|
||||
logger.info(f"Cache hit for stream key: {cache_key}")
|
||||
cached_response = json.loads(cached_response.decode('utf-8'))
|
||||
for chunk in cached_response:
|
||||
yield chunk
|
||||
time.sleep(0.03)
|
||||
return
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
result = func(self, model, messages, stream, *args, **kwargs)
|
||||
stream_cache_data = []
|
||||
|
||||
for chunk in result:
|
||||
stream_cache_data.append(chunk)
|
||||
yield chunk
|
||||
|
||||
if redis_client:
|
||||
try:
|
||||
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
|
||||
logger.info(f"Stream cache saved for key: {cache_key}")
|
||||
except redis.ConnectionError as e:
|
||||
logger.error(f"Redis connection error: {e}")
|
||||
|
||||
return wrapper
|
||||
@@ -21,6 +21,9 @@ class Settings(BaseSettings):
|
||||
VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus"
|
||||
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
||||
|
||||
# LLM Cache
|
||||
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
||||
|
||||
API_URL: str = "http://localhost:7091" # backend url for celery worker
|
||||
|
||||
API_KEY: Optional[str] = None # LLM api key
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from application.usage import gen_token_usage, stream_token_usage
|
||||
from application.cache import stream_cache, gen_cache
|
||||
|
||||
|
||||
class BaseLLM(ABC):
|
||||
def __init__(self):
|
||||
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
|
||||
|
||||
def _apply_decorator(self, method, decorator, *args, **kwargs):
|
||||
return decorator(method, *args, **kwargs)
|
||||
def _apply_decorator(self, method, decorators, *args, **kwargs):
|
||||
for decorator in decorators:
|
||||
method = decorator(method)
|
||||
return method(self, *args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def _raw_gen(self, model, messages, stream, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen(self, model, messages, stream=False, *args, **kwargs):
|
||||
return self._apply_decorator(self._raw_gen, gen_token_usage)(
|
||||
self, model=model, messages=messages, stream=stream, *args, **kwargs
|
||||
)
|
||||
decorators = [gen_token_usage, gen_cache]
|
||||
return self._apply_decorator(self._raw_gen, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen_stream(self, model, messages, stream=True, *args, **kwargs):
|
||||
return self._apply_decorator(self._raw_gen_stream, stream_token_usage)(
|
||||
self, model=model, messages=messages, stream=stream, *args, **kwargs
|
||||
)
|
||||
decorators = [stream_cache, stream_token_usage]
|
||||
return self._apply_decorator(self._raw_gen_stream, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs)
|
||||
@@ -4,7 +4,7 @@ beautifulsoup4==4.12.3
|
||||
celery==5.3.6
|
||||
dataclasses-json==0.6.7
|
||||
docx2txt==0.8
|
||||
duckduckgo-search==6.2.6
|
||||
duckduckgo-search==6.3.0
|
||||
ebooklib==0.18
|
||||
elastic-transport==8.15.0
|
||||
elasticsearch==8.15.1
|
||||
@@ -54,7 +54,7 @@ pathable==0.4.3
|
||||
pillow==10.4.0
|
||||
portalocker==2.10.1
|
||||
prance==23.6.21.0
|
||||
primp==0.6.2
|
||||
primp==0.6.3
|
||||
prompt-toolkit==3.0.47
|
||||
protobuf==5.28.2
|
||||
py==1.11.0
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import tiktoken
|
||||
import hashlib
|
||||
from flask import jsonify, make_response
|
||||
|
||||
|
||||
_encoding = None
|
||||
|
||||
|
||||
@@ -39,3 +41,8 @@ def check_required_fields(data, required_fields):
|
||||
400,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_hash(data):
|
||||
return hashlib.md5(data.encode()).hexdigest()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ services:
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- MONGO_URI=mongodb://mongo:27017/docsgpt
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
ports:
|
||||
- "7091:7091"
|
||||
volumes:
|
||||
@@ -41,6 +42,7 @@ services:
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- MONGO_URI=mongodb://mongo:27017/docsgpt
|
||||
- API_URL=http://backend:7091
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
depends_on:
|
||||
- redis
|
||||
- mongo
|
||||
|
||||
BIN
frontend/signal-desktop-keyring.gpg
Normal file
BIN
frontend/signal-desktop-keyring.gpg
Normal file
Binary file not shown.
@@ -19,7 +19,7 @@ function MainLayout() {
|
||||
<div className="dark:bg-raisin-black relative h-screen overflow-auto">
|
||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||
<div
|
||||
className={`h-[calc(100dvh-64px)] sm:h-screen ${
|
||||
className={`h-[calc(100dvh-64px)] md:h-screen ${
|
||||
!isMobile
|
||||
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
|
||||
: 'ml-0 md:ml-16'
|
||||
@@ -32,9 +32,9 @@ function MainLayout() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [,,componentMounted] = useDarkTheme();
|
||||
if(!componentMounted) {
|
||||
return <div />
|
||||
const [, , componentMounted] = useDarkTheme();
|
||||
if (!componentMounted) {
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<div className="h-full relative overflow-auto">
|
||||
|
||||
@@ -2,17 +2,15 @@ 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 openNewChat from './assets/openNewChat.svg';
|
||||
import Hamburger from './assets/hamburger.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 Info from './assets/info.svg';
|
||||
import SettingGear from './assets/settingGear.svg';
|
||||
import Twitter from './assets/TwitterX.svg';
|
||||
import UploadIcon from './assets/upload.svg';
|
||||
@@ -43,6 +41,7 @@ import {
|
||||
} from './preferences/preferenceSlice';
|
||||
import { selectQueries } from './conversation/conversationSlice';
|
||||
import Upload from './upload/Upload';
|
||||
import Help from './components/Help';
|
||||
|
||||
interface NavigationProps {
|
||||
navOpen: boolean;
|
||||
@@ -76,7 +75,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isApiKeySet = useSelector(selectApiKeyStatus);
|
||||
const [apiKeyModalState, setApiKeyModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
@@ -304,7 +302,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
{t('newChat')}
|
||||
</p>
|
||||
</NavLink>
|
||||
<div className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white">
|
||||
<div
|
||||
id="conversationsMainDiv"
|
||||
className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white"
|
||||
>
|
||||
{conversations && conversations.length > 0 ? (
|
||||
<div>
|
||||
<div className=" my-auto mx-4 mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
|
||||
@@ -333,7 +334,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white">
|
||||
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe">
|
||||
<div className="relative my-4 mx-4 flex gap-2">
|
||||
@@ -388,68 +388,51 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
</p>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 border-b-[1.5px] py-2 dark:border-b-purple-taupe">
|
||||
<NavLink
|
||||
onClick={() => {
|
||||
if (isMobile) {
|
||||
setNavOpen(!navOpen);
|
||||
}
|
||||
resetConversation();
|
||||
}}
|
||||
to="/about"
|
||||
className={({ isActive }) =>
|
||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Info}
|
||||
alt="icon"
|
||||
className="ml-2 w-5 filter dark:invert"
|
||||
/>
|
||||
<p className="my-auto pr-1 text-sm">{t('about')}</p>
|
||||
</NavLink>
|
||||
<div className="flex items-center justify-evenly gap-1 px-1">
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://discord.gg/WHJdfbQDR4'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Discord}
|
||||
alt="discord"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://twitter.com/docsgptai'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Twitter}
|
||||
alt="x"
|
||||
className="m-2 w-5 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://github.com/arc53/docsgpt'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Github}
|
||||
alt="github"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<div className="flex flex-col justify-end text-eerie-black dark:text-white">
|
||||
<div className="flex justify-between items-center px-1 py-1">
|
||||
<Help />
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://discord.gg/WHJdfbQDR4'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Discord}
|
||||
alt="discord"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://twitter.com/docsgptai'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Twitter}
|
||||
alt="x"
|
||||
className="m-2 w-5 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://github.com/arc53/docsgpt'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Github}
|
||||
alt="github"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0,0,256,256">
|
||||
<g transform="translate(-19.2,-19.2) scale(1.15,1.15)"><g fill="#949494" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(10.66667,10.66667)"><path d="M13.172,2h-7.172c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-11.172c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828c-0.375,-0.375 -0.884,-0.586 -1.414,-0.586zM15,18h-6c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0c0,0.552 -0.448,1 -1,1zM15,14h-6c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0c0,0.552 -0.448,1 -1,1zM13,9v-5.5l5.5,5.5z"></path></g></g></g>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 8.64258V16.25C16.25 16.7473 16.0525 17.2242 15.7008 17.5758C15.3492 17.9275 14.8723 18.125 14.375 18.125H5.625C5.12772 18.125 4.65081 17.9275 4.29917 17.5758C3.94754 17.2242 3.75 16.7473 3.75 16.25V3.75C3.75 3.25272 3.94754 2.77581 4.29917 2.42417C4.65081 2.07254 5.12772 1.875 5.625 1.875H9.48242C9.81383 1.87505 10.1316 2.0067 10.366 2.24102L15.884 7.75898C16.1183 7.99335 16.2499 8.31117 16.25 8.64258Z" stroke="#ECECF1" stroke-width="1.11111" stroke-linejoin="round"/>
|
||||
<path d="M10 2.1875V6.875C10 7.20652 10.1317 7.52446 10.3661 7.75888C10.6005 7.9933 10.9185 8.125 11.25 8.125H15.9375M6.875 11.25H13.125M6.875 14.375H13.125" stroke="#ECECF1" stroke-width="1.11111" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 839 B |
@@ -1,3 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0,0,256,256">
|
||||
<g transform="translate(-19.2,-19.2) scale(1.15,1.15)"><g fill="black" fill-opacity="0.54" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(10.66667,10.66667)"><path d="M13.172,2h-7.172c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-11.172c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828c-0.375,-0.375 -0.884,-0.586 -1.414,-0.586zM15,18h-6c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0c0,0.552 -0.448,1 -1,1zM15,14h-6c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0c0,0.552 -0.448,1 -1,1zM13,9v-5.5l5.5,5.5z"></path></g></g></g>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 8.64258V16.25C16.25 16.7473 16.0525 17.2242 15.7008 17.5758C15.3492 17.9275 14.8723 18.125 14.375 18.125H5.625C5.12772 18.125 4.65081 17.9275 4.29917 17.5758C3.94754 17.2242 3.75 16.7473 3.75 16.25V3.75C3.75 3.25272 3.94754 2.77581 4.29917 2.42417C4.65081 2.07254 5.12772 1.875 5.625 1.875H9.48242C9.81383 1.87505 10.1316 2.0067 10.366 2.24102L15.884 7.75898C16.1183 7.99335 16.2499 8.31117 16.25 8.64258Z" stroke="#6E6E6E" stroke-width="1.11111" stroke-linejoin="round"/>
|
||||
<path d="M10 2.1875V6.875C10 7.20652 10.1317 7.52446 10.3661 7.75888C10.6005 7.9933 10.9185 8.125 11.25 8.125H15.9375M6.875 11.25H13.125M6.875 14.375H13.125" stroke="#6E6E6E" stroke-width="1.11111" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 951 B After Width: | Height: | Size: 839 B |
3
frontend/src/assets/envelope-dark.svg
Normal file
3
frontend/src/assets/envelope-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.04867 15.49H15.1775C16.5339 15.49 17.3172 14.7067 17.3172 13.1548V4.83755C17.3172 3.29309 16.5265 2.50977 14.9515 2.50977H2.82302C1.47463 2.50977 0.683594 3.28569 0.683594 4.83755V13.1545C0.683594 14.7138 1.48138 15.49 3.04867 15.49ZM8.10441 9.3803L2.41609 3.76816C2.58163 3.70034 2.77738 3.66273 3.01106 3.66273H14.9971C15.2305 3.66273 15.434 3.70034 15.6072 3.78327L9.92692 9.38062C9.60291 9.7043 9.31652 9.84766 9.01534 9.84766C8.71384 9.84766 8.42841 9.70398 8.10441 9.3803ZM1.83559 13.1548V4.76234L6.16717 9.01162L1.84331 13.2824C1.83592 13.2448 1.83559 13.1998 1.83559 13.1548ZM16.1646 4.84494V13.2599L11.8629 9.0113L16.1649 4.78516L16.1646 4.84494ZM3.01106 14.3377C2.79249 14.3377 2.61184 14.3075 2.4537 14.2397L6.95852 9.78723L7.44838 10.2694C7.97552 10.7891 8.48017 11.0077 9.01534 11.0077C9.54249 11.0077 10.0548 10.7891 10.5823 10.2694L11.0718 9.78723L15.5696 14.2319C15.4111 14.3075 15.2154 14.3374 14.9968 14.3374L3.01106 14.3377Z" fill="#ECECF1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
frontend/src/assets/envelope.svg
Normal file
3
frontend/src/assets/envelope.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.04867 15.49H15.1775C16.5339 15.49 17.3172 14.7067 17.3172 13.1548V4.83755C17.3172 3.29309 16.5265 2.50977 14.9515 2.50977H2.82302C1.47463 2.50977 0.683594 3.28569 0.683594 4.83755V13.1545C0.683594 14.7138 1.48138 15.49 3.04867 15.49ZM8.10441 9.3803L2.41609 3.76816C2.58163 3.70034 2.77738 3.66273 3.01106 3.66273H14.9971C15.2305 3.66273 15.434 3.70034 15.6072 3.78327L9.92692 9.38062C9.60291 9.7043 9.31652 9.84766 9.01534 9.84766C8.71384 9.84766 8.42841 9.70398 8.10441 9.3803ZM1.83559 13.1548V4.76234L6.16717 9.01162L1.84331 13.2824C1.83592 13.2448 1.83559 13.1998 1.83559 13.1548ZM16.1646 4.84494V13.2599L11.8629 9.0113L16.1649 4.78516L16.1646 4.84494ZM3.01106 14.3377C2.79249 14.3377 2.61184 14.3075 2.4537 14.2397L6.95852 9.78723L7.44838 10.2694C7.97552 10.7891 8.48017 11.0077 9.01534 11.0077C9.54249 11.0077 10.0548 10.7891 10.5823 10.2694L11.0718 9.78723L15.5696 14.2319C15.4111 14.3075 15.2154 14.3374 14.9968 14.3374L3.01106 14.3377Z" fill="#6F6F6F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
80
frontend/src/components/Help.tsx
Normal file
80
frontend/src/components/Help.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import Info from '../assets/info.svg';
|
||||
import PageIcon from '../assets/documentation.svg';
|
||||
import EmailIcon from '../assets/envelope.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const Help = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-sm" ref={dropdownRef}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={toggleDropdown}
|
||||
className="my-auto mx-4 w-full flex items-center h-9 gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img src={Info} alt="info" className="ml-1 w-5 filter dark:invert" />
|
||||
{t('help')}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`absolute translate-x-4 -translate-y-28 z-10 w-48 shadow-lg bg-white dark:bg-[#444654] rounded-xl`}
|
||||
>
|
||||
<a
|
||||
href="https://docs.docsgpt.cloud/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-4 px-4 py-2 text-black dark:text-white hover:bg-bright-gray dark:hover:bg-[#545561] rounded-t-xl"
|
||||
>
|
||||
<img
|
||||
src={PageIcon}
|
||||
alt="Documentation"
|
||||
className="filter dark:invert"
|
||||
width={20}
|
||||
/>
|
||||
{t('documentation')}
|
||||
</a>
|
||||
<a
|
||||
href="mailto:contact@arc53.com"
|
||||
className="flex items-start gap-4 px-4 py-2 text-black dark:text-white hover:bg-bright-gray dark:hover:bg-[#545561] rounded-b-xl"
|
||||
>
|
||||
<img
|
||||
src={EmailIcon}
|
||||
alt="Email Us"
|
||||
className="filter dark:invert p-0.5"
|
||||
width={20}
|
||||
/>
|
||||
{t('emailUs')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
36
frontend/src/components/ShareButton.tsx
Normal file
36
frontend/src/components/ShareButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import ShareIcon from '../assets/share.svg';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
|
||||
type ShareButtonProps = {
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export default function ShareButton({ conversationId }: ShareButtonProps) {
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
title="Share"
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="absolute top-4 right-20 z-20 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
138
frontend/src/components/SkeletonLoader.tsx
Normal file
138
frontend/src/components/SkeletonLoader.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface SkeletonLoaderProps {
|
||||
count?: number;
|
||||
component?: 'default' | 'analysis' | 'chatbot' | 'logs';
|
||||
}
|
||||
|
||||
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
count = 1,
|
||||
component = 'default',
|
||||
}) => {
|
||||
const [skeletonCount, setSkeletonCount] = useState(count);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (windowWidth > 1024) {
|
||||
setSkeletonCount(1);
|
||||
} else if (windowWidth > 768) {
|
||||
setSkeletonCount(count);
|
||||
} else {
|
||||
setSkeletonCount(Math.min(count, 2));
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [count]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{component === 'default' ? (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`p-6 ${skeletonCount === 1 ? 'w-full' : 'w-60'} dark:bg-raisin-black rounded-3xl animate-pulse`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-5/6 mb-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : component === 'analysis' ? (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/3 mb-4"></div>
|
||||
<div className="grid grid-cols-6 gap-2 items-end">
|
||||
<div className="h-32 bg-gray-600 rounded"></div>
|
||||
<div className="h-24 bg-gray-600 rounded"></div>
|
||||
<div className="h-40 bg-gray-600 rounded"></div>
|
||||
<div className="h-28 bg-gray-600 rounded"></div>
|
||||
<div className="h-36 bg-gray-600 rounded"></div>
|
||||
<div className="h-20 bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/4 mb-4"></div>
|
||||
<div className="h-32 bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : component === 'chatbot' ? (
|
||||
<div className="space-y-2 p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse">
|
||||
<div className="grid grid-cols-4 gap-2 p-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-2"></div>
|
||||
|
||||
{[...Array(skeletonCount * 6)].map((_, idx) => (
|
||||
<div key={idx} className="grid grid-cols-4 gap-2 p-2 space-x-2">
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-1/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLoader;
|
||||
@@ -121,9 +121,12 @@ function SourceDropdown({
|
||||
className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe"
|
||||
onClick={handleEmptyDocumentSelect}
|
||||
>
|
||||
<span className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3" onClick = {() => {
|
||||
handlePostDocumentSelect(null);
|
||||
}}>
|
||||
<span
|
||||
className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3"
|
||||
onClick={() => {
|
||||
handlePostDocumentSelect(null);
|
||||
}}
|
||||
>
|
||||
{t('none')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import newChatIcon from '../assets/openNewChat.svg';
|
||||
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';
|
||||
@@ -19,6 +18,7 @@ import { AppDispatch } from '../store';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { handleSendFeedback } from './conversationHandlers';
|
||||
import { FEEDBACK, Query } from './conversationModels';
|
||||
import ShareIcon from '../assets/share.svg';
|
||||
import {
|
||||
addQuery,
|
||||
fetchAnswer,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
SyntheticEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Exit from '../assets/exit.svg';
|
||||
@@ -75,6 +81,36 @@ export default function ConversationTile({
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const preventScroll = useCallback((event: WheelEvent | TouchEvent) => {
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const conversationsMainDiv = document.getElementById(
|
||||
'conversationsMainDiv',
|
||||
);
|
||||
|
||||
if (conversationsMainDiv) {
|
||||
if (isOpen) {
|
||||
conversationsMainDiv.addEventListener('wheel', preventScroll, {
|
||||
passive: false,
|
||||
});
|
||||
conversationsMainDiv.addEventListener('touchmove', preventScroll, {
|
||||
passive: false,
|
||||
});
|
||||
} else {
|
||||
conversationsMainDiv.removeEventListener('wheel', preventScroll);
|
||||
conversationsMainDiv.removeEventListener('touchmove', preventScroll);
|
||||
}
|
||||
|
||||
return () => {
|
||||
conversationsMainDiv.removeEventListener('wheel', preventScroll);
|
||||
conversationsMainDiv.removeEventListener('touchmove', preventScroll);
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function onClear() {
|
||||
setConversationsName(conversation.name);
|
||||
setIsEdit(false);
|
||||
@@ -147,7 +183,7 @@ export default function ConversationTile({
|
||||
<button
|
||||
onClick={(event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
setOpen(true);
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
className="mr-2 flex w-4 justify-center"
|
||||
>
|
||||
|
||||
@@ -38,9 +38,11 @@ export function handleFetchAnswer(
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
};
|
||||
if (selectedDocs && 'id' in selectedDocs)
|
||||
if (selectedDocs && 'id' in selectedDocs) {
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
}
|
||||
payload.retriever = selectedDocs?.retriever as string;
|
||||
return conversationService
|
||||
.answer(payload, signal)
|
||||
@@ -84,26 +86,16 @@ export function handleFetchAnswerSteaming(
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
};
|
||||
if (selectedDocs && 'id' in selectedDocs)
|
||||
if (selectedDocs && 'id' in selectedDocs) {
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
}
|
||||
payload.retriever = selectedDocs?.retriever as string;
|
||||
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
conversationService
|
||||
.answerStream(
|
||||
{
|
||||
question: question,
|
||||
active_docs: selectedDocs?.id as string,
|
||||
history: JSON.stringify(history),
|
||||
conversation_id: conversationId,
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
},
|
||||
signal,
|
||||
)
|
||||
.answerStream(payload, signal)
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
@@ -169,20 +161,13 @@ export function handleSearch(
|
||||
conversation_id: conversation_id,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
};
|
||||
if (selectedDocs && 'id' in selectedDocs)
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
payload.retriever = selectedDocs?.retriever as string;
|
||||
return conversationService
|
||||
.search({
|
||||
question: question,
|
||||
active_docs: selectedDocs?.id as string,
|
||||
conversation_id,
|
||||
history,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
})
|
||||
.search(payload)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
|
||||
@@ -40,4 +40,5 @@ export interface RetrievalPayload {
|
||||
prompt_id?: string | null;
|
||||
chunks: string;
|
||||
token_limit: number;
|
||||
isNoneDoc: boolean;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"sourceDocs": "Source",
|
||||
"none": "None",
|
||||
"cancel": "Cancel",
|
||||
"help": "Help",
|
||||
"emailUs": "Email us",
|
||||
"documentation": "documentation",
|
||||
"demo": [
|
||||
{
|
||||
"header": "Learn about DocsGPT",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"sourceDocs": "Fuente",
|
||||
"none": "Nada",
|
||||
"cancel": "Cancelar",
|
||||
"help": "Asistencia",
|
||||
"emailUs": "Envíanos un correo",
|
||||
"documentation": "documentación",
|
||||
"demo": [
|
||||
{
|
||||
"header": "Aprende sobre DocsGPT",
|
||||
|
||||
@@ -25,7 +25,7 @@ i18n
|
||||
zh: {
|
||||
translation: zh,
|
||||
},
|
||||
"zh-TW": {
|
||||
'zh-TW': {
|
||||
translation: zhTW,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"sourceDocs": "ソース",
|
||||
"none": "なし",
|
||||
"cancel": "キャンセル",
|
||||
"help": "ヘルプ",
|
||||
"emailUs": "メールを送る",
|
||||
"documentation": "ドキュメント",
|
||||
"demo": [
|
||||
{
|
||||
"header": "DocsGPTについて学ぶ",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"sourceDocs": "原始文件",
|
||||
"none": "無",
|
||||
"cancel": "取消",
|
||||
"help": "聯繫支援",
|
||||
"emailUs": "寄送電子郵件給我們",
|
||||
"documentation": "文件",
|
||||
"demo": [
|
||||
{
|
||||
"header": "了解 DocsGPT",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"sourceDocs": "源",
|
||||
"none": "无",
|
||||
"cancel": "取消",
|
||||
"help": "联系支持",
|
||||
"emailUs": "给我们发邮件",
|
||||
"documentation": "文档",
|
||||
"demo": [
|
||||
{
|
||||
"header": "了解 DocsGPT",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
@@ -6,6 +6,7 @@ import Trash from '../assets/trash.svg';
|
||||
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
|
||||
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
|
||||
import { APIKeyData } from './types';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
|
||||
export default function APIKeys() {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,8 +14,10 @@ export default function APIKeys() {
|
||||
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
|
||||
const [newKey, setNewKey] = React.useState('');
|
||||
const [apiKeys, setApiKeys] = React.useState<APIKeyData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const handleFetchKeys = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await userService.getAPIKeys();
|
||||
if (!response.ok) {
|
||||
@@ -24,6 +27,8 @@ export default function APIKeys() {
|
||||
setApiKeys(apiKeys);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,6 +80,7 @@ export default function APIKeys() {
|
||||
React.useEffect(() => {
|
||||
handleFetchKeys();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col max-w-[876px]">
|
||||
@@ -100,41 +106,45 @@ export default function APIKeys() {
|
||||
)}
|
||||
<div className="mt-[27px] w-full">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.apiKeys.name')}</th>
|
||||
<th>{t('settings.apiKeys.sourceDoc')}</th>
|
||||
<th>{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!apiKeys?.length && (
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} component={'chatbot'} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={4} className="!p-4">
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
<th>{t('settings.apiKeys.name')}</th>
|
||||
<th>{t('settings.apiKeys.sourceDoc')}</th>
|
||||
<th>{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
{apiKeys?.map((element, index) => (
|
||||
<tr key={index}>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!apiKeys?.length && (
|
||||
<tr>
|
||||
<td colSpan={4} className="!p-4">
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{apiKeys?.map((element, index) => (
|
||||
<tr key={index}>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import React from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
@@ -17,6 +17,8 @@ import { formatDate } from '../utils/dateTimeUtils';
|
||||
import { APIKeyData } from './types';
|
||||
|
||||
import type { ChartData } from 'chart.js';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
@@ -35,37 +37,37 @@ const filterOptions = [
|
||||
];
|
||||
|
||||
export default function Analytics() {
|
||||
const [messagesData, setMessagesData] = React.useState<Record<
|
||||
const [messagesData, setMessagesData] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [tokenUsageData, setTokenUsageData] = React.useState<Record<
|
||||
const [tokenUsageData, setTokenUsageData] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [feedbackData, setFeedbackData] = React.useState<Record<
|
||||
const [feedbackData, setFeedbackData] = useState<Record<
|
||||
string,
|
||||
{
|
||||
positive: number;
|
||||
negative: number;
|
||||
}
|
||||
{ positive: number; negative: number }
|
||||
> | null>(null);
|
||||
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [messagesFilter, setMessagesFilter] = React.useState<{
|
||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
||||
const [messagesFilter, setMessagesFilter] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
const [tokenUsageFilter, setTokenUsageFilter] = React.useState<{
|
||||
const [tokenUsageFilter, setTokenUsageFilter] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
const [feedbackFilter, setFeedbackFilter] = React.useState<{
|
||||
const [feedbackFilter, setFeedbackFilter] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>({ label: '30 Days', value: 'last_30_days' });
|
||||
|
||||
const [loadingMessages, setLoadingMessages] = useState(true);
|
||||
const [loadingTokens, setLoadingTokens] = useState(true);
|
||||
const [loadingFeedback, setLoadingFeedback] = useState(true);
|
||||
|
||||
const fetchChatbots = async () => {
|
||||
try {
|
||||
const response = await userService.getAPIKeys();
|
||||
@@ -80,6 +82,7 @@ export default function Analytics() {
|
||||
};
|
||||
|
||||
const fetchMessagesData = async (chatbot_id?: string, filter?: string) => {
|
||||
setLoadingMessages(true);
|
||||
try {
|
||||
const response = await userService.getMessageAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
@@ -92,10 +95,13 @@ export default function Analytics() {
|
||||
setMessagesData(data.messages);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingMessages(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTokenData = async (chatbot_id?: string, filter?: string) => {
|
||||
setLoadingTokens(true);
|
||||
try {
|
||||
const response = await userService.getTokenAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
@@ -108,10 +114,13 @@ export default function Analytics() {
|
||||
setTokenUsageData(data.token_usage);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingTokens(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => {
|
||||
setLoadingFeedback(true);
|
||||
try {
|
||||
const response = await userService.getFeedbackAnalytics({
|
||||
api_key_id: chatbot_id,
|
||||
@@ -124,30 +133,33 @@ export default function Analytics() {
|
||||
setFeedbackData(data.feedback);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingFeedback(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
fetchChatbots();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = messagesFilter;
|
||||
fetchMessagesData(id, filter?.value);
|
||||
}, [selectedChatbot, messagesFilter]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = tokenUsageFilter;
|
||||
fetchTokenData(id, filter?.value);
|
||||
}, [selectedChatbot, tokenUsageFilter]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const id = selectedChatbot?.id;
|
||||
const filter = feedbackFilter;
|
||||
fetchFeedbackData(id, filter?.value);
|
||||
}, [selectedChatbot, feedbackFilter]);
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -181,8 +193,10 @@ export default function Analytics() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Messages Analytics */}
|
||||
<div className="mt-8 w-full flex flex-col [@media(min-width:1080px)]:flex-row gap-3">
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Messages
|
||||
@@ -208,26 +222,32 @@ export default function Analytics() {
|
||||
id="legend-container-1"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(messagesData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages',
|
||||
data: Object.values(messagesData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-1"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
{loadingMessages ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(messagesData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages',
|
||||
data: Object.values(messagesData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-1"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
|
||||
{/* Token Usage Analytics */}
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Token Usage
|
||||
@@ -253,31 +273,37 @@ export default function Analytics() {
|
||||
id="legend-container-2"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: Object.values(tokenUsageData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-2"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
{loadingTokens ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: Object.values(tokenUsageData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-2"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
|
||||
{/* Feedback Analytics */}
|
||||
<div className="mt-8 w-full flex flex-col gap-3">
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
User Feedback
|
||||
Feedback
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
@@ -300,32 +326,36 @@ export default function Analytics() {
|
||||
id="legend-container-3"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Positive',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.positive,
|
||||
),
|
||||
backgroundColor: '#8BD154',
|
||||
},
|
||||
{
|
||||
label: 'Negative',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.negative,
|
||||
),
|
||||
backgroundColor: '#D15454',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-3"
|
||||
maxTicksLimitInX={10}
|
||||
isStacked={true}
|
||||
/>
|
||||
{loadingFeedback ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Positive Feedback',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.positive,
|
||||
),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
{
|
||||
label: 'Negative Feedback',
|
||||
data: Object.values(feedbackData || {}).map(
|
||||
(item) => item.negative,
|
||||
),
|
||||
backgroundColor: '#FF6384',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-3"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -6,6 +7,7 @@ import userService from '../api/services/userService';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import DropdownMenu from '../components/DropdownMenu';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import { Doc, DocumentsProps } from '../models/misc';
|
||||
import { getDocs } from '../preferences/preferenceApi';
|
||||
import { setSourceDocs } from '../preferences/preferenceSlice';
|
||||
@@ -33,6 +35,7 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const syncOptions = [
|
||||
{ label: 'Never', value: 'never' },
|
||||
{ label: 'Daily', value: 'daily' },
|
||||
@@ -41,6 +44,7 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
];
|
||||
|
||||
const handleManageSync = (doc: Doc, sync_frequency: string) => {
|
||||
setLoading(true);
|
||||
userService
|
||||
.manageSync({ source_id: doc.id, sync_frequency })
|
||||
.then(() => {
|
||||
@@ -49,81 +53,91 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
.then((data) => {
|
||||
dispatch(setSourceDocs(data));
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col relative">
|
||||
<div className="z-10 w-full overflow-x-auto">
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.documents.name')}</th>
|
||||
<th>{t('settings.documents.date')}</th>
|
||||
<th>{t('settings.documents.tokenUsage')}</th>
|
||||
<th>{t('settings.documents.type')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!documents?.length && (
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={5} className="!p-4">
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
<th>{t('settings.documents.name')}</th>
|
||||
<th>{t('settings.documents.date')}</th>
|
||||
<th>{t('settings.documents.tokenUsage')}</th>
|
||||
<th>{t('settings.documents.type')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
{documents &&
|
||||
documents.map((document, index) => (
|
||||
<tr key={index}>
|
||||
<td>{document.name}</td>
|
||||
<td>{document.date}</td>
|
||||
<td>
|
||||
{document.tokens ? formatTokens(+document.tokens) : ''}
|
||||
</td>
|
||||
<td>
|
||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex flex-row items-center">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!documents?.length && (
|
||||
<tr>
|
||||
<td colSpan={5} className="!p-4">
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{documents &&
|
||||
documents.map((document, index) => (
|
||||
<tr key={index}>
|
||||
<td>{document.name}</td>
|
||||
<td>{document.date}</td>
|
||||
<td>
|
||||
{document.tokens ? formatTokens(+document.tokens) : ''}
|
||||
</td>
|
||||
<td>
|
||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex flex-row items-center">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Documents.propTypes = {
|
||||
documents: PropTypes.array.isRequired,
|
||||
handleDeleteDocument: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Documents;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import ChevronRight from '../assets/chevron-right.svg';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import { APIKeyData, LogData } from './types';
|
||||
import CoppyButton from '../components/CopyButton';
|
||||
|
||||
export default function Logs() {
|
||||
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [logs, setLogs] = React.useState<LogData[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
||||
const [logs, setLogs] = useState<LogData[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingChatbots, setLoadingChatbots] = useState(true);
|
||||
const [loadingLogs, setLoadingLogs] = useState(true);
|
||||
|
||||
const fetchChatbots = async () => {
|
||||
setLoadingChatbots(true);
|
||||
try {
|
||||
const response = await userService.getAPIKeys();
|
||||
if (!response.ok) {
|
||||
@@ -24,10 +27,13 @@ export default function Logs() {
|
||||
setChatbots(chatbots);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingChatbots(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoadingLogs(true);
|
||||
try {
|
||||
const response = await userService.getLogs({
|
||||
page: page,
|
||||
@@ -38,20 +44,23 @@ export default function Logs() {
|
||||
throw new Error('Failed to fetch logs');
|
||||
}
|
||||
const olderLogs = await response.json();
|
||||
setLogs([...logs, ...olderLogs.logs]);
|
||||
setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]);
|
||||
setHasMore(olderLogs.has_more);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
fetchChatbots();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (hasMore) fetchLogs();
|
||||
}, [page, selectedChatbot]);
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -59,38 +68,47 @@ export default function Logs() {
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Filter by chatbot
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((chatbot) => ({
|
||||
label: chatbot.name,
|
||||
value: chatbot.id,
|
||||
})),
|
||||
{ label: 'None', value: '' },
|
||||
]}
|
||||
placeholder="Select chatbot"
|
||||
onSelect={(chatbot: { label: string; value: string }) => {
|
||||
setSelectedChatbot(
|
||||
chatbots.find((item) => item.id === chatbot.value),
|
||||
);
|
||||
setLogs([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
}}
|
||||
selectedValue={
|
||||
(selectedChatbot && {
|
||||
label: selectedChatbot.name,
|
||||
value: selectedChatbot.id,
|
||||
}) ||
|
||||
null
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
{loadingChatbots ? (
|
||||
<SkeletonLoader />
|
||||
) : (
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((chatbot) => ({
|
||||
label: chatbot.name,
|
||||
value: chatbot.id,
|
||||
})),
|
||||
{ label: 'None', value: '' },
|
||||
]}
|
||||
placeholder="Select chatbot"
|
||||
onSelect={(chatbot: { label: string; value: string }) => {
|
||||
setSelectedChatbot(
|
||||
chatbots.find((item) => item.id === chatbot.value),
|
||||
);
|
||||
setLogs([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
}}
|
||||
selectedValue={
|
||||
(selectedChatbot && {
|
||||
label: selectedChatbot.name,
|
||||
value: selectedChatbot.id,
|
||||
}) ||
|
||||
null
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<LogsTable logs={logs} setPage={setPage} />
|
||||
{loadingLogs ? (
|
||||
<SkeletonLoader component={'logs'} />
|
||||
) : (
|
||||
<LogsTable logs={logs} setPage={setPage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -102,15 +120,16 @@ type LogsTableProps = {
|
||||
};
|
||||
|
||||
function LogsTable({ logs, setPage }: LogsTableProps) {
|
||||
const observerRef = React.useRef<any>();
|
||||
const firstObserver = React.useCallback((node: HTMLDivElement) => {
|
||||
const observerRef = useRef<any>();
|
||||
const firstObserver = useCallback((node: HTMLDivElement) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current = new IntersectionObserver((enteries) => {
|
||||
if (enteries[0].isIntersecting) setPage((prev) => prev + 1);
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) setPage((prev) => prev + 1);
|
||||
});
|
||||
}
|
||||
if (node && observerRef.current) observerRef.current.observe(node);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
|
||||
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
|
||||
|
||||
@@ -22,17 +22,23 @@ class TestAnthropicLLM(unittest.TestCase):
|
||||
mock_response = Mock()
|
||||
mock_response.completion = "test completion"
|
||||
|
||||
with patch.object(self.llm.anthropic.completions, "create", return_value=mock_response) as mock_create:
|
||||
response = self.llm.gen("test_model", messages)
|
||||
self.assertEqual(response, "test completion")
|
||||
with patch("application.cache.get_redis_instance") as mock_make_redis:
|
||||
mock_redis_instance = mock_make_redis.return_value
|
||||
mock_redis_instance.get.return_value = None
|
||||
mock_redis_instance.set = Mock()
|
||||
|
||||
prompt_expected = "### Context \n context \n ### Question \n question"
|
||||
mock_create.assert_called_with(
|
||||
model="test_model",
|
||||
max_tokens_to_sample=300,
|
||||
stream=False,
|
||||
prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}"
|
||||
)
|
||||
with patch.object(self.llm.anthropic.completions, "create", return_value=mock_response) as mock_create:
|
||||
response = self.llm.gen("test_model", messages)
|
||||
self.assertEqual(response, "test completion")
|
||||
|
||||
prompt_expected = "### Context \n context \n ### Question \n question"
|
||||
mock_create.assert_called_with(
|
||||
model="test_model",
|
||||
max_tokens_to_sample=300,
|
||||
stream=False,
|
||||
prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}"
|
||||
)
|
||||
mock_redis_instance.set.assert_called_once()
|
||||
|
||||
def test_gen_stream(self):
|
||||
messages = [
|
||||
@@ -41,17 +47,23 @@ class TestAnthropicLLM(unittest.TestCase):
|
||||
]
|
||||
mock_responses = [Mock(completion="response_1"), Mock(completion="response_2")]
|
||||
|
||||
with patch.object(self.llm.anthropic.completions, "create", return_value=iter(mock_responses)) as mock_create:
|
||||
responses = list(self.llm.gen_stream("test_model", messages))
|
||||
self.assertListEqual(responses, ["response_1", "response_2"])
|
||||
with patch("application.cache.get_redis_instance") as mock_make_redis:
|
||||
mock_redis_instance = mock_make_redis.return_value
|
||||
mock_redis_instance.get.return_value = None
|
||||
mock_redis_instance.set = Mock()
|
||||
|
||||
prompt_expected = "### Context \n context \n ### Question \n question"
|
||||
mock_create.assert_called_with(
|
||||
model="test_model",
|
||||
prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}",
|
||||
max_tokens_to_sample=300,
|
||||
stream=True
|
||||
)
|
||||
with patch.object(self.llm.anthropic.completions, "create", return_value=iter(mock_responses)) as mock_create:
|
||||
responses = list(self.llm.gen_stream("test_model", messages))
|
||||
self.assertListEqual(responses, ["response_1", "response_2"])
|
||||
|
||||
prompt_expected = "### Context \n context \n ### Question \n question"
|
||||
mock_create.assert_called_with(
|
||||
model="test_model",
|
||||
prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}",
|
||||
max_tokens_to_sample=300,
|
||||
stream=True
|
||||
)
|
||||
mock_redis_instance.set.assert_called_once()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -52,28 +52,38 @@ class TestSagemakerAPILLM(unittest.TestCase):
|
||||
self.response['Body'].read.return_value.decode.return_value = json.dumps(self.result)
|
||||
|
||||
def test_gen(self):
|
||||
with patch.object(self.sagemaker.runtime, 'invoke_endpoint',
|
||||
return_value=self.response) as mock_invoke_endpoint:
|
||||
output = self.sagemaker.gen(None, self.messages)
|
||||
mock_invoke_endpoint.assert_called_once_with(
|
||||
EndpointName=self.sagemaker.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=self.body_bytes
|
||||
)
|
||||
self.assertEqual(output,
|
||||
self.result[0]['generated_text'][len(self.prompt):])
|
||||
with patch('application.cache.get_redis_instance') as mock_make_redis:
|
||||
mock_redis_instance = mock_make_redis.return_value
|
||||
mock_redis_instance.get.return_value = None
|
||||
|
||||
with patch.object(self.sagemaker.runtime, 'invoke_endpoint',
|
||||
return_value=self.response) as mock_invoke_endpoint:
|
||||
output = self.sagemaker.gen(None, self.messages)
|
||||
mock_invoke_endpoint.assert_called_once_with(
|
||||
EndpointName=self.sagemaker.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=self.body_bytes
|
||||
)
|
||||
self.assertEqual(output,
|
||||
self.result[0]['generated_text'][len(self.prompt):])
|
||||
mock_make_redis.assert_called_once()
|
||||
mock_redis_instance.set.assert_called_once()
|
||||
|
||||
def test_gen_stream(self):
|
||||
with patch.object(self.sagemaker.runtime, 'invoke_endpoint_with_response_stream',
|
||||
return_value=self.response) as mock_invoke_endpoint:
|
||||
output = list(self.sagemaker.gen_stream(None, self.messages))
|
||||
mock_invoke_endpoint.assert_called_once_with(
|
||||
EndpointName=self.sagemaker.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=self.body_bytes_stream
|
||||
)
|
||||
self.assertEqual(output, [])
|
||||
|
||||
with patch('application.cache.get_redis_instance') as mock_make_redis:
|
||||
mock_redis_instance = mock_make_redis.return_value
|
||||
mock_redis_instance.get.return_value = None
|
||||
|
||||
with patch.object(self.sagemaker.runtime, 'invoke_endpoint_with_response_stream',
|
||||
return_value=self.response) as mock_invoke_endpoint:
|
||||
output = list(self.sagemaker.gen_stream(None, self.messages))
|
||||
mock_invoke_endpoint.assert_called_once_with(
|
||||
EndpointName=self.sagemaker.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=self.body_bytes_stream
|
||||
)
|
||||
self.assertEqual(output, [])
|
||||
mock_redis_instance.set.assert_called_once()
|
||||
class TestLineIterator(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
131
tests/test_cache.py
Normal file
131
tests/test_cache.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import unittest
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from application.cache import gen_cache_key, stream_cache, gen_cache
|
||||
from application.utils import get_hash
|
||||
|
||||
|
||||
# Test for gen_cache_key function
|
||||
def test_make_gen_cache_key():
|
||||
messages = [
|
||||
{'role': 'user', 'content': 'test_user_message'},
|
||||
{'role': 'system', 'content': 'test_system_message'},
|
||||
]
|
||||
model = "test_docgpt"
|
||||
|
||||
# Manually calculate the expected hash
|
||||
expected_combined = f"{model}_{json.dumps(messages, sort_keys=True)}"
|
||||
expected_hash = get_hash(expected_combined)
|
||||
cache_key = gen_cache_key(*messages, model=model)
|
||||
|
||||
assert cache_key == expected_hash
|
||||
|
||||
def test_gen_cache_key_invalid_message_format():
|
||||
# Test when messages is not a list
|
||||
with unittest.TestCase.assertRaises(unittest.TestCase, ValueError) as context:
|
||||
gen_cache_key("This is not a list", model="docgpt")
|
||||
assert str(context.exception) == "All messages must be dictionaries."
|
||||
|
||||
# Test for gen_cache decorator
|
||||
@patch('application.cache.get_redis_instance') # Mock the Redis client
|
||||
def test_gen_cache_hit(mock_make_redis):
|
||||
# Arrange
|
||||
mock_redis_instance = MagicMock()
|
||||
mock_make_redis.return_value = mock_redis_instance
|
||||
mock_redis_instance.get.return_value = b"cached_result" # Simulate a cache hit
|
||||
|
||||
@gen_cache
|
||||
def mock_function(self, model, messages):
|
||||
return "new_result"
|
||||
|
||||
messages = [{'role': 'user', 'content': 'test_user_message'}]
|
||||
model = "test_docgpt"
|
||||
|
||||
# Act
|
||||
result = mock_function(None, model, messages)
|
||||
|
||||
# Assert
|
||||
assert result == "cached_result" # Should return cached result
|
||||
mock_redis_instance.get.assert_called_once() # Ensure Redis get was called
|
||||
mock_redis_instance.set.assert_not_called() # Ensure the function result is not cached again
|
||||
|
||||
|
||||
@patch('application.cache.get_redis_instance') # Mock the Redis client
|
||||
def test_gen_cache_miss(mock_make_redis):
|
||||
# Arrange
|
||||
mock_redis_instance = MagicMock()
|
||||
mock_make_redis.return_value = mock_redis_instance
|
||||
mock_redis_instance.get.return_value = None # Simulate a cache miss
|
||||
|
||||
@gen_cache
|
||||
def mock_function(self, model, messages):
|
||||
return "new_result"
|
||||
|
||||
messages = [
|
||||
{'role': 'user', 'content': 'test_user_message'},
|
||||
{'role': 'system', 'content': 'test_system_message'},
|
||||
]
|
||||
model = "test_docgpt"
|
||||
# Act
|
||||
result = mock_function(None, model, messages)
|
||||
|
||||
# Assert
|
||||
assert result == "new_result"
|
||||
mock_redis_instance.get.assert_called_once()
|
||||
|
||||
@patch('application.cache.get_redis_instance')
|
||||
def test_stream_cache_hit(mock_make_redis):
|
||||
# Arrange
|
||||
mock_redis_instance = MagicMock()
|
||||
mock_make_redis.return_value = mock_redis_instance
|
||||
|
||||
cached_chunk = json.dumps(["chunk1", "chunk2"]).encode('utf-8')
|
||||
mock_redis_instance.get.return_value = cached_chunk
|
||||
|
||||
@stream_cache
|
||||
def mock_function(self, model, messages, stream):
|
||||
yield "new_chunk"
|
||||
|
||||
messages = [{'role': 'user', 'content': 'test_user_message'}]
|
||||
model = "test_docgpt"
|
||||
|
||||
# Act
|
||||
result = list(mock_function(None, model, messages, stream=True))
|
||||
|
||||
# Assert
|
||||
assert result == ["chunk1", "chunk2"] # Should return cached chunks
|
||||
mock_redis_instance.get.assert_called_once()
|
||||
mock_redis_instance.set.assert_not_called()
|
||||
|
||||
|
||||
@patch('application.cache.get_redis_instance')
|
||||
def test_stream_cache_miss(mock_make_redis):
|
||||
# Arrange
|
||||
mock_redis_instance = MagicMock()
|
||||
mock_make_redis.return_value = mock_redis_instance
|
||||
mock_redis_instance.get.return_value = None # Simulate a cache miss
|
||||
|
||||
@stream_cache
|
||||
def mock_function(self, model, messages, stream):
|
||||
yield "new_chunk"
|
||||
|
||||
messages = [
|
||||
{'role': 'user', 'content': 'This is the context'},
|
||||
{'role': 'system', 'content': 'Some other message'},
|
||||
{'role': 'user', 'content': 'What is the answer?'}
|
||||
]
|
||||
model = "test_docgpt"
|
||||
|
||||
# Act
|
||||
result = list(mock_function(None, model, messages, stream=True))
|
||||
|
||||
# Assert
|
||||
assert result == ["new_chunk"]
|
||||
mock_redis_instance.get.assert_called_once()
|
||||
mock_redis_instance.set.assert_called_once()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user