Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
ec4b7da528 build(deps): bump the npm_and_yarn group across 2 directories with 5 updates
Bumps the npm_and_yarn group with 3 updates in the /docs directory: [cross-spawn](https://github.com/moxystudio/node-cross-spawn), [nextra](https://github.com/shuding/nextra) and [nextra-theme-docs](https://github.com/shuding/nextra).
Bumps the npm_and_yarn group with 1 update in the /extensions/react-widget directory: [dompurify](https://github.com/cure53/DOMPurify).


Updates `cross-spawn` from 7.0.3 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

Updates `nextra` from 2.13.2 to 4.2.17
- [Release notes](https://github.com/shuding/nextra/releases)
- [Commits](https://github.com/shuding/nextra/compare/nextra@2.13.2...nextra@4.2.17)

Updates `nextra-theme-docs` from 2.13.2 to 4.2.17
- [Release notes](https://github.com/shuding/nextra/releases)
- [Commits](https://github.com/shuding/nextra/compare/nextra-theme-docs@2.13.2...nextra-theme-docs@4.2.17)

Updates `micromatch` from 4.0.5 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

Updates `dompurify` from 3.2.4 to 3.2.6
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.2.4...3.2.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-version: 7.0.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nextra
  dependency-version: 4.2.17
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nextra-theme-docs
  dependency-version: 4.2.17
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: micromatch
  dependency-version: 4.0.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.2.6
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 00:16:46 +00:00
40 changed files with 3632 additions and 3598 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@@ -52,10 +52,8 @@
- [x] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
- [x] New input box in the conversation menu (April 2025)
- [x] Add triggerable actions / tools (webhook) (April 2025)
- [x] Agent optimisations (May 2025)
- [ ] Anthropic Tool compatibility (June 2025)
- [ ] MCP support (June 2025)
- [ ] Add OAuth 2.0 authentication for tools and sources (July 2025)
- [ ] Anthropic Tool compatibility (May 2025)
- [ ] Add OAuth 2.0 authentication for tools and sources
- [ ] Agent scheduling
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!

View File

@@ -37,18 +37,16 @@ def upload_index_files():
"""Upload two files(index.faiss, index.pkl) to the user's folder."""
if "user" not in request.form:
return {"status": "no user"}
user = request.form["user"]
user = secure_filename(request.form["user"])
if "name" not in request.form:
return {"status": "no name"}
job_name = request.form["name"]
tokens = request.form["tokens"]
retriever = request.form["retriever"]
id = request.form["id"]
type = request.form["type"]
job_name = secure_filename(request.form["name"])
tokens = secure_filename(request.form["tokens"])
retriever = secure_filename(request.form["retriever"])
id = secure_filename(request.form["id"])
type = secure_filename(request.form["type"])
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
sync_frequency = request.form["sync_frequency"] if "sync_frequency" in request.form else None
original_file_path = request.form.get("original_file_path")
sync_frequency = secure_filename(request.form["sync_frequency"]) if "sync_frequency" in request.form else None
storage = StorageCreator.get_storage()
index_base_path = f"indexes/{id}"
@@ -87,7 +85,6 @@ def upload_index_files():
"retriever": retriever,
"remote_data": remote_data,
"sync_frequency": sync_frequency,
"file_path": original_file_path,
}
},
)
@@ -105,7 +102,6 @@ def upload_index_files():
"retriever": retriever,
"remote_data": remote_data,
"sync_frequency": sync_frequency,
"file_path": original_file_path,
}
)
return {"status": "ok"}

View File

@@ -6,25 +6,16 @@ import secrets
import shutil
import uuid
from functools import wraps
from typing import Optional, Tuple
from bson.binary import Binary, UuidRepresentation
from bson.dbref import DBRef
from bson.objectid import ObjectId
from flask import (
Blueprint,
current_app,
jsonify,
make_response,
redirect,
request,
Response,
)
from flask import Blueprint, current_app, jsonify, make_response, redirect, request
from flask_restx import fields, inputs, Namespace, Resource
from pymongo import ReturnDocument
from werkzeug.utils import secure_filename
from application.agents.tools.tool_manager import ToolManager
from pymongo import ReturnDocument
from application.api.user.tasks import (
ingest,
@@ -37,12 +28,7 @@ from application.core.settings import settings
from application.extensions import api
from application.storage.storage_creator import StorageCreator
from application.tts.google_tts import GoogleTTS
from application.utils import (
check_required_fields,
generate_image_url,
safe_filename,
validate_function_name,
)
from application.utils import check_required_fields, validate_function_name
from application.vectorstore.vector_creator import VectorCreator
storage = StorageCreator.get_storage()
@@ -157,29 +143,6 @@ def get_vector_store(source_id):
return store
def handle_image_upload(
request, existing_url: str, user: str, storage, base_path: str = "attachments/"
) -> Tuple[str, Optional[Response]]:
image_url = existing_url
if "image" in request.files:
file = request.files["image"]
if file.filename != "":
filename = secure_filename(file.filename)
upload_path = f"{settings.UPLOAD_FOLDER.rstrip('/')}/{user}/{base_path.rstrip('/')}/{uuid.uuid4()}_{filename}"
try:
storage.save_file(file, upload_path)
image_url = upload_path
except Exception as e:
current_app.logger.error(f"Error uploading image: {e}")
return None, make_response(
jsonify({"success": False, "message": "Image upload failed"}),
400,
)
return image_url, None
@user_ns.route("/api/delete_conversation")
class DeleteConversation(Resource):
@api.doc(
@@ -290,7 +253,7 @@ class GetSingleConversation(Resource):
)
if not conversation:
return make_response(jsonify({"status": "not found"}), 404)
# Process queries to include attachment names
queries = conversation["queries"]
for query in queries:
@@ -302,18 +265,13 @@ class GetSingleConversation(Resource):
{"_id": ObjectId(attachment_id)}
)
if attachment:
attachment_details.append(
{
"id": str(attachment["_id"]),
"fileName": attachment.get(
"filename", "Unknown file"
),
}
)
attachment_details.append({
"id": str(attachment["_id"]),
"fileName": attachment.get("filename", "Unknown file")
})
except Exception as e:
current_app.logger.error(
f"Error retrieving attachment {attachment_id}: {e}",
exc_info=True,
f"Error retrieving attachment {attachment_id}: {e}", exc_info=True
)
query["attachments"] = attachment_details
except Exception as err:
@@ -539,30 +497,29 @@ class UploadFile(Resource):
),
400,
)
user = decoded_token.get("sub")
job_name = request.form["name"]
# Create safe versions for filesystem operations
safe_user = safe_filename(user)
dir_name = safe_filename(job_name)
user = secure_filename(decoded_token.get("sub"))
job_name = secure_filename(request.form["name"])
try:
from application.storage.storage_creator import StorageCreator
storage = StorageCreator.get_storage()
base_path = f"{settings.UPLOAD_FOLDER}/{safe_user}/{dir_name}"
base_path = f"{settings.UPLOAD_FOLDER}/{user}/{job_name}"
if len(files) > 1:
temp_files = []
for file in files:
filename = safe_filename(file.filename)
filename = secure_filename(file.filename)
temp_path = f"{base_path}/temp/{filename}"
storage.save_file(file, temp_path)
temp_files.append(temp_path)
print(f"Saved file: {filename}")
zip_filename = f"{dir_name}.zip"
zip_filename = f"{job_name}.zip"
zip_path = f"{base_path}/{zip_filename}"
zip_temp_path = None
def create_zip_archive(temp_paths, dir_name, storage):
def create_zip_archive(temp_paths, job_name, storage):
import tempfile
with tempfile.NamedTemporaryFile(
@@ -602,7 +559,7 @@ class UploadFile(Resource):
return zip_output_path
try:
zip_temp_path = create_zip_archive(temp_files, dir_name, storage)
zip_temp_path = create_zip_archive(temp_files, job_name, storage)
with open(zip_temp_path, "rb") as zip_file:
storage.save_file(zip_file, zip_path)
task = ingest.delay(
@@ -627,8 +584,6 @@ class UploadFile(Resource):
job_name,
zip_filename,
user,
dir_name,
safe_user,
)
finally:
# Clean up temporary files
@@ -649,7 +604,7 @@ class UploadFile(Resource):
# For single file
file = files[0]
filename = safe_filename(file.filename)
filename = secure_filename(file.filename)
file_path = f"{base_path}/{filename}"
storage.save_file(file, file_path)
@@ -676,8 +631,6 @@ class UploadFile(Resource):
job_name,
filename, # Corrected variable for single-file case
user,
dir_name,
safe_user,
)
except Exception as err:
current_app.logger.error(f"Error uploading file: {err}", exc_info=True)
@@ -1111,28 +1064,27 @@ class UpdatePrompt(Resource):
@user_ns.route("/api/get_agent")
class GetAgent(Resource):
@api.doc(params={"id": "Agent ID"}, description="Get agent by ID")
@api.doc(params={"id": "ID of the agent"}, description="Get a single agent by ID")
def get(self):
if not (decoded_token := request.decoded_token):
return {"success": False}, 401
if not (agent_id := request.args.get("id")):
return {"success": False, "message": "ID required"}, 400
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
agent_id = request.args.get("id")
if not agent_id:
return make_response(
jsonify({"success": False, "message": "ID is required"}), 400
)
try:
agent = agents_collection.find_one(
{"_id": ObjectId(agent_id), "user": decoded_token["sub"]}
{"_id": ObjectId(agent_id), "user": user}
)
if not agent:
return {"status": "Not found"}, 404
return make_response(jsonify({"status": "Not found"}), 404)
data = {
"id": str(agent["_id"]),
"name": agent["name"],
"description": agent.get("description", ""),
"image": (
generate_image_url(agent["image"]) if agent.get("image") else ""
),
"source": (
str(source_doc["_id"])
if isinstance(agent.get("source"), DBRef)
@@ -1159,20 +1111,19 @@ class GetAgent(Resource):
"shared_metadata": agent.get("shared_metadata", {}),
"shared_token": agent.get("shared_token", ""),
}
return make_response(jsonify(data), 200)
except Exception as e:
current_app.logger.error(f"Agent fetch error: {e}", exc_info=True)
return {"success": False}, 400
except Exception as err:
current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify(data), 200)
@user_ns.route("/api/get_agents")
class GetAgents(Resource):
@api.doc(description="Retrieve agents for the user")
def get(self):
if not (decoded_token := request.decoded_token):
return {"success": False}, 401
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
try:
user_doc = ensure_user_doc(user)
@@ -1184,9 +1135,6 @@ class GetAgents(Resource):
"id": str(agent["_id"]),
"name": agent["name"],
"description": agent.get("description", ""),
"image": (
generate_image_url(agent["image"]) if agent.get("image") else ""
),
"source": (
str(source_doc["_id"])
if isinstance(agent.get("source"), DBRef)
@@ -1231,8 +1179,8 @@ class CreateAgent(Resource):
"description": fields.String(
required=True, description="Description of the agent"
),
"image": fields.Raw(
required=False, description="Image file upload", type="file"
"image": fields.String(
required=False, description="Image URL or identifier"
),
"source": fields.String(required=True, description="Source ID"),
"chunks": fields.Integer(required=True, description="Chunks count"),
@@ -1251,20 +1199,12 @@ class CreateAgent(Resource):
@api.expect(create_agent_model)
@api.doc(description="Create a new agent")
def post(self):
if not (decoded_token := request.decoded_token):
return {"success": False}, 401
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
if request.content_type == "application/json":
data = request.get_json()
else:
print(request.form)
data = request.form.to_dict()
if "tools" in data:
try:
data["tools"] = json.loads(data["tools"])
except json.JSONDecodeError:
data["tools"] = []
print(f"Received data: {data}")
data = request.get_json()
if data.get("status") not in ["draft", "published"]:
return make_response(
jsonify({"success": False, "message": "Invalid status"}), 400
@@ -1285,20 +1225,13 @@ class CreateAgent(Resource):
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
image_url, error = handle_image_upload(request, "", user, storage)
if error:
return make_response(
jsonify({"success": False, "message": "Image upload failed"}), 400
)
try:
key = str(uuid.uuid4())
new_agent = {
"user": user,
"name": data.get("name"),
"description": data.get("description", ""),
"image": image_url,
"image": data.get("image", ""),
"source": (
DBRef("sources", ObjectId(data.get("source")))
if ObjectId.is_valid(data.get("source"))
@@ -1356,18 +1289,11 @@ class UpdateAgent(Resource):
@api.expect(update_agent_model)
@api.doc(description="Update an existing agent")
def put(self, agent_id):
if not (decoded_token := request.decoded_token):
return {"success": False}, 401
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
if request.content_type == "application/json":
data = request.get_json()
else:
data = request.form.to_dict()
if "tools" in data:
try:
data["tools"] = json.loads(data["tools"])
except json.JSONDecodeError:
data["tools"] = []
data = request.get_json()
if not ObjectId.is_valid(agent_id):
return make_response(
@@ -1392,15 +1318,6 @@ class UpdateAgent(Resource):
),
404,
)
image_url, error = handle_image_upload(
request, existing_agent.get("image", ""), user, storage
)
if error:
return make_response(
jsonify({"success": False, "message": "Image upload failed"}), 400
)
update_fields = {}
allowed_fields = [
"name",
@@ -1472,8 +1389,6 @@ class UpdateAgent(Resource):
)
else:
update_fields[field] = data[field]
if image_url:
update_fields["image"] = image_url
if not update_fields:
return make_response(
jsonify({"success": False, "message": "No update data provided"}), 400
@@ -1622,9 +1537,6 @@ class PinnedAgents(Resource):
"id": str(agent["_id"]),
"name": agent.get("name", ""),
"description": agent.get("description", ""),
"image": (
generate_image_url(agent["image"]) if agent.get("image") else ""
),
"source": (
str(db.dereference(agent["source"])["_id"])
if "source" in agent
@@ -1785,11 +1697,6 @@ class SharedAgent(Resource):
"id": agent_id,
"user": shared_agent.get("user", ""),
"name": shared_agent.get("name", ""),
"image": (
generate_image_url(shared_agent["image"])
if shared_agent.get("image")
else ""
),
"description": shared_agent.get("description", ""),
"tools": shared_agent.get("tools", []),
"tool_details": resolve_tool_details(shared_agent.get("tools", [])),
@@ -1865,9 +1772,6 @@ class SharedAgents(Resource):
"id": str(agent["_id"]),
"name": agent.get("name", ""),
"description": agent.get("description", ""),
"image": (
generate_image_url(agent["image"]) if agent.get("image") else ""
),
"tools": agent.get("tools", []),
"tool_details": resolve_tool_details(agent.get("tools", [])),
"agent_type": agent.get("agent_type", ""),
@@ -2332,7 +2236,7 @@ class GetPubliclySharedConversations(Resource):
conversation_queries = conversation["queries"][
: (shared["first_n_queries"])
]
for query in conversation_queries:
if "attachments" in query and query["attachments"]:
attachment_details = []
@@ -2342,18 +2246,13 @@ class GetPubliclySharedConversations(Resource):
{"_id": ObjectId(attachment_id)}
)
if attachment:
attachment_details.append(
{
"id": str(attachment["_id"]),
"fileName": attachment.get(
"filename", "Unknown file"
),
}
)
attachment_details.append({
"id": str(attachment["_id"]),
"fileName": attachment.get("filename", "Unknown file")
})
except Exception as e:
current_app.logger.error(
f"Error retrieving attachment {attachment_id}: {e}",
exc_info=True,
f"Error retrieving attachment {attachment_id}: {e}", exc_info=True
)
query["attachments"] = attachment_details
else:
@@ -3558,7 +3457,7 @@ class StoreAttachment(Resource):
jsonify({"status": "error", "message": "Missing file"}),
400,
)
user = safe_filename(decoded_token.get("sub"))
user = secure_filename(decoded_token.get("sub"))
try:
attachment_id = ObjectId()
@@ -3589,30 +3488,3 @@ class StoreAttachment(Resource):
except Exception as err:
current_app.logger.error(f"Error storing attachment: {err}", exc_info=True)
return make_response(jsonify({"success": False, "error": str(err)}), 400)
@user_ns.route("/api/images/<path:image_path>")
class ServeImage(Resource):
@api.doc(description="Serve an image from storage")
def get(self, image_path):
try:
file_obj = storage.get_file(image_path)
extension = image_path.split(".")[-1].lower()
content_type = f"image/{extension}"
if extension == "jpg":
content_type = "image/jpeg"
response = make_response(file_obj.read())
response.headers.set("Content-Type", content_type)
response.headers.set("Cache-Control", "max-age=86400")
return response
except FileNotFoundError:
return make_response(
jsonify({"success": False, "message": "Image not found"}), 404
)
except Exception as e:
current_app.logger.error(f"Error serving image: {e}")
return make_response(
jsonify({"success": False, "message": "Error retrieving image"}), 500
)

View File

@@ -11,8 +11,8 @@ from application.worker import (
@celery.task(bind=True)
def ingest(self, directory, formats, job_name, filename, user, dir_name, user_dir):
resp = ingest_worker(self, directory, formats, job_name, filename, user, dir_name, user_dir)
def ingest(self, directory, formats, name_job, filename, user):
resp = ingest_worker(self, directory, formats, name_job, filename, user)
return resp

View File

@@ -16,7 +16,6 @@ class Settings(BaseSettings):
None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo
)
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
EMBEDDINGS_PATH: Optional[str] = "./models/all-mpnet-base-v2" # Set None for SentenceTransformer to manage download
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
@@ -104,7 +103,6 @@ class Settings(BaseSettings):
FLASK_DEBUG_MODE: bool = False
STORAGE_TYPE: str = "local" # local or s3
URL_STRATEGY: str = "backend" # backend or s3
JWT_SECRET_KEY: str = ""

View File

@@ -14,5 +14,5 @@ class LLMHandlerCreator:
def create_handler(cls, llm_type: str, *args, **kwargs) -> LLMHandler:
handler_class = cls.handlers.get(llm_type.lower())
if not handler_class:
handler_class = OpenAILLMHandler
raise ValueError(f"No LLM handler class found for type {llm_type}")
return handler_class(*args, **kwargs)

View File

@@ -1,14 +1,13 @@
"""S3 storage implementation."""
import io
from typing import BinaryIO, List, Callable
import os
from typing import BinaryIO, Callable, List
import boto3
from application.core.settings import settings
from botocore.exceptions import ClientError
from application.storage.base import BaseStorage
from botocore.exceptions import ClientError
from application.core.settings import settings
class S3Storage(BaseStorage):
@@ -21,21 +20,18 @@ class S3Storage(BaseStorage):
Args:
bucket_name: S3 bucket name (optional, defaults to settings)
"""
self.bucket_name = bucket_name or getattr(
settings, "S3_BUCKET_NAME", "docsgpt-test-bucket"
)
self.bucket_name = bucket_name or getattr(settings, "S3_BUCKET_NAME", "docsgpt-test-bucket")
# Get credentials from settings
aws_access_key_id = getattr(settings, "SAGEMAKER_ACCESS_KEY", None)
aws_secret_access_key = getattr(settings, "SAGEMAKER_SECRET_KEY", None)
region_name = getattr(settings, "SAGEMAKER_REGION", None)
self.s3 = boto3.client(
"s3",
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name,
region_name=region_name
)
def save_file(self, file_data: BinaryIO, path: str) -> dict:
@@ -45,16 +41,17 @@ class S3Storage(BaseStorage):
region = getattr(settings, "SAGEMAKER_REGION", None)
return {
"storage_type": "s3",
"bucket_name": self.bucket_name,
"uri": f"s3://{self.bucket_name}/{path}",
"region": region,
'storage_type': 's3',
'bucket_name': self.bucket_name,
'uri': f's3://{self.bucket_name}/{path}',
'region': region
}
def get_file(self, path: str) -> BinaryIO:
"""Get a file from S3 storage."""
if not self.file_exists(path):
raise FileNotFoundError(f"File not found: {path}")
file_obj = io.BytesIO()
self.s3.download_fileobj(self.bucket_name, path, file_obj)
file_obj.seek(0)
@@ -79,17 +76,18 @@ class S3Storage(BaseStorage):
def list_files(self, directory: str) -> List[str]:
"""List all files in a directory in S3 storage."""
# Ensure directory ends with a slash if it's not empty
if directory and not directory.endswith('/'):
directory += '/'
if directory and not directory.endswith("/"):
directory += "/"
result = []
paginator = self.s3.get_paginator("list_objects_v2")
paginator = self.s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
for page in pages:
if "Contents" in page:
for obj in page["Contents"]:
result.append(obj["Key"])
if 'Contents' in page:
for obj in page['Contents']:
result.append(obj['Key'])
return result
def process_file(self, path: str, processor_func: Callable, **kwargs):
@@ -100,24 +98,22 @@ class S3Storage(BaseStorage):
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
import logging
import tempfile
import logging
if not self.file_exists(path):
raise FileNotFoundError(f"File not found in S3: {path}")
with tempfile.NamedTemporaryFile(
suffix=os.path.splitext(path)[1], delete=True
) as temp_file:
with tempfile.NamedTemporaryFile(suffix=os.path.splitext(path)[1], delete=True) as temp_file:
try:
# Download the file from S3 to the temporary file
self.s3.download_fileobj(self.bucket_name, path, temp_file)
temp_file.flush()
return processor_func(local_path=temp_file.name, **kwargs)
except Exception as e:
logging.error(f"Error processing S3 file {path}: {e}", exc_info=True)

View File

@@ -1,12 +1,8 @@
import hashlib
import os
import re
import uuid
import tiktoken
from flask import jsonify, make_response
from werkzeug.utils import secure_filename
from application.core.settings import settings
_encoding = None
@@ -19,31 +15,6 @@ def get_encoding():
return _encoding
def safe_filename(filename):
"""
Creates a safe filename that preserves the original extension.
Uses secure_filename, but ensures a proper filename is returned even with non-Latin characters.
Args:
filename (str): The original filename
Returns:
str: A safe filename that can be used for storage
"""
if not filename:
return str(uuid.uuid4())
_, extension = os.path.splitext(filename)
safe_name = secure_filename(filename)
# If secure_filename returns just the extension or an empty string
if not safe_name or safe_name == extension.lstrip("."):
return f"{str(uuid.uuid4())}{extension}"
return safe_name
def num_tokens_from_string(string: str) -> int:
encoding = get_encoding()
if isinstance(string, str):
@@ -138,14 +109,3 @@ def validate_function_name(function_name):
if not re.match(r"^[a-zA-Z0-9_-]+$", function_name):
return False
return True
def generate_image_url(image_path):
strategy = getattr(settings, "URL_STRATEGY", "backend")
if strategy == "s3":
bucket_name = getattr(settings, "S3_BUCKET_NAME", "docsgpt-test-bucket")
region_name = getattr(settings, "SAGEMAKER_REGION", "eu-central-1")
return f"https://{bucket_name}.s3.{region_name}.amazonaws.com/{image_path}"
else:
base_url = getattr(settings, "API_URL", "http://localhost:7091")
return f"{base_url}/api/images/{image_path}"

View File

@@ -74,11 +74,17 @@ class BaseVectorStore(ABC):
embeddings_name,
openai_api_key=embeddings_key
)
elif embeddings_name == "huggingface_sentence-transformers/all-mpnet-base-v2":
if os.path.exists("./models/all-mpnet-base-v2"):
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name = "./models/all-mpnet-base-v2",
)
else:
embedding_instance = EmbeddingsSingleton.get_instance(
embeddings_name,
)
else:
model_identifier = embeddings_name
if settings.EMBEDDINGS_PATH and os.path.exists(settings.EMBEDDINGS_PATH):
model_identifier = settings.EMBEDDINGS_PATH
embedding_instance = EmbeddingsSingleton.get_instance(model_identifier)
embedding_instance = EmbeddingsSingleton.get_instance(embeddings_name)
return embedding_instance

View File

@@ -194,7 +194,7 @@ def run_agent_logic(agent_config, input_data):
# Define the main function for ingesting and processing documents.
def ingest_worker(
self, directory, formats, job_name, filename, user, dir_name=None, user_dir=None, retriever="classic"
self, directory, formats, name_job, filename, user, retriever="classic"
):
"""
Ingest and process documents.
@@ -203,11 +203,9 @@ def ingest_worker(
self: Reference to the instance of the task.
directory (str): Specifies the directory for ingesting ('inputs' or 'temp').
formats (list of str): List of file extensions to consider for ingestion (e.g., [".rst", ".md"]).
job_name (str): Name of the job for this ingestion task (original, unsanitized).
name_job (str): Name of the job for this ingestion task.
filename (str): Name of the file to be ingested.
user (str): Identifier for the user initiating the ingestion (original, unsanitized).
dir_name (str, optional): Sanitized directory name for filesystem operations.
user_dir (str, optional): Sanitized user ID for filesystem operations.
user (str): Identifier for the user initiating the ingestion.
retriever (str): Type of retriever to use for processing the documents.
Returns:
@@ -218,13 +216,13 @@ def ingest_worker(
limit = None
exclude = True
sample = False
storage = StorageCreator.get_storage()
full_path = os.path.join(directory, user_dir, dir_name)
full_path = os.path.join(directory, user, name_job)
source_file_path = os.path.join(full_path, filename)
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": job_name})
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": name_job})
# Create temporary working directory
with tempfile.TemporaryDirectory() as temp_dir:
@@ -285,14 +283,13 @@ def ingest_worker(
for i in range(min(5, len(raw_docs))):
logging.info(f"Sample document {i}: {raw_docs[i]}")
file_data = {
"name": job_name, # Use original job_name
"name": name_job,
"file": filename,
"user": user, # Use original user
"user": user,
"tokens": tokens,
"retriever": retriever,
"id": str(id),
"type": "local",
"original_file_path": source_file_path,
}
upload_index(vector_store_path, file_data)
@@ -304,9 +301,9 @@ def ingest_worker(
return {
"directory": directory,
"formats": formats,
"name_job": job_name, # Use original job_name
"name_job": name_job,
"filename": filename,
"user": user, # Use original user
"user": user,
"limited": False,
}

View File

@@ -28,9 +28,9 @@ services:
ports:
- "7091:7091"
volumes:
- ../application/indexes:/app/indexes
- ../application/indexes:/app/application/indexes
- ../application/inputs:/app/inputs
- ../application/vectors:/app/vectors
- ../application/vectors:/app/application/vectors
depends_on:
- redis
- mongo
@@ -50,9 +50,9 @@ services:
- API_URL=http://backend:7091
- CACHE_REDIS_URL=redis://redis:6379/2
volumes:
- ../application/indexes:/app/indexes
- ../application/indexes:/app/application/indexes
- ../application/inputs:/app/inputs
- ../application/vectors:/app/vectors
- ../application/vectors:/app/application/vectors
depends_on:
- redis
- mongo

5417
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@
"@vercel/analytics": "^1.1.1",
"docsgpt-react": "^0.5.1",
"next": "^15.3.3",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"nextra": "^4.2.17",
"nextra-theme-docs": "^4.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}

View File

@@ -5388,9 +5388,9 @@
}
},
"node_modules/dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"

View File

@@ -8,7 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
"@reduxjs/toolkit": "^2.5.1",
"chart.js": "^4.4.4",
"clsx": "^2.1.1",
"i18next": "^24.2.0",
@@ -19,7 +19,7 @@
"react-chartjs-2": "^5.3.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.8",
"react-dropzone": "^14.3.5",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
@@ -28,8 +28,7 @@
"react-syntax-highlighter": "^15.6.1",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"tailwind-merge": "^3.3.1"
"remark-math": "^6.0.0"
},
"devDependencies": {
"@types/mermaid": "^9.1.0",
@@ -1198,13 +1197,11 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz",
"integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
@@ -1545,18 +1542,6 @@
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@@ -9201,10 +9186,9 @@
}
},
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"version": "14.3.5",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
"integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
@@ -10485,16 +10469,6 @@
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"dev": true
},
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",

View File

@@ -19,7 +19,7 @@
]
},
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
"@reduxjs/toolkit": "^2.5.1",
"chart.js": "^4.4.4",
"clsx": "^2.1.1",
"i18next": "^24.2.0",
@@ -30,7 +30,7 @@
"react-chartjs-2": "^5.3.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.8",
"react-dropzone": "^14.3.5",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
@@ -39,8 +39,7 @@
"react-syntax-highlighter": "^15.6.1",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"tailwind-merge": "^3.3.1"
"remark-math": "^6.0.0"
},
"devDependencies": {
"@types/mermaid": "^9.1.0",

View File

@@ -81,27 +81,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
useState<ActiveState>('INACTIVE');
const [recentAgents, setRecentAgents] = useState<Agent[]>([]);
const navRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
navRef.current &&
!navRef.current.contains(event.target as Node) &&
(isMobile || isTablet) &&
navOpen
) {
setNavOpen(false);
}
}
const navRef = useRef(null);
//event listener only for mobile/tablet when nav is open
if ((isMobile || isTablet) && navOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [navOpen, isMobile, isTablet, setNavOpen]);
async function fetchRecentAgents() {
try {
const response = await userService.getPinnedAgents(token);
@@ -420,13 +401,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<div className="flex items-center gap-2">
<div className="flex w-6 justify-center">
<img
src={
agent.image && agent.image.trim() !== ''
? agent.image
: Robot
}
src={agent.image ?? Robot}
alt="agent-logo"
className="h-6 w-6 rounded-full object-contain"
className="h-6 w-6"
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">

View File

@@ -83,9 +83,9 @@ export default function AgentCard({
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
src={agent.image ?? Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full object-contain"
className="h-7 w-7 rounded-full"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">

View File

@@ -6,23 +6,24 @@ import ConversationMessages from '../conversation/ConversationMessages';
import { Query } from '../conversation/conversationModels';
import {
addQuery,
fetchPreviewAnswer,
handlePreviewAbort,
fetchAnswer,
handleAbort,
resendQuery,
resetPreview,
selectPreviewQueries,
selectPreviewStatus,
} from './agentPreviewSlice';
resetConversation,
selectQueries,
selectStatus,
} from '../conversation/conversationSlice';
import { selectSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
export default function AgentPreview() {
const dispatch = useDispatch<AppDispatch>();
const queries = useSelector(selectPreviewQueries);
const status = useSelector(selectPreviewStatus);
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const selectedAgent = useSelector(selectSelectedAgent);
const [input, setInput] = useState('');
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const fetchStream = useRef<any>(null);
@@ -30,7 +31,7 @@ export default function AgentPreview() {
const handleFetchAnswer = useCallback(
({ question, index }: { question: string; index?: number }) => {
fetchStream.current = dispatch(
fetchPreviewAnswer({ question, indx: index }),
fetchAnswer({ question, indx: index, isPreview: true }),
);
},
[dispatch],
@@ -94,11 +95,11 @@ export default function AgentPreview() {
};
useEffect(() => {
dispatch(resetPreview());
dispatch(resetConversation());
return () => {
if (fetchStream.current) fetchStream.current.abort();
handlePreviewAbort();
dispatch(resetPreview());
handleAbort();
dispatch(resetConversation());
};
}, [dispatch]);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
@@ -6,7 +6,6 @@ import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import SourceIcon from '../assets/source.svg';
import Dropdown from '../components/Dropdown';
import { FileUpload } from '../components/FileUpload';
import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
import AgentDetailsModal from '../modals/AgentDetailsModal';
import ConfirmationModal from '../modals/ConfirmationModal';
@@ -49,7 +48,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
agent_type: '',
status: '',
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [prompts, setPrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
@@ -108,13 +106,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
);
};
const handleUpload = useCallback((files: File[]) => {
if (files && files.length > 0) {
const file = files[0];
setImageFile(file);
}
}, []);
const handleCancel = () => {
if (selectedAgent) dispatch(setSelectedAgent(null));
navigate('/agents');
@@ -127,80 +118,42 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
};
const handleSaveDraft = async () => {
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
formData.append('agent_type', agent.agent_type);
formData.append('status', 'draft');
if (imageFile) formData.append('image', imageFile);
if (agent.tools && agent.tools.length > 0)
formData.append('tools', JSON.stringify(agent.tools));
else formData.append('tools', '[]');
try {
const response =
effectiveMode === 'new'
? await userService.createAgent(formData, token)
: await userService.updateAgent(agent.id || '', formData, token);
if (!response.ok) throw new Error('Failed to create agent draft');
const data = await response.json();
if (effectiveMode === 'new') {
setEffectiveMode('draft');
setAgent((prev) => ({
...prev,
id: data.id,
image: data.image || prev.image,
}));
}
} catch (error) {
console.error('Error saving draft:', error);
throw new Error('Failed to save draft');
const response =
effectiveMode === 'new'
? await userService.createAgent({ ...agent, status: 'draft' }, token)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'draft' },
token,
);
if (!response.ok) throw new Error('Failed to create agent draft');
const data = await response.json();
if (effectiveMode === 'new') {
setEffectiveMode('draft');
setAgent((prev) => ({ ...prev, id: data.id }));
}
};
const handlePublish = async () => {
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
formData.append('agent_type', agent.agent_type);
formData.append('status', 'published');
if (imageFile) formData.append('image', imageFile);
if (agent.tools && agent.tools.length > 0)
formData.append('tools', JSON.stringify(agent.tools));
else formData.append('tools', '[]');
try {
const response =
effectiveMode === 'new'
? await userService.createAgent(formData, token)
: await userService.updateAgent(agent.id || '', formData, token);
if (!response.ok) throw new Error('Failed to publish agent');
const data = await response.json();
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
if (effectiveMode === 'new' || effectiveMode === 'draft') {
setEffectiveMode('edit');
setAgent((prev) => ({
...prev,
status: 'published',
image: data.image || prev.image,
}));
setAgentDetails('ACTIVE');
}
} catch (error) {
console.error('Error publishing agent:', error);
throw new Error('Failed to publish agent');
const response =
effectiveMode === 'new'
? await userService.createAgent(
{ ...agent, status: 'published' },
token,
)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'published' },
token,
);
if (!response.ok) throw new Error('Failed to publish agent');
const data = await response.json();
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
if (effectiveMode === 'new' || effectiveMode === 'draft') {
setEffectiveMode('edit');
setAgent((prev) => ({ ...prev, status: 'published' }));
setAgentDetails('ACTIVE');
}
};
@@ -372,21 +325,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
setAgent({ ...agent, description: e.target.value })
}
/>
<div className="mt-3">
<FileUpload
showPreview
className="dark:bg-[#222327]"
onUpload={handleUpload}
onRemove={() => setImageFile(null)}
uploadText={[
{ text: 'Click to upload', colorClass: 'text-[#7D54D1]' },
{
text: ' or drag and drop',
colorClass: 'text-[#525252]',
},
]}
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Source</h2>
@@ -395,11 +333,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<button
ref={sourceAnchorButtonRef}
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
className={`w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] dark:bg-[#222327] ${
selectedSourceIds.size > 0
? 'text-jet dark:text-bright-gray'
: 'text-gray-400 dark:text-silver'
}`}
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
>
{selectedSourceIds.size > 0
? Array.from(selectedSourceIds)
@@ -502,11 +436,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<button
ref={toolAnchorButtonRef}
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
className={`w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] dark:bg-[#222327] ${
selectedToolIds.size > 0
? 'text-jet dark:text-bright-gray'
: 'text-gray-400 dark:text-silver'
}`}
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
>
{selectedToolIds.size > 0
? Array.from(selectedToolIds)

View File

@@ -57,7 +57,9 @@ export default function SharedAgent() {
const handleFetchAnswer = useCallback(
({ question, index }: { question: string; index?: number }) => {
fetchStream.current = dispatch(fetchAnswer({ question, indx: index }));
fetchStream.current = dispatch(
fetchAnswer({ question, indx: index, isPreview: false }),
);
},
[dispatch],
);
@@ -153,13 +155,9 @@ export default function SharedAgent() {
<div className="relative h-full w-full">
<div className="absolute left-4 top-5 hidden items-center gap-3 sm:flex">
<img
src={
sharedAgent.image && sharedAgent.image.trim() !== ''
? sharedAgent.image
: Robot
}
src={sharedAgent.image ?? Robot}
alt="agent-logo"
className="h-6 w-6 rounded-full object-contain"
className="h-6 w-6"
/>
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
{sharedAgent.name}

View File

@@ -6,10 +6,7 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
className="h-full w-full rounded-full object-contain"
/>
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">

View File

@@ -1,319 +0,0 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
Answer,
ConversationState,
Query,
Status,
} from '../conversation/conversationModels';
import {
handleFetchAnswer,
handleFetchAnswerSteaming,
} from '../conversation/conversationHandlers';
import {
selectCompletedAttachments,
clearAttachments,
} from '../upload/uploadSlice';
import store from '../store';
const initialState: ConversationState = {
queries: [],
status: 'idle',
conversationId: null,
};
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
let abortController: AbortController | null = null;
export function handlePreviewAbort() {
if (abortController) {
abortController.abort();
abortController = null;
}
}
export const fetchPreviewAnswer = createAsyncThunk<
Answer,
{ question: string; indx?: number }
>(
'agentPreview/fetchAnswer',
async ({ question, indx }, { dispatch, getState }) => {
if (abortController) abortController.abort();
abortController = new AbortController();
const { signal } = abortController;
const state = getState() as RootState;
const attachmentIds = selectCompletedAttachments(state)
.filter((a) => a.id)
.map((a) => a.id) as string[];
if (attachmentIds.length > 0) {
dispatch(clearAttachments());
}
if (state.preference) {
if (API_STREAMING) {
await handleFetchAnswerSteaming(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.agentPreview.queries,
null, // No conversation ID for previews
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
(event) => {
const data = JSON.parse(event.data);
const targetIndex = indx ?? state.agentPreview.queries.length - 1;
if (data.type === 'end') {
dispatch(agentPreviewSlice.actions.setStatus('idle'));
} else if (data.type === 'thought') {
dispatch(
updateThought({
index: targetIndex,
query: { thought: data.thought },
}),
);
} else if (data.type === 'source') {
dispatch(
updateStreamingSource({
index: targetIndex,
query: { sources: data.source ?? [] },
}),
);
} else if (data.type === 'tool_call') {
dispatch(
updateToolCall({
index: targetIndex,
tool_call: data.data,
}),
);
} else if (data.type === 'error') {
dispatch(agentPreviewSlice.actions.setStatus('failed'));
dispatch(
agentPreviewSlice.actions.raiseError({
index: targetIndex,
message: data.error,
}),
);
} else {
dispatch(
updateStreamingQuery({
index: targetIndex,
query: { response: data.answer },
}),
);
}
},
indx,
state.preference.selectedAgent?.id,
attachmentIds,
false, // Don't save preview conversations
);
} else {
// Non-streaming implementation
const answer = await handleFetchAnswer(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.agentPreview.queries,
null, // No conversation ID for previews
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
state.preference.selectedAgent?.id,
attachmentIds,
false, // Don't save preview conversations
);
if (answer) {
const sourcesPrepped = answer.sources.map(
(source: { title: string }) => {
if (source && source.title) {
const titleParts = source.title.split('/');
return {
...source,
title: titleParts[titleParts.length - 1],
};
}
return source;
},
);
const targetIndex = indx ?? state.agentPreview.queries.length - 1;
dispatch(
updateQuery({
index: targetIndex,
query: {
response: answer.answer,
thought: answer.thought,
sources: sourcesPrepped,
tool_calls: answer.toolCalls,
},
}),
);
dispatch(agentPreviewSlice.actions.setStatus('idle'));
}
}
}
return {
conversationId: null,
title: null,
answer: '',
query: question,
result: '',
thought: '',
sources: [],
tool_calls: [],
};
},
);
export const agentPreviewSlice = createSlice({
name: 'agentPreview',
initialState,
reducers: {
addQuery(state, action: PayloadAction<Query>) {
state.queries.push(action.payload);
},
resendQuery(
state,
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
) {
state.queries = [
...state.queries.splice(0, action.payload.index),
action.payload,
];
},
updateStreamingQuery(
state,
action: PayloadAction<{
index: number;
query: Partial<Query>;
}>,
) {
const { index, query } = action.payload;
if (state.status === 'idle') return;
if (query.response != undefined) {
state.queries[index].response =
(state.queries[index].response || '') + query.response;
}
},
updateThought(
state,
action: PayloadAction<{
index: number;
query: Partial<Query>;
}>,
) {
const { index, query } = action.payload;
if (query.thought != undefined) {
state.queries[index].thought =
(state.queries[index].thought || '') + query.thought;
}
},
updateStreamingSource(
state,
action: PayloadAction<{
index: number;
query: Partial<Query>;
}>,
) {
const { index, query } = action.payload;
if (!state.queries[index].sources) {
state.queries[index].sources = query?.sources;
} else if (query.sources) {
state.queries[index].sources!.push(...query.sources);
}
},
updateToolCall(state, action) {
const { index, tool_call } = action.payload;
if (!state.queries[index].tool_calls) {
state.queries[index].tool_calls = [];
}
const existingIndex = state.queries[index].tool_calls.findIndex(
(call) => call.call_id === tool_call.call_id,
);
if (existingIndex !== -1) {
const existingCall = state.queries[index].tool_calls[existingIndex];
state.queries[index].tool_calls[existingIndex] = {
...existingCall,
...tool_call,
};
} else state.queries[index].tool_calls.push(tool_call);
},
updateQuery(
state,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { index, query } = action.payload;
state.queries[index] = {
...state.queries[index],
...query,
};
},
setStatus(state, action: PayloadAction<Status>) {
state.status = action.payload;
},
raiseError(
state,
action: PayloadAction<{
index: number;
message: string;
}>,
) {
const { index, message } = action.payload;
state.queries[index].error = message;
},
resetPreview: (state) => {
state.queries = initialState.queries;
state.status = initialState.status;
state.conversationId = initialState.conversationId;
handlePreviewAbort();
},
},
extraReducers(builder) {
builder
.addCase(fetchPreviewAnswer.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPreviewAnswer.rejected, (state, action) => {
if (action.meta.aborted) {
state.status = 'idle';
return state;
}
state.status = 'failed';
state.queries[state.queries.length - 1].error = 'Something went wrong';
});
},
});
type RootState = ReturnType<typeof store.getState>;
export const selectPreviewQueries = (state: RootState) =>
state.agentPreview.queries;
export const selectPreviewStatus = (state: RootState) =>
state.agentPreview.status;
export const {
addQuery,
updateQuery,
resendQuery,
updateStreamingQuery,
updateThought,
updateStreamingSource,
updateToolCall,
setStatus,
raiseError,
resetPreview,
} = agentPreviewSlice.actions;
export default agentPreviewSlice.reducer;

View File

@@ -324,21 +324,17 @@ function AgentCard({
iconWidth: 14,
iconHeight: 14,
},
...(agent.status === 'published'
? [
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary' as const,
iconWidth: 18,
iconHeight: 18,
},
]
: []),
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary',
iconWidth: 18,
iconHeight: 18,
},
{
icon: Trash,
label: 'Delete',
@@ -430,16 +426,16 @@ function AgentCard({
setIsOpen={setIsMenuOpen}
options={menuOptions}
anchorRef={menuRef}
position="bottom-right"
position="top-right"
offset={{ x: 0, y: 0 }}
/>
</div>
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
src={agent.image ?? Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full object-contain"
className="h-7 w-7 rounded-full"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>

View File

@@ -1,21 +1,16 @@
export const baseURL =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const getHeaders = (
token: string | null,
customHeaders = {},
isFormData = false,
): HeadersInit => {
const headers: HeadersInit = {
const defaultHeaders = {
'Content-Type': 'application/json',
};
const getHeaders = (token: string | null, customHeaders = {}): HeadersInit => {
return {
...defaultHeaders,
...(token ? { Authorization: `Bearer ${token}` } : {}),
...customHeaders,
};
if (!isFormData) {
headers['Content-Type'] = 'application/json';
}
return headers;
};
const apiClient = {
@@ -49,21 +44,6 @@ const apiClient = {
return response;
}),
postFormData: (
url: string,
formData: FormData,
token: string | null,
headers = {},
signal?: AbortSignal,
): Promise<Response> => {
return fetch(`${baseURL}${url}`, {
method: 'POST',
headers: getHeaders(token, headers, true),
body: formData,
signal,
});
},
put: (
url: string,
data: any,
@@ -80,21 +60,6 @@ const apiClient = {
return response;
}),
putFormData: (
url: string,
formData: FormData,
token: string | null,
headers = {},
signal?: AbortSignal,
): Promise<Response> => {
return fetch(`${baseURL}${url}`, {
method: 'PUT',
headers: getHeaders(token, headers, true),
body: formData,
signal,
});
},
delete: (
url: string,
token: string | null,

View File

@@ -22,13 +22,13 @@ const userService = {
getAgents: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENTS, token),
createAgent: (data: any, token: string | null): Promise<any> =>
apiClient.postFormData(endpoints.USER.CREATE_AGENT, data, token),
apiClient.post(endpoints.USER.CREATE_AGENT, data, token),
updateAgent: (
agent_id: string,
data: any,
token: string | null,
): Promise<any> =>
apiClient.putFormData(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
deleteAgent: (id: string, token: string | null): Promise<any> =>
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
getPinnedAgents: (token: string | null): Promise<any> =>

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>

Before

Width:  |  Height:  |  Size: 262 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4028 7.35671V12.6427C12.4043 12.844 12.3655 13.0436 12.2889 13.2298C12.2122 13.4159 12.0991 13.5849 11.9564 13.7269C11.8136 13.8688 11.6439 13.9808 11.4573 14.0563C11.2706 14.1318 11.0708 14.1694 10.8695 14.1667H3.36483C3.16278 14.1693 2.96226 14.1314 2.77509 14.0553C2.58792 13.9791 2.41789 13.8663 2.27504 13.7234C2.13219 13.5804 2.01941 13.4104 1.94335 13.2232C1.86728 13.036 1.82948 12.8354 1.83217 12.6334V5.12871C1.82975 4.92668 1.86776 4.7262 1.94396 4.53908C2.02017 4.35196 2.13302 4.18196 2.27589 4.0391C2.41875 3.89623 2.58875 3.78338 2.77587 3.70717C2.963 3.63097 3.16347 3.59296 3.3655 3.59537H8.65083M14.1648 1.83337L7.1175 8.88071M14.1648 1.83337H10.6408M14.1648 1.83337V5.35737" stroke="#7D54D1" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 895 B

View File

@@ -1,3 +0,0 @@
<svg width="40" height="39" viewBox="0 0 40 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9477 3.02295H33.8827C35.898 3.02295 37.5388 4.6819 37.5388 6.71923V22.9819C37.5388 25.0193 35.898 26.6782 33.8827 26.6782H11.9477C9.9328 26.6782 8.29192 25.0193 8.29192 22.9819V6.71923C8.29192 4.6819 9.9328 3.02295 11.9477 3.02295ZM33.8827 5.97992H11.9477C11.5442 5.97992 11.2167 6.31098 11.2167 6.71916V20.6741L15.2527 16.595C16.2515 15.5839 17.8791 15.5839 18.8792 16.595L20.6486 18.3795L26.0799 11.7888C26.5653 11.2003 27.276 10.8603 28.0335 10.856C28.7953 10.8735 29.5046 11.1841 29.9946 11.765L34.614 17.2147V6.71923C34.614 6.31104 34.2866 5.97992 33.8827 5.97992ZM6.40446 25.1242C7.16068 27.3803 9.243 28.8957 11.584 28.8957H32.8128L31.4954 33.1312C31.1223 34.5715 29.7916 35.5487 28.3352 35.5487C28.051 35.5485 27.768 35.5117 27.4929 35.4393L4.88059 29.3169C3.13614 28.8305 2.09642 27.0048 2.55267 25.2438L6.10025 13.2714V23.3516C6.10025 23.8543 6.17497 24.3567 6.35333 24.9542L6.40446 25.1242ZM18.53 10.4151C18.53 12.0459 17.2186 13.3721 15.6055 13.3721C13.9926 13.3721 12.6808 12.0458 12.6808 10.4151C12.6808 8.78445 13.9925 7.45815 15.6055 7.45815C17.2186 7.45815 18.53 8.78438 18.53 10.4151Z" fill="#A3A3A3"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,19 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.394 4.001H8.982C8.32776 4.00074 7.67989 4.12939 7.07539 4.3796C6.47089 4.62982 5.92162 4.99669 5.45896 5.45926C4.9963 5.92182 4.62932 6.47102 4.37898 7.07547C4.12865 7.67992 3.99987 8.32776 4 8.982V14.394C3.99974 15.0483 4.12842 15.6963 4.3787 16.3008C4.62897 16.9054 4.99593 17.4547 5.45861 17.9174C5.92128 18.3801 6.4706 18.747 7.07516 18.9973C7.67972 19.2476 8.32768 19.3763 8.982 19.376H14.394C15.0483 19.3763 15.6963 19.2476 16.3008 18.9973C16.9054 18.747 17.4547 18.3801 17.9174 17.9174C18.3801 17.4547 18.747 16.9054 18.9973 16.3008C19.2476 15.6963 19.3763 15.0483 19.376 14.394V8.982C19.3763 8.32768 19.2476 7.67972 18.9973 7.07516C18.747 6.4706 18.3801 5.92128 17.9174 5.45861C17.4547 4.99593 16.9054 4.62897 16.3008 4.3787C15.6963 4.12842 15.0483 3.99974 14.394 4V4.001Z" stroke="url(#paint0_linear_9044_3689)" stroke-width="1.5"/>
<path d="M19.606 15.5881H21.225C21.4968 15.5881 21.7576 15.4801 21.9498 15.2879C22.142 15.0956 22.25 14.8349 22.25 14.5631V9.43809C22.25 9.16624 22.142 8.90553 21.9498 8.7133C21.7576 8.52108 21.4968 8.41309 21.225 8.41309H19.605M4.395 15.5881H2.775C2.6404 15.5881 2.50711 15.5616 2.38275 15.5101C2.25839 15.4586 2.1454 15.3831 2.05022 15.2879C1.95504 15.1927 1.87953 15.0797 1.82802 14.9553C1.77651 14.831 1.75 14.6977 1.75 14.5631V9.43809C1.75 9.16624 1.85799 8.90553 2.05022 8.7133C2.24244 8.52108 2.50315 8.41309 2.775 8.41309H4.395" stroke="url(#paint1_linear_9044_3689)" stroke-width="1.5"/>
<path d="M2.76562 8.41323V4.31323M21.2256 8.41323L21.2156 4.31323M8.91562 8.76323V11.4612M15.0656 8.76323V11.4612M9.94062 15.5882H14.0406" stroke="url(#paint2_linear_9044_3689)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.394 1.001H7.982C7.32776 1.00074 6.67989 1.12939 6.07539 1.3796C5.47089 1.62982 4.92162 1.99669 4.45896 2.45926C3.9963 2.92182 3.62932 3.47102 3.37898 4.07547C3.12865 4.67992 2.99987 5.32776 3 5.982V11.394C2.99974 12.0483 3.12842 12.6963 3.3787 13.3008C3.62897 13.9054 3.99593 14.4547 4.45861 14.9174C4.92128 15.3801 5.4706 15.747 6.07516 15.9973C6.67972 16.2476 7.32768 16.3763 7.982 16.376H13.394C14.0483 16.3763 14.6963 16.2476 15.3008 15.9973C15.9054 15.747 16.4547 15.3801 16.9174 14.9174C17.3801 14.4547 17.747 13.9054 17.9973 13.3008C18.2476 12.6963 18.3763 12.0483 18.376 11.394V5.982C18.3763 5.32768 18.2476 4.67972 17.9973 4.07516C17.747 3.4706 17.3801 2.92128 16.9174 2.45861C16.4547 1.99593 15.9054 1.62897 15.3008 1.3787C14.6963 1.12842 14.0483 0.999738 13.394 1V1.001Z" stroke="url(#paint0_linear_8958_15228)" stroke-width="1.5"/>
<path d="M18.606 12.5881H20.225C20.4968 12.5881 20.7576 12.4801 20.9498 12.2879C21.142 12.0956 21.25 11.8349 21.25 11.5631V6.43809C21.25 6.16624 21.142 5.90553 20.9498 5.7133C20.7576 5.52108 20.4968 5.41309 20.225 5.41309H18.605M3.395 12.5881H1.775C1.6404 12.5881 1.50711 12.5616 1.38275 12.5101C1.25839 12.4586 1.1454 12.3831 1.05022 12.2879C0.955035 12.1927 0.879535 12.0797 0.828023 11.9553C0.776512 11.831 0.75 11.6977 0.75 11.5631V6.43809C0.75 6.16624 0.857991 5.90553 1.05022 5.7133C1.24244 5.52108 1.50315 5.41309 1.775 5.41309H3.395" stroke="url(#paint1_linear_8958_15228)" stroke-width="1.5"/>
<path d="M1.76562 5.41323V1.31323M20.2256 5.41323L20.2156 1.31323M7.91562 5.76323V8.46123M14.0656 5.76323V8.46123M8.94062 12.5882H13.0406" stroke="url(#paint2_linear_8958_15228)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_9044_3689" x1="11.688" y1="4" x2="11.688" y2="19.376" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_8958_15228" x1="10.688" y1="1" x2="10.688" y2="16.376" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint1_linear_9044_3689" x1="12" y1="8.41309" x2="12" y2="15.5881" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear_8958_15228" x1="11" y1="5.41309" x2="11" y2="12.5881" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint2_linear_9044_3689" x1="11.9956" y1="4.31323" x2="11.9956" y2="15.5882" gradientUnits="userSpaceOnUse">
<linearGradient id="paint2_linear_8958_15228" x1="10.9956" y1="1.31323" x2="10.9956" y2="12.5882" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -53,7 +53,7 @@ function Dropdown({
darkBorderColor?: string;
showEdit?: boolean;
onEdit?: (value: { name: string; id: string; type: string }) => void;
showDelete?: boolean | ((option: any) => boolean);
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
placeholderTextColor?: string;
@@ -173,15 +173,8 @@ function Dropdown({
)}
{showDelete && onDelete && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete?.(typeof option === 'string' ? option : option.id);
}}
className={`${
typeof showDelete === 'function' && !showDelete(option)
? 'hidden'
: ''
} mr-2 h-4 w-4 cursor-pointer hover:opacity-50`}
onClick={() => onDelete(option.id)}
disabled={option.type === 'public'}
>
<img
src={Trash}

View File

@@ -1,229 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { twMerge } from 'tailwind-merge';
import Cross from '../assets/cross.svg';
import ImagesIcon from '../assets/images.svg';
interface FileUploadProps {
onUpload: (files: File[]) => void;
onRemove?: (file: File) => void;
multiple?: boolean;
maxFiles?: number;
maxSize?: number; // in bytes
accept?: Record<string, string[]>; // e.g. { 'image/*': ['.png', '.jpg'] }
showPreview?: boolean;
previewSize?: number;
children?: React.ReactNode;
className?: string;
activeClassName?: string;
acceptClassName?: string;
rejectClassName?: string;
uploadText?: string | { text: string; colorClass?: string }[];
dragActiveText?: string;
fileTypeText?: string;
sizeLimitText?: string;
disabled?: boolean;
validator?: (file: File) => { isValid: boolean; error?: string };
}
export const FileUpload = ({
onUpload,
onRemove,
multiple = false,
maxFiles = 1,
maxSize = 5 * 1024 * 1024,
accept = { 'image/*': ['.jpeg', '.png', '.jpg'] },
showPreview = false,
previewSize = 80,
children,
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
activeClassName = 'border-blue-500 bg-blue-50',
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
uploadText = 'Click to upload or drag and drop',
dragActiveText = 'Drop the files here',
fileTypeText = 'PNG, JPG, JPEG up to',
sizeLimitText = 'MB',
disabled = false,
validator,
}: FileUploadProps) => {
const [errors, setErrors] = useState<string[]>([]);
const [preview, setPreview] = useState<string | null>(null);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const validateFile = (file: File) => {
const defaultValidation = {
isValid: true,
error: '',
};
if (validator) {
const customValidation = validator(file);
if (!customValidation.isValid) {
return customValidation;
}
}
if (file.size > maxSize) {
return {
isValid: false,
error: `File exceeds ${maxSize / 1024 / 1024}MB limit`,
};
}
return defaultValidation;
};
const onDrop = useCallback(
(acceptedFiles: File[], fileRejections: any[]) => {
setErrors([]);
if (fileRejections.length > 0) {
const newErrors = fileRejections
.map(({ errors }) => errors.map((e: any) => e.message))
.flat();
setErrors(newErrors);
return;
}
const validationResults = acceptedFiles.map(validateFile);
const invalidFiles = validationResults.filter((r) => !r.isValid);
if (invalidFiles.length > 0) {
setErrors(invalidFiles.map((f) => f.error!));
return;
}
const filesToUpload = multiple ? acceptedFiles : [acceptedFiles[0]];
onUpload(filesToUpload);
const file = multiple ? acceptedFiles[0] : acceptedFiles[0];
setCurrentFile(file);
if (showPreview && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => setPreview(reader.result as string);
reader.readAsDataURL(file);
}
},
[onUpload, multiple, maxSize, validator],
);
const {
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
} = useDropzone({
onDrop,
multiple,
maxFiles,
maxSize,
accept,
disabled,
});
const currentClassName = twMerge(
'border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
className,
isDragActive && activeClassName,
isDragAccept && acceptClassName,
isDragReject && rejectClassName,
disabled && 'opacity-50 cursor-not-allowed',
);
const handleRemove = () => {
setPreview(null);
setCurrentFile(null);
if (onRemove && currentFile) onRemove(currentFile);
};
const renderPreview = () => (
<div
className="relative"
style={{ width: previewSize, height: previewSize }}
>
<img
src={preview ?? undefined}
alt="preview"
className="h-full w-full rounded-md object-cover"
/>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemove();
}}
className="absolute -right-2 -top-2 rounded-full bg-[#7D54D1] p-1 transition-colors hover:bg-[#714cbc]"
>
<img src={Cross} alt="remove" className="h-3 w-3" />
</button>
</div>
);
const renderUploadText = () => {
if (Array.isArray(uploadText)) {
return (
<p className="text-sm font-semibold">
{uploadText.map((segment, i) => (
<span key={i} className={segment.colorClass || ''}>
{segment.text}
</span>
))}
</p>
);
}
return <p className="text-sm font-semibold">{uploadText}</p>;
};
const defaultContent = (
<div className="flex flex-col items-center gap-2">
{showPreview && preview ? (
renderPreview()
) : (
<div
style={{ width: previewSize, height: previewSize }}
className="flex items-center justify-center"
>
<img src={ImagesIcon} className="h-10 w-10" />
</div>
)}
<div className="text-center">
<div className="text-sm font-medium">
{isDragActive ? (
<p className="text-sm font-semibold">{dragActiveText}</p>
) : (
renderUploadText()
)}
</div>
<p className="mt-1 text-xs text-[#A3A3A3]">
{fileTypeText} {maxSize / 1024 / 1024}
{sizeLimitText}
</p>
</div>
</div>
);
return (
<div className="relative">
<div {...getRootProps({ className: currentClassName })}>
<input {...getInputProps()} />
{children || defaultContent}
{errors.length > 0 && (
<div className="absolute left-0 right-0 mt-[2px] px-4 text-xs text-red-600">
{errors.map((error, i) => (
<p key={i} className="truncate">
{error}
</p>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import 'katex/dist/katex.min.css';
import { forwardRef, Fragment, useRef, useState, useEffect } from 'react';
import { forwardRef, Fragment, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
@@ -90,24 +90,14 @@ const ConversationBubble = forwardRef<
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
const [isQuestionHovered, setIsQuestionHovered] = useState(false);
const [editInputBox, setEditInputBox] = useState<string>('');
const messageRef = useRef<HTMLDivElement>(null);
const [shouldShowToggle, setShouldShowToggle] = useState(false);
const [isLikeClicked, setIsLikeClicked] = useState(false);
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
const editableQueryRef = useRef<HTMLDivElement | null>(null);
const [isQuestionCollapsed, setIsQuestionCollapsed] = useState(true);
useOutsideAlerter(editableQueryRef, () => setIsEditClicked(false), [], true);
useEffect(() => {
if (messageRef.current) {
const height = messageRef.current.scrollHeight;
setShouldShowToggle(height > 84);
}
}, [message]);
const handleEditClick = () => {
setIsEditClicked(false);
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
@@ -156,31 +146,14 @@ const ConversationBubble = forwardRef<
/>
{!isEditClicked && (
<>
<div className="relative mr-2 flex w-full flex-col">
<div className="ml-2 mr-2 flex max-w-full items-start gap-2 whitespace-pre-wrap break-words rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-5 py-4 text-sm leading-normal text-white sm:text-base">
<div
ref={messageRef}
className={`${isQuestionCollapsed ? 'line-clamp-4' : ''} w-full`}
>
{message}
</div>
{shouldShowToggle && (
<button
onClick={(e) => {
e.stopPropagation();
setIsQuestionCollapsed(!isQuestionCollapsed);
}}
className="ml-1 rounded-full p-2 hover:bg-[#D9D9D933]"
>
<img
src={ChevronDown}
alt="Toggle"
width={24}
height={24}
className={`transform invert transition-transform duration-200 ${isQuestionCollapsed ? '' : 'rotate-180'}`}
/>
</button>
)}
<div className="mr-2 flex flex-col">
<div
style={{
wordBreak: 'break-word',
}}
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
>
{message}
</div>
</div>
<button

View File

@@ -11,7 +11,13 @@ import {
handleFetchAnswer,
handleFetchAnswerSteaming,
} from './conversationHandlers';
import { Answer, ConversationState, Query, Status } from './conversationModels';
import {
Answer,
Attachment,
ConversationState,
Query,
Status,
} from './conversationModels';
import { ToolCallsType } from './types';
const initialState: ConversationState = {
@@ -32,64 +38,65 @@ export function handleAbort() {
export const fetchAnswer = createAsyncThunk<
Answer,
{ question: string; indx?: number }
>('fetchAnswer', async ({ question, indx }, { dispatch, getState }) => {
if (abortController) abortController.abort();
abortController = new AbortController();
const { signal } = abortController;
{ question: string; indx?: number; isPreview?: boolean }
>(
'fetchAnswer',
async ({ question, indx, isPreview = false }, { dispatch, getState }) => {
if (abortController) abortController.abort();
abortController = new AbortController();
const { signal } = abortController;
let isSourceUpdated = false;
const state = getState() as RootState;
const attachmentIds = selectCompletedAttachments(state)
.filter((a) => a.id)
.map((a) => a.id) as string[];
let isSourceUpdated = false;
const state = getState() as RootState;
const attachmentIds = selectCompletedAttachments(state)
.filter((a) => a.id)
.map((a) => a.id) as string[];
if (attachmentIds.length > 0) {
dispatch(clearAttachments());
}
if (attachmentIds.length > 0) {
dispatch(clearAttachments());
}
const currentConversationId = state.conversation.conversationId;
const currentConversationId = state.conversation.conversationId;
const conversationIdToSend = isPreview ? null : currentConversationId;
const save_conversation = isPreview ? false : true;
if (state.preference) {
if (API_STREAMING) {
await handleFetchAnswerSteaming(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.conversation.queries,
currentConversationId,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
(event) => {
const data = JSON.parse(event.data);
const targetIndex = indx ?? state.conversation.queries.length - 1;
if (state.preference) {
if (API_STREAMING) {
await handleFetchAnswerSteaming(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.conversation.queries,
conversationIdToSend,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
(event) => {
const data = JSON.parse(event.data);
const targetIndex = indx ?? state.conversation.queries.length - 1;
// Only process events if they match the current conversation
if (currentConversationId === state.conversation.conversationId) {
if (data.type === 'end') {
dispatch(conversationSlice.actions.setStatus('idle'));
getConversations(state.preference.token)
.then((fetchedConversations) => {
dispatch(setConversations(fetchedConversations));
})
.catch((error) => {
console.error('Failed to fetch conversations: ', error);
});
if (!isPreview) {
getConversations(state.preference.token)
.then((fetchedConversations) => {
dispatch(setConversations(fetchedConversations));
})
.catch((error) => {
console.error('Failed to fetch conversations: ', error);
});
}
if (!isSourceUpdated) {
dispatch(
updateStreamingSource({
conversationId: currentConversationId,
index: targetIndex,
query: { sources: [] },
}),
);
}
} else if (data.type === 'id') {
// Only update the conversationId if it's currently null
const currentState = getState() as RootState;
if (currentState.conversation.conversationId === null) {
if (!isPreview) {
dispatch(
updateConversationId({
query: { conversationId: data.id },
@@ -100,7 +107,6 @@ export const fetchAnswer = createAsyncThunk<
const result = data.thought;
dispatch(
updateThought({
conversationId: currentConversationId,
index: targetIndex,
query: { thought: result },
}),
@@ -109,7 +115,6 @@ export const fetchAnswer = createAsyncThunk<
isSourceUpdated = true;
dispatch(
updateStreamingSource({
conversationId: currentConversationId,
index: targetIndex,
query: { sources: data.source ?? [] },
}),
@@ -126,7 +131,6 @@ export const fetchAnswer = createAsyncThunk<
dispatch(conversationSlice.actions.setStatus('failed'));
dispatch(
conversationSlice.actions.raiseError({
conversationId: currentConversationId,
index: targetIndex,
message: data.error,
}),
@@ -134,87 +138,88 @@ export const fetchAnswer = createAsyncThunk<
} else {
dispatch(
updateStreamingQuery({
conversationId: currentConversationId,
index: targetIndex,
query: { response: data.answer },
}),
);
}
}
},
indx,
state.preference.selectedAgent?.id,
attachmentIds,
true, // Always save conversation
);
} else {
const answer = await handleFetchAnswer(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
state.preference.selectedAgent?.id,
attachmentIds,
true, // Always save conversation
);
if (answer) {
let sourcesPrepped = [];
sourcesPrepped = answer.sources.map((source: { title: string }) => {
if (source && source.title) {
const titleParts = source.title.split('/');
return {
...source,
title: titleParts[titleParts.length - 1],
};
}
return source;
});
const targetIndex = indx ?? state.conversation.queries.length - 1;
dispatch(
updateQuery({
index: targetIndex,
query: {
response: answer.answer,
thought: answer.thought,
sources: sourcesPrepped,
tool_calls: answer.toolCalls,
},
}),
},
indx,
state.preference.selectedAgent?.id,
attachmentIds,
save_conversation,
);
dispatch(
updateConversationId({
query: { conversationId: answer.conversationId },
}),
} else {
const answer = await handleFetchAnswer(
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,
state.preference.token_limit,
state.preference.selectedAgent?.id,
attachmentIds,
save_conversation,
);
getConversations(state.preference.token)
.then((fetchedConversations) => {
dispatch(setConversations(fetchedConversations));
})
.catch((error) => {
console.error('Failed to fetch conversations: ', error);
if (answer) {
let sourcesPrepped = [];
sourcesPrepped = answer.sources.map((source: { title: string }) => {
if (source && source.title) {
const titleParts = source.title.split('/');
return {
...source,
title: titleParts[titleParts.length - 1],
};
}
return source;
});
dispatch(conversationSlice.actions.setStatus('idle'));
const targetIndex = indx ?? state.conversation.queries.length - 1;
dispatch(
updateQuery({
index: targetIndex,
query: {
response: answer.answer,
thought: answer.thought,
sources: sourcesPrepped,
tool_calls: answer.toolCalls,
},
}),
);
if (!isPreview) {
dispatch(
updateConversationId({
query: { conversationId: answer.conversationId },
}),
);
getConversations(state.preference.token)
.then((fetchedConversations) => {
dispatch(setConversations(fetchedConversations));
})
.catch((error) => {
console.error('Failed to fetch conversations: ', error);
});
}
dispatch(conversationSlice.actions.setStatus('idle'));
}
}
}
}
return {
conversationId: null,
title: null,
answer: '',
query: question,
result: '',
thought: '',
sources: [],
tool_calls: [],
};
});
return {
conversationId: null,
title: null,
answer: '',
query: question,
result: '',
thought: '',
sources: [],
tool_calls: [],
};
},
);
export const conversationSlice = createSlice({
name: 'conversation',
@@ -237,20 +242,18 @@ export const conversationSlice = createSlice({
},
updateStreamingQuery(
state,
action: PayloadAction<{
conversationId: string | null;
index: number;
query: Partial<Query>;
}>,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { conversationId, index, query } = action.payload;
// Only update if this update is for the current conversation
if (state.status === 'idle' || state.conversationId !== conversationId)
return;
if (state.status === 'idle') return;
const { index, query } = action.payload;
if (query.response != undefined) {
state.queries[index].response =
(state.queries[index].response || '') + query.response;
} else {
state.queries[index] = {
...state.queries[index],
...query,
};
}
},
updateConversationId(
@@ -262,35 +265,28 @@ export const conversationSlice = createSlice({
},
updateThought(
state,
action: PayloadAction<{
conversationId: string | null;
index: number;
query: Partial<Query>;
}>,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { conversationId, index, query } = action.payload;
if (state.conversationId !== conversationId) return;
const { index, query } = action.payload;
if (query.thought != undefined) {
state.queries[index].thought =
(state.queries[index].thought || '') + query.thought;
} else {
state.queries[index] = {
...state.queries[index],
...query,
};
}
},
updateStreamingSource(
state,
action: PayloadAction<{
conversationId: string | null;
index: number;
query: Partial<Query>;
}>,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { conversationId, index, query } = action.payload;
if (state.conversationId !== conversationId) return;
const { index, query } = action.payload;
if (!state.queries[index].sources) {
state.queries[index].sources = query?.sources;
} else if (query.sources) {
state.queries[index].sources!.push(...query.sources);
} else {
state.queries[index].sources!.push(query.sources![0]);
}
},
updateToolCall(state, action) {
@@ -327,15 +323,9 @@ export const conversationSlice = createSlice({
},
raiseError(
state,
action: PayloadAction<{
conversationId: string | null;
index: number;
message: string;
}>,
action: PayloadAction<{ index: number; message: string }>,
) {
const { conversationId, index, message } = action.payload;
if (state.conversationId !== conversationId) return;
const { index, message } = action.payload;
state.queries[index].error = message;
},

View File

@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -94,40 +94,24 @@ export default function AgentDetailsModal({
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
Public Link
</h2>
</div>
{sharedToken ? (
<div className="flex flex-col gap-2">
<p className="inline break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
<a
href={`${baseURL}/shared/agent/${sharedToken}`}
target="_blank"
rel="noreferrer"
>
{`${baseURL}/shared/agent/${sharedToken}`}
</a>
{sharedToken && (
<div className="mb-1">
<CopyButton
textToCopy={`${baseURL}/shared/agent/${sharedToken}`}
padding="p-1"
className="absolute -mt-0.5 ml-1 inline-flex"
/>
</div>
)}
</div>
{sharedToken ? (
<div className="flex flex-col flex-wrap items-start gap-2">
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
{`${baseURL}/shared/agent/${sharedToken}`}
</p>
<a
href="https://docs.docsgpt.cloud/Agents/basics#core-components-of-an-agent"
className="flex w-fit items-center gap-1 text-purple-30 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
<span className="text-sm">Learn more</span>
<img
src="/src/assets/external-link.svg"
alt="External link"
className="h-3 w-3"
/>
</a>
</div>
) : (
<button
className="flex w-28 items-center justify-center rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
className="hover:bg-vi</button>olets-are-blue flex w-28 items-center justify-center rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
onClick={handleGeneratePublicLink}
>
{loadingStates.publicLink ? (
@@ -143,37 +127,11 @@ export default function AgentDetailsModal({
API Key
</h2>
{apiKey ? (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
{apiKey}
{!apiKey.includes('...') && (
<CopyButton
textToCopy={apiKey}
padding="p-1"
className="absolute -mt-0.5 ml-1 inline-flex"
/>
)}
</div>
{!apiKey.includes('...') && (
<a
href={`https://widget.docsgpt.cloud/?api-key=${apiKey}`}
className="group ml-8 flex w-[101px] items-center justify-center gap-1 rounded-[62px] border border-purple-30 py-1.5 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
target="_blank"
rel="noopener noreferrer"
>
Test
<img
src="/src/assets/external-link.svg"
alt="External link"
className="h-3 w-3 group-hover:brightness-0 group-hover:invert"
/>
</a>
)}
</div>
</div>
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
{apiKey}
</span>
) : (
<button className="w-28 rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white">
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
Generate
</button>
)}
@@ -183,36 +141,21 @@ export default function AgentDetailsModal({
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
Webhook URL
</h2>
{webhookUrl && (
<div className="mb-1">
<CopyButton textToCopy={webhookUrl} padding="p-1" />
</div>
)}
</div>
{webhookUrl ? (
<div className="flex flex-col gap-2">
<p className="break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
<a href={webhookUrl} target="_blank" rel="noreferrer">
{webhookUrl}
</a>
<CopyButton
textToCopy={webhookUrl}
padding="p-1"
className="absolute -mt-0.5 ml-1 inline-flex"
/>
<div className="flex flex-col flex-wrap items-start gap-2">
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
{webhookUrl}
</p>
<a
href="https://docs.docsgpt.cloud/Agents/basics#core-components-of-an-agent"
className="flex w-fit items-center gap-1 text-purple-30 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
<span className="text-sm">Learn more</span>
<img
src="/src/assets/external-link.svg"
alt="External link"
className="h-3 w-3"
/>
</a>
</div>
) : (
<button
className="flex w-28 items-center justify-center rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
className="hover:bg-vi</button>olets-are-blue flex w-28 items-center justify-center rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
onClick={handleGenerateWebhook}
>
{loadingStates.webhook ? (

View File

@@ -177,7 +177,7 @@ export default function Prompts({
rounded="3xl"
border="border"
showEdit
showDelete={(prompt) => prompt.type !== 'public'}
showDelete
onEdit={({
id,
name,

View File

@@ -8,7 +8,6 @@ import {
prefSlice,
} from './preferences/preferenceSlice';
import uploadReducer from './upload/uploadSlice';
import agentPreviewReducer from './agents/agentPreviewSlice';
const key = localStorage.getItem('DocsGPTApiKey');
const prompt = localStorage.getItem('DocsGPTPrompt');
@@ -55,7 +54,6 @@ const store = configureStore({
conversation: conversationSlice.reducer,
sharedConversation: sharedConversationSlice.reducer,
upload: uploadReducer,
agentPreview: agentPreviewReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),

View File

@@ -4,9 +4,6 @@ module.exports = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
'roboto': ['Roboto', 'sans-serif'],
},
spacing: {
112: '28rem',
128: '32rem',