Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into feat/sources-in-react-widget

This commit is contained in:
utin-francis-peter
2024-11-04 16:36:13 +01:00
43 changed files with 536 additions and 96 deletions

24
.github/labeler.yml vendored
View File

@@ -1,23 +1,31 @@
repo:
- '*'
- changed-files:
- any-glob-to-any-file: '*'
github:
- .github/**/*
- changed-files:
- any-glob-to-any-file: '.github/**/*'
application:
- application/**/*
- changed-files:
- any-glob-to-any-file: 'application/**/*'
docs:
- docs/**/*
- changed-files:
- any-glob-to-any-file: 'docs/**/*'
extensions:
- extensions/**/*
- changed-files:
- any-glob-to-any-file: 'extensions/**/*'
frontend:
- frontend/**/*
- changed-files:
- any-glob-to-any-file: 'frontend/**/*'
scripts:
- scripts/**/*
- changed-files:
- any-glob-to-any-file: 'scripts/**/*'
tests:
- tests/**/*
- changed-files:
- any-glob-to-any-file: 'tests/**/*'

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -34,7 +34,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
file: './application/Dockerfile'
platforms: linux/amd64

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -35,7 +35,7 @@ jobs:
# Runs a single command using the runners shell
- name: Build and push Docker images to docker.io and ghcr.io
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
file: './frontend/Dockerfile'
platforms: linux/amd64, linux/arm64

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -36,7 +36,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
file: './application/Dockerfile'
platforms: linux/amd64

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -36,7 +36,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
file: './frontend/Dockerfile'
platforms: linux/amd64

View File

@@ -10,7 +10,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@@ -23,8 +23,6 @@ Say goodbye to time-consuming manual searches, and let <strong><a href="https://
</div>
### 🎃 [Hacktoberfest Prizes, Rules & Q&A](https://github.com/arc53/DocsGPT/blob/main/HACKTOBERFEST.md) 🎃
### Production Support / Help for Companies:
We're eager to provide personalized assistance when deploying your DocsGPT to a live environment.

View File

@@ -11,8 +11,8 @@ from bson.objectid import ObjectId
from flask import Blueprint, current_app, make_response, request, Response
from flask_restx import fields, Namespace, Resource
from pymongo import MongoClient
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.error import bad_request
from application.extensions import api
@@ -22,7 +22,7 @@ from application.utils import check_required_fields
logger = logging.getLogger(__name__)
mongo = MongoClient(settings.MONGO_URI)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
@@ -269,9 +269,6 @@ class Stream(Resource):
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"selectedDocs": fields.String(
required=False, description="Selected documents"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
@@ -303,10 +300,9 @@ class Stream(Resource):
history = json.loads(history)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
if "selectedDocs" in data and data["selectedDocs"] is None:
chunks = 0
else:
chunks = int(data.get("chunks", 2))
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
@@ -333,7 +329,8 @@ class Stream(Resource):
)
prompt = get_prompt(prompt_id)
if "isNoneDoc" in data and data["isNoneDoc"] is True:
chunks = 0
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,

View File

@@ -1,13 +1,13 @@
import os
import datetime
from flask import Blueprint, request, send_from_directory
from pymongo import MongoClient
from werkzeug.utils import secure_filename
from bson.objectid import ObjectId
from application.core.mongo_db import MongoDB
from application.core.settings import settings
mongo = MongoClient(settings.MONGO_URI)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
conversations_collection = db["conversations"]
sources_collection = db["sources"]

View File

