update latest changes
20
README.md
@@ -49,11 +49,11 @@
|
|||||||
- [x] Manually updating chunks in the app UI (Feb 2025)
|
- [x] Manually updating chunks in the app UI (Feb 2025)
|
||||||
- [x] Devcontainer for easy development (Feb 2025)
|
- [x] Devcontainer for easy development (Feb 2025)
|
||||||
- [x] ReACT agent (March 2025)
|
- [x] ReACT agent (March 2025)
|
||||||
- [ ] Anthropic Tool compatibility
|
- [ ] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
|
||||||
- [ ] New input box in the conversation menu
|
- [ ] New input box in the conversation menu (April 2025)
|
||||||
- [ ] Add triggerable actions / tools (webhook)
|
- [ ] Anthropic Tool compatibility (April 2025)
|
||||||
|
- [ ] Add triggerable actions / tools (webhook) (April 2025)
|
||||||
- [ ] Add OAuth 2.0 authentication for tools and sources
|
- [ ] Add OAuth 2.0 authentication for tools and sources
|
||||||
- [ ] Chatbots menu re-design to handle tools, agent types, and more
|
|
||||||
- [ ] Agent scheduling
|
- [ ] 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!
|
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!
|
||||||
@@ -95,13 +95,15 @@ A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available
|
|||||||
./setup.sh
|
./setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This interactive script will guide you through setting up DocsGPT. It offers four options: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. The script will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
|
|
||||||
|
|
||||||
**For Windows:**
|
**For Windows:**
|
||||||
|
|
||||||
2. **Follow the Docker Deployment Guide:**
|
2. **Run the PowerShell setup script:**
|
||||||
|
|
||||||
Please refer to the [Docker Deployment documentation](https://docs.docsgpt.cloud/Deploying/Docker-Deploying) for detailed step-by-step instructions on setting up DocsGPT using Docker.
|
```powershell
|
||||||
|
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Either script will guide you through setting up DocsGPT. Four options available: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. Scripts will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
|
||||||
|
|
||||||
**Navigate to http://localhost:5173/**
|
**Navigate to http://localhost:5173/**
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ To stop DocsGPT, open a terminal in the `DocsGPT` directory and run:
|
|||||||
```bash
|
```bash
|
||||||
docker compose -f deployment/docker-compose.yaml down
|
docker compose -f deployment/docker-compose.yaml down
|
||||||
```
|
```
|
||||||
(or use the specific `docker compose down` command shown after running `setup.sh`).
|
(or use the specific `docker compose down` command shown after running the setup script).
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
|
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from application.core.mongo_db import MongoDB
|
|||||||
from application.llm.llm_creator import LLMCreator
|
from application.llm.llm_creator import LLMCreator
|
||||||
from application.logging import build_stack_data, log_activity, LogContext
|
from application.logging import build_stack_data, log_activity, LogContext
|
||||||
from application.retriever.base import BaseRetriever
|
from application.retriever.base import BaseRetriever
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(ABC):
|
class BaseAgent(ABC):
|
||||||
@@ -23,7 +24,7 @@ class BaseAgent(ABC):
|
|||||||
prompt: str = "",
|
prompt: str = "",
|
||||||
chat_history: Optional[List[Dict]] = None,
|
chat_history: Optional[List[Dict]] = None,
|
||||||
decoded_token: Optional[Dict] = None,
|
decoded_token: Optional[Dict] = None,
|
||||||
attachments: Optional[List[Dict]]=None,
|
attachments: Optional[List[Dict]] = None,
|
||||||
):
|
):
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.llm_name = llm_name
|
self.llm_name = llm_name
|
||||||
@@ -58,6 +59,27 @@ class BaseAgent(ABC):
|
|||||||
) -> Generator[Dict, None, None]:
|
) -> Generator[Dict, None, None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _get_tools(self, api_key: str = None) -> Dict[str, Dict]:
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo["docsgpt"]
|
||||||
|
agents_collection = db["agents"]
|
||||||
|
tools_collection = db["user_tools"]
|
||||||
|
|
||||||
|
agent_data = agents_collection.find_one({"key": api_key or self.user_api_key})
|
||||||
|
tool_ids = agent_data.get("tools", []) if agent_data else []
|
||||||
|
|
||||||
|
tools = (
|
||||||
|
tools_collection.find(
|
||||||
|
{"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}}
|
||||||
|
)
|
||||||
|
if tool_ids
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
tools = list(tools)
|
||||||
|
tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {}
|
||||||
|
|
||||||
|
return tools_by_id
|
||||||
|
|
||||||
def _get_user_tools(self, user="local"):
|
def _get_user_tools(self, user="local"):
|
||||||
mongo = MongoDB.get_client()
|
mongo = MongoDB.get_client()
|
||||||
db = mongo["docsgpt"]
|
db = mongo["docsgpt"]
|
||||||
@@ -243,9 +265,11 @@ class BaseAgent(ABC):
|
|||||||
tools_dict: Dict,
|
tools_dict: Dict,
|
||||||
messages: List[Dict],
|
messages: List[Dict],
|
||||||
log_context: Optional[LogContext] = None,
|
log_context: Optional[LogContext] = None,
|
||||||
attachments: Optional[List[Dict]] = None
|
attachments: Optional[List[Dict]] = None,
|
||||||
):
|
):
|
||||||
resp = self.llm_handler.handle_response(self, resp, tools_dict, messages, attachments)
|
resp = self.llm_handler.handle_response(
|
||||||
|
self, resp, tools_dict, messages, attachments
|
||||||
|
)
|
||||||
if log_context:
|
if log_context:
|
||||||
data = build_stack_data(self.llm_handler)
|
data = build_stack_data(self.llm_handler)
|
||||||
log_context.stacks.append({"component": "llm_handler", "data": data})
|
log_context.stacks.append({"component": "llm_handler", "data": data})
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ from application.logging import LogContext
|
|||||||
|
|
||||||
from application.retriever.base import BaseRetriever
|
from application.retriever.base import BaseRetriever
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ClassicAgent(BaseAgent):
|
class ClassicAgent(BaseAgent):
|
||||||
def _gen_inner(
|
def _gen_inner(
|
||||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||||
) -> Generator[Dict, None, None]:
|
) -> Generator[Dict, None, None]:
|
||||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||||
|
if self.user_api_key:
|
||||||
tools_dict = self._get_user_tools(self.user)
|
tools_dict = self._get_tools(self.user_api_key)
|
||||||
|
else:
|
||||||
|
tools_dict = self._get_user_tools(self.user)
|
||||||
self._prepare_tools(tools_dict)
|
self._prepare_tools(tools_dict)
|
||||||
|
|
||||||
messages = self._build_messages(self.prompt, query, retrieved_data)
|
messages = self._build_messages(self.prompt, query, retrieved_data)
|
||||||
@@ -33,7 +37,7 @@ class ClassicAgent(BaseAgent):
|
|||||||
yield {"answer": resp.message.content}
|
yield {"answer": resp.message.content}
|
||||||
return
|
return
|
||||||
|
|
||||||
resp = self._llm_handler(resp, tools_dict, messages, log_context,attachments)
|
resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
|
||||||
|
|
||||||
if isinstance(resp, str):
|
if isinstance(resp, str):
|
||||||
yield {"answer": resp}
|
yield {"answer": resp}
|
||||||
@@ -44,10 +48,13 @@ class ClassicAgent(BaseAgent):
|
|||||||
):
|
):
|
||||||
yield {"answer": resp.message.content}
|
yield {"answer": resp.message.content}
|
||||||
else:
|
else:
|
||||||
completion = self.llm.gen_stream(
|
# completion = self.llm.gen_stream(
|
||||||
model=self.gpt_model, messages=messages, tools=self.tools
|
# model=self.gpt_model, messages=messages, tools=self.tools
|
||||||
)
|
# )
|
||||||
for line in completion:
|
# log type of resp
|
||||||
|
logger.info(f"Response type: {type(resp)}")
|
||||||
|
logger.info(f"Response: {resp}")
|
||||||
|
for line in resp:
|
||||||
if isinstance(line, str):
|
if isinstance(line, str):
|
||||||
yield {"answer": line}
|
yield {"answer": line}
|
||||||
|
|
||||||
|
|||||||
@@ -33,15 +33,53 @@ class LLMHandler(ABC):
|
|||||||
|
|
||||||
logger.info(f"Preparing messages with {len(attachments)} attachments")
|
logger.info(f"Preparing messages with {len(attachments)} attachments")
|
||||||
|
|
||||||
# Check if the LLM has its own custom attachment handling implementation
|
supported_types = agent.llm.get_supported_attachment_types()
|
||||||
if hasattr(agent.llm, "prepare_messages_with_attachments") and agent.llm.__class__.__name__ != "BaseLLM":
|
|
||||||
logger.info(f"Using {agent.llm.__class__.__name__}'s own prepare_messages_with_attachments method")
|
|
||||||
return agent.llm.prepare_messages_with_attachments(messages, attachments)
|
|
||||||
|
|
||||||
# Otherwise, append attachment content to the system prompt
|
supported_attachments = []
|
||||||
|
unsupported_attachments = []
|
||||||
|
|
||||||
|
for attachment in attachments:
|
||||||
|
mime_type = attachment.get('mime_type')
|
||||||
|
if not mime_type:
|
||||||
|
import mimetypes
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if file_path:
|
||||||
|
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||||
|
else:
|
||||||
|
unsupported_attachments.append(attachment)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mime_type in supported_types:
|
||||||
|
supported_attachments.append(attachment)
|
||||||
|
else:
|
||||||
|
unsupported_attachments.append(attachment)
|
||||||
|
|
||||||
|
# Process supported attachments with the LLM's custom method
|
||||||
|
prepared_messages = messages
|
||||||
|
if supported_attachments:
|
||||||
|
logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method")
|
||||||
|
prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments)
|
||||||
|
|
||||||
|
# Process unsupported attachments with the default method
|
||||||
|
if unsupported_attachments:
|
||||||
|
logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method")
|
||||||
|
prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments)
|
||||||
|
|
||||||
|
return prepared_messages
|
||||||
|
|
||||||
|
def _append_attachment_content_to_system(self, messages, attachments):
|
||||||
|
"""
|
||||||
|
Default method to append attachment content to the system prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages (list): List of message dictionaries.
|
||||||
|
attachments (list): List of attachment dictionaries with content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Messages with attachment context added to the system prompt.
|
||||||
|
"""
|
||||||
prepared_messages = messages.copy()
|
prepared_messages = messages.copy()
|
||||||
|
|
||||||
# Build attachment content string
|
|
||||||
attachment_texts = []
|
attachment_texts = []
|
||||||
for attachment in attachments:
|
for attachment in attachments:
|
||||||
logger.info(f"Adding attachment {attachment.get('id')} to context")
|
logger.info(f"Adding attachment {attachment.get('id')} to context")
|
||||||
@@ -122,12 +160,13 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
text_buffer = ""
|
||||||
while True:
|
while True:
|
||||||
tool_calls = {}
|
tool_calls = {}
|
||||||
for chunk in resp:
|
for chunk in resp:
|
||||||
if isinstance(chunk, str) and len(chunk) > 0:
|
if isinstance(chunk, str) and len(chunk) > 0:
|
||||||
return
|
yield chunk
|
||||||
|
continue
|
||||||
elif hasattr(chunk, "delta"):
|
elif hasattr(chunk, "delta"):
|
||||||
chunk_delta = chunk.delta
|
chunk_delta = chunk.delta
|
||||||
|
|
||||||
@@ -206,12 +245,17 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
tool_calls = {}
|
tool_calls = {}
|
||||||
|
if hasattr(chunk_delta, "content") and chunk_delta.content:
|
||||||
|
# Add to buffer or yield immediately based on your preference
|
||||||
|
text_buffer += chunk_delta.content
|
||||||
|
yield text_buffer
|
||||||
|
text_buffer = ""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasattr(chunk, "finish_reason")
|
hasattr(chunk, "finish_reason")
|
||||||
and chunk.finish_reason == "stop"
|
and chunk.finish_reason == "stop"
|
||||||
):
|
):
|
||||||
return
|
return resp
|
||||||
elif isinstance(chunk, str) and len(chunk) == 0:
|
elif isinstance(chunk, str) and len(chunk) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -298,6 +342,9 @@ class GoogleLLMHandler(LLMHandler):
|
|||||||
"content": [function_response_part.to_json_dict()],
|
"content": [function_response_part.to_json_dict()],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
tool_call_found = False
|
||||||
|
yield result
|
||||||
|
|
||||||
if not tool_call_found:
|
if not tool_call_found:
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ class ReActAgent(BaseAgent):
|
|||||||
) -> Generator[Dict, None, None]:
|
) -> Generator[Dict, None, None]:
|
||||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||||
|
|
||||||
tools_dict = self._get_user_tools(self.user)
|
if self.user_api_key:
|
||||||
|
tools_dict = self._get_tools(self.user_api_key)
|
||||||
|
else:
|
||||||
|
tools_dict = self._get_user_tools(self.user)
|
||||||
self._prepare_tools(tools_dict)
|
self._prepare_tools(tools_dict)
|
||||||
|
|
||||||
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
|
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ db = mongo["docsgpt"]
|
|||||||
conversations_collection = db["conversations"]
|
conversations_collection = db["conversations"]
|
||||||
sources_collection = db["sources"]
|
sources_collection = db["sources"]
|
||||||
prompts_collection = db["prompts"]
|
prompts_collection = db["prompts"]
|
||||||
api_key_collection = db["api_keys"]
|
agents_collection = db["agents"]
|
||||||
user_logs_collection = db["user_logs"]
|
user_logs_collection = db["user_logs"]
|
||||||
attachments_collection = db["attachments"]
|
attachments_collection = db["attachments"]
|
||||||
|
|
||||||
@@ -86,19 +86,42 @@ def run_async_chain(chain, question, chat_history):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_data_from_api_key(api_key):
|
def get_agent_key(agent_id, user_id):
|
||||||
data = api_key_collection.find_one({"key": api_key})
|
if not agent_id:
|
||||||
# # Raise custom exception if the API key is not found
|
return None
|
||||||
if data is None:
|
|
||||||
raise Exception("Invalid API Key, please generate new key", 401)
|
|
||||||
|
|
||||||
if "source" in data and isinstance(data["source"], DBRef):
|
try:
|
||||||
source_doc = db.dereference(data["source"])
|
agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
|
||||||
|
if agent is None:
|
||||||
|
raise Exception("Agent not found", 404)
|
||||||
|
|
||||||
|
if agent.get("user") == user_id:
|
||||||
|
agents_collection.update_one(
|
||||||
|
{"_id": ObjectId(agent_id)},
|
||||||
|
{"$set": {"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)}},
|
||||||
|
)
|
||||||
|
return str(agent["key"])
|
||||||
|
|
||||||
|
raise Exception("Unauthorized access to the agent", 403)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_agent_key: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_from_api_key(api_key):
|
||||||
|
data = agents_collection.find_one({"key": api_key})
|
||||||
|
if not data:
|
||||||
|
raise Exception("Invalid API Key, please generate a new key", 401)
|
||||||
|
|
||||||
|
source = data.get("source")
|
||||||
|
if isinstance(source, DBRef):
|
||||||
|
source_doc = db.dereference(source)
|
||||||
data["source"] = str(source_doc["_id"])
|
data["source"] = str(source_doc["_id"])
|
||||||
if "retriever" in source_doc:
|
data["retriever"] = source_doc.get("retriever", data.get("retriever"))
|
||||||
data["retriever"] = source_doc["retriever"]
|
|
||||||
else:
|
else:
|
||||||
data["source"] = {}
|
data["source"] = {}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -128,7 +151,8 @@ def save_conversation(
|
|||||||
llm,
|
llm,
|
||||||
decoded_token,
|
decoded_token,
|
||||||
index=None,
|
index=None,
|
||||||
api_key=None
|
api_key=None,
|
||||||
|
agent_id=None,
|
||||||
):
|
):
|
||||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
current_time = datetime.datetime.now(datetime.timezone.utc)
|
||||||
if conversation_id is not None and index is not None:
|
if conversation_id is not None and index is not None:
|
||||||
@@ -202,7 +226,9 @@ def save_conversation(
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
if api_key:
|
if api_key:
|
||||||
api_key_doc = api_key_collection.find_one({"key": api_key})
|
if agent_id:
|
||||||
|
conversation_data["agent_id"] = agent_id
|
||||||
|
api_key_doc = agents_collection.find_one({"key": api_key})
|
||||||
if api_key_doc:
|
if api_key_doc:
|
||||||
conversation_data["api_key"] = api_key_doc["key"]
|
conversation_data["api_key"] = api_key_doc["key"]
|
||||||
conversation_id = conversations_collection.insert_one(
|
conversation_id = conversations_collection.insert_one(
|
||||||
@@ -234,6 +260,7 @@ def complete_stream(
|
|||||||
index=None,
|
index=None,
|
||||||
should_save_conversation=True,
|
should_save_conversation=True,
|
||||||
attachments=None,
|
attachments=None,
|
||||||
|
agent_id=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
||||||
@@ -241,7 +268,9 @@ def complete_stream(
|
|||||||
|
|
||||||
if attachments:
|
if attachments:
|
||||||
attachment_ids = [attachment["id"] for attachment in attachments]
|
attachment_ids = [attachment["id"] for attachment in attachments]
|
||||||
logger.info(f"Processing request with {len(attachments)} attachments: {attachment_ids}")
|
logger.info(
|
||||||
|
f"Processing request with {len(attachments)} attachments: {attachment_ids}"
|
||||||
|
)
|
||||||
|
|
||||||
answer = agent.gen(query=question, retriever=retriever)
|
answer = agent.gen(query=question, retriever=retriever)
|
||||||
|
|
||||||
@@ -294,7 +323,8 @@ def complete_stream(
|
|||||||
llm,
|
llm,
|
||||||
decoded_token,
|
decoded_token,
|
||||||
index,
|
index,
|
||||||
api_key=user_api_key
|
api_key=user_api_key,
|
||||||
|
agent_id=agent_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
conversation_id = None
|
conversation_id = None
|
||||||
@@ -366,7 +396,9 @@ class Stream(Resource):
|
|||||||
required=False, description="Index of the query to update"
|
required=False, description="Index of the query to update"
|
||||||
),
|
),
|
||||||
"save_conversation": fields.Boolean(
|
"save_conversation": fields.Boolean(
|
||||||
required=False, default=True, description="Whether to save the conversation"
|
required=False,
|
||||||
|
default=True,
|
||||||
|
description="Whether to save the conversation",
|
||||||
),
|
),
|
||||||
"attachments": fields.List(
|
"attachments": fields.List(
|
||||||
fields.String, required=False, description="List of attachment IDs"
|
fields.String, required=False, description="List of attachment IDs"
|
||||||
@@ -400,6 +432,14 @@ class Stream(Resource):
|
|||||||
chunks = int(data.get("chunks", 2))
|
chunks = int(data.get("chunks", 2))
|
||||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||||
retriever_name = data.get("retriever", "classic")
|
retriever_name = data.get("retriever", "classic")
|
||||||
|
agent_id = data.get("agent_id", None)
|
||||||
|
agent_type = settings.AGENT_NAME
|
||||||
|
agent_key = get_agent_key(agent_id, request.decoded_token.get("sub"))
|
||||||
|
|
||||||
|
if agent_key:
|
||||||
|
data.update({"api_key": agent_key})
|
||||||
|
else:
|
||||||
|
agent_id = None
|
||||||
|
|
||||||
if "api_key" in data:
|
if "api_key" in data:
|
||||||
data_key = get_data_from_api_key(data["api_key"])
|
data_key = get_data_from_api_key(data["api_key"])
|
||||||
@@ -408,6 +448,7 @@ class Stream(Resource):
|
|||||||
source = {"active_docs": data_key.get("source")}
|
source = {"active_docs": data_key.get("source")}
|
||||||
retriever_name = data_key.get("retriever", retriever_name)
|
retriever_name = data_key.get("retriever", retriever_name)
|
||||||
user_api_key = data["api_key"]
|
user_api_key = data["api_key"]
|
||||||
|
agent_type = data_key.get("agent_type", agent_type)
|
||||||
decoded_token = {"sub": data_key.get("user")}
|
decoded_token = {"sub": data_key.get("user")}
|
||||||
|
|
||||||
elif "active_docs" in data:
|
elif "active_docs" in data:
|
||||||
@@ -424,7 +465,9 @@ class Stream(Resource):
|
|||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response({"error": "Unauthorized"}, 401)
|
return make_response({"error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
attachments = get_attachments_content(attachment_ids, decoded_token.get("sub"))
|
attachments = get_attachments_content(
|
||||||
|
attachment_ids, decoded_token.get("sub")
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"/stream - request_data: {data}, source: {source}, attachments: {len(attachments)}",
|
f"/stream - request_data: {data}, source: {source}, attachments: {len(attachments)}",
|
||||||
@@ -436,7 +479,7 @@ class Stream(Resource):
|
|||||||
chunks = 0
|
chunks = 0
|
||||||
|
|
||||||
agent = AgentCreator.create_agent(
|
agent = AgentCreator.create_agent(
|
||||||
settings.AGENT_NAME,
|
agent_type,
|
||||||
endpoint="stream",
|
endpoint="stream",
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_NAME,
|
||||||
gpt_model=gpt_model,
|
gpt_model=gpt_model,
|
||||||
@@ -471,6 +514,7 @@ class Stream(Resource):
|
|||||||
isNoneDoc=data.get("isNoneDoc"),
|
isNoneDoc=data.get("isNoneDoc"),
|
||||||
index=index,
|
index=index,
|
||||||
should_save_conversation=save_conv,
|
should_save_conversation=save_conv,
|
||||||
|
agent_id=agent_id,
|
||||||
),
|
),
|
||||||
mimetype="text/event-stream",
|
mimetype="text/event-stream",
|
||||||
)
|
)
|
||||||
@@ -552,6 +596,7 @@ class Answer(Resource):
|
|||||||
chunks = int(data.get("chunks", 2))
|
chunks = int(data.get("chunks", 2))
|
||||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||||
retriever_name = data.get("retriever", "classic")
|
retriever_name = data.get("retriever", "classic")
|
||||||
|
agent_type = settings.AGENT_NAME
|
||||||
|
|
||||||
if "api_key" in data:
|
if "api_key" in data:
|
||||||
data_key = get_data_from_api_key(data["api_key"])
|
data_key = get_data_from_api_key(data["api_key"])
|
||||||
@@ -560,6 +605,7 @@ class Answer(Resource):
|
|||||||
source = {"active_docs": data_key.get("source")}
|
source = {"active_docs": data_key.get("source")}
|
||||||
retriever_name = data_key.get("retriever", retriever_name)
|
retriever_name = data_key.get("retriever", retriever_name)
|
||||||
user_api_key = data["api_key"]
|
user_api_key = data["api_key"]
|
||||||
|
agent_type = data_key.get("agent_type", agent_type)
|
||||||
decoded_token = {"sub": data_key.get("user")}
|
decoded_token = {"sub": data_key.get("user")}
|
||||||
|
|
||||||
elif "active_docs" in data:
|
elif "active_docs" in data:
|
||||||
@@ -584,7 +630,7 @@ class Answer(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
agent = AgentCreator.create_agent(
|
agent = AgentCreator.create_agent(
|
||||||
settings.AGENT_NAME,
|
agent_type,
|
||||||
endpoint="api/answer",
|
endpoint="api/answer",
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_NAME,
|
||||||
gpt_model=gpt_model,
|
gpt_model=gpt_model,
|
||||||
@@ -611,6 +657,7 @@ class Answer(Resource):
|
|||||||
source_log_docs = []
|
source_log_docs = []
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
stream_ended = False
|
stream_ended = False
|
||||||
|
thought = ""
|
||||||
|
|
||||||
for line in complete_stream(
|
for line in complete_stream(
|
||||||
question=question,
|
question=question,
|
||||||
@@ -633,6 +680,8 @@ class Answer(Resource):
|
|||||||
source_log_docs = event["source"]
|
source_log_docs = event["source"]
|
||||||
elif event["type"] == "tool_calls":
|
elif event["type"] == "tool_calls":
|
||||||
tool_calls = event["tool_calls"]
|
tool_calls = event["tool_calls"]
|
||||||
|
elif event["type"] == "thought":
|
||||||
|
thought = event["thought"]
|
||||||
elif event["type"] == "error":
|
elif event["type"] == "error":
|
||||||
logger.error(f"Error from stream: {event['error']}")
|
logger.error(f"Error from stream: {event['error']}")
|
||||||
return bad_request(500, event["error"])
|
return bad_request(500, event["error"])
|
||||||
@@ -664,6 +713,7 @@ class Answer(Resource):
|
|||||||
conversation_id,
|
conversation_id,
|
||||||
question,
|
question,
|
||||||
response_full,
|
response_full,
|
||||||
|
thought,
|
||||||
source_log_docs,
|
source_log_docs,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
llm,
|
llm,
|
||||||
@@ -825,18 +875,12 @@ def get_attachments_content(attachment_ids, user):
|
|||||||
attachments = []
|
attachments = []
|
||||||
for attachment_id in attachment_ids:
|
for attachment_id in attachment_ids:
|
||||||
try:
|
try:
|
||||||
attachment_doc = attachments_collection.find_one({
|
attachment_doc = attachments_collection.find_one(
|
||||||
"_id": ObjectId(attachment_id),
|
{"_id": ObjectId(attachment_id), "user": user}
|
||||||
"user": user
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if attachment_doc:
|
if attachment_doc:
|
||||||
attachments.append({
|
attachments.append(attachment_doc)
|
||||||
"id": str(attachment_doc["_id"]),
|
|
||||||
"content": attachment_doc["content"],
|
|
||||||
"token_count": attachment_doc.get("token_count", 0),
|
|
||||||
"path": attachment_doc.get("path", "")
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error retrieving attachment {attachment_id}: {e}")
|
logger.error(f"Error retrieving attachment {attachment_id}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ conversations_collection = db["conversations"]
|
|||||||
sources_collection = db["sources"]
|
sources_collection = db["sources"]
|
||||||
prompts_collection = db["prompts"]
|
prompts_collection = db["prompts"]
|
||||||
feedback_collection = db["feedback"]
|
feedback_collection = db["feedback"]
|
||||||
api_key_collection = db["api_keys"]
|
agents_collection = db["agents"]
|
||||||
token_usage_collection = db["token_usage"]
|
token_usage_collection = db["token_usage"]
|
||||||
shared_conversations_collections = db["shared_conversations"]
|
shared_conversations_collections = db["shared_conversations"]
|
||||||
user_logs_collection = db["user_logs"]
|
user_logs_collection = db["user_logs"]
|
||||||
@@ -138,14 +138,24 @@ class GetConversations(Resource):
|
|||||||
try:
|
try:
|
||||||
conversations = (
|
conversations = (
|
||||||
conversations_collection.find(
|
conversations_collection.find(
|
||||||
{"api_key": {"$exists": False}, "user": decoded_token.get("sub")}
|
{
|
||||||
|
"$or": [
|
||||||
|
{"api_key": {"$exists": False}},
|
||||||
|
{"agent_id": {"$exists": True}},
|
||||||
|
],
|
||||||
|
"user": decoded_token.get("sub"),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.sort("date", -1)
|
.sort("date", -1)
|
||||||
.limit(30)
|
.limit(30)
|
||||||
)
|
)
|
||||||
|
|
||||||
list_conversations = [
|
list_conversations = [
|
||||||
{"id": str(conversation["_id"]), "name": conversation["name"]}
|
{
|
||||||
|
"id": str(conversation["_id"]),
|
||||||
|
"name": conversation["name"],
|
||||||
|
"agent_id": conversation.get("agent_id", None),
|
||||||
|
}
|
||||||
for conversation in conversations
|
for conversation in conversations
|
||||||
]
|
]
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -179,7 +189,12 @@ class GetSingleConversation(Resource):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error retrieving conversation: {err}")
|
current_app.logger.error(f"Error retrieving conversation: {err}")
|
||||||
return make_response(jsonify({"success": False}), 400)
|
return make_response(jsonify({"success": False}), 400)
|
||||||
return make_response(jsonify(conversation["queries"]), 200)
|
|
||||||
|
data = {
|
||||||
|
"queries": conversation["queries"],
|
||||||
|
"agent_id": conversation.get("agent_id"),
|
||||||
|
}
|
||||||
|
return make_response(jsonify(data), 200)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/update_conversation_name")
|
@user_ns.route("/api/update_conversation_name")
|
||||||
@@ -920,124 +935,398 @@ class UpdatePrompt(Resource):
|
|||||||
return make_response(jsonify({"success": True}), 200)
|
return make_response(jsonify({"success": True}), 200)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/get_api_keys")
|
@user_ns.route("/api/get_agent")
|
||||||
class GetApiKeys(Resource):
|
class GetAgent(Resource):
|
||||||
@api.doc(description="Retrieve API keys for the user")
|
@api.doc(params={"id": "ID of the agent"}, description="Get a single agent by ID")
|
||||||
|
def get(self):
|
||||||
|
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": user}
|
||||||
|
)
|
||||||
|
if not agent:
|
||||||
|
return make_response(jsonify({"status": "Not found"}), 404)
|
||||||
|
data = {
|
||||||
|
"id": str(agent["_id"]),
|
||||||
|
"name": agent["name"],
|
||||||
|
"description": agent["description"],
|
||||||
|
"source": (
|
||||||
|
str(db.dereference(agent["source"])["_id"])
|
||||||
|
if "source" in agent and isinstance(agent["source"], DBRef)
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"chunks": agent["chunks"],
|
||||||
|
"retriever": agent.get("retriever", ""),
|
||||||
|
"prompt_id": agent["prompt_id"],
|
||||||
|
"tools": agent.get("tools", []),
|
||||||
|
"agent_type": agent["agent_type"],
|
||||||
|
"status": agent["status"],
|
||||||
|
"createdAt": agent["createdAt"],
|
||||||
|
"updatedAt": agent["updatedAt"],
|
||||||
|
"lastUsedAt": agent["lastUsedAt"],
|
||||||
|
"key": f"{agent['key'][:4]}...{agent['key'][-4:]}",
|
||||||
|
}
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error retrieving agent: {err}")
|
||||||
|
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):
|
def get(self):
|
||||||
decoded_token = request.decoded_token
|
decoded_token = request.decoded_token
|
||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response(jsonify({"success": False}), 401)
|
return make_response(jsonify({"success": False}), 401)
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
try:
|
try:
|
||||||
keys = api_key_collection.find({"user": user})
|
agents = agents_collection.find({"user": user})
|
||||||
list_keys = []
|
list_agents = [
|
||||||
for key in keys:
|
{
|
||||||
if "source" in key and isinstance(key["source"], DBRef):
|
"id": str(agent["_id"]),
|
||||||
source = db.dereference(key["source"])
|
"name": agent["name"],
|
||||||
if source is None:
|
"description": agent["description"],
|
||||||
continue
|
"source": (
|
||||||
source_name = source["name"]
|
str(db.dereference(agent["source"])["_id"])
|
||||||
elif "retriever" in key:
|
if "source" in agent and isinstance(agent["source"], DBRef)
|
||||||
source_name = key["retriever"]
|
else ""
|
||||||
else:
|
),
|
||||||
continue
|
"chunks": agent["chunks"],
|
||||||
|
"retriever": agent.get("retriever", ""),
|
||||||
list_keys.append(
|
"prompt_id": agent["prompt_id"],
|
||||||
{
|
"tools": agent.get("tools", []),
|
||||||
"id": str(key["_id"]),
|
"agent_type": agent["agent_type"],
|
||||||
"name": key["name"],
|
"status": agent["status"],
|
||||||
"key": key["key"][:4] + "..." + key["key"][-4:],
|
"created_at": agent["createdAt"],
|
||||||
"source": source_name,
|
"updated_at": agent["updatedAt"],
|
||||||
"prompt_id": key["prompt_id"],
|
"last_used_at": agent["lastUsedAt"],
|
||||||
"chunks": key["chunks"],
|
"key": f"{agent['key'][:4]}...{agent['key'][-4:]}",
|
||||||
}
|
}
|
||||||
)
|
for agent in agents
|
||||||
|
if "source" in agent or "retriever" in agent
|
||||||
|
]
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error retrieving API keys: {err}")
|
current_app.logger.error(f"Error retrieving agents: {err}")
|
||||||
return make_response(jsonify({"success": False}), 400)
|
return make_response(jsonify({"success": False}), 400)
|
||||||
return make_response(jsonify(list_keys), 200)
|
return make_response(jsonify(list_agents), 200)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/create_api_key")
|
@user_ns.route("/api/create_agent")
|
||||||
class CreateApiKey(Resource):
|
class CreateAgent(Resource):
|
||||||
create_api_key_model = api.model(
|
create_agent_model = api.model(
|
||||||
"CreateApiKeyModel",
|
"CreateAgentModel",
|
||||||
{
|
{
|
||||||
"name": fields.String(required=True, description="Name of the API key"),
|
"name": fields.String(required=True, description="Name of the agent"),
|
||||||
"prompt_id": fields.String(required=True, description="Prompt ID"),
|
"description": fields.String(
|
||||||
|
required=True, description="Description of the agent"
|
||||||
|
),
|
||||||
|
"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"),
|
"chunks": fields.Integer(required=True, description="Chunks count"),
|
||||||
"source": fields.String(description="Source ID (optional)"),
|
"retriever": fields.String(required=True, description="Retriever ID"),
|
||||||
"retriever": fields.String(description="Retriever (optional)"),
|
"prompt_id": fields.String(required=True, description="Prompt ID"),
|
||||||
|
"tools": fields.List(
|
||||||
|
fields.String, required=False, description="List of tool identifiers"
|
||||||
|
),
|
||||||
|
"agent_type": fields.String(required=True, description="Type of the agent"),
|
||||||
|
"status": fields.String(
|
||||||
|
required=True, description="Status of the agent (draft or published)"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.expect(create_api_key_model)
|
@api.expect(create_agent_model)
|
||||||
@api.doc(description="Create a new API key")
|
@api.doc(description="Create a new agent")
|
||||||
def post(self):
|
def post(self):
|
||||||
decoded_token = request.decoded_token
|
decoded_token = request.decoded_token
|
||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response(jsonify({"success": False}), 401)
|
return make_response(jsonify({"success": False}), 401)
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
required_fields = ["name", "prompt_id", "chunks"]
|
|
||||||
|
if data.get("status") not in ["draft", "published"]:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid status"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
required_fields = []
|
||||||
|
if data.get("status") == "published":
|
||||||
|
required_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"source",
|
||||||
|
"chunks",
|
||||||
|
"retriever",
|
||||||
|
"prompt_id",
|
||||||
|
"agent_type",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
required_fields = ["name"]
|
||||||
missing_fields = check_required_fields(data, required_fields)
|
missing_fields = check_required_fields(data, required_fields)
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
return missing_fields
|
return missing_fields
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = str(uuid.uuid4())
|
key = str(uuid.uuid4())
|
||||||
new_api_key = {
|
new_agent = {
|
||||||
"name": data["name"],
|
|
||||||
"key": key,
|
|
||||||
"user": user,
|
"user": user,
|
||||||
"prompt_id": data["prompt_id"],
|
"name": data.get("name"),
|
||||||
"chunks": data["chunks"],
|
"description": data.get("description", ""),
|
||||||
|
"image": data.get("image", ""),
|
||||||
|
"source": (
|
||||||
|
DBRef("sources", ObjectId(data.get("source")))
|
||||||
|
if ObjectId.is_valid(data.get("source"))
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"chunks": data.get("chunks", ""),
|
||||||
|
"retriever": data.get("retriever", ""),
|
||||||
|
"prompt_id": data.get("prompt_id", ""),
|
||||||
|
"tools": data.get("tools", []),
|
||||||
|
"agent_type": data.get("agent_type", ""),
|
||||||
|
"status": data.get("status"),
|
||||||
|
"createdAt": datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
"updatedAt": datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
"lastUsedAt": None,
|
||||||
|
"key": key,
|
||||||
}
|
}
|
||||||
if "source" in data and ObjectId.is_valid(data["source"]):
|
|
||||||
new_api_key["source"] = DBRef("sources", ObjectId(data["source"]))
|
|
||||||
if "retriever" in data:
|
|
||||||
new_api_key["retriever"] = data["retriever"]
|
|
||||||
|
|
||||||
resp = api_key_collection.insert_one(new_api_key)
|
resp = agents_collection.insert_one(new_agent)
|
||||||
new_id = str(resp.inserted_id)
|
new_id = str(resp.inserted_id)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error creating API key: {err}")
|
current_app.logger.error(f"Error creating agent: {err}")
|
||||||
return make_response(jsonify({"success": False}), 400)
|
return make_response(jsonify({"success": False}), 400)
|
||||||
|
|
||||||
return make_response(jsonify({"id": new_id, "key": key}), 201)
|
return make_response(jsonify({"id": new_id, "key": key}), 201)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/delete_api_key")
|
@user_ns.route("/api/update_agent/<string:agent_id>")
|
||||||
class DeleteApiKey(Resource):
|
class UpdateAgent(Resource):
|
||||||
delete_api_key_model = api.model(
|
update_agent_model = api.model(
|
||||||
"DeleteApiKeyModel",
|
"UpdateAgentModel",
|
||||||
{"id": fields.String(required=True, description="API Key ID to delete")},
|
{
|
||||||
|
"name": fields.String(required=True, description="New name of the agent"),
|
||||||
|
"description": fields.String(
|
||||||
|
required=True, description="New description of the agent"
|
||||||
|
),
|
||||||
|
"image": fields.String(
|
||||||
|
required=False, description="New image URL or identifier"
|
||||||
|
),
|
||||||
|
"source": fields.String(required=True, description="Source ID"),
|
||||||
|
"chunks": fields.Integer(required=True, description="Chunks count"),
|
||||||
|
"retriever": fields.String(required=True, description="Retriever ID"),
|
||||||
|
"prompt_id": fields.String(required=True, description="Prompt ID"),
|
||||||
|
"tools": fields.List(
|
||||||
|
fields.String, required=False, description="List of tool identifiers"
|
||||||
|
),
|
||||||
|
"agent_type": fields.String(required=True, description="Type of the agent"),
|
||||||
|
"status": fields.String(
|
||||||
|
required=True, description="Status of the agent (draft or published)"
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.expect(delete_api_key_model)
|
@api.expect(update_agent_model)
|
||||||
@api.doc(description="Delete an API key by ID")
|
@api.doc(description="Update an existing agent")
|
||||||
def post(self):
|
def put(self, agent_id):
|
||||||
decoded_token = request.decoded_token
|
decoded_token = request.decoded_token
|
||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response(jsonify({"success": False}), 401)
|
return make_response(jsonify({"success": False}), 401)
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
required_fields = ["id"]
|
|
||||||
missing_fields = check_required_fields(data, required_fields)
|
if not ObjectId.is_valid(agent_id):
|
||||||
if missing_fields:
|
return make_response(
|
||||||
return missing_fields
|
jsonify({"success": False, "message": "Invalid agent ID format"}), 400
|
||||||
|
)
|
||||||
|
oid = ObjectId(agent_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = api_key_collection.delete_one(
|
existing_agent = agents_collection.find_one({"_id": oid, "user": user})
|
||||||
{"_id": ObjectId(data["id"]), "user": user}
|
|
||||||
)
|
|
||||||
if result.deleted_count == 0:
|
|
||||||
return {"success": False, "message": "API Key not found"}, 404
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error deleting API key: {err}")
|
return make_response(
|
||||||
return {"success": False}, 400
|
current_app.logger.error(f"Error finding agent {agent_id}: {err}"),
|
||||||
|
jsonify({"success": False, "message": "Database error finding agent"}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
return {"success": True}, 200
|
if not existing_agent:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{"success": False, "message": "Agent not found or not authorized"}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_fields = {}
|
||||||
|
allowed_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"image",
|
||||||
|
"source",
|
||||||
|
"chunks",
|
||||||
|
"retriever",
|
||||||
|
"prompt_id",
|
||||||
|
"tools",
|
||||||
|
"agent_type",
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in allowed_fields:
|
||||||
|
if field in data:
|
||||||
|
if field == "status":
|
||||||
|
new_status = data.get("status")
|
||||||
|
if new_status not in ["draft", "published"]:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{"success": False, "message": "Invalid status value"}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
update_fields[field] = new_status
|
||||||
|
elif field == "source":
|
||||||
|
source_id = data.get("source")
|
||||||
|
if source_id and ObjectId.is_valid(source_id):
|
||||||
|
update_fields[field] = DBRef("sources", ObjectId(source_id))
|
||||||
|
elif source_id:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Invalid source ID format provided",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
update_fields[field] = ""
|
||||||
|
else:
|
||||||
|
update_fields[field] = data[field]
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "No update data provided"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
final_status = update_fields.get("status", existing_agent.get("status"))
|
||||||
|
if final_status == "published":
|
||||||
|
required_published_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"source",
|
||||||
|
"chunks",
|
||||||
|
"retriever",
|
||||||
|
"prompt_id",
|
||||||
|
"agent_type",
|
||||||
|
]
|
||||||
|
missing_published_fields = []
|
||||||
|
for req_field in required_published_fields:
|
||||||
|
final_value = update_fields.get(
|
||||||
|
req_field, existing_agent.get(req_field)
|
||||||
|
)
|
||||||
|
if req_field == "source" and final_value:
|
||||||
|
if not isinstance(final_value, DBRef):
|
||||||
|
missing_published_fields.append(req_field)
|
||||||
|
|
||||||
|
if missing_published_fields:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"Cannot publish agent. Missing or invalid required fields: {', '.join(missing_published_fields)}",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_fields["updatedAt"] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = agents_collection.update_one(
|
||||||
|
{"_id": oid, "user": user}, {"$set": update_fields}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.matched_count == 0:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Agent not found or update failed unexpectedly",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
if result.modified_count == 0 and result.matched_count == 1:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Agent found, but no changes were applied.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
304,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error updating agent {agent_id}: {err}")
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Database error during update"}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"id": agent_id,
|
||||||
|
"message": "Agent updated successfully",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@user_ns.route("/api/delete_agent")
|
||||||
|
class DeleteAgent(Resource):
|
||||||
|
@api.doc(params={"id": "ID of the agent"}, description="Delete an agent by ID")
|
||||||
|
def delete(self):
|
||||||
|
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:
|
||||||
|
deleted_agent = agents_collection.find_one_and_delete(
|
||||||
|
{"_id": ObjectId(agent_id), "user": user}
|
||||||
|
)
|
||||||
|
if not deleted_agent:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Agent not found"}), 404
|
||||||
|
)
|
||||||
|
deleted_id = str(deleted_agent["_id"])
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error deleting agent: {err}")
|
||||||
|
return make_response(jsonify({"success": False}), 400)
|
||||||
|
|
||||||
|
return make_response(jsonify({"id": deleted_id}), 200)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/share")
|
@user_ns.route("/api/share")
|
||||||
@@ -1112,9 +1401,7 @@ class ShareConversation(Resource):
|
|||||||
if "retriever" in data:
|
if "retriever" in data:
|
||||||
new_api_key_data["retriever"] = data["retriever"]
|
new_api_key_data["retriever"] = data["retriever"]
|
||||||
|
|
||||||
pre_existing_api_document = api_key_collection.find_one(
|
pre_existing_api_document = agents_collection.find_one(new_api_key_data)
|
||||||
new_api_key_data
|
|
||||||
)
|
|
||||||
if pre_existing_api_document:
|
if pre_existing_api_document:
|
||||||
api_uuid = pre_existing_api_document["key"]
|
api_uuid = pre_existing_api_document["key"]
|
||||||
pre_existing = shared_conversations_collections.find_one(
|
pre_existing = shared_conversations_collections.find_one(
|
||||||
@@ -1173,7 +1460,7 @@ class ShareConversation(Resource):
|
|||||||
if "retriever" in data:
|
if "retriever" in data:
|
||||||
new_api_key_data["retriever"] = data["retriever"]
|
new_api_key_data["retriever"] = data["retriever"]
|
||||||
|
|
||||||
api_key_collection.insert_one(new_api_key_data)
|
agents_collection.insert_one(new_api_key_data)
|
||||||
shared_conversations_collections.insert_one(
|
shared_conversations_collections.insert_one(
|
||||||
{
|
{
|
||||||
"uuid": explicit_binary,
|
"uuid": explicit_binary,
|
||||||
@@ -1331,9 +1618,9 @@ class GetMessageAnalytics(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
api_key = (
|
api_key = (
|
||||||
api_key_collection.find_one(
|
agents_collection.find_one({"_id": ObjectId(api_key_id), "user": user})[
|
||||||
{"_id": ObjectId(api_key_id), "user": user}
|
"key"
|
||||||
)["key"]
|
]
|
||||||
if api_key_id
|
if api_key_id
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -1455,9 +1742,9 @@ class GetTokenAnalytics(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
api_key = (
|
api_key = (
|
||||||
api_key_collection.find_one(
|
agents_collection.find_one({"_id": ObjectId(api_key_id), "user": user})[
|
||||||
{"_id": ObjectId(api_key_id), "user": user}
|
"key"
|
||||||
)["key"]
|
]
|
||||||
if api_key_id
|
if api_key_id
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -1614,9 +1901,9 @@ class GetFeedbackAnalytics(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
api_key = (
|
api_key = (
|
||||||
api_key_collection.find_one(
|
agents_collection.find_one({"_id": ObjectId(api_key_id), "user": user})[
|
||||||
{"_id": ObjectId(api_key_id), "user": user}
|
"key"
|
||||||
)["key"]
|
]
|
||||||
if api_key_id
|
if api_key_id
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -1779,7 +2066,7 @@ class GetUserLogs(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
api_key = (
|
api_key = (
|
||||||
api_key_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
agents_collection.find_one({"_id": ObjectId(api_key_id)})["key"]
|
||||||
if api_key_id
|
if api_key_id
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -2506,39 +2793,39 @@ class StoreAttachment(Resource):
|
|||||||
user = secure_filename(decoded_token.get("sub"))
|
user = secure_filename(decoded_token.get("sub"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
attachment_id = ObjectId()
|
||||||
original_filename = secure_filename(file.filename)
|
original_filename = secure_filename(file.filename)
|
||||||
folder_name = original_filename
|
|
||||||
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, "attachments",folder_name)
|
save_dir = os.path.join(
|
||||||
|
current_dir,
|
||||||
|
settings.UPLOAD_FOLDER,
|
||||||
|
user,
|
||||||
|
"attachments",
|
||||||
|
str(attachment_id),
|
||||||
|
)
|
||||||
os.makedirs(save_dir, exist_ok=True)
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
# Create directory structure: user/attachments/filename/
|
|
||||||
file_path = os.path.join(save_dir, original_filename)
|
file_path = os.path.join(save_dir, original_filename)
|
||||||
|
|
||||||
# Handle filename conflicts
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
name_parts = os.path.splitext(original_filename)
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
|
||||||
new_filename = f"{name_parts[0]}_{timestamp}{name_parts[1]}"
|
|
||||||
file_path = os.path.join(save_dir, new_filename)
|
|
||||||
original_filename = new_filename
|
|
||||||
|
|
||||||
file.save(file_path)
|
file.save(file_path)
|
||||||
file_info = {"folder": folder_name, "filename": original_filename}
|
file_info = {
|
||||||
|
"filename": original_filename,
|
||||||
|
"attachment_id": str(attachment_id),
|
||||||
|
}
|
||||||
current_app.logger.info(f"Saved file: {file_path}")
|
current_app.logger.info(f"Saved file: {file_path}")
|
||||||
|
|
||||||
# Start async task to process single file
|
# Start async task to process single file
|
||||||
task = store_attachment.delay(
|
task = store_attachment.delay(save_dir, file_info, user)
|
||||||
save_dir,
|
|
||||||
file_info,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
|
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify(
|
||||||
"success": True,
|
{
|
||||||
"task_id": task.id,
|
"success": True,
|
||||||
"message": "File uploaded successfully. Processing started."
|
"task_id": task.id,
|
||||||
}),
|
"message": "File uploaded successfully. Processing started.",
|
||||||
200
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|||||||
@@ -55,3 +55,12 @@ class BaseLLM(ABC):
|
|||||||
|
|
||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
raise NotImplementedError("Subclass must implement _supports_tools method")
|
raise NotImplementedError("Subclass must implement _supports_tools method")
|
||||||
|
|
||||||
|
def get_supported_attachment_types(self):
|
||||||
|
"""
|
||||||
|
Return a list of MIME types supported by this LLM for file uploads.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of supported MIME types
|
||||||
|
"""
|
||||||
|
return [] # Default: no attachments supported
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import json
|
||||||
|
|
||||||
from application.llm.base import BaseLLM
|
from application.llm.base import BaseLLM
|
||||||
|
|
||||||
@@ -9,6 +13,138 @@ class GoogleLLM(BaseLLM):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.user_api_key = user_api_key
|
self.user_api_key = user_api_key
|
||||||
|
self.client = genai.Client(api_key=self.api_key)
|
||||||
|
|
||||||
|
def get_supported_attachment_types(self):
|
||||||
|
"""
|
||||||
|
Return a list of MIME types supported by Google Gemini for file uploads.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of supported MIME types
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
'application/pdf',
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/webp',
|
||||||
|
'image/gif'
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare_messages_with_attachments(self, messages, attachments=None):
|
||||||
|
"""
|
||||||
|
Process attachments using Google AI's file API for more efficient handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages (list): List of message dictionaries.
|
||||||
|
attachments (list): List of attachment dictionaries with content and metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Messages formatted with file references for Google AI API.
|
||||||
|
"""
|
||||||
|
if not attachments:
|
||||||
|
return messages
|
||||||
|
|
||||||
|
prepared_messages = messages.copy()
|
||||||
|
|
||||||
|
# Find the user message to attach files to the last one
|
||||||
|
user_message_index = None
|
||||||
|
for i in range(len(prepared_messages) - 1, -1, -1):
|
||||||
|
if prepared_messages[i].get("role") == "user":
|
||||||
|
user_message_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if user_message_index is None:
|
||||||
|
user_message = {"role": "user", "content": []}
|
||||||
|
prepared_messages.append(user_message)
|
||||||
|
user_message_index = len(prepared_messages) - 1
|
||||||
|
|
||||||
|
if isinstance(prepared_messages[user_message_index].get("content"), str):
|
||||||
|
text_content = prepared_messages[user_message_index]["content"]
|
||||||
|
prepared_messages[user_message_index]["content"] = [
|
||||||
|
{"type": "text", "text": text_content}
|
||||||
|
]
|
||||||
|
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
|
||||||
|
prepared_messages[user_message_index]["content"] = []
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for attachment in attachments:
|
||||||
|
mime_type = attachment.get('mime_type')
|
||||||
|
if not mime_type:
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if file_path:
|
||||||
|
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
if mime_type in self.get_supported_attachment_types():
|
||||||
|
try:
|
||||||
|
file_uri = self._upload_file_to_google(attachment)
|
||||||
|
logging.info(f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}")
|
||||||
|
files.append({"file_uri": file_uri, "mime_type": mime_type})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"GoogleLLM: Error uploading file: {e}")
|
||||||
|
if 'content' in attachment:
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"[File could not be processed: {attachment.get('path', 'unknown')}]"
|
||||||
|
})
|
||||||
|
|
||||||
|
if files:
|
||||||
|
logging.info(f"GoogleLLM: Adding {len(files)} files to message")
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"files": files
|
||||||
|
})
|
||||||
|
|
||||||
|
return prepared_messages
|
||||||
|
|
||||||
|
def _upload_file_to_google(self, attachment):
|
||||||
|
"""
|
||||||
|
Upload a file to Google AI and return the file URI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attachment (dict): Attachment dictionary with path and metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Google AI file URI for the uploaded file.
|
||||||
|
"""
|
||||||
|
if 'google_file_uri' in attachment:
|
||||||
|
return attachment['google_file_uri']
|
||||||
|
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if not file_path:
|
||||||
|
raise ValueError("No file path provided in attachment")
|
||||||
|
|
||||||
|
if not os.path.isabs(file_path):
|
||||||
|
current_dir = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
file_path = os.path.join(current_dir, "application", file_path)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
|
mime_type = attachment.get('mime_type')
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.files.upload(file=file_path)
|
||||||
|
|
||||||
|
file_uri = response.uri
|
||||||
|
|
||||||
|
from application.core.mongo_db import MongoDB
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo["docsgpt"]
|
||||||
|
attachments_collection = db["attachments"]
|
||||||
|
if '_id' in attachment:
|
||||||
|
attachments_collection.update_one(
|
||||||
|
{"_id": attachment['_id']},
|
||||||
|
{"$set": {"google_file_uri": file_uri}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return file_uri
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error uploading file to Google AI: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def _clean_messages_google(self, messages):
|
def _clean_messages_google(self, messages):
|
||||||
cleaned_messages = []
|
cleaned_messages = []
|
||||||
@@ -26,7 +162,7 @@ class GoogleLLM(BaseLLM):
|
|||||||
elif isinstance(content, list):
|
elif isinstance(content, list):
|
||||||
for item in content:
|
for item in content:
|
||||||
if "text" in item:
|
if "text" in item:
|
||||||
parts.append(types.Part.from_text(item["text"]))
|
parts.append(types.Part.from_text(text=item["text"]))
|
||||||
elif "function_call" in item:
|
elif "function_call" in item:
|
||||||
parts.append(
|
parts.append(
|
||||||
types.Part.from_function_call(
|
types.Part.from_function_call(
|
||||||
@@ -41,6 +177,14 @@ class GoogleLLM(BaseLLM):
|
|||||||
response=item["function_response"]["response"],
|
response=item["function_response"]["response"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif "files" in item:
|
||||||
|
for file_data in item["files"]:
|
||||||
|
parts.append(
|
||||||
|
types.Part.from_uri(
|
||||||
|
file_uri=file_data["file_uri"],
|
||||||
|
mime_type=file_data["mime_type"]
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unexpected content dictionary format:{item}"
|
f"Unexpected content dictionary format:{item}"
|
||||||
@@ -146,11 +290,25 @@ class GoogleLLM(BaseLLM):
|
|||||||
cleaned_tools = self._clean_tools_format(tools)
|
cleaned_tools = self._clean_tools_format(tools)
|
||||||
config.tools = cleaned_tools
|
config.tools = cleaned_tools
|
||||||
|
|
||||||
|
# Check if we have both tools and file attachments
|
||||||
|
has_attachments = False
|
||||||
|
for message in messages:
|
||||||
|
for part in message.parts:
|
||||||
|
if hasattr(part, 'file_data') and part.file_data is not None:
|
||||||
|
has_attachments = True
|
||||||
|
break
|
||||||
|
if has_attachments:
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.info(f"GoogleLLM: Starting stream generation. Model: {model}, Messages: {json.dumps(messages, default=str)}, Has attachments: {has_attachments}")
|
||||||
|
|
||||||
response = client.models.generate_content_stream(
|
response = client.models.generate_content_stream(
|
||||||
model=model,
|
model=model,
|
||||||
contents=messages,
|
contents=messages,
|
||||||
config=config,
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
if hasattr(chunk, "candidates") and chunk.candidates:
|
if hasattr(chunk, "candidates") and chunk.candidates:
|
||||||
for candidate in chunk.candidates:
|
for candidate in chunk.candidates:
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import mimetypes
|
||||||
|
import logging
|
||||||
|
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
from application.llm.base import BaseLLM
|
from application.llm.base import BaseLLM
|
||||||
@@ -65,6 +69,15 @@ class OpenAILLM(BaseLLM):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
content_parts = []
|
||||||
|
if "text" in item:
|
||||||
|
content_parts.append({"type": "text", "text": item["text"]})
|
||||||
|
elif "type" in item and item["type"] == "text" and "text" in item:
|
||||||
|
content_parts.append(item)
|
||||||
|
elif "type" in item and item["type"] == "file" and "file" in item:
|
||||||
|
content_parts.append(item)
|
||||||
|
cleaned_messages.append({"role": role, "content": content_parts})
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unexpected content dictionary format: {item}"
|
f"Unexpected content dictionary format: {item}"
|
||||||
@@ -133,6 +146,183 @@ class OpenAILLM(BaseLLM):
|
|||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_supported_attachment_types(self):
|
||||||
|
"""
|
||||||
|
Return a list of MIME types supported by OpenAI for file uploads.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of supported MIME types
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
'application/pdf',
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/webp',
|
||||||
|
'image/gif'
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare_messages_with_attachments(self, messages, attachments=None):
|
||||||
|
"""
|
||||||
|
Process attachments using OpenAI's file API for more efficient handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages (list): List of message dictionaries.
|
||||||
|
attachments (list): List of attachment dictionaries with content and metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Messages formatted with file references for OpenAI API.
|
||||||
|
"""
|
||||||
|
if not attachments:
|
||||||
|
return messages
|
||||||
|
|
||||||
|
prepared_messages = messages.copy()
|
||||||
|
|
||||||
|
# Find the user message to attach file_id to the last one
|
||||||
|
user_message_index = None
|
||||||
|
for i in range(len(prepared_messages) - 1, -1, -1):
|
||||||
|
if prepared_messages[i].get("role") == "user":
|
||||||
|
user_message_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if user_message_index is None:
|
||||||
|
user_message = {"role": "user", "content": []}
|
||||||
|
prepared_messages.append(user_message)
|
||||||
|
user_message_index = len(prepared_messages) - 1
|
||||||
|
|
||||||
|
if isinstance(prepared_messages[user_message_index].get("content"), str):
|
||||||
|
text_content = prepared_messages[user_message_index]["content"]
|
||||||
|
prepared_messages[user_message_index]["content"] = [
|
||||||
|
{"type": "text", "text": text_content}
|
||||||
|
]
|
||||||
|
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
|
||||||
|
prepared_messages[user_message_index]["content"] = []
|
||||||
|
|
||||||
|
for attachment in attachments:
|
||||||
|
mime_type = attachment.get('mime_type')
|
||||||
|
if not mime_type:
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if file_path:
|
||||||
|
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
if mime_type and mime_type.startswith('image/'):
|
||||||
|
try:
|
||||||
|
base64_image = self._get_base64_image(attachment)
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:{mime_type};base64,{base64_image}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error processing image attachment: {e}")
|
||||||
|
if 'content' in attachment:
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]"
|
||||||
|
})
|
||||||
|
# Handle PDFs using the file API
|
||||||
|
elif mime_type == 'application/pdf':
|
||||||
|
try:
|
||||||
|
file_id = self._upload_file_to_openai(attachment)
|
||||||
|
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"type": "file",
|
||||||
|
"file": {"file_id": file_id}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error uploading PDF to OpenAI: {e}")
|
||||||
|
if 'content' in attachment:
|
||||||
|
prepared_messages[user_message_index]["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"File content:\n\n{attachment['content']}"
|
||||||
|
})
|
||||||
|
|
||||||
|
return prepared_messages
|
||||||
|
|
||||||
|
def _get_base64_image(self, attachment):
|
||||||
|
"""
|
||||||
|
Convert an image file to base64 encoding.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attachment (dict): Attachment dictionary with path and metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Base64-encoded image data.
|
||||||
|
"""
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if not file_path:
|
||||||
|
raise ValueError("No file path provided in attachment")
|
||||||
|
|
||||||
|
if not os.path.isabs(file_path):
|
||||||
|
current_dir = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
file_path = os.path.join(current_dir, "application", file_path)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
|
with open(file_path, "rb") as image_file:
|
||||||
|
return base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
def _upload_file_to_openai(self, attachment): ##pdfs
|
||||||
|
"""
|
||||||
|
Upload a file to OpenAI and return the file_id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attachment (dict): Attachment dictionary with path and metadata.
|
||||||
|
Expected keys:
|
||||||
|
- path: Path to the file
|
||||||
|
- id: Optional MongoDB ID for caching
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: OpenAI file_id for the uploaded file.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
if 'openai_file_id' in attachment:
|
||||||
|
return attachment['openai_file_id']
|
||||||
|
|
||||||
|
file_path = attachment.get('path')
|
||||||
|
if not file_path:
|
||||||
|
raise ValueError("No file path provided in attachment")
|
||||||
|
|
||||||
|
if not os.path.isabs(file_path):
|
||||||
|
current_dir = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
file_path = os.path.join(current_dir,"application", file_path)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as file:
|
||||||
|
response = self.client.files.create(
|
||||||
|
file=file,
|
||||||
|
purpose="assistants"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_id = response.id
|
||||||
|
|
||||||
|
from application.core.mongo_db import MongoDB
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo["docsgpt"]
|
||||||
|
attachments_collection = db["attachments"]
|
||||||
|
if '_id' in attachment:
|
||||||
|
attachments_collection.update_one(
|
||||||
|
{"_id": attachment['_id']},
|
||||||
|
{"$set": {"openai_file_id": file_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error uploading file to OpenAI: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class AzureOpenAILLM(OpenAILLM):
|
class AzureOpenAILLM(OpenAILLM):
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Flask==3.1.0
|
|||||||
faiss-cpu==1.9.0.post1
|
faiss-cpu==1.9.0.post1
|
||||||
flask-restx==1.3.0
|
flask-restx==1.3.0
|
||||||
google-genai==1.3.0
|
google-genai==1.3.0
|
||||||
google-generativeai==0.8.3
|
google-generativeai==0.8.5
|
||||||
gTTS==2.5.4
|
gTTS==2.5.4
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
html2text==2024.2.26
|
html2text==2024.2.26
|
||||||
@@ -41,7 +41,7 @@ lxml==5.3.1
|
|||||||
markupsafe==3.0.2
|
markupsafe==3.0.2
|
||||||
marshmallow==3.26.1
|
marshmallow==3.26.1
|
||||||
mpmath==1.3.0
|
mpmath==1.3.0
|
||||||
multidict==6.1.0
|
multidict==6.3.2
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
networkx==3.4.2
|
networkx==3.4.2
|
||||||
numpy==2.2.1
|
numpy==2.2.1
|
||||||
|
|||||||
@@ -328,34 +328,30 @@ def attachment_worker(self, directory, file_info, user):
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import mimetypes
|
||||||
from application.utils import num_tokens_from_string
|
from application.utils import num_tokens_from_string
|
||||||
|
|
||||||
mongo = MongoDB.get_client()
|
mongo = MongoDB.get_client()
|
||||||
db = mongo["docsgpt"]
|
db = mongo["docsgpt"]
|
||||||
attachments_collection = db["attachments"]
|
attachments_collection = db["attachments"]
|
||||||
|
|
||||||
job_name = file_info["folder"]
|
filename = file_info["filename"]
|
||||||
logging.info(f"Processing attachment: {job_name}", extra={"user": user, "job": job_name})
|
attachment_id = file_info["attachment_id"]
|
||||||
|
|
||||||
|
logging.info(f"Processing attachment: {attachment_id}/{filename}", extra={"user": user})
|
||||||
|
|
||||||
self.update_state(state="PROGRESS", meta={"current": 10})
|
self.update_state(state="PROGRESS", meta={"current": 10})
|
||||||
|
|
||||||
folder_name = file_info["folder"]
|
|
||||||
filename = file_info["filename"]
|
|
||||||
|
|
||||||
file_path = os.path.join(directory, filename)
|
file_path = os.path.join(directory, filename)
|
||||||
|
|
||||||
|
|
||||||
logging.info(f"Processing file: {file_path}", extra={"user": user, "job": job_name})
|
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
logging.warning(f"File not found: {file_path}", extra={"user": user, "job": job_name})
|
logging.warning(f"File not found: {file_path}", extra={"user": user})
|
||||||
return {"error": "File not found"}
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reader = SimpleDirectoryReader(
|
reader = SimpleDirectoryReader(
|
||||||
input_files=[file_path]
|
input_files=[file_path]
|
||||||
)
|
)
|
||||||
|
|
||||||
documents = reader.load_data()
|
documents = reader.load_data()
|
||||||
|
|
||||||
self.update_state(state="PROGRESS", meta={"current": 50})
|
self.update_state(state="PROGRESS", meta={"current": 50})
|
||||||
@@ -364,33 +360,37 @@ def attachment_worker(self, directory, file_info, user):
|
|||||||
content = documents[0].text
|
content = documents[0].text
|
||||||
token_count = num_tokens_from_string(content)
|
token_count = num_tokens_from_string(content)
|
||||||
|
|
||||||
file_path_relative = f"{user}/attachments/{folder_name}/{filename}"
|
file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}"
|
||||||
|
|
||||||
attachment_id = attachments_collection.insert_one({
|
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
doc_id = ObjectId(attachment_id)
|
||||||
|
attachments_collection.insert_one({
|
||||||
|
"_id": doc_id,
|
||||||
"user": user,
|
"user": user,
|
||||||
"path": file_path_relative,
|
"path": file_path_relative,
|
||||||
"content": content,
|
"content": content,
|
||||||
"token_count": token_count,
|
"token_count": token_count,
|
||||||
|
"mime_type": mime_type,
|
||||||
"date": datetime.datetime.now(),
|
"date": datetime.datetime.now(),
|
||||||
}).inserted_id
|
})
|
||||||
|
|
||||||
logging.info(f"Stored attachment with ID: {attachment_id}",
|
logging.info(f"Stored attachment with ID: {attachment_id}",
|
||||||
extra={"user": user, "job": job_name})
|
extra={"user": user})
|
||||||
|
|
||||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"attachment_id": str(attachment_id),
|
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"folder": folder_name,
|
|
||||||
"path": file_path_relative,
|
"path": file_path_relative,
|
||||||
"token_count": token_count
|
"token_count": token_count,
|
||||||
|
"attachment_id": attachment_id,
|
||||||
|
"mime_type": mime_type
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logging.warning("No content was extracted from the file",
|
logging.warning("No content was extracted from the file",
|
||||||
extra={"user": user, "job": job_name})
|
extra={"user": user})
|
||||||
return {"error": "No content was extracted from the file"}
|
raise ValueError("No content was extracted from the file")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error processing file {filename}: {e}",
|
logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True)
|
||||||
extra={"user": user, "job": job_name}, exc_info=True)
|
raise
|
||||||
return {"error": f"Error processing file: {str(e)}"}
|
|
||||||
|
|||||||
@@ -73,9 +73,44 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This
|
|||||||
|
|
||||||
## Launching DocsGPT (Windows)
|
## Launching DocsGPT (Windows)
|
||||||
|
|
||||||
For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker.
|
For Windows users, we provide a PowerShell script that offers the same functionality as the macOS/Linux setup script.
|
||||||
|
|
||||||
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding.
|
**Steps:**
|
||||||
|
|
||||||
|
1. **Download the DocsGPT Repository:**
|
||||||
|
|
||||||
|
First, you need to download the DocsGPT repository to your local machine. You can do this using Git:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://github.com/arc53/DocsGPT.git
|
||||||
|
cd DocsGPT
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the `setup.ps1` script:**
|
||||||
|
|
||||||
|
Execute the PowerShell setup script:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Follow the interactive setup:**
|
||||||
|
|
||||||
|
Just like the Linux/macOS script, the PowerShell script will guide you through setting DocsGPT.
|
||||||
|
The script will handle environment configuration and start DocsGPT based on your selections.
|
||||||
|
|
||||||
|
4. **Access DocsGPT in your browser:**
|
||||||
|
|
||||||
|
Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application.
|
||||||
|
|
||||||
|
5. **Stopping DocsGPT:**
|
||||||
|
|
||||||
|
To stop DocsGPT run the Docker Compose down command displayed at the end of the setup script's execution.
|
||||||
|
|
||||||
|
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. The script will attempt to start Docker if it's not running, but you may need to start it manually if there are issues.
|
||||||
|
|
||||||
|
**Alternative Method:**
|
||||||
|
If you prefer a more manual approach, you can follow our [Docker Deployment documentation](/Deploying/Docker-Deploying) for detailed instructions on setting up DocsGPT on Windows using Docker commands directly.
|
||||||
|
|
||||||
## Advanced Configuration
|
## Advanced Configuration
|
||||||
|
|
||||||
|
|||||||
22
frontend/package-lock.json
generated
@@ -51,8 +51,8 @@
|
|||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^15.3.0",
|
"lint-staged": "^15.3.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.14",
|
"vite": "^5.4.14",
|
||||||
@@ -8812,10 +8812,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.4.2",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -8839,10 +8840,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-tailwindcss": {
|
"node_modules/prettier-plugin-tailwindcss": {
|
||||||
"version": "0.6.9",
|
"version": "0.6.11",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
|
||||||
"integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==",
|
"integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.21.3"
|
"node": ">=14.21.3"
|
||||||
},
|
},
|
||||||
@@ -8851,7 +8853,7 @@
|
|||||||
"@prettier/plugin-pug": "*",
|
"@prettier/plugin-pug": "*",
|
||||||
"@shopify/prettier-plugin-liquid": "*",
|
"@shopify/prettier-plugin-liquid": "*",
|
||||||
"@trivago/prettier-plugin-sort-imports": "*",
|
"@trivago/prettier-plugin-sort-imports": "*",
|
||||||
"@zackad/prettier-plugin-twig-melody": "*",
|
"@zackad/prettier-plugin-twig": "*",
|
||||||
"prettier": "^3.0",
|
"prettier": "^3.0",
|
||||||
"prettier-plugin-astro": "*",
|
"prettier-plugin-astro": "*",
|
||||||
"prettier-plugin-css-order": "*",
|
"prettier-plugin-css-order": "*",
|
||||||
@@ -8878,7 +8880,7 @@
|
|||||||
"@trivago/prettier-plugin-sort-imports": {
|
"@trivago/prettier-plugin-sort-imports": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@zackad/prettier-plugin-twig-melody": {
|
"@zackad/prettier-plugin-twig": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"prettier-plugin-astro": {
|
"prettier-plugin-astro": {
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.5",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^15.3.0",
|
"lint-staged": "^15.3.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.14",
|
"vite": "^5.4.14",
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ module.exports = {
|
|||||||
semi: true,
|
semi: true,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
}
|
plugins: ['prettier-plugin-tailwindcss'],
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import useTokenAuth from './hooks/useTokenAuth';
|
|||||||
import Navigation from './Navigation';
|
import Navigation from './Navigation';
|
||||||
import PageNotFound from './PageNotFound';
|
import PageNotFound from './PageNotFound';
|
||||||
import Setting from './settings';
|
import Setting from './settings';
|
||||||
|
import Agents from './agents';
|
||||||
|
|
||||||
function AuthWrapper({ children }: { children: React.ReactNode }) {
|
function AuthWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthLoading } = useTokenAuth();
|
const { isAuthLoading } = useTokenAuth();
|
||||||
|
|
||||||
if (isAuthLoading) {
|
if (isAuthLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center">
|
<div className="flex h-screen items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -31,7 +32,7 @@ function MainLayout() {
|
|||||||
const [navOpen, setNavOpen] = useState(!isMobile);
|
const [navOpen, setNavOpen] = useState(!isMobile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-raisin-black relative h-screen overflow-auto">
|
<div className="relative h-screen overflow-auto dark:bg-raisin-black">
|
||||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
<div
|
<div
|
||||||
className={`h-[calc(100dvh-64px)] md:h-screen ${
|
className={`h-[calc(100dvh-64px)] md:h-screen ${
|
||||||
@@ -52,7 +53,7 @@ export default function App() {
|
|||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative overflow-auto">
|
<div className="relative h-full overflow-auto">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
@@ -64,6 +65,7 @@ export default function App() {
|
|||||||
<Route index element={<Conversation />} />
|
<Route index element={<Conversation />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/settings/*" element={<Setting />} />
|
<Route path="/settings/*" element={<Setting />} />
|
||||||
|
<Route path="/agents/*" element={<Agents />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/share/:identifier" element={<SharedConversation />} />
|
<Route path="/share/:identifier" element={<SharedConversation />} />
|
||||||
<Route path="/*" element={<PageNotFound />} />
|
<Route path="/*" element={<PageNotFound />} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { NavLink, useNavigate } from 'react-router-dom';
|
import { NavLink, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Agent } from './agents/types';
|
||||||
import conversationService from './api/services/conversationService';
|
import conversationService from './api/services/conversationService';
|
||||||
import userService from './api/services/userService';
|
import userService from './api/services/userService';
|
||||||
import Add from './assets/add.svg';
|
import Add from './assets/add.svg';
|
||||||
@@ -12,11 +13,12 @@ import Expand from './assets/expand.svg';
|
|||||||
import Github from './assets/github.svg';
|
import Github from './assets/github.svg';
|
||||||
import Hamburger from './assets/hamburger.svg';
|
import Hamburger from './assets/hamburger.svg';
|
||||||
import openNewChat from './assets/openNewChat.svg';
|
import openNewChat from './assets/openNewChat.svg';
|
||||||
|
import Robot from './assets/robot.svg';
|
||||||
import SettingGear from './assets/settingGear.svg';
|
import SettingGear from './assets/settingGear.svg';
|
||||||
|
import Spark from './assets/spark.svg';
|
||||||
import SpinnerDark from './assets/spinner-dark.svg';
|
import SpinnerDark from './assets/spinner-dark.svg';
|
||||||
import Spinner from './assets/spinner.svg';
|
import Spinner from './assets/spinner.svg';
|
||||||
import Twitter from './assets/TwitterX.svg';
|
import Twitter from './assets/TwitterX.svg';
|
||||||
import UploadIcon from './assets/upload.svg';
|
|
||||||
import Help from './components/Help';
|
import Help from './components/Help';
|
||||||
import {
|
import {
|
||||||
handleAbort,
|
handleAbort,
|
||||||
@@ -33,13 +35,15 @@ import JWTModal from './modals/JWTModal';
|
|||||||
import { ActiveState } from './models/misc';
|
import { ActiveState } from './models/misc';
|
||||||
import { getConversations } from './preferences/preferenceApi';
|
import { getConversations } from './preferences/preferenceApi';
|
||||||
import {
|
import {
|
||||||
selectApiKeyStatus,
|
|
||||||
selectConversationId,
|
selectConversationId,
|
||||||
selectConversations,
|
selectConversations,
|
||||||
selectModalStateDeleteConv,
|
selectModalStateDeleteConv,
|
||||||
|
selectSelectedAgent,
|
||||||
selectToken,
|
selectToken,
|
||||||
setConversations,
|
setConversations,
|
||||||
setModalStateDeleteConv,
|
setModalStateDeleteConv,
|
||||||
|
setSelectedAgent,
|
||||||
|
setAgents,
|
||||||
} from './preferences/preferenceSlice';
|
} from './preferences/preferenceSlice';
|
||||||
import Upload from './upload/Upload';
|
import Upload from './upload/Upload';
|
||||||
|
|
||||||
@@ -50,36 +54,28 @@ interface NavigationProps {
|
|||||||
|
|
||||||
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
const queries = useSelector(selectQueries);
|
const queries = useSelector(selectQueries);
|
||||||
const conversations = useSelector(selectConversations);
|
const conversations = useSelector(selectConversations);
|
||||||
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
|
||||||
const conversationId = useSelector(selectConversationId);
|
const conversationId = useSelector(selectConversationId);
|
||||||
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
|
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
||||||
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [isDarkTheme] = useDarkTheme();
|
const [isDarkTheme] = useDarkTheme();
|
||||||
const { t } = useTranslation();
|
|
||||||
const isApiKeySet = useSelector(selectApiKeyStatus);
|
|
||||||
|
|
||||||
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
|
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
|
||||||
|
|
||||||
|
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
|
||||||
const [uploadModalState, setUploadModalState] =
|
const [uploadModalState, setUploadModalState] =
|
||||||
useState<ActiveState>('INACTIVE');
|
useState<ActiveState>('INACTIVE');
|
||||||
|
const [recentAgents, setRecentAgents] = useState<Agent[]>([]);
|
||||||
|
|
||||||
const navRef = useRef(null);
|
const navRef = useRef(null);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversations?.data) {
|
|
||||||
fetchConversations();
|
|
||||||
}
|
|
||||||
if (queries.length === 0) {
|
|
||||||
resetConversation();
|
|
||||||
}
|
|
||||||
}, [conversations?.data, dispatch]);
|
|
||||||
|
|
||||||
async function fetchConversations() {
|
async function fetchConversations() {
|
||||||
dispatch(setConversations({ ...conversations, loading: true }));
|
dispatch(setConversations({ ...conversations, loading: true }));
|
||||||
return await getConversations(token)
|
return await getConversations(token)
|
||||||
@@ -92,6 +88,29 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAgents() {
|
||||||
|
const response = await userService.getAgents(token);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||||
|
const data: Agent[] = await response.json();
|
||||||
|
dispatch(setAgents(data));
|
||||||
|
setRecentAgents(
|
||||||
|
data
|
||||||
|
.filter((agent: Agent) => agent.status === 'published')
|
||||||
|
.sort(
|
||||||
|
(a: Agent, b: Agent) =>
|
||||||
|
new Date(b.last_used_at ?? 0).getTime() -
|
||||||
|
new Date(a.last_used_at ?? 0).getTime(),
|
||||||
|
)
|
||||||
|
.slice(0, 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (recentAgents.length === 0) getAgents();
|
||||||
|
if (!conversations?.data) fetchConversations();
|
||||||
|
if (queries.length === 0) resetConversation();
|
||||||
|
}, [conversations?.data, dispatch]);
|
||||||
|
|
||||||
const handleDeleteAllConversations = () => {
|
const handleDeleteAllConversations = () => {
|
||||||
setIsDeletingConversation(true);
|
setIsDeletingConversation(true);
|
||||||
conversationService
|
conversationService
|
||||||
@@ -113,18 +132,34 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAgentClick = (agent: Agent) => {
|
||||||
|
resetConversation();
|
||||||
|
dispatch(setSelectedAgent(agent));
|
||||||
|
if (isMobile) setNavOpen(!navOpen);
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
const handleConversationClick = (index: string) => {
|
const handleConversationClick = (index: string) => {
|
||||||
conversationService
|
conversationService
|
||||||
.getConversation(index, token)
|
.getConversation(index, token)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
dispatch(setConversation(data));
|
dispatch(setConversation(data.queries));
|
||||||
dispatch(
|
dispatch(
|
||||||
updateConversationId({
|
updateConversationId({
|
||||||
query: { conversationId: index },
|
query: { conversationId: index },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
if (data.agent_id) {
|
||||||
|
userService.getAgent(data.agent_id, token).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
response.json().then((agent: Agent) => {
|
||||||
|
dispatch(setSelectedAgent(agent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else dispatch(setSelectedAgent(null));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +171,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
query: { conversationId: null },
|
query: { conversationId: null },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
dispatch(setSelectedAgent(null));
|
||||||
};
|
};
|
||||||
|
|
||||||
const newChat = () => {
|
const newChat = () => {
|
||||||
@@ -170,8 +206,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!navOpen && (
|
{!navOpen && (
|
||||||
<div className="duration-25 absolute top-3 left-3 z-20 hidden transition-all md:block">
|
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all md:block">
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNavOpen(!navOpen);
|
setNavOpen(!navOpen);
|
||||||
@@ -198,7 +234,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="text-[#949494] font-medium text-[20px]">
|
<div className="text-[20px] font-medium text-[#949494]">
|
||||||
DocsGPT
|
DocsGPT
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,13 +244,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
ref={navRef}
|
ref={navRef}
|
||||||
className={`${
|
className={`${
|
||||||
!navOpen && '-ml-96 md:-ml-[18rem]'
|
!navOpen && '-ml-96 md:-ml-[18rem]'
|
||||||
} duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-r-[1px] border-b-0 bg-lotion dark:bg-chinese-black transition-all dark:border-r-purple-taupe dark:text-white`}
|
} duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-b-0 border-r-[1px] bg-lotion transition-all dark:border-r-purple-taupe dark:bg-chinese-black dark:text-white`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="my-auto mx-4 flex cursor-pointer gap-1.5"
|
className="mx-4 my-auto flex cursor-pointer gap-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setNavOpen(!navOpen);
|
setNavOpen(!navOpen);
|
||||||
@@ -252,7 +288,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`${
|
`${
|
||||||
isActive ? 'bg-transparent' : ''
|
isActive ? 'bg-transparent' : ''
|
||||||
} group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray dark:border-purple-taupe dark:text-white hover:bg-transparent`
|
} group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray hover:bg-transparent dark:border-purple-taupe dark:text-white`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -269,7 +305,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white"
|
className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white"
|
||||||
>
|
>
|
||||||
{conversations?.loading && !isDeletingConversation && (
|
{conversations?.loading && !isDeletingConversation && (
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<img
|
<img
|
||||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||||
className="animate-spin cursor-pointer bg-transparent"
|
className="animate-spin cursor-pointer bg-transparent"
|
||||||
@@ -277,10 +313,77 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{conversations?.data && conversations.data.length > 0 ? (
|
{recentAgents?.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="my-auto mx-4 mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
|
<div className="mx-4 my-auto mt-2 flex h-6 items-center">
|
||||||
<p className="mt-1 ml-4 text-sm font-semibold">{t('chats')}</p>
|
<p className="ml-4 mt-1 text-sm font-semibold">Agents</p>
|
||||||
|
</div>
|
||||||
|
<div className="agents-container">
|
||||||
|
<div>
|
||||||
|
{recentAgents.map((agent, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
|
||||||
|
agent.id === selectedAgent?.id && !conversationId
|
||||||
|
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleAgentClick(agent)}
|
||||||
|
>
|
||||||
|
<div className="flex w-6 justify-center">
|
||||||
|
<img
|
||||||
|
src={agent.image ?? Robot}
|
||||||
|
alt="agent-logo"
|
||||||
|
className="h-6 w-6 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||||
|
{agent.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setSelectedAgent(null));
|
||||||
|
navigate('/agents');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-6 justify-center">
|
||||||
|
<img
|
||||||
|
src={Spark}
|
||||||
|
alt="manage-agents"
|
||||||
|
className="h-[18px] w-[18px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||||
|
Manage Agents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||||
|
onClick={() => navigate('/agents')}
|
||||||
|
>
|
||||||
|
<div className="flex w-6 justify-center">
|
||||||
|
<img
|
||||||
|
src={Spark}
|
||||||
|
alt="manage-agents"
|
||||||
|
className="h-[18px] w-[18px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||||
|
Manage Agents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{conversations?.data && conversations.data.length > 0 ? (
|
||||||
|
<div className="mt-7">
|
||||||
|
<div className="mx-4 my-auto mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
|
||||||
|
<p className="ml-4 mt-1 text-sm font-semibold">{t('chats')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="conversations-container">
|
<div className="conversations-container">
|
||||||
{conversations.data?.map((conversation) => (
|
{conversations.data?.map((conversation) => (
|
||||||
@@ -316,7 +419,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
}}
|
}}
|
||||||
to="/settings"
|
to="/settings"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
`mx-4 my-auto flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||||
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
|
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -324,15 +427,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<img
|
<img
|
||||||
src={SettingGear}
|
src={SettingGear}
|
||||||
alt="Settings"
|
alt="Settings"
|
||||||
className="ml-2 w- filter dark:invert"
|
className="w- ml-2 filter dark:invert"
|
||||||
/>
|
/>
|
||||||
<p className="my-auto text-sm text-eerie-black dark:text-white">
|
<p className="my-auto text-sm text-eerie-black dark:text-white">
|
||||||
{t('settings.label')}
|
{t('settings.label')}
|
||||||
</p>
|
</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-end text-eerie-black dark:text-white">
|
<div className="flex flex-col justify-end text-eerie-black dark:text-white">
|
||||||
<div className="flex justify-between items-center py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<Help />
|
<Help />
|
||||||
|
|
||||||
<div className="flex items-center gap-1 pr-4">
|
<div className="flex items-center gap-1 pr-4">
|
||||||
@@ -381,9 +484,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
|
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
|
||||||
<div className="flex gap-6 items-center h-full ml-6 ">
|
<div className="ml-6 flex h-full items-center gap-6">
|
||||||
<button
|
<button
|
||||||
className=" h-6 w-6 md:hidden"
|
className="h-6 w-6 md:hidden"
|
||||||
onClick={() => setNavOpen(true)}
|
onClick={() => setNavOpen(true)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -392,7 +495,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="w-7 filter dark:invert"
|
className="w-7 filter dark:invert"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="text-[#949494] font-medium text-[20px]">DocsGPT</div>
|
<div className="text-[20px] font-medium text-[#949494]">DocsGPT</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DeleteConvModal
|
<DeleteConvModal
|
||||||
|
|||||||
32
frontend/src/agents/AgentLogs.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
|
import Analytics from '../settings/Analytics';
|
||||||
|
import Logs from '../settings/Logs';
|
||||||
|
|
||||||
|
export default function AgentLogs() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { agentId } = useParams();
|
||||||
|
return (
|
||||||
|
<div className="p-4 md:p-12">
|
||||||
|
<div className="flex items-center gap-3 px-4">
|
||||||
|
<button
|
||||||
|
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||||
|
onClick={() => navigate('/agents')}
|
||||||
|
>
|
||||||
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<p className="mt-px text-sm font-semibold text-eerie-black dark:text-bright-gray">
|
||||||
|
Back to all agents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||||
|
<h1 className="m-0 text-[40px] font-bold text-[#212121] dark:text-white">
|
||||||
|
Agent Logs
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Analytics agentId={agentId} />
|
||||||
|
<Logs agentId={agentId} tableHeader="Agent endpoint logs" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
frontend/src/agents/AgentPreview.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
|
import ConversationMessages from '../conversation/ConversationMessages';
|
||||||
|
import { Query } from '../conversation/conversationModels';
|
||||||
|
import {
|
||||||
|
addQuery,
|
||||||
|
fetchAnswer,
|
||||||
|
handleAbort,
|
||||||
|
resendQuery,
|
||||||
|
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(selectQueries);
|
||||||
|
const status = useSelector(selectStatus);
|
||||||
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||||
|
|
||||||
|
const fetchStream = useRef<any>(null);
|
||||||
|
|
||||||
|
const handleFetchAnswer = useCallback(
|
||||||
|
({ question, index }: { question: string; index?: number }) => {
|
||||||
|
fetchStream.current = dispatch(
|
||||||
|
fetchAnswer({ question, indx: index, isPreview: true }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuestion = useCallback(
|
||||||
|
({
|
||||||
|
question,
|
||||||
|
isRetry = false,
|
||||||
|
index = undefined,
|
||||||
|
}: {
|
||||||
|
question: string;
|
||||||
|
isRetry?: boolean;
|
||||||
|
index?: number;
|
||||||
|
}) => {
|
||||||
|
const trimmedQuestion = question.trim();
|
||||||
|
if (trimmedQuestion === '') return;
|
||||||
|
|
||||||
|
if (index !== undefined) {
|
||||||
|
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||||
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
|
} else {
|
||||||
|
if (!isRetry) {
|
||||||
|
const newQuery: Query = { prompt: trimmedQuestion };
|
||||||
|
dispatch(addQuery(newQuery));
|
||||||
|
}
|
||||||
|
handleFetchAnswer({ question: trimmedQuestion, index: undefined });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, handleFetchAnswer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuestionSubmission = (
|
||||||
|
updatedQuestion?: string,
|
||||||
|
updated?: boolean,
|
||||||
|
indx?: number,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
updated === true &&
|
||||||
|
updatedQuestion !== undefined &&
|
||||||
|
indx !== undefined
|
||||||
|
) {
|
||||||
|
handleQuestion({
|
||||||
|
question: updatedQuestion,
|
||||||
|
index: indx,
|
||||||
|
isRetry: false,
|
||||||
|
});
|
||||||
|
} else if (input.trim() && status !== 'loading') {
|
||||||
|
const currentInput = input.trim();
|
||||||
|
if (lastQueryReturnedErr && queries.length > 0) {
|
||||||
|
const lastQueryIndex = queries.length - 1;
|
||||||
|
handleQuestion({
|
||||||
|
question: currentInput,
|
||||||
|
isRetry: true,
|
||||||
|
index: lastQueryIndex,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleQuestion({
|
||||||
|
question: currentInput,
|
||||||
|
isRetry: false,
|
||||||
|
index: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleQuestionSubmission();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(resetConversation());
|
||||||
|
return () => {
|
||||||
|
if (fetchStream.current) fetchStream.current.abort();
|
||||||
|
handleAbort();
|
||||||
|
dispatch(resetConversation());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queries.length > 0) {
|
||||||
|
const lastQuery = queries[queries.length - 1];
|
||||||
|
setLastQueryReturnedErr(!!lastQuery.error);
|
||||||
|
} else setLastQueryReturnedErr(false);
|
||||||
|
}, [queries]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
||||||
|
<div className="h-[512px] w-full overflow-y-auto">
|
||||||
|
<ConversationMessages
|
||||||
|
handleQuestion={handleQuestion}
|
||||||
|
handleQuestionSubmission={handleQuestionSubmission}
|
||||||
|
queries={queries}
|
||||||
|
status={status}
|
||||||
|
showHeroOnEmpty={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||||
|
<MessageInput
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onSubmit={() => handleQuestionSubmission()}
|
||||||
|
loading={status === 'loading'}
|
||||||
|
showSourceButton={selectedAgent ? false : true}
|
||||||
|
showToolButton={selectedAgent ? false : true}
|
||||||
|
/>
|
||||||
|
<p className="w-full self-center bg-transparent pt-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline">
|
||||||
|
This is a preview of the agent. You can publish it to start using it
|
||||||
|
in conversations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
614
frontend/src/agents/NewAgent.tsx
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
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 MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
|
||||||
|
import AgentDetailsModal from '../modals/AgentDetailsModal';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
|
import { ActiveState, Doc, Prompt } from '../models/misc';
|
||||||
|
import {
|
||||||
|
selectSelectedAgent,
|
||||||
|
selectSourceDocs,
|
||||||
|
selectToken,
|
||||||
|
setSelectedAgent,
|
||||||
|
} from '../preferences/preferenceSlice';
|
||||||
|
import PromptsModal from '../preferences/PromptsModal';
|
||||||
|
import { UserToolType } from '../settings/types';
|
||||||
|
import AgentPreview from './AgentPreview';
|
||||||
|
import { Agent } from './types';
|
||||||
|
|
||||||
|
const embeddingsName =
|
||||||
|
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||||
|
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||||
|
|
||||||
|
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { agentId } = useParams();
|
||||||
|
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
const sourceDocs = useSelector(selectSourceDocs);
|
||||||
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
|
||||||
|
const [effectiveMode, setEffectiveMode] = useState(mode);
|
||||||
|
const [agent, setAgent] = useState<Agent>({
|
||||||
|
id: agentId || '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
image: '',
|
||||||
|
source: '',
|
||||||
|
chunks: '',
|
||||||
|
retriever: '',
|
||||||
|
prompt_id: '',
|
||||||
|
tools: [],
|
||||||
|
agent_type: '',
|
||||||
|
status: '',
|
||||||
|
});
|
||||||
|
const [prompts, setPrompts] = useState<
|
||||||
|
{ name: string; id: string; type: string }[]
|
||||||
|
>([]);
|
||||||
|
const [userTools, setUserTools] = useState<OptionType[]>([]);
|
||||||
|
const [isSourcePopupOpen, setIsSourcePopupOpen] = useState(false);
|
||||||
|
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||||
|
const [selectedSourceIds, setSelectedSourceIds] = useState<
|
||||||
|
Set<string | number>
|
||||||
|
>(new Set());
|
||||||
|
const [selectedToolIds, setSelectedToolIds] = useState<Set<string | number>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [deleteConfirmation, setDeleteConfirmation] =
|
||||||
|
useState<ActiveState>('INACTIVE');
|
||||||
|
const [agentDetails, setAgentDetails] = useState<ActiveState>('INACTIVE');
|
||||||
|
const [addPromptModal, setAddPromptModal] = useState<ActiveState>('INACTIVE');
|
||||||
|
|
||||||
|
const sourceAnchorButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const toolAnchorButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const modeConfig = {
|
||||||
|
new: {
|
||||||
|
heading: 'New Agent',
|
||||||
|
buttonText: 'Create Agent',
|
||||||
|
showDelete: false,
|
||||||
|
showSaveDraft: true,
|
||||||
|
showLogs: false,
|
||||||
|
showAccessDetails: false,
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
heading: 'Edit Agent',
|
||||||
|
buttonText: 'Save Changes',
|
||||||
|
showDelete: true,
|
||||||
|
showSaveDraft: false,
|
||||||
|
showLogs: true,
|
||||||
|
showAccessDetails: true,
|
||||||
|
},
|
||||||
|
draft: {
|
||||||
|
heading: 'New Agent (Draft)',
|
||||||
|
buttonText: 'Publish Draft',
|
||||||
|
showDelete: true,
|
||||||
|
showSaveDraft: true,
|
||||||
|
showLogs: false,
|
||||||
|
showAccessDetails: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const chunks = ['0', '2', '4', '6', '8', '10'];
|
||||||
|
const agentTypes = [
|
||||||
|
{ label: 'Classic', value: 'classic' },
|
||||||
|
{ label: 'ReAct', value: 'react' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isPublishable = () => {
|
||||||
|
return (
|
||||||
|
agent.name &&
|
||||||
|
agent.description &&
|
||||||
|
(agent.source || agent.retriever) &&
|
||||||
|
agent.chunks &&
|
||||||
|
agent.prompt_id &&
|
||||||
|
agent.agent_type
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (selectedAgent) dispatch(setSelectedAgent(null));
|
||||||
|
navigate('/agents');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (agentId: string) => {
|
||||||
|
const response = await userService.deleteAgent(agentId, token);
|
||||||
|
if (!response.ok) throw new Error('Failed to delete agent');
|
||||||
|
navigate('/agents');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDraft = async () => {
|
||||||
|
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 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') {
|
||||||
|
setAgentDetails('ACTIVE');
|
||||||
|
setEffectiveMode('edit');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getTools = async () => {
|
||||||
|
const response = await userService.getUserTools(token);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch tools');
|
||||||
|
const data = await response.json();
|
||||||
|
const tools: OptionType[] = data.tools.map((tool: UserToolType) => ({
|
||||||
|
id: tool.id,
|
||||||
|
label: tool.displayName,
|
||||||
|
icon: `/toolIcons/tool_${tool.name}.svg`,
|
||||||
|
}));
|
||||||
|
setUserTools(tools);
|
||||||
|
};
|
||||||
|
const getPrompts = async () => {
|
||||||
|
const response = await userService.getPrompts(token);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch prompts');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setPrompts(data);
|
||||||
|
};
|
||||||
|
getTools();
|
||||||
|
getPrompts();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((mode === 'edit' || mode === 'draft') && agentId) {
|
||||||
|
const getAgent = async () => {
|
||||||
|
const response = await userService.getAgent(agentId, token);
|
||||||
|
if (!response.ok) {
|
||||||
|
navigate('/agents');
|
||||||
|
throw new Error('Failed to fetch agent');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.source) setSelectedSourceIds(new Set([data.source]));
|
||||||
|
else if (data.retriever)
|
||||||
|
setSelectedSourceIds(new Set([data.retriever]));
|
||||||
|
if (data.tools) setSelectedToolIds(new Set(data.tools));
|
||||||
|
if (data.status === 'draft') setEffectiveMode('draft');
|
||||||
|
setAgent(data);
|
||||||
|
};
|
||||||
|
getAgent();
|
||||||
|
}
|
||||||
|
}, [agentId, mode, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedSource = Array.from(selectedSourceIds).map((id) =>
|
||||||
|
sourceDocs?.find(
|
||||||
|
(source) =>
|
||||||
|
source.id === id || source.retriever === id || source.name === id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (selectedSource[0]?.model === embeddingsName) {
|
||||||
|
if (selectedSource[0] && 'id' in selectedSource[0]) {
|
||||||
|
setAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
source: selectedSource[0]?.id || 'default',
|
||||||
|
retriever: '',
|
||||||
|
}));
|
||||||
|
} else
|
||||||
|
setAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
source: '',
|
||||||
|
retriever: selectedSource[0]?.retriever || 'classic',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [selectedSourceIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedTool = Array.from(selectedToolIds).map((id) =>
|
||||||
|
userTools.find((tool) => tool.id === id),
|
||||||
|
);
|
||||||
|
setAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tools: selectedTool
|
||||||
|
.map((tool) => tool?.id)
|
||||||
|
.filter((id): id is string => typeof id === 'string'),
|
||||||
|
}));
|
||||||
|
}, [selectedToolIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPublishable()) dispatch(setSelectedAgent(agent));
|
||||||
|
}, [agent, dispatch]);
|
||||||
|
return (
|
||||||
|
<div className="p-4 md:p-12">
|
||||||
|
<div className="flex items-center gap-3 px-4">
|
||||||
|
<button
|
||||||
|
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<p className="mt-px text-sm font-semibold text-eerie-black dark:text-bright-gray">
|
||||||
|
Back to all agents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||||
|
<h1 className="m-0 text-[40px] font-bold text-[#212121] dark:text-white">
|
||||||
|
{modeConfig[effectiveMode].heading}
|
||||||
|
</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
<button
|
||||||
|
className="mr-4 rounded-3xl py-2 text-sm font-medium text-purple-30 dark:bg-transparent dark:text-light-gray"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{modeConfig[effectiveMode].showDelete && agent.id && (
|
||||||
|
<button
|
||||||
|
className="group flex items-center gap-2 rounded-3xl border border-solid border-red-2000 px-5 py-2 text-sm font-medium text-red-2000 transition-colors hover:bg-red-2000 hover:text-white"
|
||||||
|
onClick={() => setDeleteConfirmation('ACTIVE')}
|
||||||
|
>
|
||||||
|
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{modeConfig[effectiveMode].showSaveDraft && (
|
||||||
|
<button
|
||||||
|
className="hover:bg-vi</button>olets-are-blue 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={handleSaveDraft}
|
||||||
|
>
|
||||||
|
Save Draft
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{modeConfig[effectiveMode].showAccessDetails && (
|
||||||
|
<button
|
||||||
|
className="hover:bg-vi</button>olets-are-blue 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={() => navigate(`/agents/logs/${agent.id}`)}
|
||||||
|
>
|
||||||
|
Logs
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{modeConfig[effectiveMode].showAccessDetails && (
|
||||||
|
<button
|
||||||
|
className="hover:bg-vi</button>olets-are-blue 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={() => setAgentDetails('ACTIVE')}
|
||||||
|
>
|
||||||
|
Access Details
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
disabled={!isPublishable()}
|
||||||
|
className={`${!isPublishable() && 'cursor-not-allowed opacity-30'} rounded-3xl bg-purple-30 px-5 py-2 text-sm font-medium text-white hover:bg-violets-are-blue`}
|
||||||
|
onClick={handlePublish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex w-full grid-cols-5 flex-col gap-10 min-[1180px]:grid min-[1180px]:gap-5">
|
||||||
|
<div className="col-span-2 flex flex-col gap-5">
|
||||||
|
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
|
<h2 className="text-lg font-semibold">Meta</h2>
|
||||||
|
<input
|
||||||
|
className="mt-3 w-full rounded-3xl border border-silver bg-white px-5 py-3 text-sm text-jet outline-none placeholder:text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-bright-gray placeholder:dark:text-silver"
|
||||||
|
type="text"
|
||||||
|
value={agent.name}
|
||||||
|
placeholder="Agent name"
|
||||||
|
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="mt-3 h-32 w-full rounded-3xl border border-silver bg-white px-5 py-4 text-sm text-jet outline-none placeholder:text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-bright-gray placeholder:dark:text-silver"
|
||||||
|
placeholder="Describe your agent"
|
||||||
|
value={agent.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAgent({ ...agent, description: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
<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 text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
|
||||||
|
>
|
||||||
|
{selectedSourceIds.size > 0
|
||||||
|
? Array.from(selectedSourceIds)
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
sourceDocs?.find(
|
||||||
|
(source) =>
|
||||||
|
source.id === id ||
|
||||||
|
source.name === id ||
|
||||||
|
source.retriever === id,
|
||||||
|
)?.name,
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
: 'Select source'}
|
||||||
|
</button>
|
||||||
|
<MultiSelectPopup
|
||||||
|
isOpen={isSourcePopupOpen}
|
||||||
|
onClose={() => setIsSourcePopupOpen(false)}
|
||||||
|
anchorRef={sourceAnchorButtonRef}
|
||||||
|
options={
|
||||||
|
sourceDocs?.map((doc: Doc) => ({
|
||||||
|
id: doc.id || doc.retriever || doc.name,
|
||||||
|
label: doc.name,
|
||||||
|
icon: SourceIcon,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
selectedIds={selectedSourceIds}
|
||||||
|
onSelectionChange={(newSelectedIds: Set<string | number>) => {
|
||||||
|
setSelectedSourceIds(newSelectedIds);
|
||||||
|
setIsSourcePopupOpen(false);
|
||||||
|
}}
|
||||||
|
title="Select Source"
|
||||||
|
searchPlaceholder="Search sources..."
|
||||||
|
noOptionsMessage="No source available"
|
||||||
|
singleSelect={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Dropdown
|
||||||
|
options={chunks}
|
||||||
|
selectedValue={agent.chunks ? agent.chunks : null}
|
||||||
|
onSelect={(value: string) =>
|
||||||
|
setAgent({ ...agent, chunks: value })
|
||||||
|
}
|
||||||
|
size="w-full"
|
||||||
|
rounded="3xl"
|
||||||
|
buttonDarkBackgroundColor="[#222327]"
|
||||||
|
border="border"
|
||||||
|
darkBorderColor="[#7E7E7E]"
|
||||||
|
placeholder="Chunks per query"
|
||||||
|
placeholderTextColor="gray-400"
|
||||||
|
darkPlaceholderTextColor="silver"
|
||||||
|
contentSize="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
|
<h2 className="text-lg font-semibold">Prompt</h2>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-1">
|
||||||
|
<div className="min-w-20 flex-grow basis-full sm:basis-0">
|
||||||
|
<Dropdown
|
||||||
|
options={prompts.map((prompt) => ({
|
||||||
|
label: prompt.name,
|
||||||
|
value: prompt.id,
|
||||||
|
}))}
|
||||||
|
selectedValue={
|
||||||
|
agent.prompt_id
|
||||||
|
? prompts.filter(
|
||||||
|
(prompt) => prompt.id === agent.prompt_id,
|
||||||
|
)[0].name || null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onSelect={(option: { label: string; value: string }) =>
|
||||||
|
setAgent({ ...agent, prompt_id: option.value })
|
||||||
|
}
|
||||||
|
size="w-full"
|
||||||
|
rounded="3xl"
|
||||||
|
buttonDarkBackgroundColor="[#222327]"
|
||||||
|
border="border"
|
||||||
|
darkBorderColor="[#7E7E7E]"
|
||||||
|
placeholder="Select a prompt"
|
||||||
|
placeholderTextColor="gray-400"
|
||||||
|
darkPlaceholderTextColor="silver"
|
||||||
|
contentSize="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="w-20 flex-shrink-0 basis-full rounded-3xl border-2 border-solid border-violets-are-blue px-5 py-[11px] text-sm text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white sm:basis-auto"
|
||||||
|
onClick={() => setAddPromptModal('ACTIVE')}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
|
<h2 className="text-lg font-semibold">Tools</h2>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-1">
|
||||||
|
<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 text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
|
||||||
|
>
|
||||||
|
{selectedToolIds.size > 0
|
||||||
|
? Array.from(selectedToolIds)
|
||||||
|
.map(
|
||||||
|
(id) => userTools.find((tool) => tool.id === id)?.label,
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
: 'Select tools'}
|
||||||
|
</button>
|
||||||
|
<MultiSelectPopup
|
||||||
|
isOpen={isToolsPopupOpen}
|
||||||
|
onClose={() => setIsToolsPopupOpen(false)}
|
||||||
|
anchorRef={toolAnchorButtonRef}
|
||||||
|
options={userTools}
|
||||||
|
selectedIds={selectedToolIds}
|
||||||
|
onSelectionChange={(newSelectedIds: Set<string | number>) =>
|
||||||
|
setSelectedToolIds(newSelectedIds)
|
||||||
|
}
|
||||||
|
title="Select Tools"
|
||||||
|
searchPlaceholder="Search tools..."
|
||||||
|
noOptionsMessage="No tools available"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
|
<h2 className="text-lg font-semibold">Agent type</h2>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Dropdown
|
||||||
|
options={agentTypes}
|
||||||
|
selectedValue={
|
||||||
|
agent.agent_type
|
||||||
|
? agentTypes.find((type) => type.value === agent.agent_type)
|
||||||
|
?.label || null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onSelect={(option: { label: string; value: string }) =>
|
||||||
|
setAgent({ ...agent, agent_type: option.value })
|
||||||
|
}
|
||||||
|
size="w-full"
|
||||||
|
rounded="3xl"
|
||||||
|
buttonDarkBackgroundColor="[#222327]"
|
||||||
|
border="border"
|
||||||
|
darkBorderColor="[#7E7E7E]"
|
||||||
|
placeholder="Select type"
|
||||||
|
placeholderTextColor="gray-400"
|
||||||
|
darkPlaceholderTextColor="silver"
|
||||||
|
contentSize="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 flex flex-col gap-3 rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
|
<h2 className="text-lg font-semibold">Preview</h2>
|
||||||
|
<AgentPreviewArea />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConfirmationModal
|
||||||
|
message="Are you sure you want to delete this agent?"
|
||||||
|
modalState={deleteConfirmation}
|
||||||
|
setModalState={setDeleteConfirmation}
|
||||||
|
submitLabel="Delete"
|
||||||
|
handleSubmit={() => {
|
||||||
|
handleDelete(agent.id || '');
|
||||||
|
setDeleteConfirmation('INACTIVE');
|
||||||
|
}}
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
<AgentDetailsModal
|
||||||
|
agent={agent}
|
||||||
|
mode={effectiveMode}
|
||||||
|
modalState={agentDetails}
|
||||||
|
setModalState={setAgentDetails}
|
||||||
|
/>
|
||||||
|
<AddPromptModal
|
||||||
|
prompts={prompts}
|
||||||
|
setPrompts={setPrompts}
|
||||||
|
isOpen={addPromptModal}
|
||||||
|
onClose={() => setAddPromptModal('INACTIVE')}
|
||||||
|
onSelect={(name: string, id: string, type: string) => {
|
||||||
|
setAgent({ ...agent, prompt_id: id });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentPreviewArea() {
|
||||||
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full rounded-[30px] border border-[#F6F6F6] bg-white dark:border-[#7E7E7E] dark:bg-[#222327] max-[1180px]:h-[48rem]">
|
||||||
|
{selectedAgent?.id ? (
|
||||||
|
<div className="flex h-full w-full flex-col justify-end overflow-auto rounded-[30px]">
|
||||||
|
<AgentPreview />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
|
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
||||||
|
<p className="text-xs text-[#18181B] dark:text-[#949494]">
|
||||||
|
Published agents can be previewd here
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddPromptModal({
|
||||||
|
prompts,
|
||||||
|
setPrompts,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
prompts: Prompt[];
|
||||||
|
setPrompts?: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||||
|
isOpen: ActiveState;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect?: (name: string, id: string, type: string) => void;
|
||||||
|
}) {
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
|
const [newPromptName, setNewPromptName] = useState('');
|
||||||
|
const [newPromptContent, setNewPromptContent] = useState('');
|
||||||
|
|
||||||
|
const handleAddPrompt = async () => {
|
||||||
|
try {
|
||||||
|
const response = await userService.createPrompt(
|
||||||
|
{
|
||||||
|
name: newPromptName,
|
||||||
|
content: newPromptContent,
|
||||||
|
},
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add prompt');
|
||||||
|
}
|
||||||
|
const newPrompt = await response.json();
|
||||||
|
if (setPrompts) {
|
||||||
|
setPrompts([
|
||||||
|
...prompts,
|
||||||
|
{ name: newPromptName, id: newPrompt.id, type: 'private' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
setNewPromptName('');
|
||||||
|
setNewPromptContent('');
|
||||||
|
onSelect?.(newPromptName, newPrompt.id, newPromptContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<PromptsModal
|
||||||
|
modalState={isOpen}
|
||||||
|
setModalState={onClose}
|
||||||
|
type="ADD"
|
||||||
|
existingPrompts={prompts}
|
||||||
|
newPromptName={newPromptName}
|
||||||
|
setNewPromptName={setNewPromptName}
|
||||||
|
newPromptContent={newPromptContent}
|
||||||
|
setNewPromptContent={setNewPromptContent}
|
||||||
|
editPromptName={''}
|
||||||
|
setEditPromptName={() => {}}
|
||||||
|
editPromptContent={''}
|
||||||
|
setEditPromptContent={() => {}}
|
||||||
|
currentPromptEdit={{ id: '', name: '', type: '' }}
|
||||||
|
handleAddPrompt={handleAddPrompt}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
275
frontend/src/agents/index.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import React, { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import Copy from '../assets/copy-linear.svg';
|
||||||
|
import Edit from '../assets/edit.svg';
|
||||||
|
import Monitoring from '../assets/monitoring.svg';
|
||||||
|
import Trash from '../assets/red-trash.svg';
|
||||||
|
import Robot from '../assets/robot.svg';
|
||||||
|
import ThreeDots from '../assets/three-dots.svg';
|
||||||
|
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
|
import { selectToken, setSelectedAgent } from '../preferences/preferenceSlice';
|
||||||
|
import AgentLogs from './AgentLogs';
|
||||||
|
import NewAgent from './NewAgent';
|
||||||
|
import { Agent } from './types';
|
||||||
|
import Spinner from '../components/Spinner';
|
||||||
|
|
||||||
|
export default function Agents() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<AgentsList />} />
|
||||||
|
<Route path="/new" element={<NewAgent mode="new" />} />
|
||||||
|
<Route path="/edit/:agentId" element={<NewAgent mode="edit" />} />
|
||||||
|
<Route path="/logs/:agentId" element={<AgentLogs />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentsList() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
|
const [userAgents, setUserAgents] = useState<Agent[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const getAgents = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await userService.getAgents(token);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||||
|
const data = await response.json();
|
||||||
|
setUserAgents(data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAgents();
|
||||||
|
}, [token]);
|
||||||
|
return (
|
||||||
|
<div className="p-4 md:p-12">
|
||||||
|
<h1 className="mb-0 text-[40px] font-bold text-[#212121] dark:text-[#E0E0E0]">
|
||||||
|
Agents
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 text-[15px] text-[#71717A] dark:text-[#949494]">
|
||||||
|
Discover and create custom versions of DocsGPT that combine
|
||||||
|
instructions, extra knowledge, and any combination of skills.
|
||||||
|
</p>
|
||||||
|
{/* <div className="mt-6">
|
||||||
|
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
||||||
|
Premade by DocsGPT
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 flex w-full flex-wrap gap-4">
|
||||||
|
{Array.from({ length: 5 }, (_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative flex h-44 w-48 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 dark:bg-[#383838]"
|
||||||
|
>
|
||||||
|
<button onClick={() => {}} className="absolute right-4 top-4">
|
||||||
|
<img
|
||||||
|
src={Copy}
|
||||||
|
alt={'use-agent'}
|
||||||
|
className="h-[19px] w-[19px]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full items-center px-1">
|
||||||
|
<img
|
||||||
|
src={Robot}
|
||||||
|
alt="agent-logo"
|
||||||
|
className="h-7 w-7 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p
|
||||||
|
title={''}
|
||||||
|
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-raisin-black-light dark:text-bright-gray"
|
||||||
|
>
|
||||||
|
{}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-old-silver dark:text-sonic-silver-light">
|
||||||
|
{}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-4 right-4"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div className="mt-8 flex flex-col gap-4">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
||||||
|
Created by You
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
||||||
|
onClick={() => navigate('/agents/new')}
|
||||||
|
>
|
||||||
|
New Agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-wrap gap-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-72 w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : userAgents.length > 0 ? (
|
||||||
|
userAgents.map((agent) => (
|
||||||
|
<AgentCard
|
||||||
|
key={agent.id}
|
||||||
|
agent={agent}
|
||||||
|
setUserAgents={setUserAgents}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
|
||||||
|
<p>You don’t have any created agents yet </p>
|
||||||
|
<button
|
||||||
|
className="ml-2 rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
||||||
|
onClick={() => navigate('/agents/new')}
|
||||||
|
>
|
||||||
|
New Agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentCard({
|
||||||
|
agent,
|
||||||
|
setUserAgents,
|
||||||
|
}: {
|
||||||
|
agent: Agent;
|
||||||
|
setUserAgents: React.Dispatch<React.SetStateAction<Agent[]>>;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
||||||
|
const [deleteConfirmation, setDeleteConfirmation] =
|
||||||
|
useState<ActiveState>('INACTIVE');
|
||||||
|
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const menuOptions: MenuOption[] = [
|
||||||
|
{
|
||||||
|
icon: Monitoring,
|
||||||
|
label: 'Logs',
|
||||||
|
onClick: (e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/agents/logs/${agent.id}`);
|
||||||
|
},
|
||||||
|
variant: 'primary',
|
||||||
|
iconWidth: 14,
|
||||||
|
iconHeight: 14,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Edit,
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: (e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/agents/edit/${agent.id}`);
|
||||||
|
},
|
||||||
|
variant: 'primary',
|
||||||
|
iconWidth: 14,
|
||||||
|
iconHeight: 14,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Trash,
|
||||||
|
label: 'Delete',
|
||||||
|
onClick: (e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteConfirmation('ACTIVE');
|
||||||
|
},
|
||||||
|
variant: 'danger',
|
||||||
|
iconWidth: 12,
|
||||||
|
iconHeight: 12,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
dispatch(setSelectedAgent(agent));
|
||||||
|
navigate(`/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (agentId: string) => {
|
||||||
|
const response = await userService.deleteAgent(agentId, token);
|
||||||
|
if (!response.ok) throw new Error('Failed to delete agent');
|
||||||
|
const data = await response.json();
|
||||||
|
setUserAgents((prevAgents) =>
|
||||||
|
prevAgents.filter((prevAgent) => prevAgent.id !== data.id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative flex h-44 w-48 cursor-pointer flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 dark:bg-[#383838]"
|
||||||
|
onClick={(e) => handleClick()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsMenuOpen(true);
|
||||||
|
}}
|
||||||
|
className="absolute right-4 top-4 z-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<img src={ThreeDots} alt={'use-agent'} className="h-[19px] w-[19px]" />
|
||||||
|
<ContextMenu
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
setIsOpen={setIsMenuOpen}
|
||||||
|
options={menuOptions}
|
||||||
|
anchorRef={menuRef}
|
||||||
|
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 ?? Robot}
|
||||||
|
alt={`${agent.name}`}
|
||||||
|
className="h-7 w-7 rounded-full"
|
||||||
|
/>
|
||||||
|
{agent.status === 'draft' && (
|
||||||
|
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p
|
||||||
|
title={agent.name}
|
||||||
|
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-[#020617] dark:text-[#E0E0E0]"
|
||||||
|
>
|
||||||
|
{agent.name}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-[#64748B] dark:text-sonic-silver-light">
|
||||||
|
{agent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConfirmationModal
|
||||||
|
message="Are you sure you want to delete this agent?"
|
||||||
|
modalState={deleteConfirmation}
|
||||||
|
setModalState={setDeleteConfirmation}
|
||||||
|
submitLabel="Delete"
|
||||||
|
handleSubmit={() => {
|
||||||
|
handleDelete(agent.id || '');
|
||||||
|
setDeleteConfirmation('INACTIVE');
|
||||||
|
}}
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
frontend/src/agents/types/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export type Agent = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
source: string;
|
||||||
|
chunks: string;
|
||||||
|
retriever: string;
|
||||||
|
prompt_id: string;
|
||||||
|
tools: string[];
|
||||||
|
agent_type: string;
|
||||||
|
status: string;
|
||||||
|
key?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
last_used_at?: string;
|
||||||
|
};
|
||||||
@@ -8,6 +8,11 @@ const endpoints = {
|
|||||||
API_KEYS: '/api/get_api_keys',
|
API_KEYS: '/api/get_api_keys',
|
||||||
CREATE_API_KEY: '/api/create_api_key',
|
CREATE_API_KEY: '/api/create_api_key',
|
||||||
DELETE_API_KEY: '/api/delete_api_key',
|
DELETE_API_KEY: '/api/delete_api_key',
|
||||||
|
AGENT: (id: string) => `/api/get_agent?id=${id}`,
|
||||||
|
AGENTS: '/api/get_agents',
|
||||||
|
CREATE_AGENT: '/api/create_agent',
|
||||||
|
UPDATE_AGENT: (agent_id: string) => `/api/update_agent/${agent_id}`,
|
||||||
|
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
|
||||||
PROMPTS: '/api/get_prompts',
|
PROMPTS: '/api/get_prompts',
|
||||||
CREATE_PROMPT: '/api/create_prompt',
|
CREATE_PROMPT: '/api/create_prompt',
|
||||||
DELETE_PROMPT: '/api/delete_prompt',
|
DELETE_PROMPT: '/api/delete_prompt',
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ const userService = {
|
|||||||
apiClient.post(endpoints.USER.CREATE_API_KEY, data, token),
|
apiClient.post(endpoints.USER.CREATE_API_KEY, data, token),
|
||||||
deleteAPIKey: (data: any, token: string | null): Promise<any> =>
|
deleteAPIKey: (data: any, token: string | null): Promise<any> =>
|
||||||
apiClient.post(endpoints.USER.DELETE_API_KEY, data, token),
|
apiClient.post(endpoints.USER.DELETE_API_KEY, data, token),
|
||||||
|
getAgent: (id: string, token: string | null): Promise<any> =>
|
||||||
|
apiClient.get(endpoints.USER.AGENT(id), token),
|
||||||
|
getAgents: (token: string | null): Promise<any> =>
|
||||||
|
apiClient.get(endpoints.USER.AGENTS, token),
|
||||||
|
createAgent: (data: any, token: string | null): Promise<any> =>
|
||||||
|
apiClient.post(endpoints.USER.CREATE_AGENT, data, token),
|
||||||
|
updateAgent: (
|
||||||
|
agent_id: string,
|
||||||
|
data: any,
|
||||||
|
token: string | null,
|
||||||
|
): Promise<any> =>
|
||||||
|
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),
|
||||||
getPrompts: (token: string | null): Promise<any> =>
|
getPrompts: (token: string | null): Promise<any> =>
|
||||||
apiClient.get(endpoints.USER.PROMPTS, token),
|
apiClient.get(endpoints.USER.PROMPTS, token),
|
||||||
createPrompt: (data: any, token: string | null): Promise<any> =>
|
createPrompt: (data: any, token: string | null): Promise<any> =>
|
||||||
|
|||||||
4
frontend/src/assets/copy-linear.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.75 8.70825C3.75 6.46942 3.75 5.34921 4.44588 4.65413C5.14096 3.95825 6.26117 3.95825 8.5 3.95825H10.875C13.1138 3.95825 14.234 3.95825 14.9291 4.65413C15.625 5.34921 15.625 6.46942 15.625 8.70825V12.6666C15.625 14.9054 15.625 16.0256 14.9291 16.7207C14.234 17.4166 13.1138 17.4166 10.875 17.4166H8.5C6.26117 17.4166 5.14096 17.4166 4.44588 16.7207C3.75 16.0256 3.75 14.9054 3.75 12.6666V8.70825Z" stroke="#B9B9B9" stroke-width="1.5"/>
|
||||||
|
<path d="M3.75 15.0416C3.12011 15.0416 2.51602 14.7914 2.07062 14.346C1.62522 13.9006 1.375 13.2965 1.375 12.6666V7.91658C1.375 4.93121 1.375 3.43813 2.30283 2.51109C3.23067 1.58404 4.72296 1.58325 7.70833 1.58325H10.875C11.5049 1.58325 12.109 1.83347 12.5544 2.27887C12.9998 2.72427 13.25 3.32836 13.25 3.95825" stroke="#B9B9B9" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 901 B |
3
frontend/src/assets/monitoring.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="19" height="17" viewBox="0 0 19 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.85 16.087C4.84924 16.2663 4.91145 16.4239 5.03693 16.5494C5.16233 16.6747 5.31971 16.7373 5.499 16.7373L5.4997 16.7373C5.67889 16.7364 5.83627 16.6743 5.9617 16.5497C6.08758 16.4247 6.15 16.267 6.15 16.0873V10.7993C6.15 10.619 6.08842 10.4607 5.96269 10.3358C5.83772 10.2117 5.68058 10.1501 5.5017 10.1493C5.32173 10.1484 5.16366 10.2105 5.03793 10.3362C4.9124 10.4618 4.85 10.6195 4.85 10.7993V16.087ZM4.85 16.087C4.85 16.0868 4.85 16.0867 4.85 16.0866L5 16.0873M4.85 16.087V16.0873H5M5 16.0873C4.99933 16.2286 5.047 16.3473 5.143 16.4433C5.239 16.5393 5.35767 16.5873 5.499 16.5873L5 16.0873ZM17.4984 0.850356C17.6844 0.844257 17.8447 0.916616 17.9702 1.05647C18.0903 1.18276 18.1491 1.33782 18.1439 1.51269C18.1389 1.6837 18.0787 1.83386 17.9605 1.95296L17.9601 1.95335L12.7431 7.17035L12.7421 7.17131C12.5779 7.33262 12.3846 7.45674 12.1641 7.54381C11.9451 7.63023 11.7235 7.67428 11.5 7.67428C11.277 7.67428 11.0585 7.63017 10.8453 7.54316M17.4984 0.850356L10.8453 7.54316M17.4984 0.850356C17.3204 0.855502 17.1648 0.921665 17.0399 1.04721L17.4984 0.850356ZM10.8453 7.54316C10.632 7.45637 10.4379 7.33351 10.2633 7.17548L10.2578 7.1705L10.258 7.17037L7.83596 4.74937L7.83593 4.74935C7.75166 4.66508 7.6439 4.62029 7.5 4.62029C7.3561 4.62029 7.24834 4.66508 7.16407 4.74935L1.96007 9.95335C1.83516 10.0783 1.67973 10.1443 1.50197 10.1502L1.50128 10.1502C1.31531 10.1555 1.15551 10.0825 1.03084 9.94215C0.910023 9.81657 0.850921 9.66177 0.856065 9.48688C0.861095 9.31586 0.921352 9.1658 1.03993 9.04722L6.25793 3.82922L6.25859 3.82856C6.43436 3.65497 6.63139 3.52612 6.84951 3.44477C7.06174 3.36537 7.27883 3.32528 7.5 3.32528C7.72114 3.32528 7.94133 3.36536 8.15989 3.44418C8.38508 3.5254 8.58031 3.65486 8.74407 3.83122L11.164 6.2502L11.1641 6.25022C11.2483 6.33449 11.3561 6.37928 11.5 6.37928C11.6439 6.37928 11.7517 6.33449 11.8359 6.25022L11.8359 6.25021L17.0396 1.04758L10.8453 7.54316ZM1.5 16.7373L1.5007 16.7373C1.68022 16.7364 1.83649 16.6741 1.9617 16.5497C2.08758 16.4247 2.15 16.267 2.15 16.0873V15.2993C2.15 15.119 2.08842 14.9607 1.96269 14.8358C1.83772 14.7117 1.68058 14.6501 1.5017 14.6493C1.32173 14.6484 1.16366 14.7105 1.03793 14.8362C0.912396 14.9618 0.85 15.1195 0.85 15.2993V16.0873C0.85 16.2663 0.91197 16.4235 1.03657 16.549C1.16172 16.675 1.31979 16.7373 1.5 16.7373ZM9.499 16.7373L9.4997 16.7373C9.67889 16.7364 9.83627 16.6743 9.9617 16.5497C10.0876 16.4247 10.15 16.267 10.15 16.0873V12.7993C10.15 12.619 10.0884 12.4607 9.96269 12.3358C9.83772 12.2117 9.68058 12.1501 9.5017 12.1493C9.32173 12.1484 9.16366 12.2105 9.03793 12.3362C8.9124 12.4618 8.85 12.6195 8.85 12.7993V16.0873C8.85 16.2663 8.91197 16.4235 9.03656 16.549C9.16158 16.6749 9.31924 16.7373 9.499 16.7373ZM13.499 16.7373L13.4997 16.7373C13.6789 16.7364 13.8363 16.6743 13.9617 16.5497C14.0876 16.4247 14.15 16.267 14.15 16.0873V11.2993C14.15 11.119 14.0884 10.9607 13.9627 10.8358C13.8377 10.7117 13.6806 10.6501 13.5017 10.6493C13.3217 10.6484 13.1637 10.7105 13.0379 10.8362C12.9124 10.9618 12.85 11.1195 12.85 11.2993L12.85 16.0866L13 16.0873H12.85V16.0868C12.8492 16.2662 12.9114 16.4238 13.0369 16.5494C13.1623 16.6747 13.3197 16.7373 13.499 16.7373ZM17.499 16.7373L17.4997 16.7373C17.6789 16.7364 17.8363 16.6743 17.9617 16.5497C18.0876 16.4247 18.15 16.267 18.15 16.0873V7.29928C18.15 7.11895 18.0884 6.9607 17.9627 6.83585C17.8377 6.71175 17.6806 6.65013 17.5017 6.64929C17.3217 6.64844 17.1637 6.7105 17.0379 6.83622C16.9124 6.96176 16.85 7.11954 16.85 7.29928L16.85 16.0866L17 16.0873H16.85V16.0868C16.8492 16.2662 16.9114 16.4238 17.0369 16.5494C17.1623 16.6747 17.3197 16.7373 17.499 16.7373Z" fill="#747474" stroke="#747474" stroke-width="0.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,3 +1,3 @@
|
|||||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.66427 17.7747C6.66427 18.2167 6.84488 18.6406 7.16637 18.9532C7.48787 19.2658 7.9239 19.4413 8.37856 19.4413H15.2357C15.6904 19.4413 16.1264 19.2658 16.4479 18.9532C16.7694 18.6406 16.95 18.2167 16.95 17.7747V7.77468H6.66427V17.7747ZM8.37856 9.44135H15.2357V17.7747H8.37856V9.44135ZM14.8071 5.27468L13.95 4.44135H9.66427L8.80713 5.27468H5.80713V6.94135H17.8071V5.27468H14.8071Z" fill="#D30000"/>
|
<path d="M1.66574 13.7747C1.66574 14.2168 1.84635 14.6407 2.16784 14.9533C2.48933 15.2658 2.92537 15.4414 3.38002 15.4414H10.2372C10.6918 15.4414 11.1279 15.2658 11.4493 14.9533C11.7708 14.6407 11.9515 14.2168 11.9515 13.7747V3.77474H1.66574V13.7747ZM3.38002 5.44141H10.2372V13.7747H3.38002V5.44141ZM9.80859 1.27474L8.95145 0.441406H4.66574L3.80859 1.27474H0.808594V2.94141H12.8086V1.27474H9.80859Z" fill="#f44336"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 520 B |
22
frontend/src/assets/robot.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<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_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_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_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"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
12
frontend/src/assets/science-spark-dark.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_8836_14948)">
|
||||||
|
<path d="M25.7094 18.5522C22.8007 20.7575 20.0485 23.1618 17.4725 25.7479C5.33468 37.8856 -0.811037 51.4105 3.7385 55.9643C8.28803 60.5138 21.8171 54.3638 33.9507 42.2303C36.5369 39.6529 38.9412 36.8992 41.1463 33.9891" stroke="#3F4147" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M41.1421 33.989C48.2993 43.4892 51.2142 52.4261 47.6804 55.9599C43.1266 60.5137 29.6018 54.3637 17.464 42.2302C5.33477 30.0881 -0.810942 16.5676 3.73859 12.0138C7.27238 8.48424 16.2093 11.3992 25.7095 18.5521" stroke="#3F4147" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M23.5781 33.9891C23.5781 34.5551 23.8029 35.0978 24.2031 35.498C24.6033 35.8982 25.1461 36.123 25.7121 36.123C26.278 36.123 26.8208 35.8982 27.221 35.498C27.6212 35.0978 27.846 34.5551 27.846 33.9891C27.846 33.4232 27.6212 32.8804 27.221 32.4802C26.8208 32.08 26.278 31.8552 25.7121 31.8552C25.1461 31.8552 24.6033 32.08 24.2031 32.4802C23.8029 32.8804 23.5781 33.4232 23.5781 33.9891ZM34.2904 15.4069C32.9461 15.1721 32.9461 13.2473 34.2904 13.0169C36.6655 12.6055 38.864 11.4952 40.6049 9.82805C42.3458 8.16089 43.5501 6.01251 44.0638 3.65746L44.1407 3.28616C44.4309 1.96312 46.3173 1.95459 46.616 3.27335L46.7184 3.70441C47.2541 6.04779 48.4698 8.18083 50.2131 9.83599C51.9563 11.4912 54.1495 12.5947 56.5174 13.0083C57.8661 13.2431 57.8661 15.1807 56.5174 15.4111C54.1495 15.8247 51.9563 16.9283 50.2131 18.5835C48.4698 20.2386 47.2541 22.3717 46.7184 24.7151L46.616 25.1461C46.3173 26.4692 44.4309 26.4606 44.1407 25.1376L44.0553 24.7663C43.5415 22.4112 42.3372 20.2629 40.5963 18.5957C38.8554 16.9285 36.657 15.8183 34.2819 15.4069H34.2904Z" stroke="#5F6167" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_8836_14948">
|
||||||
|
<rect width="59.75" height="59.75" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
12
frontend/src/assets/science-spark.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_8612_14680)">
|
||||||
|
<path d="M25.7094 18.5525C22.8007 20.7577 20.0485 23.162 17.4725 25.7481C5.33468 37.8859 -0.811037 51.4107 3.7385 55.9645C8.28803 60.5141 21.8171 54.3641 33.9507 42.2306C36.5369 39.6532 38.9412 36.8995 41.1463 33.9893" stroke="#DCDCDC" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M41.1421 33.9891C48.2993 43.4893 51.2142 52.4262 47.6804 55.96C43.1266 60.5138 29.6018 54.3638 17.464 42.2303C5.33477 30.0883 -0.810942 16.5677 3.73859 12.0139C7.27238 8.48436 16.2093 11.3993 25.7095 18.5522" stroke="#DCDCDC" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M23.5781 33.9891C23.5781 34.5551 23.8029 35.0978 24.2031 35.498C24.6033 35.8982 25.1461 36.123 25.7121 36.123C26.278 36.123 26.8208 35.8982 27.221 35.498C27.6212 35.0978 27.846 34.5551 27.846 33.9891C27.846 33.4232 27.6212 32.8804 27.221 32.4802C26.8208 32.08 26.278 31.8552 25.7121 31.8552C25.1461 31.8552 24.6033 32.08 24.2031 32.4802C23.8029 32.8804 23.5781 33.4232 23.5781 33.9891ZM34.2904 15.4069C32.9461 15.1721 32.9461 13.2473 34.2904 13.0169C36.6655 12.6055 38.864 11.4952 40.6049 9.82805C42.3458 8.16089 43.5501 6.01251 44.0638 3.65746L44.1407 3.28616C44.4309 1.96312 46.3173 1.95459 46.616 3.27335L46.7184 3.70441C47.2541 6.04779 48.4698 8.18083 50.2131 9.83599C51.9563 11.4912 54.1495 12.5947 56.5174 13.0083C57.8661 13.2431 57.8661 15.1807 56.5174 15.4111C54.1495 15.8247 51.9563 16.9283 50.2131 18.5835C48.4698 20.2386 47.2541 22.3717 46.7184 24.7151L46.616 25.1461C46.3173 26.4692 44.4309 26.4606 44.1407 25.1376L44.0553 24.7663C43.5415 22.4112 42.3372 20.2629 40.5963 18.5957C38.8554 16.9285 36.657 15.8183 34.2819 15.4069H34.2904Z" stroke="#999999" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_8612_14680">
|
||||||
|
<rect width="59.75" height="59.75" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
3
frontend/src/assets/spark.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.04071 11.3081H1.51004C1.33769 11.3088 1.1726 11.3776 1.05073 11.4995C0.928855 11.6213 0.860077 11.7864 0.859375 11.9588V17.4921C0.859375 17.8521 1.15137 18.1428 1.51004 18.1428H7.04337C7.40337 18.1428 7.69404 17.8508 7.69404 17.4921V11.9588C7.69334 11.7864 7.62456 11.6213 7.50269 11.4995C7.38082 11.3776 7.21573 11.3088 7.04337 11.3081H7.04071ZM17.1594 11.3081H11.626C11.4537 11.3088 11.2886 11.3776 11.1667 11.4995C11.0449 11.6213 10.9761 11.7864 10.9754 11.9588V17.4921C10.9754 17.8521 11.266 18.1428 11.626 18.1428H17.1594C17.5194 18.1428 17.81 17.8508 17.81 17.4921V11.9588C17.8093 11.7864 17.7406 11.6213 17.6187 11.4995C17.4968 11.3776 17.3317 11.3088 17.1594 11.3081ZM7.04071 1.19079H1.51004C1.33769 1.19149 1.1726 1.26027 1.05073 1.38214C0.928855 1.50401 0.860077 1.6691 0.859375 1.84145V7.37479C0.859375 7.73479 1.15137 8.02545 1.51004 8.02545H7.04337C7.40337 8.02545 7.69404 7.73345 7.69404 7.37479V1.84012C7.69334 1.66777 7.62456 1.50268 7.50269 1.3808C7.38082 1.25893 7.21573 1.19016 7.04337 1.18945L7.04071 1.19079ZM10.862 5.11212C10.4607 5.04279 10.4607 4.46812 10.862 4.39745C11.5717 4.27403 12.2286 3.94205 12.7489 3.44385C13.2692 2.94565 13.6293 2.3038 13.7834 1.60012L13.8074 1.48945C13.894 1.09345 14.458 1.08945 14.5487 1.48545L14.578 1.61479C14.7379 2.31545 15.1012 2.95326 15.6224 3.4481C16.1436 3.94294 16.7994 4.27276 17.5074 4.39612C17.91 4.46545 17.91 5.04412 17.5074 5.11479C16.7994 5.23815 16.1436 5.56796 15.6224 6.0628C15.1012 6.55765 14.7379 7.19545 14.578 7.89612L14.5487 8.02412C14.458 8.42012 13.8954 8.41745 13.8074 8.02145L13.7834 7.91079C13.6295 7.20686 13.2695 6.56472 12.7492 6.06627C12.2289 5.56781 11.5719 5.23564 10.862 5.11212Z" stroke="#747474" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
3
frontend/src/assets/white-trash.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.66574 13.7747C1.66574 14.2168 1.84635 14.6407 2.16784 14.9533C2.48933 15.2658 2.92537 15.4414 3.38002 15.4414H10.2372C10.6918 15.4414 11.1279 15.2658 11.4493 14.9533C11.7708 14.6407 11.9515 14.2168 11.9515 13.7747V3.77474H1.66574V13.7747ZM3.38002 5.44141H10.2372V13.7747H3.38002V5.44141ZM9.80859 1.27474L8.95145 0.441406H4.66574L3.80859 1.27474H0.808594V2.94141H12.8086V1.27474H9.80859Z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 520 B |
@@ -90,7 +90,7 @@ export default function ContextMenu({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion"
|
className="flex w-32 flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2 md:w-36"
|
||||||
style={{ minWidth: '144px' }}
|
style={{ minWidth: '144px' }}
|
||||||
>
|
>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
@@ -102,26 +102,22 @@ export default function ContextMenu({
|
|||||||
option.onClick(event);
|
option.onClick(event);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${
|
||||||
flex justify-start items-center gap-4 p-3
|
option.variant === 'danger'
|
||||||
transition-colors duration-200 ease-in-out
|
? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey'
|
||||||
${index === 0 ? 'rounded-t-xl' : ''}
|
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey'
|
||||||
${index === options.length - 1 ? 'rounded-b-xl' : ''}
|
} `}
|
||||||
${
|
|
||||||
option.variant === 'danger'
|
|
||||||
? 'dark:text-red-2000 dark:hover:bg-charcoal-grey text-rosso-corsa hover:bg-bright-gray'
|
|
||||||
: 'dark:text-bright-gray dark:hover:bg-charcoal-grey text-eerie-black hover:bg-bright-gray'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{option.icon && (
|
{option.icon && (
|
||||||
<img
|
<div className="flex w-4 justify-center">
|
||||||
width={option.iconWidth || 16}
|
<img
|
||||||
height={option.iconHeight || 16}
|
width={option.iconWidth || 16}
|
||||||
src={option.icon}
|
height={option.iconHeight || 16}
|
||||||
alt={option.label}
|
src={option.icon}
|
||||||
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
|
alt={option.label}
|
||||||
/>
|
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||||
import Edit from '../assets/edit.svg';
|
import Edit from '../assets/edit.svg';
|
||||||
import Trash from '../assets/trash.svg';
|
import Trash from '../assets/trash.svg';
|
||||||
@@ -9,6 +10,10 @@ function Dropdown({
|
|||||||
onSelect,
|
onSelect,
|
||||||
size = 'w-32',
|
size = 'w-32',
|
||||||
rounded = 'xl',
|
rounded = 'xl',
|
||||||
|
buttonBackgroundColor = 'white',
|
||||||
|
buttonDarkBackgroundColor = 'transparent',
|
||||||
|
optionsBackgroundColor = 'white',
|
||||||
|
optionsDarkBackgroundColor = 'dark-charcoal',
|
||||||
border = 'border-2',
|
border = 'border-2',
|
||||||
borderColor = 'silver',
|
borderColor = 'silver',
|
||||||
darkBorderColor = 'dim-gray',
|
darkBorderColor = 'dim-gray',
|
||||||
@@ -17,6 +22,8 @@ function Dropdown({
|
|||||||
showDelete,
|
showDelete,
|
||||||
onDelete,
|
onDelete,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
placeholderTextColor = 'gray-500',
|
||||||
|
darkPlaceholderTextColor = 'gray-400',
|
||||||
contentSize = 'text-base',
|
contentSize = 'text-base',
|
||||||
}: {
|
}: {
|
||||||
options:
|
options:
|
||||||
@@ -37,6 +44,10 @@ function Dropdown({
|
|||||||
| ((value: { value: number; description: string }) => void);
|
| ((value: { value: number; description: string }) => void);
|
||||||
size?: string;
|
size?: string;
|
||||||
rounded?: 'xl' | '3xl';
|
rounded?: 'xl' | '3xl';
|
||||||
|
buttonBackgroundColor?: string;
|
||||||
|
buttonDarkBackgroundColor?: string;
|
||||||
|
optionsBackgroundColor?: string;
|
||||||
|
optionsDarkBackgroundColor?: string;
|
||||||
border?: 'border' | 'border-2';
|
border?: 'border' | 'border-2';
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
darkBorderColor?: string;
|
darkBorderColor?: string;
|
||||||
@@ -45,6 +56,8 @@ function Dropdown({
|
|||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
onDelete?: (value: string) => void;
|
onDelete?: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
placeholderTextColor?: string;
|
||||||
|
darkPlaceholderTextColor?: string;
|
||||||
contentSize?: string;
|
contentSize?: string;
|
||||||
}) {
|
}) {
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
@@ -71,7 +84,7 @@ function Dropdown({
|
|||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
typeof selectedValue === 'string'
|
typeof selectedValue === 'string'
|
||||||
? 'relative mt-2'
|
? 'relative'
|
||||||
: 'relative align-middle',
|
: 'relative align-middle',
|
||||||
size,
|
size,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
@@ -79,7 +92,7 @@ function Dropdown({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${darkBorderColor} dark:bg-transparent ${
|
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-${buttonBackgroundColor} px-5 py-3 dark:border-${darkBorderColor} dark:bg-${buttonDarkBackgroundColor} ${
|
||||||
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -89,8 +102,9 @@ function Dropdown({
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
className={`truncate dark:text-bright-gray ${
|
className={`truncate ${selectedValue && `dark:text-bright-gray`} ${
|
||||||
!selectedValue && 'text-silver dark:text-gray-400'
|
!selectedValue &&
|
||||||
|
`text-${placeholderTextColor} dark:text-${darkPlaceholderTextColor}`
|
||||||
} ${contentSize}`}
|
} ${contentSize}`}
|
||||||
>
|
>
|
||||||
{selectedValue && 'label' in selectedValue
|
{selectedValue && 'label' in selectedValue
|
||||||
@@ -116,7 +130,7 @@ function Dropdown({
|
|||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${darkBorderColor} dark:bg-dark-charcoal`}
|
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-${optionsBackgroundColor} shadow-lg dark:border-${darkBorderColor} dark:bg-${optionsDarkBackgroundColor}`}
|
||||||
>
|
>
|
||||||
{options.map((option: any, index) => (
|
{options.map((option: any, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const Input = ({
|
|||||||
className = '',
|
className = '',
|
||||||
colorVariant = 'silver',
|
colorVariant = 'silver',
|
||||||
borderVariant = 'thick',
|
borderVariant = 'thick',
|
||||||
|
textSize = 'medium',
|
||||||
children,
|
children,
|
||||||
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
||||||
onChange,
|
onChange,
|
||||||
@@ -28,6 +29,10 @@ const Input = ({
|
|||||||
thin: 'border',
|
thin: 'border',
|
||||||
thick: 'border-2',
|
thick: 'border-2',
|
||||||
};
|
};
|
||||||
|
const textSizeStyles = {
|
||||||
|
small: 'text-sm',
|
||||||
|
medium: 'text-base',
|
||||||
|
};
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -35,15 +40,7 @@ const Input = ({
|
|||||||
<div className={`relative ${className}`}>
|
<div className={`relative ${className}`}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={`peer h-[42px] w-full rounded-full px-3 py-1
|
className={`peer h-[42px] w-full rounded-full bg-transparent px-3 py-1 text-jet placeholder-transparent outline-none dark:text-bright-gray ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||||
bg-transparent outline-none
|
|
||||||
text-jet dark:text-bright-gray
|
|
||||||
placeholder-transparent
|
|
||||||
${colorStyles[colorVariant]}
|
|
||||||
${borderStyles[borderVariant]}
|
|
||||||
[&:-webkit-autofill]:bg-transparent
|
|
||||||
[&:-webkit-autofill]:appearance-none
|
|
||||||
[&:-webkit-autofill_selected]:bg-transparent`}
|
|
||||||
type={type}
|
type={type}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
@@ -61,15 +58,11 @@ const Input = ({
|
|||||||
{placeholder && (
|
{placeholder && (
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={`absolute left-3 -top-2.5 px-2 text-xs transition-all
|
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400 ${labelBgClassName}`}
|
||||||
peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base
|
|
||||||
peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs
|
|
||||||
peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400
|
|
||||||
cursor-none pointer-events-none ${labelBgClassName}`}
|
|
||||||
>
|
>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{required && (
|
{required && (
|
||||||
<span className="text-[#D30000] dark:text-[#D42626] ml-0.5">*</span>
|
<span className="ml-0.5 text-[#D30000] dark:text-[#D42626]">*</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,43 +1,59 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDarkTheme } from '../hooks';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import userService from '../api/services/userService';
|
|
||||||
import endpoints from '../api/endpoints';
|
import endpoints from '../api/endpoints';
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import AlertIcon from '../assets/alert.svg';
|
||||||
|
import ClipIcon from '../assets/clip.svg';
|
||||||
|
import ExitIcon from '../assets/exit.svg';
|
||||||
import PaperPlane from '../assets/paper_plane.svg';
|
import PaperPlane from '../assets/paper_plane.svg';
|
||||||
import SourceIcon from '../assets/source.svg';
|
import SourceIcon from '../assets/source.svg';
|
||||||
import ToolIcon from '../assets/tool.svg';
|
|
||||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||||
import Spinner from '../assets/spinner.svg';
|
import Spinner from '../assets/spinner.svg';
|
||||||
|
import ToolIcon from '../assets/tool.svg';
|
||||||
|
import {
|
||||||
|
addAttachment,
|
||||||
|
removeAttachment,
|
||||||
|
selectAttachments,
|
||||||
|
updateAttachment,
|
||||||
|
} from '../conversation/conversationSlice';
|
||||||
|
import { useDarkTheme } from '../hooks';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
|
import {
|
||||||
|
selectSelectedDocs,
|
||||||
|
selectToken,
|
||||||
|
} from '../preferences/preferenceSlice';
|
||||||
|
import Upload from '../upload/Upload';
|
||||||
|
import { getOS, isTouchDevice } from '../utils/browserUtils';
|
||||||
import SourcesPopup from './SourcesPopup';
|
import SourcesPopup from './SourcesPopup';
|
||||||
import ToolsPopup from './ToolsPopup';
|
import ToolsPopup from './ToolsPopup';
|
||||||
import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice';
|
|
||||||
import { ActiveState } from '../models/misc';
|
|
||||||
import Upload from '../upload/Upload';
|
|
||||||
import ClipIcon from '../assets/clip.svg';
|
|
||||||
import { setAttachments } from '../conversation/conversationSlice';
|
|
||||||
|
|
||||||
interface MessageInputProps {
|
type MessageInputProps = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
showSourceButton?: boolean;
|
||||||
|
showToolButton?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
interface UploadState {
|
type UploadState = {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
attachment_id?: string;
|
attachment_id?: string;
|
||||||
token_count?: number;
|
token_count?: number;
|
||||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function MessageInput({
|
export default function MessageInput({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
loading,
|
loading,
|
||||||
|
showSourceButton = true,
|
||||||
|
showToolButton = true,
|
||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDarkTheme] = useDarkTheme();
|
const [isDarkTheme] = useDarkTheme();
|
||||||
@@ -46,14 +62,37 @@ export default function MessageInput({
|
|||||||
const toolButtonRef = useRef<HTMLButtonElement>(null);
|
const toolButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
|
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
|
||||||
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||||
const [uploadModalState, setUploadModalState] = useState<ActiveState>('INACTIVE');
|
const [uploadModalState, setUploadModalState] =
|
||||||
const [uploads, setUploads] = useState<UploadState[]>([]);
|
useState<ActiveState>('INACTIVE');
|
||||||
|
|
||||||
const selectedDocs = useSelector(selectSelectedDocs);
|
const selectedDocs = useSelector(selectSelectedDocs);
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
|
const attachments = useSelector(selectAttachments);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const browserOS = getOS();
|
||||||
|
const isTouch = isTouchDevice();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
((browserOS === 'win' || browserOS === 'linux') &&
|
||||||
|
event.ctrlKey &&
|
||||||
|
event.key === 'k') ||
|
||||||
|
(browserOS === 'mac' && event.metaKey && event.key === 'k')
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSourcesPopupOpen(!isSourcesPopupOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [browserOS]);
|
||||||
|
|
||||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!e.target.files || e.target.files.length === 0) return;
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
|
|
||||||
@@ -64,56 +103,59 @@ export default function MessageInput({
|
|||||||
const apiHost = import.meta.env.VITE_API_HOST;
|
const apiHost = import.meta.env.VITE_API_HOST;
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
const uploadState: UploadState = {
|
const newAttachment = {
|
||||||
taskId: '',
|
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: 'uploading'
|
status: 'uploading' as const,
|
||||||
|
taskId: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
setUploads(prev => [...prev, uploadState]);
|
dispatch(addAttachment(newAttachment));
|
||||||
const uploadIndex = uploads.length;
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', (event) => {
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
const progress = Math.round((event.loaded / event.total) * 100);
|
const progress = Math.round((event.loaded / event.total) * 100);
|
||||||
setUploads(prev => prev.map((upload, index) =>
|
dispatch(
|
||||||
index === uploadIndex
|
updateAttachment({
|
||||||
? { ...upload, progress }
|
taskId: newAttachment.taskId,
|
||||||
: upload
|
updates: { progress },
|
||||||
));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const response = JSON.parse(xhr.responseText);
|
const response = JSON.parse(xhr.responseText);
|
||||||
console.log('File uploaded successfully:', response);
|
|
||||||
|
|
||||||
if (response.task_id) {
|
if (response.task_id) {
|
||||||
setUploads(prev => prev.map((upload, index) =>
|
dispatch(
|
||||||
index === uploadIndex
|
updateAttachment({
|
||||||
? { ...upload, taskId: response.task_id, status: 'processing' }
|
taskId: newAttachment.taskId,
|
||||||
: upload
|
updates: {
|
||||||
));
|
taskId: response.task_id,
|
||||||
|
status: 'processing',
|
||||||
|
progress: 10,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUploads(prev => prev.map((upload, index) =>
|
dispatch(
|
||||||
index === uploadIndex
|
updateAttachment({
|
||||||
? { ...upload, status: 'failed' }
|
taskId: newAttachment.taskId,
|
||||||
: upload
|
updates: { status: 'failed' },
|
||||||
));
|
}),
|
||||||
console.error('Error uploading file:', xhr.responseText);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
setUploads(prev => prev.map((upload, index) =>
|
dispatch(
|
||||||
index === uploadIndex
|
updateAttachment({
|
||||||
? { ...upload, status: 'failed' }
|
taskId: newAttachment.taskId,
|
||||||
: upload
|
updates: { status: 'failed' },
|
||||||
));
|
}),
|
||||||
console.error('Network error during file upload');
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||||
@@ -123,64 +165,63 @@ export default function MessageInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutIds: number[] = [];
|
|
||||||
|
|
||||||
const checkTaskStatus = () => {
|
const checkTaskStatus = () => {
|
||||||
const processingUploads = uploads.filter(upload =>
|
const processingAttachments = attachments.filter(
|
||||||
upload.status === 'processing' && upload.taskId
|
(att) => att.status === 'processing' && att.taskId,
|
||||||
);
|
);
|
||||||
|
|
||||||
processingUploads.forEach(upload => {
|
processingAttachments.forEach((attachment) => {
|
||||||
userService
|
userService
|
||||||
.getTaskStatus(upload.taskId, null)
|
.getTaskStatus(attachment.taskId!, null)
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('Task status:', data);
|
if (data.status === 'SUCCESS') {
|
||||||
|
dispatch(
|
||||||
setUploads(prev => prev.map(u => {
|
updateAttachment({
|
||||||
if (u.taskId !== upload.taskId) return u;
|
taskId: attachment.taskId!,
|
||||||
|
updates: {
|
||||||
if (data.status === 'SUCCESS') {
|
status: 'completed',
|
||||||
return {
|
progress: 100,
|
||||||
...u,
|
id: data.result?.attachment_id,
|
||||||
status: 'completed',
|
token_count: data.result?.token_count,
|
||||||
progress: 100,
|
},
|
||||||
attachment_id: data.result?.attachment_id,
|
}),
|
||||||
token_count: data.result?.token_count
|
);
|
||||||
};
|
} else if (data.status === 'FAILURE') {
|
||||||
} else if (data.status === 'FAILURE') {
|
dispatch(
|
||||||
return { ...u, status: 'failed' };
|
updateAttachment({
|
||||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
taskId: attachment.taskId!,
|
||||||
return { ...u, progress: data.result.current };
|
updates: { status: 'failed' },
|
||||||
}
|
}),
|
||||||
return u;
|
);
|
||||||
}));
|
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||||
|
dispatch(
|
||||||
if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') {
|
updateAttachment({
|
||||||
const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000);
|
taskId: attachment.taskId!,
|
||||||
timeoutIds.push(timeoutId);
|
updates: { progress: data.result.current },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.error('Error checking task status:', error);
|
dispatch(
|
||||||
setUploads(prev => prev.map(u =>
|
updateAttachment({
|
||||||
u.taskId === upload.taskId
|
taskId: attachment.taskId!,
|
||||||
? { ...u, status: 'failed' }
|
updates: { status: 'failed' },
|
||||||
: u
|
}),
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (uploads.some(upload => upload.status === 'processing')) {
|
const interval = setInterval(() => {
|
||||||
const timeoutId = window.setTimeout(checkTaskStatus, 2000);
|
if (attachments.some((att) => att.status === 'processing')) {
|
||||||
timeoutIds.push(timeoutId);
|
checkTaskStatus();
|
||||||
}
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
return () => {
|
return () => clearInterval(interval);
|
||||||
timeoutIds.forEach(id => clearTimeout(id));
|
}, [attachments, dispatch]);
|
||||||
};
|
|
||||||
}, [uploads]);
|
|
||||||
|
|
||||||
const handleInput = () => {
|
const handleInput = () => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@@ -213,41 +254,57 @@ export default function MessageInput({
|
|||||||
console.log('Selected document:', doc);
|
console.log('Selected document:', doc);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const completedAttachments = uploads
|
|
||||||
.filter(upload => upload.status === 'completed' && upload.attachment_id)
|
|
||||||
.map(upload => ({
|
|
||||||
fileName: upload.fileName,
|
|
||||||
id: upload.attachment_id as string
|
|
||||||
}));
|
|
||||||
|
|
||||||
dispatch(setAttachments(completedAttachments));
|
|
||||||
|
|
||||||
onSubmit();
|
onSubmit();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full mx-2">
|
<div className="mx-2 flex w-full flex-col">
|
||||||
<div className="flex flex-col w-full rounded-[23px] border dark:border-grey border-dark-gray bg-lotion dark:bg-transparent relative">
|
<div className="relative flex w-full flex-col rounded-[23px] border border-dark-gray bg-lotion dark:border-grey dark:bg-transparent">
|
||||||
<div className="flex flex-wrap gap-1.5 sm:gap-2 px-4 sm:px-6 pt-3 pb-0">
|
<div className="flex flex-wrap gap-1.5 px-4 pb-0 pt-3 sm:gap-2 sm:px-6">
|
||||||
{uploads.map((upload, index) => (
|
{attachments.map((attachment, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center px-2 sm:px-3 py-1 sm:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe bg-white dark:bg-[#1F2028] text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray"
|
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||||
|
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
title={attachment.fileName}
|
||||||
>
|
>
|
||||||
<span className="font-medium truncate max-w-[120px] sm:max-w-[150px]">{upload.fileName}</span>
|
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||||
|
{attachment.fileName}
|
||||||
|
</span>
|
||||||
|
|
||||||
{upload.status === 'completed' && (
|
{attachment.status === 'completed' && (
|
||||||
<span className="ml-2 text-green-500">✓</span>
|
<button
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
|
||||||
|
onClick={() => {
|
||||||
|
if (attachment.id) {
|
||||||
|
dispatch(removeAttachment(attachment.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Remove attachment"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ExitIcon}
|
||||||
|
alt="Remove"
|
||||||
|
className="h-2.5 w-2.5 filter dark:invert"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{upload.status === 'failed' && (
|
{attachment.status === 'failed' && (
|
||||||
<span className="ml-2 text-red-500">✗</span>
|
<img
|
||||||
|
src={AlertIcon}
|
||||||
|
alt="Upload failed"
|
||||||
|
className="ml-2 h-3.5 w-3.5"
|
||||||
|
title="Upload failed"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(upload.status === 'uploading' || upload.status === 'processing') && (
|
{(attachment.status === 'uploading' ||
|
||||||
<div className="ml-2 w-4 h-4 relative">
|
attachment.status === 'processing') && (
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24">
|
<div className="relative ml-2 h-4 w-4">
|
||||||
|
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||||
|
{/* Background circle */}
|
||||||
<circle
|
<circle
|
||||||
className="text-gray-200 dark:text-gray-700"
|
className="text-gray-200 dark:text-gray-700"
|
||||||
cx="12"
|
cx="12"
|
||||||
@@ -266,7 +323,7 @@ export default function MessageInput({
|
|||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray="62.83"
|
strokeDasharray="62.83"
|
||||||
strokeDashoffset={62.83 - (upload.progress / 100) * 62.83}
|
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
|
||||||
transform="rotate(-90 12 12)"
|
transform="rotate(-90 12 12)"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -287,41 +344,67 @@ export default function MessageInput({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
placeholder={t('inputPlaceholder')}
|
placeholder={t('inputPlaceholder')}
|
||||||
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion dark:bg-transparent py-3 sm:py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-4 sm:px-6 no-scrollbar"
|
className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5"
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
aria-label={t('inputPlaceholder')}
|
aria-label={t('inputPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center px-3 sm:px-4 py-1.5 sm:py-2">
|
<div className="flex items-center px-3 py-1.5 sm:px-4 sm:py-2">
|
||||||
<div className="flex-grow flex flex-wrap gap-1 sm:gap-2">
|
<div className="flex flex-grow flex-wrap gap-1 sm:gap-2">
|
||||||
<button
|
{showSourceButton && (
|
||||||
ref={sourceButtonRef}
|
<button
|
||||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] xs:max-w-[150px]"
|
ref={sourceButtonRef}
|
||||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
className="xs:px-3 xs:py-1.5 flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C] sm:max-w-[150px]"
|
||||||
>
|
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||||
<img src={SourceIcon} alt="Sources" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
title={
|
||||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
selectedDocs
|
||||||
{selectedDocs
|
? selectedDocs.name
|
||||||
? selectedDocs.name
|
: t('conversation.sources.title')
|
||||||
: t('conversation.sources.title')}
|
}
|
||||||
</span>
|
>
|
||||||
</button>
|
<img
|
||||||
|
src={SourceIcon}
|
||||||
|
alt="Sources"
|
||||||
|
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4"
|
||||||
|
/>
|
||||||
|
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||||
|
{selectedDocs
|
||||||
|
? selectedDocs.name
|
||||||
|
: t('conversation.sources.title')}
|
||||||
|
</span>
|
||||||
|
{!isTouch && (
|
||||||
|
<span className="ml-1 hidden text-[10px] text-gray-500 dark:text-gray-400 sm:inline-block">
|
||||||
|
{browserOS === 'mac' ? '(⌘K)' : '(ctrl+K)'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
{showToolButton && (
|
||||||
ref={toolButtonRef}
|
<button
|
||||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] xs:max-w-[150px]"
|
ref={toolButtonRef}
|
||||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
|
||||||
>
|
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||||
<img src={ToolIcon} alt="Tools" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
>
|
||||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
<img
|
||||||
{t('settings.tools.label')}
|
src={ToolIcon}
|
||||||
</span>
|
alt="Tools"
|
||||||
</button>
|
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||||
<label className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors cursor-pointer">
|
/>
|
||||||
<img src={ClipIcon} alt="Attach" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5" />
|
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium">
|
{t('settings.tools.label')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<label className="xs:px-3 xs:py-1.5 flex cursor-pointer items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]">
|
||||||
|
<img
|
||||||
|
src={ClipIcon}
|
||||||
|
alt="Attach"
|
||||||
|
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||||
|
/>
|
||||||
|
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||||
Attach
|
Attach
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -337,18 +420,18 @@ export default function MessageInput({
|
|||||||
<button
|
<button
|
||||||
onClick={loading ? undefined : handleSubmit}
|
onClick={loading ? undefined : handleSubmit}
|
||||||
aria-label={loading ? t('loading') : t('send')}
|
aria-label={loading ? t('loading') : t('send')}
|
||||||
className={`flex items-center justify-center p-2 sm:p-2.5 rounded-full ${loading ? 'bg-gray-300 dark:bg-gray-600' : 'bg-black dark:bg-white'} ml-auto flex-shrink-0`}
|
className={`flex items-center justify-center rounded-full p-2 sm:p-2.5 ${loading ? 'bg-gray-300 dark:bg-gray-600' : 'bg-black dark:bg-white'} ml-auto flex-shrink-0`}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<img
|
<img
|
||||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||||
className="w-3.5 sm:w-4 h-3.5 sm:h-4 animate-spin"
|
className="h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4"
|
||||||
alt={t('loading')}
|
alt={t('loading')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className={`w-3.5 sm:w-4 h-3.5 sm:h-4 ${isDarkTheme ? 'filter invert' : ''}`}
|
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${isDarkTheme ? 'invert filter' : ''}`}
|
||||||
src={PaperPlane}
|
src={PaperPlane}
|
||||||
alt={t('send')}
|
alt={t('send')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
278
frontend/src/components/MultiSelectPopup.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import CheckmarkIcon from '../assets/checkmark.svg';
|
||||||
|
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||||
|
import NoFilesIcon from '../assets/no-files.svg';
|
||||||
|
import { useDarkTheme } from '../hooks';
|
||||||
|
import Input from './Input';
|
||||||
|
|
||||||
|
export type OptionType = {
|
||||||
|
id: string | number;
|
||||||
|
label: string;
|
||||||
|
icon?: string | React.ReactNode;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MultiSelectPopupProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
anchorRef: React.RefObject<HTMLElement>;
|
||||||
|
options: OptionType[];
|
||||||
|
selectedIds: Set<string | number>;
|
||||||
|
onSelectionChange: (newSelectedIds: Set<string | number>) => void;
|
||||||
|
title?: string;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
noOptionsMessage?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
footerContent?: React.ReactNode;
|
||||||
|
showSearch?: boolean;
|
||||||
|
singleSelect?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MultiSelectPopup({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
anchorRef,
|
||||||
|
options,
|
||||||
|
selectedIds,
|
||||||
|
onSelectionChange,
|
||||||
|
title,
|
||||||
|
searchPlaceholder,
|
||||||
|
noOptionsMessage,
|
||||||
|
loading = false,
|
||||||
|
footerContent,
|
||||||
|
showSearch = true,
|
||||||
|
singleSelect = false,
|
||||||
|
}: MultiSelectPopupProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDarkTheme] = useDarkTheme();
|
||||||
|
|
||||||
|
const popupRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [popupPosition, setPopupPosition] = useState({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
maxHeight: 0,
|
||||||
|
showAbove: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredOptions = options.filter((option) =>
|
||||||
|
option.label.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionClick = (optionId: string | number) => {
|
||||||
|
let newSelectedIds: Set<string | number>;
|
||||||
|
if (singleSelect) newSelectedIds = new Set<string | number>();
|
||||||
|
else newSelectedIds = new Set(selectedIds);
|
||||||
|
if (newSelectedIds.has(optionId)) {
|
||||||
|
newSelectedIds.delete(optionId);
|
||||||
|
} else newSelectedIds.add(optionId);
|
||||||
|
onSelectionChange(newSelectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = (icon: string | React.ReactNode) => {
|
||||||
|
if (typeof icon === 'string') {
|
||||||
|
if (icon.startsWith('/') || icon.startsWith('http')) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt=""
|
||||||
|
className="mr-3 h-5 w-5 flex-shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="mr-3 h-5 w-5 flex-shrink-0" aria-hidden="true">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span className="mr-3 flex-shrink-0">{icon}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isOpen || !anchorRef.current) return;
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (!anchorRef.current) return;
|
||||||
|
|
||||||
|
const rect = anchorRef.current.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const popupPadding = 16;
|
||||||
|
const popupMinWidth = 300;
|
||||||
|
const popupMaxWidth = 462;
|
||||||
|
const popupDefaultHeight = 300;
|
||||||
|
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
const spaceBelow = viewportHeight - rect.bottom;
|
||||||
|
const showAbove =
|
||||||
|
spaceBelow < popupDefaultHeight && spaceAbove >= popupDefaultHeight;
|
||||||
|
|
||||||
|
const maxHeight = Math.max(
|
||||||
|
150,
|
||||||
|
showAbove ? spaceAbove - popupPadding : spaceBelow - popupPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableWidth = viewportWidth - 20;
|
||||||
|
const calculatedWidth = Math.min(popupMaxWidth, availableWidth);
|
||||||
|
|
||||||
|
let left = rect.left;
|
||||||
|
if (left + calculatedWidth > viewportWidth - 10) {
|
||||||
|
left = viewportWidth - calculatedWidth - 10;
|
||||||
|
}
|
||||||
|
left = Math.max(10, left);
|
||||||
|
|
||||||
|
setPopupPosition({
|
||||||
|
top: showAbove ? rect.top - 8 : rect.bottom + 8,
|
||||||
|
left: left,
|
||||||
|
maxHeight: Math.min(600, maxHeight),
|
||||||
|
showAbove,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePosition();
|
||||||
|
window.addEventListener('resize', updatePosition);
|
||||||
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
|
};
|
||||||
|
}, [isOpen, anchorRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
popupRef.current &&
|
||||||
|
!popupRef.current.contains(event.target as Node) &&
|
||||||
|
anchorRef.current &&
|
||||||
|
!anchorRef.current.contains(event.target as Node)
|
||||||
|
)
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [onClose, anchorRef, isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) setSearchTerm('');
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className="fixed z-[9999] flex flex-col rounded-lg border border-light-silver bg-lotion shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033] dark:border-dim-gray dark:bg-charleston-green-2"
|
||||||
|
style={{
|
||||||
|
top: popupPosition.showAbove ? undefined : popupPosition.top,
|
||||||
|
bottom: popupPosition.showAbove
|
||||||
|
? window.innerHeight - popupPosition.top + 8
|
||||||
|
: undefined,
|
||||||
|
left: popupPosition.left,
|
||||||
|
maxWidth: `${Math.min(462, window.innerWidth - 20)}px`,
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: `${popupPosition.maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(title || showSearch) && (
|
||||||
|
<div className="flex-shrink-0 p-4">
|
||||||
|
{title && (
|
||||||
|
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{showSearch && (
|
||||||
|
<Input
|
||||||
|
id="multi-select-search"
|
||||||
|
name="multi-select-search"
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
searchPlaceholder ||
|
||||||
|
t('settings.tools.searchPlaceholder', 'Search...')
|
||||||
|
}
|
||||||
|
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
||||||
|
borderVariant="thin"
|
||||||
|
className="mb-4"
|
||||||
|
textSize="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mx-4 mb-4 flex-grow overflow-auto rounded-md border border-[#D9D9D9] dark:border-dim-gray">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full items-center justify-center py-4">
|
||||||
|
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full overflow-y-auto [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-400 dark:[&::-webkit-scrollbar-thumb]:bg-gray-600 [&::-webkit-scrollbar-track]:bg-gray-200 dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C] [&::-webkit-scrollbar]:w-2">
|
||||||
|
{filteredOptions.length === 0 ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
|
||||||
|
<img
|
||||||
|
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||||
|
alt="No options found"
|
||||||
|
className="mx-auto mb-3 h-16 w-16"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{searchTerm
|
||||||
|
? 'No results found'
|
||||||
|
: noOptionsMessage || 'No options available'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredOptions.map((option) => {
|
||||||
|
const isSelected = selectedIds.has(option.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => handleOptionClick(option.id)}
|
||||||
|
className="flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0 hover:bg-gray-100 dark:border-dim-gray dark:hover:bg-charleston-green-3"
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
>
|
||||||
|
<div className="mr-3 flex flex-grow items-center overflow-hidden">
|
||||||
|
{option.icon && renderIcon(option.icon)}
|
||||||
|
<p
|
||||||
|
className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
title={option.label}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div
|
||||||
|
className={`flex h-4 w-4 items-center justify-center rounded-sm border border-[#C6C6C6] bg-white dark:border-[#757783] dark:bg-charleston-green-2`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<img
|
||||||
|
src={CheckmarkIcon}
|
||||||
|
alt="checkmark"
|
||||||
|
width={10}
|
||||||
|
height={10}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{footerContent && (
|
||||||
|
<div className="flex-shrink-0 border-t border-light-silver p-4 dark:border-dim-gray">
|
||||||
|
{footerContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ArrowLeft from '../assets/arrow-left.svg';
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
import ArrowRight from '../assets/arrow-right.svg';
|
import ArrowRight from '../assets/arrow-right.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type HiddenGradientType = 'left' | 'right' | undefined;
|
type HiddenGradientType = 'left' | 'right' | undefined;
|
||||||
|
|
||||||
@@ -10,7 +11,6 @@ const useTabs = () => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
t('settings.general.label'),
|
t('settings.general.label'),
|
||||||
t('settings.documents.label'),
|
t('settings.documents.label'),
|
||||||
t('settings.apiKeys.label'),
|
|
||||||
t('settings.analytics.label'),
|
t('settings.analytics.label'),
|
||||||
t('settings.logs.label'),
|
t('settings.logs.label'),
|
||||||
t('settings.tools.label'),
|
t('settings.tools.label'),
|
||||||
@@ -48,18 +48,18 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
[containerRef.current],
|
[containerRef.current],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="relative mt-6 flex flex-row items-center space-x-1 md:space-x-0 overflow-auto">
|
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
|
||||||
<div
|
<div
|
||||||
className={`${hiddenGradient === 'left' ? 'hidden' : ''} md:hidden absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black pointer-events-none`}
|
className={`${hiddenGradient === 'left' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black md:hidden`}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`${hiddenGradient === 'right' ? 'hidden' : ''} md:hidden absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black pointer-events-none`}
|
className={`${hiddenGradient === 'right' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black md:hidden`}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div className="md:hidden z-10">
|
<div className="z-10 md:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollTabs(-1)}
|
onClick={() => scrollTabs(-1)}
|
||||||
className="flex h-6 w-6 items-center rounded-full justify-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
aria-label="Scroll tabs left"
|
aria-label="Scroll tabs left"
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
||||||
@@ -67,7 +67,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex flex-nowrap overflow-x-auto no-scrollbar md:space-x-4 scroll-smooth snap-x"
|
className="no-scrollbar flex snap-x flex-nowrap overflow-x-auto scroll-smooth md:space-x-4"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Settings tabs"
|
aria-label="Settings tabs"
|
||||||
>
|
>
|
||||||
@@ -75,7 +75,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
|
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? 'bg-[#F4F4F5] text-neutral-900 dark:bg-dark-charcoal dark:text-white'
|
? 'bg-[#F4F4F5] text-neutral-900 dark:bg-dark-charcoal dark:text-white'
|
||||||
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
|
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
|
||||||
@@ -89,10 +89,10 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden z-10">
|
<div className="z-10 md:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollTabs(1)}
|
onClick={() => scrollTabs(1)}
|
||||||
className="flex h-6 w-6 rounded-full items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
aria-label="Scroll tabs right"
|
aria-label="Scroll tabs right"
|
||||||
>
|
>
|
||||||
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export default function SourcesPopup({
|
|||||||
<div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200 flex-shrink-0">
|
<div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200 flex-shrink-0">
|
||||||
<a
|
<a
|
||||||
href="/settings/documents"
|
href="/settings/documents"
|
||||||
className="text-violets-are-blue text-base font-medium flex items-center gap-2"
|
className="text-violets-are-blue text-base font-medium inline-flex items-center gap-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Go to Documents
|
Go to Documents
|
||||||
|
|||||||
@@ -217,10 +217,10 @@ export default function ToolsPopup({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-4 flex-shrink-0">
|
<div className="p-4 flex-shrink-0 opacity-75 hover:opacity-100 transition-opacity duration-200">
|
||||||
<a
|
<a
|
||||||
href="/settings/tools"
|
href="/settings/tools"
|
||||||
className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center"
|
className="text-base text-purple-30 font-medium inline-flex items-center"
|
||||||
>
|
>
|
||||||
{t('settings.tools.manageTools')}
|
{t('settings.tools.manageTools')}
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type InputProps = {
|
|||||||
value: string | string[] | number;
|
value: string | string[] | number;
|
||||||
colorVariant?: 'silver' | 'jet' | 'gray';
|
colorVariant?: 'silver' | 'jet' | 'gray';
|
||||||
borderVariant?: 'thin' | 'thick';
|
borderVariant?: 'thin' | 'thick';
|
||||||
|
textSize?: 'small' | 'medium';
|
||||||
isAutoFocused?: boolean;
|
isAutoFocused?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useDropzone } from 'react-dropzone';
|
|
||||||
import DragFileUpload from '../assets/DragFileUpload.svg';
|
import DragFileUpload from '../assets/DragFileUpload.svg';
|
||||||
import newChatIcon from '../assets/openNewChat.svg';
|
import newChatIcon from '../assets/openNewChat.svg';
|
||||||
import ShareIcon from '../assets/share.svg';
|
import ShareIcon from '../assets/share.svg';
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
import { useMediaQuery } from '../hooks';
|
import { useMediaQuery } from '../hooks';
|
||||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
import {
|
import {
|
||||||
selectConversationId,
|
selectConversationId,
|
||||||
|
selectSelectedAgent,
|
||||||
selectToken,
|
selectToken,
|
||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
import { AppDispatch } from '../store';
|
import { AppDispatch } from '../store';
|
||||||
|
import Upload from '../upload/Upload';
|
||||||
import { handleSendFeedback } from './conversationHandlers';
|
import { handleSendFeedback } from './conversationHandlers';
|
||||||
|
import ConversationMessages from './ConversationMessages';
|
||||||
import { FEEDBACK, Query } from './conversationModels';
|
import { FEEDBACK, Query } from './conversationModels';
|
||||||
import {
|
import {
|
||||||
addQuery,
|
addQuery,
|
||||||
@@ -24,28 +30,29 @@ import {
|
|||||||
updateConversationId,
|
updateConversationId,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
} from './conversationSlice';
|
} from './conversationSlice';
|
||||||
import Upload from '../upload/Upload';
|
|
||||||
import { ActiveState } from '../models/misc';
|
|
||||||
import ConversationMessages from './ConversationMessages';
|
|
||||||
import MessageInput from '../components/MessageInput';
|
|
||||||
|
|
||||||
export default function Conversation() {
|
export default function Conversation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isMobile } = useMediaQuery();
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
const queries = useSelector(selectQueries);
|
const queries = useSelector(selectQueries);
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const conversationId = useSelector(selectConversationId);
|
const conversationId = useSelector(selectConversationId);
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
const [input, setInput] = useState('');
|
|
||||||
const fetchStream = useRef<any>(null);
|
const [input, setInput] = useState<string>('');
|
||||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
|
||||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isMobile } = useMediaQuery();
|
|
||||||
const [uploadModalState, setUploadModalState] =
|
const [uploadModalState, setUploadModalState] =
|
||||||
useState<ActiveState>('INACTIVE');
|
useState<ActiveState>('INACTIVE');
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [lastQueryReturnedErr, setLastQueryReturnedErr] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||||
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
|
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fetchStream = useRef<any>(null);
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
setUploadModalState('ACTIVE');
|
setUploadModalState('ACTIVE');
|
||||||
setFiles(acceptedFiles);
|
setFiles(acceptedFiles);
|
||||||
@@ -83,35 +90,36 @@ export default function Conversation() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const handleFetchAnswer = useCallback(
|
||||||
if (queries.length) {
|
({ question, index }: { question: string; index?: number }) => {
|
||||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
fetchStream.current = dispatch(fetchAnswer({ question, indx: index }));
|
||||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
|
},
|
||||||
}
|
[dispatch, selectedAgent],
|
||||||
}, [queries[queries.length - 1]]);
|
);
|
||||||
|
|
||||||
const handleQuestion = ({
|
const handleQuestion = useCallback(
|
||||||
question,
|
({
|
||||||
isRetry = false,
|
question,
|
||||||
updated = null,
|
isRetry = false,
|
||||||
indx = undefined,
|
index = undefined,
|
||||||
}: {
|
}: {
|
||||||
question: string;
|
question: string;
|
||||||
isRetry?: boolean;
|
isRetry?: boolean;
|
||||||
updated?: boolean | null;
|
index?: number;
|
||||||
indx?: number;
|
}) => {
|
||||||
}) => {
|
const trimmedQuestion = question.trim();
|
||||||
if (updated === true) {
|
if (trimmedQuestion === '') return;
|
||||||
!isRetry &&
|
|
||||||
dispatch(resendQuery({ index: indx as number, prompt: question }));
|
if (index !== undefined) {
|
||||||
fetchStream.current = dispatch(fetchAnswer({ question, indx }));
|
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||||
} else {
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
question = question.trim();
|
} else {
|
||||||
if (question === '') return;
|
if (!isRetry) dispatch(addQuery({ prompt: trimmedQuestion }));
|
||||||
!isRetry && dispatch(addQuery({ prompt: question }));
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
}
|
||||||
}
|
},
|
||||||
};
|
[dispatch, handleFetchAnswer],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||||
const prevFeedback = query.feedback;
|
const prevFeedback = query.feedback;
|
||||||
@@ -143,10 +151,9 @@ export default function Conversation() {
|
|||||||
indx?: number,
|
indx?: number,
|
||||||
) => {
|
) => {
|
||||||
if (updated === true) {
|
if (updated === true) {
|
||||||
handleQuestion({ question: updatedQuestion as string, updated, indx });
|
handleQuestion({ question: updatedQuestion as string, index: indx });
|
||||||
} else if (input && status !== 'loading') {
|
} else if (input && status !== 'loading') {
|
||||||
if (lastQueryReturnedErr) {
|
if (lastQueryReturnedErr) {
|
||||||
// update last failed query with new prompt
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateQuery({
|
updateQuery({
|
||||||
index: queries.length - 1,
|
index: queries.length - 1,
|
||||||
@@ -167,6 +174,7 @@ export default function Conversation() {
|
|||||||
setInput('');
|
setInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetConversation = () => {
|
const resetConversation = () => {
|
||||||
dispatch(setConversation([]));
|
dispatch(setConversation([]));
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -175,22 +183,29 @@ export default function Conversation() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const newChat = () => {
|
const newChat = () => {
|
||||||
if (queries && queries.length > 0) resetConversation();
|
if (queries && queries.length > 0) resetConversation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queries.length) {
|
||||||
|
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||||
|
queries[queries.length - 1].response && setLastQueryReturnedErr(false);
|
||||||
|
}
|
||||||
|
}, [queries[queries.length - 1]]);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 h-full justify-end">
|
<div className="flex h-full flex-col justify-end gap-1">
|
||||||
{conversationId && queries.length > 0 && (
|
{conversationId && queries.length > 0 && (
|
||||||
<div className="absolute top-4 right-20">
|
<div className="absolute right-20 top-4">
|
||||||
<div className="flex mt-2 items-center gap-4">
|
<div className="mt-2 flex items-center gap-4">
|
||||||
{isMobile && queries.length > 0 && (
|
{isMobile && queries.length > 0 && (
|
||||||
<button
|
<button
|
||||||
title="Open New Chat"
|
title="Open New Chat"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
newChat();
|
newChat();
|
||||||
}}
|
}}
|
||||||
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="h-5 w-5 filter dark:invert"
|
className="h-5 w-5 filter dark:invert"
|
||||||
@@ -205,7 +220,7 @@ export default function Conversation() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShareModalState(true);
|
setShareModalState(true);
|
||||||
}}
|
}}
|
||||||
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="h-5 w-5 filter dark:invert"
|
className="h-5 w-5 filter dark:invert"
|
||||||
@@ -233,7 +248,7 @@ export default function Conversation() {
|
|||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1300px] h-auto py-1">
|
<div className="z-3 flex h-auto w-full max-w-[1300px] flex-col items-end self-center rounded-2xl bg-opacity-0 py-1 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className="flex w-full items-center rounded-[40px]"
|
className="flex w-full items-center rounded-[40px]"
|
||||||
@@ -247,20 +262,22 @@ export default function Conversation() {
|
|||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onSubmit={handleQuestionSubmission}
|
onSubmit={handleQuestionSubmission}
|
||||||
loading={status === 'loading'}
|
loading={status === 'loading'}
|
||||||
|
showSourceButton={selectedAgent ? false : true}
|
||||||
|
showToolButton={selectedAgent ? false : true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-4000 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-sonic-silver md:inline md:w-full">
|
<p className="hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline md:w-full">
|
||||||
{t('tagline')}
|
{t('tagline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{handleDragActive && (
|
{handleDragActive && (
|
||||||
<div className="pointer-events-none fixed top-0 left-0 z-30 flex flex-col size-full items-center justify-center bg-opacity-50 bg-white dark:bg-gray-alpha">
|
<div className="pointer-events-none fixed left-0 top-0 z-30 flex size-full flex-col items-center justify-center bg-white bg-opacity-50 dark:bg-gray-alpha">
|
||||||
<img className="filter dark:invert" src={DragFileUpload} />
|
<img className="filter dark:invert" src={DragFileUpload} />
|
||||||
<span className="px-2 text-2xl font-bold text-outer-space dark:text-silver">
|
<span className="px-2 text-2xl font-bold text-outer-space dark:text-silver">
|
||||||
{t('modals.uploadDoc.drag.title')}
|
{t('modals.uploadDoc.drag.title')}
|
||||||
</span>
|
</span>
|
||||||
<span className="p-2 text-s w-48 text-center text-outer-space dark:text-silver">
|
<span className="text-s w-48 p-2 text-center text-outer-space dark:text-silver">
|
||||||
{t('modals.uploadDoc.drag.description')}
|
{t('modals.uploadDoc.drag.description')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ const ConversationBubble = forwardRef<
|
|||||||
updated?: boolean,
|
updated?: boolean,
|
||||||
index?: number,
|
index?: number,
|
||||||
) => void;
|
) => void;
|
||||||
attachments?: { fileName: string; id: string }[];
|
|
||||||
}
|
}
|
||||||
>(function ConversationBubble(
|
>(function ConversationBubble(
|
||||||
{
|
{
|
||||||
@@ -73,7 +72,6 @@ const ConversationBubble = forwardRef<
|
|||||||
retryBtn,
|
retryBtn,
|
||||||
questionNumber,
|
questionNumber,
|
||||||
handleUpdatedQuestionSubmission,
|
handleUpdatedQuestionSubmission,
|
||||||
attachments,
|
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -100,35 +98,6 @@ const ConversationBubble = forwardRef<
|
|||||||
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
||||||
};
|
};
|
||||||
let bubble;
|
let bubble;
|
||||||
const renderAttachments = () => {
|
|
||||||
if (!attachments || attachments.length === 0) return null;
|
|
||||||
return (
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
|
||||||
{attachments.map((attachment, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center rounded-md bg-gray-100 px-2 py-1 text-sm dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="mr-1 h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>{attachment.fileName}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (type === 'QUESTION') {
|
if (type === 'QUESTION') {
|
||||||
bubble = (
|
bubble = (
|
||||||
<div
|
<div
|
||||||
@@ -157,7 +126,6 @@ const ConversationBubble = forwardRef<
|
|||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
{renderAttachments()}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface ConversationMessagesProps {
|
|||||||
handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void;
|
handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void;
|
||||||
queries: Query[];
|
queries: Query[];
|
||||||
status: Status;
|
status: Status;
|
||||||
|
showHeroOnEmpty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConversationMessages({
|
export default function ConversationMessages({
|
||||||
@@ -31,6 +32,7 @@ export default function ConversationMessages({
|
|||||||
queries,
|
queries,
|
||||||
status,
|
status,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
|
showHeroOnEmpty = true,
|
||||||
}: ConversationMessagesProps) {
|
}: ConversationMessagesProps) {
|
||||||
const [isDarkTheme] = useDarkTheme();
|
const [isDarkTheme] = useDarkTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -141,7 +143,7 @@ export default function ConversationMessages({
|
|||||||
ref={conversationRef}
|
ref={conversationRef}
|
||||||
onWheel={handleUserInterruption}
|
onWheel={handleUserInterruption}
|
||||||
onTouchMove={handleUserInterruption}
|
onTouchMove={handleUserInterruption}
|
||||||
className="flex justify-center w-full overflow-y-auto h-screen sm:pt-12"
|
className="flex justify-center w-full overflow-y-auto h-full sm:pt-12"
|
||||||
>
|
>
|
||||||
{queries.length > 0 && !hasScrolledToLast && (
|
{queries.length > 0 && !hasScrolledToLast && (
|
||||||
<button
|
<button
|
||||||
@@ -161,7 +163,6 @@ export default function ConversationMessages({
|
|||||||
{queries.length > 0 ? (
|
{queries.length > 0 ? (
|
||||||
queries.map((query, index) => (
|
queries.map((query, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
|
|
||||||
<ConversationBubble
|
<ConversationBubble
|
||||||
className={'first:mt-5'}
|
className={'first:mt-5'}
|
||||||
key={`${index}QUESTION`}
|
key={`${index}QUESTION`}
|
||||||
@@ -170,14 +171,13 @@ export default function ConversationMessages({
|
|||||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||||
questionNumber={index}
|
questionNumber={index}
|
||||||
sources={query.sources}
|
sources={query.sources}
|
||||||
attachments={query.attachments}
|
|
||||||
/>
|
/>
|
||||||
{prepResponseView(query, index)}
|
{prepResponseView(query, index)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : showHeroOnEmpty ? (
|
||||||
<Hero handleQuestion={handleQuestion} />
|
<Hero handleQuestion={handleQuestion} />
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export function handleFetchAnswer(
|
|||||||
promptId: string | null,
|
promptId: string | null,
|
||||||
chunks: string,
|
chunks: string,
|
||||||
token_limit: number,
|
token_limit: number,
|
||||||
|
agentId?: string,
|
||||||
attachments?: string[],
|
attachments?: string[],
|
||||||
|
save_conversation: boolean = true,
|
||||||
): Promise<
|
): Promise<
|
||||||
| {
|
| {
|
||||||
result: any;
|
result: any;
|
||||||
@@ -50,6 +52,8 @@ export function handleFetchAnswer(
|
|||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
token_limit: token_limit,
|
token_limit: token_limit,
|
||||||
isNoneDoc: selectedDocs === null,
|
isNoneDoc: selectedDocs === null,
|
||||||
|
agent_id: agentId,
|
||||||
|
save_conversation: save_conversation,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add attachments to payload if they exist
|
// Add attachments to payload if they exist
|
||||||
@@ -97,7 +101,9 @@ export function handleFetchAnswerSteaming(
|
|||||||
token_limit: number,
|
token_limit: number,
|
||||||
onEvent: (event: MessageEvent) => void,
|
onEvent: (event: MessageEvent) => void,
|
||||||
indx?: number,
|
indx?: number,
|
||||||
|
agentId?: string,
|
||||||
attachments?: string[],
|
attachments?: string[],
|
||||||
|
save_conversation: boolean = true,
|
||||||
): Promise<Answer> {
|
): Promise<Answer> {
|
||||||
history = history.map((item) => {
|
history = history.map((item) => {
|
||||||
return {
|
return {
|
||||||
@@ -116,6 +122,8 @@ export function handleFetchAnswerSteaming(
|
|||||||
token_limit: token_limit,
|
token_limit: token_limit,
|
||||||
isNoneDoc: selectedDocs === null,
|
isNoneDoc: selectedDocs === null,
|
||||||
index: indx,
|
index: indx,
|
||||||
|
agent_id: agentId,
|
||||||
|
save_conversation: save_conversation,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add attachments to payload if they exist
|
// Add attachments to payload if they exist
|
||||||
|
|||||||
@@ -9,11 +9,21 @@ export interface Message {
|
|||||||
type: MESSAGE_TYPE;
|
type: MESSAGE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
id?: string;
|
||||||
|
fileName: string;
|
||||||
|
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||||
|
progress: number;
|
||||||
|
taskId?: string;
|
||||||
|
token_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConversationState {
|
export interface ConversationState {
|
||||||
queries: Query[];
|
queries: Query[];
|
||||||
status: Status;
|
status: Status;
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
attachments?: { fileName: string; id: string }[];
|
attachments: Attachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Answer {
|
export interface Answer {
|
||||||
@@ -51,5 +61,7 @@ export interface RetrievalPayload {
|
|||||||
token_limit: number;
|
token_limit: number;
|
||||||
isNoneDoc: boolean;
|
isNoneDoc: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
agent_id?: string;
|
||||||
attachments?: string[];
|
attachments?: string[];
|
||||||
|
save_conversation?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import {
|
|||||||
handleFetchAnswer,
|
handleFetchAnswer,
|
||||||
handleFetchAnswerSteaming,
|
handleFetchAnswerSteaming,
|
||||||
} from './conversationHandlers';
|
} from './conversationHandlers';
|
||||||
import { Answer, ConversationState, Query, Status } from './conversationModels';
|
import {
|
||||||
|
Answer,
|
||||||
|
Query,
|
||||||
|
Status,
|
||||||
|
ConversationState,
|
||||||
|
Attachment,
|
||||||
|
} from './conversationModels';
|
||||||
|
|
||||||
const initialState: ConversationState = {
|
const initialState: ConversationState = {
|
||||||
queries: [],
|
queries: [],
|
||||||
@@ -28,35 +34,159 @@ export function handleAbort() {
|
|||||||
|
|
||||||
export const fetchAnswer = createAsyncThunk<
|
export const fetchAnswer = createAsyncThunk<
|
||||||
Answer,
|
Answer,
|
||||||
{ question: string; indx?: number }
|
{ question: string; indx?: number; isPreview?: boolean }
|
||||||
>('fetchAnswer', async ({ question, indx }, { dispatch, getState }) => {
|
>(
|
||||||
if (abortController) {
|
'fetchAnswer',
|
||||||
abortController.abort();
|
async ({ question, indx, isPreview = false }, { dispatch, getState }) => {
|
||||||
}
|
if (abortController) abortController.abort();
|
||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
const { signal } = abortController;
|
const { signal } = abortController;
|
||||||
|
|
||||||
let isSourceUpdated = false;
|
let isSourceUpdated = false;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const attachments = state.conversation.attachments?.map(a => a.id) || [];
|
const attachmentIds = state.conversation.attachments
|
||||||
|
.filter((a) => a.id && a.status === 'completed')
|
||||||
|
.map((a) => a.id) as string[];
|
||||||
|
const currentConversationId = state.conversation.conversationId;
|
||||||
|
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||||
|
const save_conversation = isPreview ? false : true;
|
||||||
|
|
||||||
if (state.preference) {
|
if (state.preference) {
|
||||||
if (API_STREAMING) {
|
if (API_STREAMING) {
|
||||||
await handleFetchAnswerSteaming(
|
await handleFetchAnswerSteaming(
|
||||||
question,
|
question,
|
||||||
signal,
|
signal,
|
||||||
state.preference.token,
|
state.preference.token,
|
||||||
state.preference.selectedDocs!,
|
state.preference.selectedDocs!,
|
||||||
state.conversation.queries,
|
state.conversation.queries,
|
||||||
state.conversation.conversationId,
|
conversationIdToSend,
|
||||||
state.preference.prompt.id,
|
state.preference.prompt.id,
|
||||||
state.preference.chunks,
|
state.preference.chunks,
|
||||||
state.preference.token_limit,
|
state.preference.token_limit,
|
||||||
(event) => {
|
(event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||||
|
|
||||||
if (data.type === 'end') {
|
if (data.type === 'end') {
|
||||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||||
|
if (!isPreview) {
|
||||||
|
getConversations(state.preference.token)
|
||||||
|
.then((fetchedConversations) => {
|
||||||
|
dispatch(setConversations(fetchedConversations));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch conversations: ', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isSourceUpdated) {
|
||||||
|
dispatch(
|
||||||
|
updateStreamingSource({
|
||||||
|
index: targetIndex,
|
||||||
|
query: { sources: [] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'id') {
|
||||||
|
if (!isPreview) {
|
||||||
|
dispatch(
|
||||||
|
updateConversationId({
|
||||||
|
query: { conversationId: data.id },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'thought') {
|
||||||
|
const result = data.thought;
|
||||||
|
dispatch(
|
||||||
|
updateThought({
|
||||||
|
index: targetIndex,
|
||||||
|
query: { thought: result },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'source') {
|
||||||
|
isSourceUpdated = true;
|
||||||
|
dispatch(
|
||||||
|
updateStreamingSource({
|
||||||
|
index: targetIndex,
|
||||||
|
query: { sources: data.source ?? [] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'tool_calls') {
|
||||||
|
dispatch(
|
||||||
|
updateToolCalls({
|
||||||
|
index: targetIndex,
|
||||||
|
query: { tool_calls: data.tool_calls },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
// set status to 'failed'
|
||||||
|
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||||
|
dispatch(
|
||||||
|
conversationSlice.actions.raiseError({
|
||||||
|
index: targetIndex,
|
||||||
|
message: data.error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
updateStreamingQuery({
|
||||||
|
index: targetIndex,
|
||||||
|
query: { response: data.answer },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indx,
|
||||||
|
state.preference.selectedAgent?.id,
|
||||||
|
attachmentIds,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!isPreview) {
|
||||||
|
dispatch(
|
||||||
|
updateConversationId({
|
||||||
|
query: { conversationId: answer.conversationId },
|
||||||
|
}),
|
||||||
|
);
|
||||||
getConversations(state.preference.token)
|
getConversations(state.preference.token)
|
||||||
.then((fetchedConversations) => {
|
.then((fetchedConversations) => {
|
||||||
dispatch(setConversations(fetchedConversations));
|
dispatch(setConversations(fetchedConversations));
|
||||||
@@ -64,130 +194,23 @@ export const fetchAnswer = createAsyncThunk<
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to fetch conversations: ', error);
|
console.error('Failed to fetch conversations: ', error);
|
||||||
});
|
});
|
||||||
if (!isSourceUpdated) {
|
|
||||||
dispatch(
|
|
||||||
updateStreamingSource({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: { sources: [] },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (data.type === 'id') {
|
|
||||||
dispatch(
|
|
||||||
updateConversationId({
|
|
||||||
query: { conversationId: data.id },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (data.type === 'thought') {
|
|
||||||
const result = data.thought;
|
|
||||||
console.log('thought', result);
|
|
||||||
dispatch(
|
|
||||||
updateThought({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: { thought: result },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (data.type === 'source') {
|
|
||||||
isSourceUpdated = true;
|
|
||||||
dispatch(
|
|
||||||
updateStreamingSource({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: { sources: data.source ?? [] },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (data.type === 'tool_calls') {
|
|
||||||
dispatch(
|
|
||||||
updateToolCalls({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: { tool_calls: data.tool_calls },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (data.type === 'error') {
|
|
||||||
// set status to 'failed'
|
|
||||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
|
||||||
dispatch(
|
|
||||||
conversationSlice.actions.raiseError({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
message: data.error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const result = data.answer;
|
|
||||||
dispatch(
|
|
||||||
updateStreamingQuery({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: { response: result },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||||
indx,
|
}
|
||||||
attachments
|
|
||||||
);
|
|
||||||
} 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,
|
|
||||||
attachments
|
|
||||||
);
|
|
||||||
if (answer) {
|
|
||||||
let sourcesPrepped = [];
|
|
||||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
|
||||||
if (source && source.title) {
|
|
||||||
const titleParts = source.title.split('/');
|
|
||||||
return {
|
|
||||||
...source,
|
|
||||||
title: titleParts[titleParts.length - 1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return source;
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
updateQuery({
|
|
||||||
index: indx ?? state.conversation.queries.length - 1,
|
|
||||||
query: {
|
|
||||||
response: answer.answer,
|
|
||||||
thought: answer.thought,
|
|
||||||
sources: sourcesPrepped,
|
|
||||||
tool_calls: answer.toolCalls,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
updateConversationId({
|
|
||||||
query: { conversationId: answer.conversationId },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
|
||||||
getConversations(state.preference.token)
|
|
||||||
.then((fetchedConversations) => {
|
|
||||||
dispatch(setConversations(fetchedConversations));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to fetch conversations: ', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return {
|
||||||
return {
|
conversationId: null,
|
||||||
conversationId: null,
|
title: null,
|
||||||
title: null,
|
answer: '',
|
||||||
answer: '',
|
query: question,
|
||||||
query: question,
|
result: '',
|
||||||
result: '',
|
thought: '',
|
||||||
thought: '',
|
sources: [],
|
||||||
sources: [],
|
tool_calls: [],
|
||||||
tool_calls: [],
|
};
|
||||||
};
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
export const conversationSlice = createSlice({
|
export const conversationSlice = createSlice({
|
||||||
name: 'conversation',
|
name: 'conversation',
|
||||||
@@ -286,9 +309,41 @@ export const conversationSlice = createSlice({
|
|||||||
const { index, message } = action.payload;
|
const { index, message } = action.payload;
|
||||||
state.queries[index].error = message;
|
state.queries[index].error = message;
|
||||||
},
|
},
|
||||||
setAttachments: (state, action: PayloadAction<{ fileName: string; id: string }[]>) => {
|
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
|
||||||
state.attachments = action.payload;
|
state.attachments = action.payload;
|
||||||
},
|
},
|
||||||
|
addAttachment: (state, action: PayloadAction<Attachment>) => {
|
||||||
|
state.attachments.push(action.payload);
|
||||||
|
},
|
||||||
|
updateAttachment: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
taskId: string;
|
||||||
|
updates: Partial<Attachment>;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
const index = state.attachments.findIndex(
|
||||||
|
(att) => att.taskId === action.payload.taskId,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.attachments[index] = {
|
||||||
|
...state.attachments[index],
|
||||||
|
...action.payload.updates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAttachment: (state, action: PayloadAction<string>) => {
|
||||||
|
state.attachments = state.attachments.filter(
|
||||||
|
(att) => att.taskId !== action.payload && att.id !== action.payload,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
resetConversation: (state) => {
|
||||||
|
state.queries = initialState.queries;
|
||||||
|
state.status = initialState.status;
|
||||||
|
state.conversationId = initialState.conversationId;
|
||||||
|
state.attachments = initialState.attachments;
|
||||||
|
handleAbort();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
builder
|
builder
|
||||||
@@ -312,6 +367,11 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
|
|||||||
|
|
||||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||||
|
|
||||||
|
export const selectAttachments = (state: RootState) =>
|
||||||
|
state.conversation.attachments;
|
||||||
|
export const selectCompletedAttachments = (state: RootState) =>
|
||||||
|
state.conversation.attachments.filter((att) => att.status === 'completed');
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
addQuery,
|
addQuery,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
@@ -323,5 +383,9 @@ export const {
|
|||||||
updateToolCalls,
|
updateToolCalls,
|
||||||
setConversation,
|
setConversation,
|
||||||
setAttachments,
|
setAttachments,
|
||||||
|
addAttachment,
|
||||||
|
updateAttachment,
|
||||||
|
removeAttachment,
|
||||||
|
resetConversation,
|
||||||
} = conversationSlice.actions;
|
} = conversationSlice.actions;
|
||||||
export default conversationSlice.reducer;
|
export default conversationSlice.reducer;
|
||||||
|
|||||||
168
frontend/src/hooks/usePromptManager.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import { Prompt } from '../models/misc';
|
||||||
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
|
|
||||||
|
type UsePromptManagerProps = {
|
||||||
|
initialPrompts: Prompt[];
|
||||||
|
onPromptSelect: (name: string, id: string, type: string) => void;
|
||||||
|
onPromptsUpdate: (updatedPrompts: Prompt[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromptContentResponse = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromptCreateResponse = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePromptManager = ({
|
||||||
|
initialPrompts,
|
||||||
|
onPromptSelect,
|
||||||
|
onPromptsUpdate,
|
||||||
|
}: UsePromptManagerProps) => {
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
|
const [prompts, setPrompts] = useState<Prompt[]>(initialPrompts);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPrompts(initialPrompts);
|
||||||
|
}, [initialPrompts]);
|
||||||
|
|
||||||
|
const handleApiCall = async <T>(
|
||||||
|
apiCall: () => Promise<Response>,
|
||||||
|
errorMessage: string,
|
||||||
|
): Promise<T | null> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await apiCall();
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
console.error(`${errorMessage}: ${response.status} ${errorData}`);
|
||||||
|
throw new Error(`${errorMessage} (Status: ${response.status})`);
|
||||||
|
}
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(message);
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPrompt = useCallback(
|
||||||
|
async (name: string, content: string): Promise<Prompt | null> => {
|
||||||
|
const newPromptData = await handleApiCall<PromptCreateResponse>(
|
||||||
|
() => userService.createPrompt({ name, content }, token),
|
||||||
|
'Failed to add prompt',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newPromptData) {
|
||||||
|
const newPrompt: Prompt = {
|
||||||
|
name,
|
||||||
|
id: newPromptData.id,
|
||||||
|
type: 'private',
|
||||||
|
};
|
||||||
|
const updatedPrompts = [...prompts, newPrompt];
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
onPromptsUpdate(updatedPrompts);
|
||||||
|
onPromptSelect(newPrompt.name, newPrompt.id, newPrompt.type);
|
||||||
|
return newPrompt;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[token, prompts, onPromptsUpdate, onPromptSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletePrompt = useCallback(
|
||||||
|
async (idToDelete: string): Promise<void> => {
|
||||||
|
const originalPrompts = [...prompts];
|
||||||
|
const updatedPrompts = prompts.filter(
|
||||||
|
(prompt) => prompt.id !== idToDelete,
|
||||||
|
);
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
onPromptsUpdate(updatedPrompts);
|
||||||
|
|
||||||
|
const result = await handleApiCall<null>(
|
||||||
|
() => userService.deletePrompt({ id: idToDelete }, token),
|
||||||
|
'Failed to delete prompt',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === null && error) {
|
||||||
|
setPrompts(originalPrompts);
|
||||||
|
onPromptsUpdate(originalPrompts);
|
||||||
|
} else {
|
||||||
|
if (updatedPrompts.length > 0) {
|
||||||
|
onPromptSelect(
|
||||||
|
updatedPrompts[0].name,
|
||||||
|
updatedPrompts[0].id,
|
||||||
|
updatedPrompts[0].type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[token, prompts, onPromptsUpdate, onPromptSelect, error],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchPromptContent = useCallback(
|
||||||
|
async (id: string): Promise<string | null> => {
|
||||||
|
const promptDetails = await handleApiCall<PromptContentResponse>(
|
||||||
|
() => userService.getSinglePrompt(id, token),
|
||||||
|
'Failed to fetch prompt content',
|
||||||
|
);
|
||||||
|
return promptDetails ? promptDetails.content : null;
|
||||||
|
},
|
||||||
|
[token],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePrompt = useCallback(
|
||||||
|
async (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
content: string,
|
||||||
|
type: string,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const result = await handleApiCall<{ success: boolean }>(
|
||||||
|
() => userService.updatePrompt({ id, name, content }, token),
|
||||||
|
'Failed to update prompt',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const updatedPrompts = prompts.map((p) =>
|
||||||
|
p.id === id ? { ...p, name, type } : p,
|
||||||
|
);
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
onPromptsUpdate(updatedPrompts);
|
||||||
|
onPromptSelect(name, id, type);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[token, prompts, onPromptsUpdate, onPromptSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompts,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
addPrompt,
|
||||||
|
deletePrompt,
|
||||||
|
fetchPromptContent,
|
||||||
|
updatePrompt,
|
||||||
|
setError,
|
||||||
|
};
|
||||||
|
};
|
||||||
68
frontend/src/modals/AgentDetailsModal.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Agent } from '../agents/types';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
|
import WrapperModal from './WrapperModal';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
type AgentDetailsModalProps = {
|
||||||
|
agent: Agent;
|
||||||
|
mode: 'new' | 'edit' | 'draft';
|
||||||
|
modalState: ActiveState;
|
||||||
|
setModalState: (state: ActiveState) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AgentDetailsModal({
|
||||||
|
agent,
|
||||||
|
mode,
|
||||||
|
modalState,
|
||||||
|
setModalState,
|
||||||
|
}: AgentDetailsModalProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
if (modalState !== 'ACTIVE') return null;
|
||||||
|
return (
|
||||||
|
<WrapperModal
|
||||||
|
className="sm:w-[512px]"
|
||||||
|
close={() => {
|
||||||
|
// if (mode === 'new') navigate('/agents');
|
||||||
|
setModalState('INACTIVE');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-jet dark:text-bright-gray">
|
||||||
|
Access Details
|
||||||
|
</h2>
|
||||||
|
<div className="mt-8 flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
|
Public link
|
||||||
|
</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
|
API Key
|
||||||
|
</h2>
|
||||||
|
{agent.key ? (
|
||||||
|
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
||||||
|
{agent.key}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
|
Webhooks
|
||||||
|
</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WrapperModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,12 @@ export type GetDocsResponse = {
|
|||||||
nextCursor: string;
|
nextCursor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Prompt = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PromptProps = {
|
export type PromptProps = {
|
||||||
prompts: { name: string; id: string; type: string }[];
|
prompts: { name: string; id: string; type: string }[];
|
||||||
selectedPrompt: { name: string; id: string; type: string };
|
selectedPrompt: { name: string; id: string; type: string };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import conversationService from '../api/services/conversationService';
|
import conversationService from '../api/services/conversationService';
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import { Doc, GetDocsResponse } from '../models/misc';
|
import { Doc, GetDocsResponse } from '../models/misc';
|
||||||
|
import { GetConversationsResult, ConversationSummary } from './types';
|
||||||
|
|
||||||
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
|
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
|
||||||
export async function getDocs(token: string | null): Promise<Doc[] | null> {
|
export async function getDocs(token: string | null): Promise<Doc[] | null> {
|
||||||
@@ -49,23 +50,37 @@ export async function getDocsWithPagination(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConversations(token: string | null): Promise<{
|
export async function getConversations(
|
||||||
data: { name: string; id: string }[] | null;
|
token: string | null,
|
||||||
loading: boolean;
|
): Promise<GetConversationsResult> {
|
||||||
}> {
|
|
||||||
try {
|
try {
|
||||||
const response = await conversationService.getConversations(token);
|
const response = await conversationService.getConversations(token);
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const conversations: { name: string; id: string }[] = [];
|
if (!response.ok) {
|
||||||
|
console.error('Error fetching conversations:', response.statusText);
|
||||||
|
return { data: null, loading: false };
|
||||||
|
}
|
||||||
|
|
||||||
data.forEach((conversation: object) => {
|
const rawData: unknown = await response.json();
|
||||||
conversations.push(conversation as { name: string; id: string });
|
if (!Array.isArray(rawData)) {
|
||||||
});
|
console.error(
|
||||||
|
'Invalid data format received from API: Expected an array.',
|
||||||
|
rawData,
|
||||||
|
);
|
||||||
|
return { data: null, loading: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations: ConversationSummary[] = rawData.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
agent_id: item.agent_id ?? null,
|
||||||
|
}));
|
||||||
return { data: conversations, loading: false };
|
return { data: conversations, loading: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(
|
||||||
|
'An unexpected error occurred while fetching conversations:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
return { data: null, loading: false };
|
return { data: null, loading: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
PayloadAction,
|
|
||||||
createListenerMiddleware,
|
createListenerMiddleware,
|
||||||
createSlice,
|
createSlice,
|
||||||
isAnyOf,
|
isAnyOf,
|
||||||
|
PayloadAction,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
|
|
||||||
import { RootState } from '../store';
|
import { Agent } from '../agents/types';
|
||||||
import { ActiveState, Doc } from '../models/misc';
|
import { ActiveState, Doc } from '../models/misc';
|
||||||
|
import { RootState } from '../store';
|
||||||
|
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
|
||||||
|
|
||||||
export interface Preference {
|
export interface Preference {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -22,6 +24,8 @@ export interface Preference {
|
|||||||
token: string | null;
|
token: string | null;
|
||||||
modalState: ActiveState;
|
modalState: ActiveState;
|
||||||
paginatedDocuments: Doc[] | null;
|
paginatedDocuments: Doc[] | null;
|
||||||
|
agents: Agent[] | null;
|
||||||
|
selectedAgent: Agent | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Preference = {
|
const initialState: Preference = {
|
||||||
@@ -46,6 +50,8 @@ const initialState: Preference = {
|
|||||||
token: localStorage.getItem('authToken') || null,
|
token: localStorage.getItem('authToken') || null,
|
||||||
modalState: 'INACTIVE',
|
modalState: 'INACTIVE',
|
||||||
paginatedDocuments: null,
|
paginatedDocuments: null,
|
||||||
|
agents: null,
|
||||||
|
selectedAgent: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prefSlice = createSlice({
|
export const prefSlice = createSlice({
|
||||||
@@ -82,6 +88,12 @@ export const prefSlice = createSlice({
|
|||||||
setModalStateDeleteConv: (state, action: PayloadAction<ActiveState>) => {
|
setModalStateDeleteConv: (state, action: PayloadAction<ActiveState>) => {
|
||||||
state.modalState = action.payload;
|
state.modalState = action.payload;
|
||||||
},
|
},
|
||||||
|
setAgents: (state, action) => {
|
||||||
|
state.agents = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedAgent: (state, action) => {
|
||||||
|
state.selectedAgent = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +108,8 @@ export const {
|
|||||||
setTokenLimit,
|
setTokenLimit,
|
||||||
setModalStateDeleteConv,
|
setModalStateDeleteConv,
|
||||||
setPaginatedDocuments,
|
setPaginatedDocuments,
|
||||||
|
setAgents,
|
||||||
|
setSelectedAgent,
|
||||||
} = prefSlice.actions;
|
} = prefSlice.actions;
|
||||||
export default prefSlice.reducer;
|
export default prefSlice.reducer;
|
||||||
|
|
||||||
@@ -170,3 +184,6 @@ export const selectTokenLimit = (state: RootState) =>
|
|||||||
state.preference.token_limit;
|
state.preference.token_limit;
|
||||||
export const selectPaginatedDocuments = (state: RootState) =>
|
export const selectPaginatedDocuments = (state: RootState) =>
|
||||||
state.preference.paginatedDocuments;
|
state.preference.paginatedDocuments;
|
||||||
|
export const selectAgents = (state: RootState) => state.preference.agents;
|
||||||
|
export const selectSelectedAgent = (state: RootState) =>
|
||||||
|
state.preference.selectedAgent;
|
||||||
|
|||||||
10
frontend/src/preferences/types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type ConversationSummary = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
agent_id: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetConversationsResult = {
|
||||||
|
data: ConversationSummary[] | null;
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { Agent } from '../agents/types';
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import Dropdown from '../components/Dropdown';
|
import Dropdown from '../components/Dropdown';
|
||||||
import SkeletonLoader from '../components/SkeletonLoader';
|
import SkeletonLoader from '../components/SkeletonLoader';
|
||||||
@@ -19,7 +20,6 @@ import { useLoaderState } from '../hooks';
|
|||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { htmlLegendPlugin } from '../utils/chartUtils';
|
import { htmlLegendPlugin } from '../utils/chartUtils';
|
||||||
import { formatDate } from '../utils/dateTimeUtils';
|
import { formatDate } from '../utils/dateTimeUtils';
|
||||||
import { APIKeyData } from './types';
|
|
||||||
|
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartData } from 'chart.js';
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
@@ -31,7 +31,11 @@ ChartJS.register(
|
|||||||
Legend,
|
Legend,
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function Analytics() {
|
type AnalyticsProps = {
|
||||||
|
agentId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Analytics({ agentId }: AnalyticsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
@@ -67,8 +71,7 @@ export default function Analytics() {
|
|||||||
string,
|
string,
|
||||||
{ positive: number; negative: number }
|
{ positive: number; negative: number }
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
const [agent, setAgent] = useState<Agent>();
|
||||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
|
||||||
const [messagesFilter, setMessagesFilter] = useState<{
|
const [messagesFilter, setMessagesFilter] = useState<{
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -94,37 +97,33 @@ export default function Analytics() {
|
|||||||
const [loadingMessages, setLoadingMessages] = useLoaderState(true);
|
const [loadingMessages, setLoadingMessages] = useLoaderState(true);
|
||||||
const [loadingTokens, setLoadingTokens] = useLoaderState(true);
|
const [loadingTokens, setLoadingTokens] = useLoaderState(true);
|
||||||
const [loadingFeedback, setLoadingFeedback] = useLoaderState(true);
|
const [loadingFeedback, setLoadingFeedback] = useLoaderState(true);
|
||||||
const [loadingChatbots, setLoadingChatbots] = useLoaderState(true);
|
const [loadingAgent, setLoadingAgent] = useLoaderState(true);
|
||||||
|
|
||||||
const fetchChatbots = async () => {
|
const fetchAgent = async (agentId: string) => {
|
||||||
setLoadingChatbots(true);
|
setLoadingAgent(true);
|
||||||
try {
|
try {
|
||||||
const response = await userService.getAPIKeys(token);
|
const response = await userService.getAgent(agentId ?? '', token);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to fetch Chatbots');
|
||||||
throw new Error('Failed to fetch Chatbots');
|
const agent = await response.json();
|
||||||
}
|
setAgent(agent);
|
||||||
const chatbots = await response.json();
|
|
||||||
setChatbots(chatbots);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingChatbots(false);
|
setLoadingAgent(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMessagesData = async (chatbot_id?: string, filter?: string) => {
|
const fetchMessagesData = async (agent_id?: string, filter?: string) => {
|
||||||
setLoadingMessages(true);
|
setLoadingMessages(true);
|
||||||
try {
|
try {
|
||||||
const response = await userService.getMessageAnalytics(
|
const response = await userService.getMessageAnalytics(
|
||||||
{
|
{
|
||||||
api_key_id: chatbot_id,
|
api_key_id: agent_id,
|
||||||
filter_option: filter,
|
filter_option: filter,
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to fetch analytics data');
|
||||||
throw new Error('Failed to fetch analytics data');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setMessagesData(data.messages);
|
setMessagesData(data.messages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,19 +133,17 @@ export default function Analytics() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchTokenData = async (chatbot_id?: string, filter?: string) => {
|
const fetchTokenData = async (agent_id?: string, filter?: string) => {
|
||||||
setLoadingTokens(true);
|
setLoadingTokens(true);
|
||||||
try {
|
try {
|
||||||
const response = await userService.getTokenAnalytics(
|
const response = await userService.getTokenAnalytics(
|
||||||
{
|
{
|
||||||
api_key_id: chatbot_id,
|
api_key_id: agent_id,
|
||||||
filter_option: filter,
|
filter_option: filter,
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to fetch analytics data');
|
||||||
throw new Error('Failed to fetch analytics data');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setTokenUsageData(data.token_usage);
|
setTokenUsageData(data.token_usage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -156,19 +153,17 @@ export default function Analytics() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => {
|
const fetchFeedbackData = async (agent_id?: string, filter?: string) => {
|
||||||
setLoadingFeedback(true);
|
setLoadingFeedback(true);
|
||||||
try {
|
try {
|
||||||
const response = await userService.getFeedbackAnalytics(
|
const response = await userService.getFeedbackAnalytics(
|
||||||
{
|
{
|
||||||
api_key_id: chatbot_id,
|
api_key_id: agent_id,
|
||||||
filter_option: filter,
|
filter_option: filter,
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to fetch analytics data');
|
||||||
throw new Error('Failed to fetch analytics data');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setFeedbackData(data.feedback);
|
setFeedbackData(data.feedback);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -179,229 +174,182 @@ export default function Analytics() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchChatbots();
|
if (agentId) fetchAgent(agentId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = selectedChatbot?.id;
|
const id = agent?.id;
|
||||||
const filter = messagesFilter;
|
const filter = messagesFilter;
|
||||||
fetchMessagesData(id, filter?.value);
|
fetchMessagesData(id, filter?.value);
|
||||||
}, [selectedChatbot, messagesFilter]);
|
}, [agent, messagesFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = selectedChatbot?.id;
|
const id = agent?.id;
|
||||||
const filter = tokenUsageFilter;
|
const filter = tokenUsageFilter;
|
||||||
fetchTokenData(id, filter?.value);
|
fetchTokenData(id, filter?.value);
|
||||||
}, [selectedChatbot, tokenUsageFilter]);
|
}, [agent, tokenUsageFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = selectedChatbot?.id;
|
const id = agent?.id;
|
||||||
const filter = feedbackFilter;
|
const filter = feedbackFilter;
|
||||||
fetchFeedbackData(id, filter?.value);
|
fetchFeedbackData(id, filter?.value);
|
||||||
}, [selectedChatbot, feedbackFilter]);
|
}, [agent, feedbackFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<div className="flex flex-col items-start">
|
{/* Messages Analytics */}
|
||||||
{loadingChatbots ? (
|
<div className="mt-8 flex w-full flex-col gap-3 [@media(min-width:1080px)]:flex-row">
|
||||||
<SkeletonLoader component="dropdown" />
|
<div className="h-[345px] w-full overflow-hidden rounded-2xl border border-silver px-6 py-5 dark:border-silver/40 [@media(min-width:1080px)]:w-1/2">
|
||||||
) : (
|
<div className="flex flex-row items-center justify-start gap-3">
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
<p className="font-bold text-jet dark:text-bright-gray">
|
||||||
{t('settings.analytics.filterByChatbot')}
|
{t('settings.analytics.messages')}
|
||||||
</p>
|
</p>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
size="w-[55vw] sm:w-[360px]"
|
size="w-[125px]"
|
||||||
options={[
|
options={filterOptions}
|
||||||
...chatbots.map((chatbot) => ({
|
placeholder={t('settings.analytics.filterPlaceholder')}
|
||||||
label: chatbot.name,
|
onSelect={(selectedOption: { label: string; value: string }) => {
|
||||||
value: chatbot.id,
|
setMessagesFilter(selectedOption);
|
||||||
})),
|
|
||||||
{ label: t('settings.analytics.none'), value: '' },
|
|
||||||
]}
|
|
||||||
placeholder={t('settings.analytics.selectChatbot')}
|
|
||||||
onSelect={(chatbot: { label: string; value: string }) => {
|
|
||||||
setSelectedChatbot(
|
|
||||||
chatbots.find((item) => item.id === chatbot.value),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
selectedValue={
|
selectedValue={messagesFilter ?? null}
|
||||||
(selectedChatbot && {
|
|
||||||
label: selectedChatbot.name,
|
|
||||||
value: selectedChatbot.id,
|
|
||||||
}) ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
border="border"
|
||||||
darkBorderColor="dim-gray"
|
contentSize="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="relative mt-px h-[245px] w-full">
|
||||||
|
<div
|
||||||
{/* Messages Analytics */}
|
id="legend-container-1"
|
||||||
<div className="mt-8 w-full flex flex-col [@media(min-width:1080px)]:flex-row gap-3">
|
className="flex flex-row items-center justify-end"
|
||||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
></div>
|
||||||
<div className="flex flex-row items-center justify-start gap-3">
|
{loadingMessages ? (
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
<SkeletonLoader count={1} component={'analysis'} />
|
||||||
{t('settings.analytics.messages')}
|
) : (
|
||||||
</p>
|
<AnalyticsChart
|
||||||
<Dropdown
|
data={{
|
||||||
size="w-[125px]"
|
labels: Object.keys(messagesData || {}).map((item) =>
|
||||||
options={filterOptions}
|
formatDate(item),
|
||||||
placeholder={t('settings.analytics.filterPlaceholder')}
|
),
|
||||||
onSelect={(selectedOption: {
|
datasets: [
|
||||||
label: string;
|
{
|
||||||
value: string;
|
label: t('settings.analytics.messages'),
|
||||||
}) => {
|
data: Object.values(messagesData || {}),
|
||||||
setMessagesFilter(selectedOption);
|
backgroundColor: '#7D54D1',
|
||||||
|
},
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
selectedValue={messagesFilter ?? null}
|
legendID="legend-container-1"
|
||||||
rounded="3xl"
|
maxTicksLimitInX={8}
|
||||||
border="border"
|
isStacked={false}
|
||||||
contentSize="text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className="mt-px relative h-[245px] w-full">
|
|
||||||
<div
|
|
||||||
id="legend-container-1"
|
|
||||||
className="flex flex-row items-center justify-end"
|
|
||||||
></div>
|
|
||||||
{loadingMessages ? (
|
|
||||||
<SkeletonLoader count={1} component={'analysis'} />
|
|
||||||
) : (
|
|
||||||
<AnalyticsChart
|
|
||||||
data={{
|
|
||||||
labels: Object.keys(messagesData || {}).map((item) =>
|
|
||||||
formatDate(item),
|
|
||||||
),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: t('settings.analytics.messages'),
|
|
||||||
data: Object.values(messagesData || {}),
|
|
||||||
backgroundColor: '#7D54D1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
legendID="legend-container-1"
|
|
||||||
maxTicksLimitInX={8}
|
|
||||||
isStacked={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Token Usage Analytics */}
|
|
||||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
|
||||||
<div className="flex flex-row items-center justify-start gap-3">
|
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
|
||||||
{t('settings.analytics.tokenUsage')}
|
|
||||||
</p>
|
|
||||||
<Dropdown
|
|
||||||
size="w-[125px]"
|
|
||||||
options={filterOptions}
|
|
||||||
placeholder={t('settings.analytics.filterPlaceholder')}
|
|
||||||
onSelect={(selectedOption: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}) => {
|
|
||||||
setTokenUsageFilter(selectedOption);
|
|
||||||
}}
|
|
||||||
selectedValue={tokenUsageFilter ?? null}
|
|
||||||
rounded="3xl"
|
|
||||||
border="border"
|
|
||||||
contentSize="text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-px relative h-[245px] w-full">
|
|
||||||
<div
|
|
||||||
id="legend-container-2"
|
|
||||||
className="flex flex-row items-center justify-end"
|
|
||||||
></div>
|
|
||||||
{loadingTokens ? (
|
|
||||||
<SkeletonLoader count={1} component={'analysis'} />
|
|
||||||
) : (
|
|
||||||
<AnalyticsChart
|
|
||||||
data={{
|
|
||||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
|
||||||
formatDate(item),
|
|
||||||
),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: t('settings.analytics.tokenUsage'),
|
|
||||||
data: Object.values(tokenUsageData || {}),
|
|
||||||
backgroundColor: '#7D54D1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
legendID="legend-container-2"
|
|
||||||
maxTicksLimitInX={8}
|
|
||||||
isStacked={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feedback Analytics */}
|
{/* Token Usage Analytics */}
|
||||||
<div className="mt-8 w-full flex flex-col gap-3">
|
<div className="h-[345px] w-full overflow-hidden rounded-2xl border border-silver px-6 py-5 dark:border-silver/40 [@media(min-width:1080px)]:w-1/2">
|
||||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
<div className="flex flex-row items-center justify-start gap-3">
|
||||||
<div className="flex flex-row items-center justify-start gap-3">
|
<p className="font-bold text-jet dark:text-bright-gray">
|
||||||
<p className="font-bold text-jet dark:text-bright-gray">
|
{t('settings.analytics.tokenUsage')}
|
||||||
{t('settings.analytics.userFeedback')}
|
</p>
|
||||||
</p>
|
<Dropdown
|
||||||
<Dropdown
|
size="w-[125px]"
|
||||||
size="w-[125px]"
|
options={filterOptions}
|
||||||
options={filterOptions}
|
placeholder={t('settings.analytics.filterPlaceholder')}
|
||||||
placeholder={t('settings.analytics.filterPlaceholder')}
|
onSelect={(selectedOption: { label: string; value: string }) => {
|
||||||
onSelect={(selectedOption: {
|
setTokenUsageFilter(selectedOption);
|
||||||
label: string;
|
}}
|
||||||
value: string;
|
selectedValue={tokenUsageFilter ?? null}
|
||||||
}) => {
|
rounded="3xl"
|
||||||
setFeedbackFilter(selectedOption);
|
border="border"
|
||||||
|
contentSize="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-px h-[245px] w-full">
|
||||||
|
<div
|
||||||
|
id="legend-container-2"
|
||||||
|
className="flex flex-row items-center justify-end"
|
||||||
|
></div>
|
||||||
|
{loadingTokens ? (
|
||||||
|
<SkeletonLoader count={1} component={'analysis'} />
|
||||||
|
) : (
|
||||||
|
<AnalyticsChart
|
||||||
|
data={{
|
||||||
|
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||||
|
formatDate(item),
|
||||||
|
),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: t('settings.analytics.tokenUsage'),
|
||||||
|
data: Object.values(tokenUsageData || {}),
|
||||||
|
backgroundColor: '#7D54D1',
|
||||||
|
},
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
selectedValue={feedbackFilter ?? null}
|
legendID="legend-container-2"
|
||||||
rounded="3xl"
|
maxTicksLimitInX={8}
|
||||||
border="border"
|
isStacked={false}
|
||||||
contentSize="text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className="mt-px relative h-[245px] w-full">
|
</div>
|
||||||
<div
|
</div>
|
||||||
id="legend-container-3"
|
</div>
|
||||||
className="flex flex-row items-center justify-end"
|
|
||||||
></div>
|
{/* Feedback Analytics */}
|
||||||
{loadingFeedback ? (
|
<div className="mt-8 flex w-full flex-col gap-3">
|
||||||
<SkeletonLoader count={1} component={'analysis'} />
|
<div className="h-[345px] w-full overflow-hidden rounded-2xl border border-silver px-6 py-5 dark:border-silver/40">
|
||||||
) : (
|
<div className="flex flex-row items-center justify-start gap-3">
|
||||||
<AnalyticsChart
|
<p className="font-bold text-jet dark:text-bright-gray">
|
||||||
data={{
|
{t('settings.analytics.userFeedback')}
|
||||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
</p>
|
||||||
formatDate(item),
|
<Dropdown
|
||||||
),
|
size="w-[125px]"
|
||||||
datasets: [
|
options={filterOptions}
|
||||||
{
|
placeholder={t('settings.analytics.filterPlaceholder')}
|
||||||
label: t('settings.analytics.positiveFeedback'),
|
onSelect={(selectedOption: { label: string; value: string }) => {
|
||||||
data: Object.values(feedbackData || {}).map(
|
setFeedbackFilter(selectedOption);
|
||||||
(item) => item.positive,
|
}}
|
||||||
),
|
selectedValue={feedbackFilter ?? null}
|
||||||
backgroundColor: '#7D54D1',
|
rounded="3xl"
|
||||||
},
|
border="border"
|
||||||
{
|
contentSize="text-sm"
|
||||||
label: t('settings.analytics.negativeFeedback'),
|
/>
|
||||||
data: Object.values(feedbackData || {}).map(
|
</div>
|
||||||
(item) => item.negative,
|
<div className="relative mt-px h-[245px] w-full">
|
||||||
),
|
<div
|
||||||
backgroundColor: '#FF6384',
|
id="legend-container-3"
|
||||||
},
|
className="flex flex-row items-center justify-end"
|
||||||
],
|
></div>
|
||||||
}}
|
{loadingFeedback ? (
|
||||||
legendID="legend-container-3"
|
<SkeletonLoader count={1} component={'analysis'} />
|
||||||
maxTicksLimitInX={8}
|
) : (
|
||||||
isStacked={false}
|
<AnalyticsChart
|
||||||
/>
|
data={{
|
||||||
)}
|
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||||
</div>
|
formatDate(item),
|
||||||
|
),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: t('settings.analytics.positiveFeedback'),
|
||||||
|
data: Object.values(feedbackData || {}).map(
|
||||||
|
(item) => item.positive,
|
||||||
|
),
|
||||||
|
backgroundColor: '#7D54D1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.analytics.negativeFeedback'),
|
||||||
|
data: Object.values(feedbackData || {}).map(
|
||||||
|
(item) => item.negative,
|
||||||
|
),
|
||||||
|
backgroundColor: '#FF6384',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
legendID="legend-container-3"
|
||||||
|
maxTicksLimitInX={8}
|
||||||
|
isStacked={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,53 +5,35 @@ import { useSelector } from 'react-redux';
|
|||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import ChevronRight from '../assets/chevron-right.svg';
|
import ChevronRight from '../assets/chevron-right.svg';
|
||||||
import CopyButton from '../components/CopyButton';
|
import CopyButton from '../components/CopyButton';
|
||||||
import Dropdown from '../components/Dropdown';
|
|
||||||
import SkeletonLoader from '../components/SkeletonLoader';
|
import SkeletonLoader from '../components/SkeletonLoader';
|
||||||
import { useLoaderState } from '../hooks';
|
import { useLoaderState } from '../hooks';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { APIKeyData, LogData } from './types';
|
import { LogData } from './types';
|
||||||
|
|
||||||
export default function Logs() {
|
type LogsProps = {
|
||||||
const { t } = useTranslation();
|
agentId?: string;
|
||||||
|
tableHeader?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Logs({ agentId, tableHeader }: LogsProps) {
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
|
||||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
|
||||||
const [logs, setLogs] = useState<LogData[]>([]);
|
const [logs, setLogs] = useState<LogData[]>([]);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [loadingChatbots, setLoadingChatbots] = useLoaderState(true);
|
|
||||||
const [loadingLogs, setLoadingLogs] = useLoaderState(true);
|
const [loadingLogs, setLoadingLogs] = useLoaderState(true);
|
||||||
|
|
||||||
const fetchChatbots = async () => {
|
|
||||||
setLoadingChatbots(true);
|
|
||||||
try {
|
|
||||||
const response = await userService.getAPIKeys(token);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch Chatbots');
|
|
||||||
}
|
|
||||||
const chatbots = await response.json();
|
|
||||||
setChatbots(chatbots);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoadingChatbots(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
setLoadingLogs(true);
|
setLoadingLogs(true);
|
||||||
try {
|
try {
|
||||||
const response = await userService.getLogs(
|
const response = await userService.getLogs(
|
||||||
{
|
{
|
||||||
page: page,
|
page: page,
|
||||||
api_key_id: selectedChatbot?.id,
|
api_key_id: agentId,
|
||||||
page_size: 10,
|
page_size: 10,
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to fetch logs');
|
||||||
throw new Error('Failed to fetch logs');
|
|
||||||
}
|
|
||||||
const olderLogs = await response.json();
|
const olderLogs = await response.json();
|
||||||
setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]);
|
setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]);
|
||||||
setHasMore(olderLogs.has_more);
|
setHasMore(olderLogs.has_more);
|
||||||
@@ -62,62 +44,18 @@ export default function Logs() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchChatbots();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasMore) fetchLogs();
|
if (hasMore) fetchLogs();
|
||||||
}, [page, selectedChatbot]);
|
}, [page, agentId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<div className="flex flex-col items-start">
|
|
||||||
{loadingChatbots ? (
|
|
||||||
<SkeletonLoader component="dropdown" />
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<label
|
|
||||||
id="chatbot-filter-label"
|
|
||||||
className="font-bold text-jet dark:text-bright-gray"
|
|
||||||
>
|
|
||||||
{t('settings.logs.filterByChatbot')}
|
|
||||||
</label>
|
|
||||||
<Dropdown
|
|
||||||
size="w-[55vw] sm:w-[360px]"
|
|
||||||
options={[
|
|
||||||
...chatbots.map((chatbot) => ({
|
|
||||||
label: chatbot.name,
|
|
||||||
value: chatbot.id,
|
|
||||||
})),
|
|
||||||
{ label: t('settings.logs.none'), value: '' },
|
|
||||||
]}
|
|
||||||
placeholder={t('settings.logs.selectChatbot')}
|
|
||||||
onSelect={(chatbot: { label: string; value: string }) => {
|
|
||||||
setSelectedChatbot(
|
|
||||||
chatbots.find((item) => item.id === chatbot.value),
|
|
||||||
);
|
|
||||||
setLogs([]);
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
}}
|
|
||||||
selectedValue={
|
|
||||||
(selectedChatbot && {
|
|
||||||
label: selectedChatbot.name,
|
|
||||||
value: selectedChatbot.id,
|
|
||||||
}) ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
rounded="3xl"
|
|
||||||
border="border"
|
|
||||||
darkBorderColor="dim-gray"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<LogsTable logs={logs} setPage={setPage} loading={loadingLogs} />
|
<LogsTable
|
||||||
|
logs={logs}
|
||||||
|
setPage={setPage}
|
||||||
|
loading={loadingLogs}
|
||||||
|
tableHeader={tableHeader}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -127,8 +65,9 @@ type LogsTableProps = {
|
|||||||
logs: LogData[];
|
logs: LogData[];
|
||||||
setPage: React.Dispatch<React.SetStateAction<number>>;
|
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
tableHeader?: string;
|
||||||
};
|
};
|
||||||
function LogsTable({ logs, setPage, loading }: LogsTableProps) {
|
function LogsTable({ logs, setPage, loading, tableHeader }: LogsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
const [openLogId, setOpenLogId] = useState<string | null>(null);
|
const [openLogId, setOpenLogId] = useState<string | null>(null);
|
||||||
@@ -171,13 +110,13 @@ function LogsTable({ logs, setPage, loading }: LogsTableProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-table rounded-xl h-[55vh] w-full overflow-hidden bg-white dark:bg-black border border-light-silver dark:border-transparent">
|
<div className="logs-table h-[55vh] w-full overflow-hidden rounded-xl border border-light-silver bg-white dark:border-transparent dark:bg-black">
|
||||||
<div className="h-8 bg-black/10 dark:bg-[#191919] flex flex-col items-start justify-center">
|
<div className="flex h-8 flex-col items-start justify-center bg-black/10 dark:bg-[#191919]">
|
||||||
<p className="px-3 text-xs dark:text-gray-6000">
|
<p className="px-3 text-xs dark:text-gray-6000">
|
||||||
{t('settings.logs.tableHeader')}
|
{tableHeader ? tableHeader : t('settings.logs.tableHeader')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-2 p-4">
|
<div className="flex h-[51vh] flex-grow flex-col items-start gap-2 overflow-y-auto bg-transparent p-4">
|
||||||
{logs?.map((log, index) => {
|
{logs?.map((log, index) => {
|
||||||
if (index === logs.length - 1) {
|
if (index === logs.length - 1) {
|
||||||
return (
|
return (
|
||||||
@@ -211,18 +150,18 @@ function Log({
|
|||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
id={log.id}
|
id={log.id}
|
||||||
className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal rounded-xl group-open:opacity-80 [&[open]]:border [&[open]]:border-[#d9d9d9]"
|
className="group w-full rounded-xl bg-transparent hover:bg-[#F9F9F9] group-open:opacity-80 hover:dark:bg-dark-charcoal [&[open]]:border [&[open]]:border-[#d9d9d9] [&_summary::-webkit-details-marker]:hidden"
|
||||||
onToggle={(e) => {
|
onToggle={(e) => {
|
||||||
if ((e.target as HTMLDetailsElement).open) {
|
if ((e.target as HTMLDetailsElement).open) {
|
||||||
onToggle(log.id);
|
onToggle(log.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<summary className="flex flex-row items-start gap-2 text-gray-900 cursor-pointer px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-t-xl p-2">
|
<summary className="flex cursor-pointer flex-row items-start gap-2 p-2 px-4 py-3 text-gray-900 group-open:rounded-t-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
|
||||||
<img
|
<img
|
||||||
src={ChevronRight}
|
src={ChevronRight}
|
||||||
alt="Expand log entry"
|
alt="Expand log entry"
|
||||||
className="mt-[3px] w-3 h-3 transition duration-300 group-open:rotate-90"
|
className="mt-[3px] h-3 w-3 transition duration-300 group-open:rotate-90"
|
||||||
/>
|
/>
|
||||||
<span className="flex flex-row gap-2">
|
<span className="flex flex-row gap-2">
|
||||||
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
|
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
|
||||||
@@ -236,8 +175,8 @@ function Log({
|
|||||||
</h2>
|
</h2>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-b-xl">
|
<div className="px-4 py-3 group-open:rounded-b-xl group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B]">
|
||||||
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs break-words">
|
<p className="break-words px-2 text-xs leading-relaxed text-gray-700 dark:text-gray-400">
|
||||||
{JSON.stringify(filteredLog, null, 2)}
|
{JSON.stringify(filteredLog, null, 2)}
|
||||||
</p>
|
</p>
|
||||||
<div className="my-px w-fit">
|
<div className="my-px w-fit">
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation, useNavigate, Routes, Route, Navigate } from 'react-router-dom';
|
import {
|
||||||
|
Navigate,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useLocation,
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import SettingsBar from '../components/SettingsBar';
|
import SettingsBar from '../components/SettingsBar';
|
||||||
@@ -10,12 +16,11 @@ import { Doc } from '../models/misc';
|
|||||||
import {
|
import {
|
||||||
selectPaginatedDocuments,
|
selectPaginatedDocuments,
|
||||||
selectSourceDocs,
|
selectSourceDocs,
|
||||||
|
selectToken,
|
||||||
setPaginatedDocuments,
|
setPaginatedDocuments,
|
||||||
setSourceDocs,
|
setSourceDocs,
|
||||||
selectToken,
|
|
||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import APIKeys from './APIKeys';
|
|
||||||
import Documents from './Documents';
|
import Documents from './Documents';
|
||||||
import General from './General';
|
import General from './General';
|
||||||
import Logs from './Logs';
|
import Logs from './Logs';
|
||||||
@@ -27,13 +32,16 @@ export default function Settings() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(null);
|
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const getActiveTabFromPath = () => {
|
const getActiveTabFromPath = () => {
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
if (path.includes('/settings/documents')) return t('settings.documents.label');
|
if (path.includes('/settings/documents'))
|
||||||
if (path.includes('/settings/apikeys')) return t('settings.apiKeys.label');
|
return t('settings.documents.label');
|
||||||
if (path.includes('/settings/analytics')) return t('settings.analytics.label');
|
if (path.includes('/settings/analytics'))
|
||||||
|
return t('settings.analytics.label');
|
||||||
if (path.includes('/settings/logs')) return t('settings.logs.label');
|
if (path.includes('/settings/logs')) return t('settings.logs.label');
|
||||||
if (path.includes('/settings/tools')) return t('settings.tools.label');
|
if (path.includes('/settings/tools')) return t('settings.tools.label');
|
||||||
if (path.includes('/settings/widgets')) return 'Widgets';
|
if (path.includes('/settings/widgets')) return 'Widgets';
|
||||||
@@ -45,9 +53,10 @@ export default function Settings() {
|
|||||||
const handleTabChange = (tab: string) => {
|
const handleTabChange = (tab: string) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if (tab === t('settings.general.label')) navigate('/settings');
|
if (tab === t('settings.general.label')) navigate('/settings');
|
||||||
else if (tab === t('settings.documents.label')) navigate('/settings/documents');
|
else if (tab === t('settings.documents.label'))
|
||||||
else if (tab === t('settings.apiKeys.label')) navigate('/settings/apikeys');
|
navigate('/settings/documents');
|
||||||
else if (tab === t('settings.analytics.label')) navigate('/settings/analytics');
|
else if (tab === t('settings.analytics.label'))
|
||||||
|
navigate('/settings/analytics');
|
||||||
else if (tab === t('settings.logs.label')) navigate('/settings/logs');
|
else if (tab === t('settings.logs.label')) navigate('/settings/logs');
|
||||||
else if (tab === t('settings.tools.label')) navigate('/settings/tools');
|
else if (tab === t('settings.tools.label')) navigate('/settings/tools');
|
||||||
else if (tab === 'Widgets') navigate('/settings/widgets');
|
else if (tab === 'Widgets') navigate('/settings/widgets');
|
||||||
@@ -93,29 +102,37 @@ export default function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-12 h-full overflow-auto">
|
<div className="h-full overflow-auto p-4 md:p-12">
|
||||||
<p className="text-2xl font-bold text-eerie-black dark:text-bright-gray">
|
<p className="text-2xl font-bold text-eerie-black dark:text-bright-gray">
|
||||||
{t('settings.label')}
|
{t('settings.label')}
|
||||||
</p>
|
</p>
|
||||||
<SettingsBar activeTab={activeTab} setActiveTab={(tab) => handleTabChange(tab as string)} />
|
<SettingsBar
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={(tab) => handleTabChange(tab as string)}
|
||||||
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<General />} />
|
<Route index element={<General />} />
|
||||||
<Route path="documents" element={
|
<Route
|
||||||
<Documents
|
path="documents"
|
||||||
paginatedDocuments={paginatedDocuments}
|
element={
|
||||||
handleDeleteDocument={handleDeleteClick}
|
<Documents
|
||||||
/>
|
paginatedDocuments={paginatedDocuments}
|
||||||
} />
|
handleDeleteDocument={handleDeleteClick}
|
||||||
<Route path="apikeys" element={<APIKeys />} />
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="analytics" element={<Analytics />} />
|
<Route path="analytics" element={<Analytics />} />
|
||||||
<Route path="logs" element={<Logs />} />
|
<Route path="logs" element={<Logs />} />
|
||||||
<Route path="tools" element={<Tools />} />
|
<Route path="tools" element={<Tools />} />
|
||||||
<Route path="widgets" element={
|
<Route
|
||||||
<Widgets
|
path="widgets"
|
||||||
widgetScreenshot={widgetScreenshot}
|
element={
|
||||||
onWidgetScreenshotChange={updateWidgetScreenshot}
|
<Widgets
|
||||||
/>
|
widgetScreenshot={widgetScreenshot}
|
||||||
} />
|
onWidgetScreenshotChange={updateWidgetScreenshot}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/settings" replace />} />
|
<Route path="*" element={<Navigate to="/settings" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { conversationSlice } from './conversation/conversationSlice';
|
import { conversationSlice } from './conversation/conversationSlice';
|
||||||
import { sharedConversationSlice } from './conversation/sharedConversationSlice';
|
import { sharedConversationSlice } from './conversation/sharedConversationSlice';
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +41,8 @@ const preloadedState: { preference: Preference } = {
|
|||||||
],
|
],
|
||||||
modalState: 'INACTIVE',
|
modalState: 'INACTIVE',
|
||||||
paginatedDocuments: null,
|
paginatedDocuments: null,
|
||||||
|
agents: null,
|
||||||
|
selectedAgent: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
|||||||
10
frontend/src/utils/browserUtils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function getOS() {
|
||||||
|
const userAgent = window.navigator.userAgent;
|
||||||
|
if (userAgent.indexOf('Mac') !== -1) return 'mac';
|
||||||
|
if (userAgent.indexOf('Win') !== -1) return 'win';
|
||||||
|
return 'linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTouchDevice() {
|
||||||
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
}
|
||||||
782
setup.ps1
Normal file
@@ -0,0 +1,782 @@
|
|||||||
|
# DocsGPT Setup PowerShell Script for Windows
|
||||||
|
# PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||||
|
|
||||||
|
# Script execution policy - uncomment if needed
|
||||||
|
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||||
|
|
||||||
|
# Set error action preference
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Get current script directory
|
||||||
|
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$COMPOSE_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath "deployment\docker-compose.yaml"
|
||||||
|
$ENV_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath ".env"
|
||||||
|
|
||||||
|
# Function to write colored text
|
||||||
|
function Write-ColorText {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory=$true)][string]$Text,
|
||||||
|
[Parameter()][string]$ForegroundColor = "White",
|
||||||
|
[Parameter()][switch]$Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
$params = @{
|
||||||
|
ForegroundColor = $ForegroundColor
|
||||||
|
NoNewline = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Bold) {
|
||||||
|
# PowerShell doesn't have bold
|
||||||
|
Write-Host $Text @params
|
||||||
|
} else {
|
||||||
|
Write-Host $Text @params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Animation function (Windows PowerShell version of animate_dino)
|
||||||
|
function Animate-Dino {
|
||||||
|
[Console]::CursorVisible = $false
|
||||||
|
|
||||||
|
# Clear screen
|
||||||
|
Clear-Host
|
||||||
|
|
||||||
|
# Static DocsGPT text
|
||||||
|
$static_text = @(
|
||||||
|
" ____ ____ ____ _____ "
|
||||||
|
" | _ \ ___ ___ ___ / ___| _ \_ _|"
|
||||||
|
" | | | |/ _ \ / __/ __| | _| |_) || | "
|
||||||
|
" | |_| | (_) | (__\__ \ |_| | __/ | | "
|
||||||
|
" |____/ \___/ \___|___/\____|_| |_| "
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print static text
|
||||||
|
foreach ($line in $static_text) {
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dino ASCII art
|
||||||
|
$dino_lines = @(
|
||||||
|
" ######### "
|
||||||
|
" ############# "
|
||||||
|
" ##################"
|
||||||
|
" ####################"
|
||||||
|
" ######################"
|
||||||
|
" ####################### ######"
|
||||||
|
" ############################### "
|
||||||
|
" ################################## "
|
||||||
|
" ################ ############ "
|
||||||
|
" ################## ########## "
|
||||||
|
" ##################### ######## "
|
||||||
|
" ###################### ###### ### "
|
||||||
|
" ############ ########## #### ## "
|
||||||
|
" ############# ######### ##### "
|
||||||
|
" ############## ######### "
|
||||||
|
" ############## ########## "
|
||||||
|
"############ ####### "
|
||||||
|
" ###### ###### #### "
|
||||||
|
" ################ "
|
||||||
|
" ################# "
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save cursor position
|
||||||
|
$cursorPos = $Host.UI.RawUI.CursorPosition
|
||||||
|
|
||||||
|
# Build-up animation
|
||||||
|
for ($i = 0; $i -lt $dino_lines.Count; $i++) {
|
||||||
|
# Restore cursor position
|
||||||
|
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||||
|
|
||||||
|
# Display lines up to current index
|
||||||
|
for ($j = 0; $j -le $i; $j++) {
|
||||||
|
Write-Host $dino_lines[$j]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Slow down animation
|
||||||
|
Start-Sleep -Milliseconds 50
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pause at end of animation
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
||||||
|
# Clear the animation
|
||||||
|
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||||
|
|
||||||
|
# Clear from cursor to end of screen
|
||||||
|
for ($i = 0; $i -lt $dino_lines.Count; $i++) {
|
||||||
|
Write-Host (" " * $dino_lines[0].Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restore cursor position for next output
|
||||||
|
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||||
|
|
||||||
|
# Show cursor again
|
||||||
|
[Console]::CursorVisible = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and start Docker function
|
||||||
|
function Check-AndStartDocker {
|
||||||
|
# Check if Docker is running
|
||||||
|
try {
|
||||||
|
$dockerRunning = $false
|
||||||
|
|
||||||
|
# First try with 'docker info' which should work if Docker is fully operational
|
||||||
|
try {
|
||||||
|
$dockerInfo = docker info 2>&1
|
||||||
|
# If we get here without an exception, Docker is running
|
||||||
|
Write-ColorText "Docker is already running." -ForegroundColor "Green"
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
# Docker info command failed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if Docker process is running
|
||||||
|
$dockerProcess = Get-Process "Docker Desktop" -ErrorAction SilentlyContinue
|
||||||
|
if ($dockerProcess) {
|
||||||
|
# Docker Desktop is running, but might not be fully initialized
|
||||||
|
Write-ColorText "Docker Desktop is starting up. Waiting for it to be ready..." -ForegroundColor "Yellow"
|
||||||
|
|
||||||
|
# Wait for Docker to become operational
|
||||||
|
$attempts = 0
|
||||||
|
$maxAttempts = 30
|
||||||
|
|
||||||
|
while ($attempts -lt $maxAttempts) {
|
||||||
|
try {
|
||||||
|
$null = docker ps 2>&1
|
||||||
|
Write-ColorText "Docker is now operational." -ForegroundColor "Green"
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "." -NoNewline
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$attempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorText "`nDocker Desktop is running but not responding to commands. Please check Docker status." -ForegroundColor "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker is not running, attempt to start it
|
||||||
|
Write-ColorText "Docker is not running. Attempting to start Docker Desktop..." -ForegroundColor "Yellow"
|
||||||
|
|
||||||
|
# Docker Desktop locations to check
|
||||||
|
$dockerPaths = @(
|
||||||
|
"${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe",
|
||||||
|
"${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe",
|
||||||
|
"$env:LOCALAPPDATA\Docker\Docker\Docker Desktop.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
$dockerPath = $null
|
||||||
|
foreach ($path in $dockerPaths) {
|
||||||
|
if (Test-Path $path) {
|
||||||
|
$dockerPath = $path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $dockerPath) {
|
||||||
|
Write-ColorText "Docker Desktop not found. Please install Docker Desktop or start it manually." -ForegroundColor "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start Docker Desktop
|
||||||
|
try {
|
||||||
|
Start-Process $dockerPath
|
||||||
|
Write-Host -NoNewline "Waiting for Docker to start"
|
||||||
|
|
||||||
|
# Wait for Docker to be ready
|
||||||
|
$attempts = 0
|
||||||
|
$maxAttempts = 60 # 60 x 2 seconds = maximum 2 minutes wait
|
||||||
|
|
||||||
|
while ($attempts -lt $maxAttempts) {
|
||||||
|
try {
|
||||||
|
$null = docker ps 2>&1
|
||||||
|
Write-Host "`nDocker has started successfully!"
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
# Show waiting animation
|
||||||
|
Write-Host -NoNewline "."
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$attempts++
|
||||||
|
|
||||||
|
if ($attempts % 3 -eq 0) {
|
||||||
|
Write-Host "`r" -NoNewline
|
||||||
|
Write-Host "Waiting for Docker to start " -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorText "`nDocker did not start within the expected time. Please start Docker Desktop manually." -ForegroundColor "Red"
|
||||||
|
return $false
|
||||||
|
} catch {
|
||||||
|
Write-ColorText "Failed to start Docker Desktop. Please start it manually." -ForegroundColor "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorText "Error checking Docker status: $_" -ForegroundColor "Red"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to prompt the user for the main menu choice
|
||||||
|
function Prompt-MainMenu {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Welcome to DocsGPT Setup!" -ForegroundColor "White" -Bold
|
||||||
|
Write-ColorText "How would you like to proceed?" -ForegroundColor "White"
|
||||||
|
Write-ColorText "1) Use DocsGPT Public API Endpoint (simple and free)" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "2) Serve Local (with Ollama)" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "3) Connect Local Inference Engine" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "4) Connect Cloud API Provider" -ForegroundColor "Yellow"
|
||||||
|
Write-Host ""
|
||||||
|
$script:main_choice = Read-Host "Choose option (1-4)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to prompt for Local Inference Engine options
|
||||||
|
function Prompt-LocalInferenceEngineOptions {
|
||||||
|
Clear-Host
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Connect Local Inference Engine" -ForegroundColor "White" -Bold
|
||||||
|
Write-ColorText "Choose your local inference engine:" -ForegroundColor "White"
|
||||||
|
Write-ColorText "1) LLaMa.cpp" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "2) Ollama" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "3) Text Generation Inference (TGI)" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "4) SGLang" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "5) vLLM" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "6) Aphrodite" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "7) FriendliAI" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "8) LMDeploy" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||||
|
Write-Host ""
|
||||||
|
$script:engine_choice = Read-Host "Choose option (1-8, or b)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to prompt for Cloud API Provider options
|
||||||
|
function Prompt-CloudAPIProviderOptions {
|
||||||
|
Clear-Host
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Connect Cloud API Provider" -ForegroundColor "White" -Bold
|
||||||
|
Write-ColorText "Choose your Cloud API Provider:" -ForegroundColor "White"
|
||||||
|
Write-ColorText "1) OpenAI" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "2) Google (Vertex AI, Gemini)" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "3) Anthropic (Claude)" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "4) Groq" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "5) HuggingFace Inference API" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "6) Azure OpenAI" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "7) Novita" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||||
|
Write-Host ""
|
||||||
|
$script:provider_choice = Read-Host "Choose option (1-7, or b)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to prompt for Ollama CPU/GPU options
|
||||||
|
function Prompt-OllamaOptions {
|
||||||
|
Clear-Host
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Serve Local with Ollama" -ForegroundColor "White" -Bold
|
||||||
|
Write-ColorText "Choose how to serve Ollama:" -ForegroundColor "White"
|
||||||
|
Write-ColorText "1) CPU" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "2) GPU" -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||||
|
Write-Host ""
|
||||||
|
$script:ollama_choice = Read-Host "Choose option (1-2, or b)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) Use DocsGPT Public API Endpoint (simple and free)
|
||||||
|
function Use-DocsPublicAPIEndpoint {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Setting up DocsGPT Public API Endpoint..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
"LLM_NAME=docsgpt" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||||
|
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
|
||||||
|
Write-ColorText ".env file configured for DocsGPT Public API." -ForegroundColor "Green"
|
||||||
|
|
||||||
|
# Start Docker if needed
|
||||||
|
$dockerRunning = Check-AndStartDocker
|
||||||
|
if (-not $dockerRunning) {
|
||||||
|
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Run Docker compose commands
|
||||||
|
try {
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT is now running on http://localhost:5173" -ForegroundColor "Green"
|
||||||
|
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Error starting Docker Compose: $_" -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||||
|
exit 1 # Exit script with error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2) Serve Local (with Ollama)
|
||||||
|
function Serve-LocalOllama {
|
||||||
|
$script:model_name = ""
|
||||||
|
$default_model = "llama3.2:1b"
|
||||||
|
$docker_compose_file_suffix = ""
|
||||||
|
|
||||||
|
function Get-ModelNameOllama {
|
||||||
|
$model_name_input = Read-Host "Enter Ollama Model Name (press Enter for default: $default_model (1.3GB))"
|
||||||
|
if ([string]::IsNullOrEmpty($model_name_input)) {
|
||||||
|
$script:model_name = $default_model
|
||||||
|
} else {
|
||||||
|
$script:model_name = $model_name_input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Clear-Host
|
||||||
|
Prompt-OllamaOptions
|
||||||
|
|
||||||
|
switch ($ollama_choice) {
|
||||||
|
"1" { # CPU
|
||||||
|
$docker_compose_file_suffix = "cpu"
|
||||||
|
Get-ModelNameOllama
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"2" { # GPU
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "For this option to work correctly you need to have a supported GPU and configure Docker to utilize it." -ForegroundColor "Yellow"
|
||||||
|
Write-ColorText "Refer to: https://hub.docker.com/r/ollama/ollama for more information." -ForegroundColor "Yellow"
|
||||||
|
$confirm_gpu = Read-Host "Continue with GPU setup? (y/b)"
|
||||||
|
|
||||||
|
if ($confirm_gpu -eq "y" -or $confirm_gpu -eq "Y") {
|
||||||
|
$docker_compose_file_suffix = "gpu"
|
||||||
|
Get-ModelNameOllama
|
||||||
|
break
|
||||||
|
}
|
||||||
|
elseif ($confirm_gpu -eq "b" -or $confirm_gpu -eq "B") {
|
||||||
|
Clear-Host
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Invalid choice. Please choose y or b." -ForegroundColor "Red"
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"b" { Clear-Host; return }
|
||||||
|
"B" { Clear-Host; return }
|
||||||
|
default {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Invalid choice. Please choose 1-2, or b." -ForegroundColor "Red"
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrEmpty($docker_compose_file_suffix)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Configuring for Ollama ($($docker_compose_file_suffix.ToUpper()))..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
"API_KEY=xxxx" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||||
|
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"OPENAI_BASE_URL=http://host.docker.internal:11434/v1" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
|
||||||
|
Write-ColorText ".env file configured for Ollama ($($docker_compose_file_suffix.ToUpper()))." -ForegroundColor "Green"
|
||||||
|
Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow"
|
||||||
|
|
||||||
|
# Start Docker if needed
|
||||||
|
$dockerRunning = Check-AndStartDocker
|
||||||
|
if (-not $dockerRunning) {
|
||||||
|
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup compose file paths
|
||||||
|
$optional_compose = Join-Path -Path (Split-Path -Parent $COMPOSE_FILE) -ChildPath "optional\docker-compose.optional.ollama-$docker_compose_file_suffix.yaml"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Starting Docker Compose with Ollama ($docker_compose_file_suffix)..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Build the containers
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" build
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the containers
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" up -d
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for Ollama container to be ready
|
||||||
|
Write-ColorText "Waiting for Ollama container to be ready..." -ForegroundColor "White"
|
||||||
|
$ollamaReady = $false
|
||||||
|
$maxAttempts = 30 # Maximum number of attempts (30 x 5 seconds = 2.5 minutes)
|
||||||
|
$attempts = 0
|
||||||
|
|
||||||
|
while (-not $ollamaReady -and $attempts -lt $maxAttempts) {
|
||||||
|
$containerStatus = & docker compose -f "$COMPOSE_FILE" -f "$optional_compose" ps --services --filter "status=running" --format "{{.Service}}"
|
||||||
|
|
||||||
|
if ($containerStatus -like "*ollama*") {
|
||||||
|
$ollamaReady = $true
|
||||||
|
Write-ColorText "Ollama container is running." -ForegroundColor "Green"
|
||||||
|
} else {
|
||||||
|
Write-Host "Ollama container not yet ready, waiting... (Attempt $($attempts+1)/$maxAttempts)"
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
$attempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ollamaReady) {
|
||||||
|
Write-ColorText "Ollama container did not start within the expected time. Please check Docker logs for errors." -ForegroundColor "Red"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pull the Ollama model
|
||||||
|
Write-ColorText "Pulling $model_name model for Ollama..." -ForegroundColor "White"
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" exec -it ollama ollama pull "$model_name"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT is now running with Ollama ($docker_compose_file_suffix) on http://localhost:5173" -ForegroundColor "Green"
|
||||||
|
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" -f `"$optional_compose`" down" -ForegroundColor "Yellow"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) Connect Local Inference Engine
|
||||||
|
function Connect-LocalInferenceEngine {
|
||||||
|
$script:engine_name = ""
|
||||||
|
$script:openai_base_url = ""
|
||||||
|
$script:model_name = ""
|
||||||
|
|
||||||
|
function Get-ModelName {
|
||||||
|
$model_name_input = Read-Host "Enter Model Name (press Enter for None)"
|
||||||
|
if ([string]::IsNullOrEmpty($model_name_input)) {
|
||||||
|
$script:model_name = "None"
|
||||||
|
} else {
|
||||||
|
$script:model_name = $model_name_input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Clear-Host
|
||||||
|
Prompt-LocalInferenceEngineOptions
|
||||||
|
|
||||||
|
switch ($engine_choice) {
|
||||||
|
"1" { # LLaMa.cpp
|
||||||
|
$script:engine_name = "LLaMa.cpp"
|
||||||
|
$script:openai_base_url = "http://localhost:8000/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"2" { # Ollama
|
||||||
|
$script:engine_name = "Ollama"
|
||||||
|
$script:openai_base_url = "http://localhost:11434/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"3" { # TGI
|
||||||
|
$script:engine_name = "TGI"
|
||||||
|
$script:openai_base_url = "http://localhost:8080/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"4" { # SGLang
|
||||||
|
$script:engine_name = "SGLang"
|
||||||
|
$script:openai_base_url = "http://localhost:30000/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"5" { # vLLM
|
||||||
|
$script:engine_name = "vLLM"
|
||||||
|
$script:openai_base_url = "http://localhost:8000/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"6" { # Aphrodite
|
||||||
|
$script:engine_name = "Aphrodite"
|
||||||
|
$script:openai_base_url = "http://localhost:2242/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"7" { # FriendliAI
|
||||||
|
$script:engine_name = "FriendliAI"
|
||||||
|
$script:openai_base_url = "http://localhost:8997/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"8" { # LMDeploy
|
||||||
|
$script:engine_name = "LMDeploy"
|
||||||
|
$script:openai_base_url = "http://localhost:23333/v1"
|
||||||
|
Get-ModelName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"b" { Clear-Host; return }
|
||||||
|
"B" { Clear-Host; return }
|
||||||
|
default {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Invalid choice. Please choose 1-8, or b." -ForegroundColor "Red"
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrEmpty($script:engine_name)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Configuring for Local Inference Engine: $engine_name..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
"API_KEY=None" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||||
|
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"OPENAI_BASE_URL=$openai_base_url" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
|
||||||
|
Write-ColorText ".env file configured for $engine_name with OpenAI API format." -ForegroundColor "Green"
|
||||||
|
Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow"
|
||||||
|
|
||||||
|
# Start Docker if needed
|
||||||
|
$dockerRunning = Check-AndStartDocker
|
||||||
|
if (-not $dockerRunning) {
|
||||||
|
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Build the containers
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the containers
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT is now configured to connect to $engine_name at $openai_base_url" -ForegroundColor "Green"
|
||||||
|
Write-ColorText "Ensure your $engine_name inference server is running at that address" -ForegroundColor "Yellow"
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT is running at http://localhost:5173" -ForegroundColor "Green"
|
||||||
|
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4) Connect Cloud API Provider
|
||||||
|
function Connect-CloudAPIProvider {
|
||||||
|
$script:provider_name = ""
|
||||||
|
$script:llm_name = ""
|
||||||
|
$script:model_name = ""
|
||||||
|
$script:api_key = ""
|
||||||
|
|
||||||
|
function Get-APIKey {
|
||||||
|
Write-ColorText "Your API key will be stored locally in the .env file and will not be sent anywhere else" -ForegroundColor "Yellow"
|
||||||
|
$script:api_key = Read-Host "Please enter your API key"
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Clear-Host
|
||||||
|
Prompt-CloudAPIProviderOptions
|
||||||
|
|
||||||
|
switch ($provider_choice) {
|
||||||
|
"1" { # OpenAI
|
||||||
|
$script:provider_name = "OpenAI"
|
||||||
|
$script:llm_name = "openai"
|
||||||
|
$script:model_name = "gpt-4o"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"2" { # Google
|
||||||
|
$script:provider_name = "Google (Vertex AI, Gemini)"
|
||||||
|
$script:llm_name = "google"
|
||||||
|
$script:model_name = "gemini-2.0-flash"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"3" { # Anthropic
|
||||||
|
$script:provider_name = "Anthropic (Claude)"
|
||||||
|
$script:llm_name = "anthropic"
|
||||||
|
$script:model_name = "claude-3-5-sonnet-latest"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"4" { # Groq
|
||||||
|
$script:provider_name = "Groq"
|
||||||
|
$script:llm_name = "groq"
|
||||||
|
$script:model_name = "llama-3.1-8b-instant"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"5" { # HuggingFace Inference API
|
||||||
|
$script:provider_name = "HuggingFace Inference API"
|
||||||
|
$script:llm_name = "huggingface"
|
||||||
|
$script:model_name = "meta-llama/Llama-3.1-8B-Instruct"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"6" { # Azure OpenAI
|
||||||
|
$script:provider_name = "Azure OpenAI"
|
||||||
|
$script:llm_name = "azure_openai"
|
||||||
|
$script:model_name = "gpt-4o"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"7" { # Novita
|
||||||
|
$script:provider_name = "Novita"
|
||||||
|
$script:llm_name = "novita"
|
||||||
|
$script:model_name = "deepseek/deepseek-r1"
|
||||||
|
Get-APIKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"b" { Clear-Host; return }
|
||||||
|
"B" { Clear-Host; return }
|
||||||
|
default {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Invalid choice. Please choose 1-7, or b." -ForegroundColor "Red"
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrEmpty($script:provider_name)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Configuring for Cloud API Provider: $provider_name..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
"API_KEY=$api_key" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||||
|
"LLM_NAME=$llm_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||||
|
|
||||||
|
Write-ColorText ".env file configured for $provider_name." -ForegroundColor "Green"
|
||||||
|
|
||||||
|
# Start Docker if needed
|
||||||
|
$dockerRunning = Check-AndStartDocker
|
||||||
|
if (-not $dockerRunning) {
|
||||||
|
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||||
|
|
||||||
|
# Run Docker compose commands
|
||||||
|
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --build
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker compose build or up failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT is now configured to use $provider_name on http://localhost:5173" -ForegroundColor "Green"
|
||||||
|
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||||
|
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script execution
|
||||||
|
Animate-Dino
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Clear-Host
|
||||||
|
Prompt-MainMenu
|
||||||
|
|
||||||
|
$exitLoop = $false # Add this flag
|
||||||
|
|
||||||
|
switch ($main_choice) {
|
||||||
|
"1" {
|
||||||
|
Use-DocsPublicAPIEndpoint
|
||||||
|
$exitLoop = $true # Set flag to true on completion
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"2" {
|
||||||
|
Serve-LocalOllama
|
||||||
|
# Only exit the loop if user didn't press "b" to go back
|
||||||
|
if ($ollama_choice -ne "b" -and $ollama_choice -ne "B") {
|
||||||
|
$exitLoop = $true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"3" {
|
||||||
|
Connect-LocalInferenceEngine
|
||||||
|
# Only exit the loop if user didn't press "b" to go back
|
||||||
|
if ($engine_choice -ne "b" -and $engine_choice -ne "B") {
|
||||||
|
$exitLoop = $true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"4" {
|
||||||
|
Connect-CloudAPIProvider
|
||||||
|
# Only exit the loop if user didn't press "b" to go back
|
||||||
|
if ($provider_choice -ne "b" -and $provider_choice -ne "B") {
|
||||||
|
$exitLoop = $true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "Invalid choice. Please choose 1-4." -ForegroundColor "Red"
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only break out of the loop if a function completed successfully
|
||||||
|
if ($exitLoop) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-ColorText "DocsGPT Setup Complete." -ForegroundColor "Green"
|
||||||
|
|
||||||
|
exit 0
|
||||||