@@ -8,17 +8,18 @@ from bson.dbref import DBRef
from bson.objectid import ObjectId
from flask import Blueprint, jsonify, make_response, request
from flask_restx import inputs, fields, Namespace, Resource
from pymongo import MongoClient
from werkzeug.utils import secure_filename
from application.api.user.tasks import ingest, ingest_remote
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.extensions import api
from application.utils import check_required_fields
from application.vectorstore.vector_creator import VectorCreator
from application.tts.google_tts import GoogleTTS
mongo = MongoClient(settings.MONGO_URI)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
@@ -342,6 +343,7 @@ class UploadFile(Resource):
".mdx",
".json",
".xlsx",
".pptx",
],
job_name,
final_filename,
@@ -1663,3 +1665,27 @@ class ManageSync(Resource):
return make_response(jsonify({"success": False, "error": str(err)}), 400)
return make_response(jsonify({"success": True}), 200)
@user_ns.route("/api/tts")
class TextToSpeech(Resource):
tts_model = api.model(
"TextToSpeechModel",
{
"text": fields.String(required=True, description="Text to be synthesized as audio"),
},
)
@api.expect(tts_model)
@api.doc(description="Synthesize audio speech from text")
def post(self):
data = request.get_json()
text = data["text"]
try:
tts_instance = GoogleTTS()
audio_base64, detected_language = tts_instance.text_to_speech(text)
return make_response(jsonify({"success": True,'audio_base64': audio_base64,'lang':detected_language}), 200)
except Exception as err:
return make_response(jsonify({"success": False, "error": str(err)}), 400)

View File

@@ -0,0 +1,24 @@
from application.core.settings import settings
from pymongo import MongoClient
class MongoDB:
_client = None
@classmethod
def get_client(cls):
"""
Get the MongoDB client instance, creating it if necessary.
"""
if cls._client is None:
cls._client = MongoClient(settings.MONGO_URI)
return cls._client
@classmethod
def close_client(cls):
"""
Close the MongoDB client connection.
"""
if cls._client is not None:
cls._client.close()
cls._client = None

View File

@@ -12,6 +12,7 @@ from application.parser.file.markdown_parser import MarkdownParser
from application.parser.file.rst_parser import RstParser
from application.parser.file.tabular_parser import PandasCSVParser,ExcelParser
from application.parser.file.json_parser import JSONParser
from application.parser.file.pptx_parser import PPTXParser
from application.parser.schema.base import Document
DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = {
@@ -25,6 +26,7 @@ DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = {
".html": HTMLParser(),
".mdx": MarkdownParser(),
".json":JSONParser(),
".pptx":PPTXParser(),
}

View File

@@ -0,0 +1,75 @@
"""PPT parser.
Contains parsers for presentation (.pptx) files to extract slide text.
"""
from pathlib import Path
from typing import Any, Dict, List, Union
from application.parser.file.base_parser import BaseParser
class PPTXParser(BaseParser):
r"""PPTX (.pptx) parser for extracting text from PowerPoint slides.
Args:
concat_slides (bool): Specifies whether to concatenate all slide text into one document.
- If True, slide texts will be joined together as a single string.
- If False, each slide's text will be stored as a separate entry in a list.
Set to True by default.
slide_separator (str): Separator used to join slides' text content.
Only used when `concat_slides=True`. Default is "\n".
Refer to https://python-pptx.readthedocs.io/en/latest/ for more information.
"""
def __init__(
self,
*args: Any,
concat_slides: bool = True,
slide_separator: str = "\n",
**kwargs: Any
) -> None:
"""Init params."""
super().__init__(*args, **kwargs)
self._concat_slides = concat_slides
self._slide_separator = slide_separator
def _init_parser(self) -> Dict:
"""Init parser."""
return {}
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
r"""
Parse a .pptx file and extract text from each slide.
Args:
file (Path): Path to the .pptx file.
errors (str): Error handling policy ('ignore' by default).
Returns:
Union[str, List[str]]: Concatenated text if concat_slides is True,
otherwise a list of slide texts.
"""
try:
from pptx import Presentation
except ImportError:
raise ImportError("pptx module is required to read .PPTX files.")
try:
presentation = Presentation(file)
slide_texts=[]
# Iterate over each slide in the presentation
for slide in presentation.slides:
slide_text=""
# Iterate over each shape in the slide
for shape in slide.shapes:
# Check if the shape has a 'text' attribute and append that to the slide_text
if hasattr(shape,"text"):
slide_text+=shape.text
slide_texts.append(slide_text.strip())
if self._concat_slides:
return self._slide_separator.join(slide_texts)
else:
return slide_texts
except Exception as e:
raise e

View File

@@ -14,6 +14,7 @@ esutils==1.0.1
Flask==3.0.3
faiss-cpu==1.8.0.post1
flask-restx==1.3.0
gTTS==2.3.2
gunicorn==23.0.0
html2text==2024.2.26
javalang==0.13.0
@@ -65,6 +66,7 @@ pymongo==4.8.0
pypdf2==3.0.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-pptx==1.0.2
qdrant-client==1.11.0
redis==5.0.1
referencing==0.30.2
@@ -84,4 +86,4 @@ urllib3==2.2.3
vine==5.1.0
wcwidth==0.2.13
werkzeug==3.0.4
yarl==1.11.1
yarl==1.11.1

View File

@@ -75,7 +75,6 @@ class BraveRetSearch(BaseRetriever):
if len(self.chat_history) > 1:
tokens_current_history = 0
# count tokens in history
self.chat_history.reverse()
for i in self.chat_history:
if "prompt" in i and "response" in i:
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(

View File

@@ -78,7 +78,6 @@ class ClassicRAG(BaseRetriever):
if len(self.chat_history) > 1:
tokens_current_history = 0
# count tokens in history
self.chat_history.reverse()
for i in self.chat_history:
if "prompt" in i and "response" in i:
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(
@@ -97,7 +96,6 @@ class ClassicRAG(BaseRetriever):
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
)
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
for line in completion:
yield {"answer": str(line)}

View File

@@ -92,7 +92,6 @@ class DuckDuckSearch(BaseRetriever):
if len(self.chat_history) > 1:
tokens_current_history = 0
# count tokens in history
self.chat_history.reverse()
for i in self.chat_history:
if "prompt" in i and "response" in i:
tokens_batch = num_tokens_from_string(i["prompt"]) + num_tokens_from_string(

10
application/tts/base.py Normal file
View File

@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod
class BaseTTS(ABC):
def __init__(self):
pass
@abstractmethod
def text_to_speech(self, *args, **kwargs):
pass

View File

@@ -0,0 +1,29 @@
from io import BytesIO
import base64
from application.tts.base import BaseTTS
class ElevenlabsTTS(BaseTTS):
def __init__(self):
from elevenlabs.client import ElevenLabs
self.client = ElevenLabs(
api_key="ELEVENLABS_API_KEY",
)
def text_to_speech(self, text):
lang = "en"
audio = self.client.generate(
text=text,
model="eleven_multilingual_v2",
voice="Brian",
)
audio_data = BytesIO()
for chunk in audio:
audio_data.write(chunk)
audio_bytes = audio_data.getvalue()
# Encode to base64
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
return audio_base64, lang

View File

@@ -0,0 +1,19 @@
import io
import base64
from gtts import gTTS
from application.tts.base import BaseTTS
class GoogleTTS(BaseTTS):
def __init__(self):
pass
def text_to_speech(self, text):
lang = "en"
audio_fp = io.BytesIO()
tts = gTTS(text=text, lang=lang, slow=False)
tts.write_to_fp(audio_fp)
audio_fp.seek(0)
audio_base64 = base64.b64encode(audio_fp.read()).decode("utf-8")
return audio_base64, lang

View File

@@ -1,10 +1,9 @@
import sys
from pymongo import MongoClient
from datetime import datetime
from application.core.settings import settings
from application.core.mongo_db import MongoDB
from application.utils import num_tokens_from_string
mongo = MongoClient(settings.MONGO_URI)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
usage_collection = db["token_usage"]

View File

@@ -8,8 +8,8 @@ from urllib.parse import urljoin
import requests
from bson.objectid import ObjectId
from pymongo import MongoClient
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.parser.file.bulk import SimpleDirectoryReader
from application.parser.open_ai_func import call_openai_api
@@ -18,7 +18,7 @@ from application.parser.schema.base import Document
from application.parser.token_func import group_split
from application.utils import count_tokens_docs
mongo = MongoClient(settings.MONGO_URI)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
sources_collection = db["sources"]

View File

@@ -51,6 +51,9 @@ const config = {
footer: {
text: `MIT ${new Date().getFullYear()} © DocsGPT`,
},
editLink: {
content: 'Edit this page on GitHub',
},
logo() {
return (
<div className="flex items-center gap-2">

View File

@@ -1,25 +1,60 @@
import os
import re
import logging
import aiohttp
import discord
import requests
from discord.ext import commands
import dotenv
dotenv.load_dotenv()
# Replace 'YOUR_BOT_TOKEN' with your bot's token
# Enable logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Bot configuration
TOKEN = os.getenv("DISCORD_TOKEN")
PREFIX = '@DocsGPT'
BASE_API_URL = 'http://localhost:7091'
PREFIX = '!' # Command prefix
BASE_API_URL = os.getenv("API_BASE", "https://gptcloud.arc53.com")
API_URL = BASE_API_URL + "/api/answer"
API_KEY = os.getenv("API_KEY")
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix=PREFIX, intents=intents)
# Store conversation history per user
conversation_histories = {}
def chunk_string(text, max_length=2000):
"""Splits a string into chunks of a specified maximum length."""
# Create list to store the split strings
chunks = []
# Loop through the text, create substrings with max_length
while len(text) > max_length:
# Find last space within the limit
idx = text.rfind(' ', 0, max_length)
# Ensure we don't have an empty part
if idx == -1:
# If no spaces, just take chunk
chunks.append(text[:max_length])
text = text[max_length:]
else:
# Push whatever we've got up to the last space
chunks.append(text[:idx])
text = text[idx+1:]
# Catches the remaining part
chunks.append(text)
return chunks
def escape_markdown(text):
"""Escapes Discord markdown characters."""
escape_chars = r'\*_$$$$()~>#+-=|{}.!'
return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text)
def split_string(input_str):
"""Splits the input string to detect bot mentions."""
pattern = r'^<@!?{0}>\s*'.format(bot.user.id)
match = re.match(pattern, input_str)
if match:
@@ -27,42 +62,97 @@ def split_string(input_str):
return str(bot.user.id), content
return None, input_str
@bot.event
async def on_ready():
print(f'{bot.user.name} has connected to Discord!')
async def fetch_answer(question):
data = {
'sender': 'discord',
'question': question,
'history': ''
async def generate_answer(question, messages, conversation_id):
"""Generates an answer using the external API."""
payload = {
"question": question,
"api_key": API_KEY,
"history": messages,
"conversation_id": conversation_id
}
headers = {"Content-Type": "application/json",
"Accept": "application/json"}
response = requests.post(BASE_API_URL + '/api/answer', json=data, headers=headers)
if response.status_code == 200:
return response.json()['answer']
return 'Sorry, I could not fetch the answer.'
headers = {
"Content-Type": "application/json; charset=utf-8"
}
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(API_URL, json=payload, headers=headers) as resp:
if resp.status == 200:
data = await resp.json()
conversation_id = data.get("conversation_id")
answer = data.get("answer", "Sorry, I couldn't find an answer.")
return {"answer": answer, "conversation_id": conversation_id}
else:
return {"answer": "Sorry, I couldn't find an answer.", "conversation_id": None}
@bot.command(name="start")
async def start(ctx):
"""Handles the /start command."""
await ctx.send(f"Hi {ctx.author.mention}! How can I assist you today?")
@bot.command(name="custom_help")
async def custom_help_command(ctx):
"""Handles the /custom_help command."""
help_text = (
"Here are the available commands:\n"
"`!start` - Begin a new conversation with the bot\n"
"`!help` - Display this help message\n\n"
"You can also mention me or send a direct message to ask a question!"
)
await ctx.send(help_text)
@bot.event
async def on_message(message):
if message.author == bot.user:
return
content = message.content.strip()
prefix, content = split_string(content)
if prefix is None:
return
part_prefix = str(bot.user.id)
if part_prefix == prefix:
answer = await fetch_answer(content)
await message.channel.send(answer)
# Process commands first
await bot.process_commands(message)
# Check if the message is in a DM channel
if isinstance(message.channel, discord.DMChannel):
content = message.content.strip()
else:
# In guild channels, check if the message mentions the bot at the start
content = message.content.strip()
prefix, content = split_string(content)
if prefix is None:
return
part_prefix = str(bot.user.id)
if part_prefix != prefix:
return # Bot not mentioned at the start, so do not process
bot.run(TOKEN)
# Now process the message
user_id = message.author.id
if user_id not in conversation_histories:
conversation_histories[user_id] = {
"history": [],
"conversation_id": None
}
conversation = conversation_histories[user_id]
conversation["history"].append({"prompt": content})
# Generate the answer
response_doc = await generate_answer(
content,
conversation["history"],
conversation["conversation_id"]
)
answer = response_doc["answer"]
conversation_id = response_doc["conversation_id"]
answer_chunks = chunk_string(answer)
for chunk in answer_chunks:
await message.channel.send(chunk)
conversation["history"][-1]["response"] = answer
conversation["conversation_id"] = conversation_id
# Keep conversation history to last 10 exchanges
conversation["history"] = conversation["history"][-10:]
bot.run(TOKEN)

View File

@@ -3075,6 +3075,24 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/easy-speech": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/easy-speech/-/easy-speech-2.4.0.tgz",
"integrity": "sha512-wpMv29DEoeP/eyXr4aXpDqd9DvlXl7aQs7BgfKbjGVxqkmQPgNmpbF5YULaTH5bc/5qrteg5MDfCD2Zd0qr4rQ==",
"funding": [
{
"type": "GitHub",
"url": "https://github.com/sponsors/jankapunkt"
},
{
"type": "PayPal",
"url": "https://paypal.me/kuesterjan"
}
],
"engines": {
"node": ">= 14.x"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.11",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz",

View File

@@ -39,6 +39,8 @@ import {
setSelectedDocs,
setSourceDocs,
} from './preferences/preferenceSlice';
import Spinner from './assets/spinner.svg';
import SpinnerDark from './assets/spinner-dark.svg';
import { selectQueries } from './conversation/conversationSlice';
import Upload from './upload/Upload';
import Help from './components/Help';
@@ -70,6 +72,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const conversations = useSelector(selectConversations);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const conversationId = useSelector(selectConversationId);
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
@@ -91,25 +94,28 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const navigate = useNavigate();
useEffect(() => {
if (!conversations) {
if (!conversations?.data) {
fetchConversations();
}
if (queries.length === 0) {
resetConversation();
}
}, [conversations, dispatch]);
}, [conversations?.data, dispatch]);
async function fetchConversations() {
dispatch(setConversations({ ...conversations, loading: true }));
return await getConversations()
.then((fetchedConversations) => {
dispatch(setConversations(fetchedConversations));
})
.catch((error) => {
console.error('Failed to fetch conversations: ', error);
dispatch(setConversations({ data: null, loading: false }));
});
}
const handleDeleteAllConversations = () => {
setIsDeletingConversation(true);
conversationService
.deleteAll()
.then(() => {
@@ -119,6 +125,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
};
const handleDeleteConversation = (id: string) => {
setIsDeletingConversation(true);
conversationService
.delete(id, {})
.then(() => {
@@ -205,6 +212,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
setNavOpen(!isMobile);
}, [isMobile]);
useDefaultDocument();
return (
<>
{!navOpen && (
@@ -306,13 +314,22 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
id="conversationsMainDiv"
className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white"
>
{conversations && conversations.length > 0 ? (
{conversations?.loading && !isDeletingConversation && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="animate-spin cursor-pointer bg-transparent"
alt="Loading..."
/>
</div>
)}
{conversations?.data && conversations.data.length > 0 ? (
<div>
<div className=" my-auto mx-4 mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
<p className="mt-1 ml-4 text-sm font-semibold">{t('chats')}</p>
</div>
<div className="conversations-container">
{conversations?.map((conversation) => (
{conversations.data?.map((conversation) => (
<ConversationTile
key={conversation.id}
conversation={conversation}

View 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="M9 3.66667V1M12.7778 5.22222L14.6889 3.31111M14.3333 9H17M12.7778 12.7778L14.6889 14.6889M9 14.3333V17M5.22222 12.7778L3.31111 14.6889M3.66667 9H1M5.22222 5.22222L3.31111 3.31111" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 21 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 9.59652V6.40372C1 5.94773 1.18114 5.51041 1.50358 5.18797C1.82602 4.86553 2.26334 4.68439 2.71933 4.68439H5.21237C5.38045 4.68435 5.54483 4.63503 5.68518 4.54254L10.8432 1.1417C10.9728 1.05636 11.1231 1.00768 11.2781 1.00084C11.4331 0.993993 11.5871 1.02924 11.7237 1.10283C11.8603 1.17643 11.9745 1.28563 12.054 1.41885C12.1336 1.55207 12.1756 1.70435 12.1757 1.85952V14.1407C12.1756 14.2959 12.1336 14.4482 12.054 14.5814C11.9745 14.7146 11.8603 14.8238 11.7237 14.8974C11.5871 14.971 11.4331 15.0063 11.2781 14.9994C11.1231 14.9926 10.9728 14.9439 10.8432 14.8585L5.68518 11.4577C5.54483 11.3652 5.38045 11.3159 5.21237 11.3159H2.71933C2.26334 11.3159 1.82602 11.1347 1.50358 10.8123C1.18114 10.4898 1 10.0525 1 9.59652Z" stroke="#949494" stroke-width="1.4"/>
<path d="M15.1846 4.13149C15.1846 4.13149 16.4741 5.42099 16.4741 7.57016C16.4741 9.71932 15.1846 11.0088 15.1846 11.0088M17.7636 1.55249C17.7636 1.55249 19.9127 3.70166 19.9127 7.57016C19.9127 11.4387 17.7636 13.5878 17.7636 13.5878" stroke="#949494" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.14286C9.35622 1.14286 10.682 1.54502 11.8096 2.29849C12.9373 3.05197 13.8162 4.1229 14.3352 5.37588C14.8542 6.62886 14.99 8.0076 14.7254 9.33776C14.4608 10.6679 13.8077 11.8897 12.8487 12.8487C11.8897 13.8077 10.6679 14.4608 9.33776 14.7254C8.00761 14.99 6.62887 14.8542 5.37589 14.3352C4.12291 13.8162 3.05197 12.9373 2.2985 11.8096C1.54502 10.682 1.14286 9.35621 1.14286 8C1.14286 6.18137 1.86531 4.43723 3.15127 3.15127C4.43723 1.8653 6.18137 1.14286 8 1.14286ZM8 0C6.41775 0 4.87103 0.469192 3.55544 1.34824C2.23985 2.22729 1.21447 3.47672 0.608967 4.93853C0.00346626 6.40034 -0.15496 8.00887 0.153721 9.56072C0.462403 11.1126 1.22433 12.538 2.34315 13.6568C3.46197 14.7757 4.88743 15.5376 6.43928 15.8463C7.99113 16.155 9.59966 15.9965 11.0615 15.391C12.5233 14.7855 13.7727 13.7601 14.6518 12.4446C15.5308 11.129 16 9.58225 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34314C12.1566 0.842854 10.1217 0 8 0Z" fill="#949494"/>
<path d="M10.2857 5.71439V10.2858H5.71427V5.71439H10.2857ZM10.2857 4.57153H5.71427C5.41116 4.57153 5.12047 4.69194 4.90615 4.90627C4.69182 5.1206 4.57141 5.41129 4.57141 5.71439V10.2858C4.57141 10.5889 4.69182 10.8796 4.90615 11.0939C5.12047 11.3083 5.41116 11.4287 5.71427 11.4287H10.2857C10.5888 11.4287 10.8795 11.3083 11.0938 11.0939C11.3081 10.8796 11.4286 10.5889 11.4286 10.2858V5.71439C11.4286 5.41129 11.3081 5.1206 11.0938 4.90627C10.8795 4.69194 10.5888 4.57153 10.2857 4.57153Z" fill="#949494"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,94 @@
import { useState, useRef } from 'react';
import Speaker from '../assets/speaker.svg?react';
import Stopspeech from '../assets/stopspeech.svg?react';
import LoadingIcon from '../assets/Loading.svg?react'; // Add a loading icon SVG here
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export default function SpeakButton({
text,
colorLight,
colorDark,
}: {
text: string;
colorLight?: string;
colorDark?: string;
}) {
const [isSpeaking, setIsSpeaking] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSpeakHovered, setIsSpeakHovered] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
const handleSpeakClick = async () => {
if (isSpeaking) {
// Stop audio if it's currently playing
audioRef.current?.pause();
audioRef.current = null;
setIsSpeaking(false);
return;
}
try {
// Set loading state and initiate TTS request
setIsLoading(true);
const response = await fetch(apiHost + '/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const data = await response.json();
if (data.success && data.audio_base64) {
// Create and play the audio
const audio = new Audio(`data:audio/mp3;base64,${data.audio_base64}`);
audioRef.current = audio;
audio.play().then(() => {
setIsSpeaking(true);
setIsLoading(false);
// Reset when audio ends
audio.onended = () => {
setIsSpeaking(false);
audioRef.current = null;
};
});
} else {
console.error('Failed to retrieve audio.');
setIsLoading(false);
}
} catch (error) {
console.error('Error fetching audio from TTS endpoint', error);
setIsLoading(false);
}
};
return (
<div
className={`flex items-center justify-center rounded-full p-2 ${
isSpeakHovered
? `bg-[#EEEEEE] dark:bg-purple-taupe`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
}`}
>
{isLoading ? (
<LoadingIcon className="animate-spin" />
) : isSpeaking ? (
<Stopspeech
className="cursor-pointer fill-none"
onClick={handleSpeakClick}
onMouseEnter={() => setIsSpeakHovered(true)}
onMouseLeave={() => setIsSpeakHovered(false)}
/>
) : (
<Speaker
className="cursor-pointer fill-none"
onClick={handleSpeakClick}
onMouseEnter={() => setIsSpeakHovered(true)}
onMouseLeave={() => setIsSpeakHovered(false)}
/>
)}
</div>
);
}

View File

@@ -7,7 +7,6 @@ import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
import Dislike from '../assets/dislike.svg?react';
import Document from '../assets/document.svg';
@@ -23,6 +22,7 @@ import {
} from '../preferences/preferenceSlice';
import classes from './ConversationBubble.module.css';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import SpeakButton from '../components/TextToSpeechButton';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
@@ -336,6 +336,14 @@ const ConversationBubble = forwardRef<
<CopyButton text={message} />
</div>
</div>
<div
className={`relative mr-5 block items-center justify-center lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : 'hidden'}`}
>
<div>
<SpeakButton text={message} /> {/* Add SpeakButton here */}
</div>
</div>
{type === 'ERROR' && (
<div className="relative mr-5 block items-center justify-center">
<div>{retryBtn}</div>

View File

@@ -4,6 +4,7 @@
:root {
--viewport-height: 100vh;
font-synthesis: none !important;
}
@supports (height: 100dvh) {

View File

@@ -12,7 +12,7 @@
"cancel": "Cancel",
"help": "Help",
"emailUs": "Email us",
"documentation": "documentation",
"documentation": "Documentation",
"demo": [
{
"header": "Learn about DocsGPT",
@@ -86,7 +86,7 @@
"start": "Start Chatting",
"name": "Name",
"choose": "Choose Files",
"info": "Please upload .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .zip limited to 25mb",
"info": "Please upload .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip limited to 25mb",
"uploadedFiles": "Uploaded Files",
"cancel": "Cancel",
"train": "Train",

View File

@@ -12,7 +12,7 @@
"cancel": "Cancelar",
"help": "Asistencia",
"emailUs": "Envíanos un correo",
"documentation": "documentación",
"documentation": "Documentación",
"demo": [
{
"header": "Aprende sobre DocsGPT",
@@ -86,7 +86,7 @@
"start": "Empezar a chatear",
"name": "Nombre",
"choose": "Seleccionar Archivos",
"info": "Por favor, suba archivos .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .zip limitados a 25 MB",
"info": "Por favor, suba archivos .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip limitados a 25 MB",
"uploadedFiles": "Archivos Subidos",
"cancel": "Cancelar",
"train": "Entrenar",

View File

@@ -86,7 +86,7 @@
"start": "チャットを開始する",
"name": "名前",
"choose": "ファイルを選択",
"info": ".pdf, .txt, .rst, .docx, .md, .json, .zipファイルを25MBまでアップロードしてください",
"info": ".pdf, .txt, .rst, .docx, .md, .json, .pptx, .zipファイルを25MBまでアップロードしてください",
"uploadedFiles": "アップロードされたファイル",
"cancel": "キャンセル",
"train": "トレーニング",

View File

@@ -80,7 +80,7 @@
"remote": "遠端",
"name": "名稱",
"choose": "選擇檔案",
"info": "請上傳 .pdf, .txt, .rst, .docx, .md, .json, .zip 檔案,大小限制為 25MB",
"info": "請上傳 .pdf, .txt, .rst, .docx, .md, .json, .pptx, .zip 檔案,大小限制為 25MB",
"uploadedFiles": "已上傳的檔案",
"cancel": "取消",
"train": "訓練",

View File

@@ -86,7 +86,7 @@
"start": "开始聊天",
"name": "名称",
"choose": "选择文件",
"info": "请上传 .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .zip 文件,限 25MB",
"info": "请上传 .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip 文件,限 25MB",
"uploadedFiles": "已上传文件",
"cancel": "取消",
"train": "训练",

View File

@@ -21,9 +21,10 @@ export async function getDocs(): Promise<Doc[] | null> {
}
}
export async function getConversations(): Promise<
{ name: string; id: string }[] | null
> {
export async function getConversations(): Promise<{
data: { name: string; id: string }[] | null;
loading: boolean;
}> {
try {
const response = await conversationService.getConversations();
const data = await response.json();
@@ -34,10 +35,10 @@ export async function getConversations(): Promise<
conversations.push(conversation as { name: string; id: string });
});
return conversations;
return { data: conversations, loading: false };
} catch (error) {
console.log(error);
return null;
return { data: null, loading: false };
}
}

View File

@@ -15,7 +15,10 @@ export interface Preference {
token_limit: number;
selectedDocs: Doc | null;
sourceDocs: Doc[] | null;
conversations: { name: string; id: string }[] | null;
conversations: {
data: { name: string; id: string }[] | null;
loading: boolean;
};
modalState: ActiveState;
}
@@ -34,7 +37,10 @@ const initialState: Preference = {
retriever: 'classic',
} as Doc,
sourceDocs: null,
conversations: null,
conversations: {
data: null,
loading: false,
},
modalState: 'INACTIVE',
};

View File

@@ -23,7 +23,10 @@ const preloadedState: { preference: Preference } = {
chunks: JSON.parse(chunks ?? '2').toString(),
token_limit: token_limit ? parseInt(token_limit) : 2000,
selectedDocs: doc !== null ? JSON.parse(doc) : null,
conversations: null,
conversations: {
data: null,
loading: false,
},
sourceDocs: [
{
name: 'default',

View File

@@ -321,6 +321,8 @@ function Upload({
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
'.xlsx',
],
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
['.pptx'],
},
});

View File

@@ -12,8 +12,8 @@ Welcome to the LLM Document Analysis by [LexEU](https://www.lexeu.ai/) competiti
### 📆 Timeline:
- **Competition Announcement:** 1st October
- **Deadline for Submissions:** 27th October
- **Results Announcement:** Early November/ Late October
- **Deadline for Submissions:** 8th November
- **Results Announcement:** Early November
## 📜 How to Participate:

View File

@@ -4,8 +4,8 @@ source .env
if [[ -n "$OPENAI_API_BASE" ]] && [[ -n "$OPENAI_API_VERSION" ]] && [[ -n "$AZURE_DEPLOYMENT_NAME" ]] && [[ -n "$AZURE_EMBEDDINGS_DEPLOYMENT_NAME" ]]; then
echo "Running Azure Configuration"
docker compose -f docker-compose-azure.yaml build && docker compose -f docker-compose-azure.yaml up
docker compose -f docker-compose-azure.yaml up --build
else
echo "Running Plain Configuration"
docker compose build && docker compose up
docker compose up --build
fi