Compare commits

..

33 Commits

Author SHA1 Message Date
dependabot[bot]
6fe521925f chore(deps): bump langchain-community in /application
Bumps [langchain-community](https://github.com/langchain-ai/langchain) from 0.3.19 to 0.4.1.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/commits)

---
updated-dependencies:
- dependency-name: langchain-community
  dependency-version: 0.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 20:42:25 +00:00
Nikunj Kohli
9f7945fcf5 [UI/UX] Improve image upload experience — add preview & drag-to-reord… (#2095)
* [UI/UX] Improve image upload experience — add preview & drag-to-reorder in chat section

* chore(chat): remove image previews, keep drag-to-reorder

* chore(chat): prevent attachment drag from triggering upload dropzone

* Revert "chore(chat): prevent attachment drag from triggering upload dropzone"

This reverts commit dd4b96256c.

* (feat:conv) rmv drag-drop on sources

* (feat:msg-input) drop attachments

---------

Co-authored-by: ManishMadan2882 <manishmadan321@gmail.com>
2025-10-27 21:53:18 +02:00
Gayatri K
d8ec3c008c todo tool feature added to tools (#1977)
* todo tool feature added to tools

* removed configs

* fix: require user_id on TodoListTool, normalize timestamps, add tests

* lint and tests fixes

* refactor: support multiple todos per user/tool by indexing with todo_id

* modified todo_id to use auto-increamenting integer instead of UUID

* test-case fixes

* feat: fix todos

---------

Co-authored-by: Alex <a@tushynski.me>
2025-10-27 19:09:32 +02:00
Pavel
2f00691246 Merge pull request #2096 from arc53/hacktoberfest-t-shirt-image
Update HACKTOBERFEST.md with T-shirt image
2025-10-24 13:15:30 +01:00
Pavel
9b2383b074 Update HACKTOBERFEST.md with T-shirt image
Added t-shirt image.
2025-10-24 13:09:23 +01:00
Ritoban Dutta
e4e9910575 fix(ui): use dedicated sidebar open/close icons for better visual feedback of actions (#2088)
* fix(ui): use dedicated icons for sidebar toggle (panel-left-open/close)

* fix: update sidebar toggle icon colors
2025-10-24 00:25:17 +03:00
Nihar
f448e4a615 add configurable provider in settings.py and update ElevenLabs Api (#2065) (#2074) 2025-10-22 19:07:21 +03:00
Manish Madan
c4e8daf50e Frontend audit: refinements (#2083)
* (fix:attachements) sep id for redux ops

* (fix:ui) popups, toast, share modal

* (feat:agentsPreview) stable preview, ui fixes

* (fix:ui) light theme icon, sleek scroll

---------

Co-authored-by: GH Action - Upstream Sync <action@github.com>
2025-10-22 12:12:05 +03:00
Hanzalah Waheed
5aa4ec1b9f fix: cleanup ConversationBubble and fix CopyButton (#2073)
* fix: rm states for hovering. use tailwind classes instead

* fix: use group hover css intead of states

* chore: no point in having separate defaults if cant be customised

* fix: move default bg colors into conditionals
2025-10-18 21:59:15 +03:00
Siddhant Rai
125ce0aad3 test: implement full API test suite with mongomock and centralized fixtures (#2068) 2025-10-17 12:01:14 +03:00
TinTin
ababc9ae04 fix: reduce large margins between list items in answer rendering (#2058) 2025-10-16 13:59:45 +03:00
Alex
62ac90746e fix: improve error handling and loading state in fetchChunks function 2025-10-15 17:33:13 +01:00
Alex
096f6d91a2 fix: handle potential undefined value for selectedDocs in fetchAnswer 2025-10-15 17:12:52 +01:00
jay98
d28ef6b094 Refactor: use async/await in fetchChunks for correct error handling (typescript:S4822) (#2066) 2025-10-15 15:52:55 +03:00
Anurag Yadav
8fb945ab09 feedback button to show after message (#2064) 2025-10-14 18:52:31 +03:00
jay98
835d71727c fix: remove redundant conditional operator for file assignment (#2060) 2025-10-14 17:35:41 +03:00
Ali Arda Fincan
ce32dd2907 Feat: Agent Token or Request Limiting (#2041)
* Update routes.py, added token and request limits to create/update agent operations

* added usage limit check to api endpoints

cannot create agents with usage limit right now that will be implemented

* implemented api limiting as either token limiting or request limiting modes

* minor typo & bug fix
2025-10-13 21:32:46 +03:00
Manish Madan
72bc24a490 Chore: deleted unused files, dead code; minor fixes (#2059)
* (feat:pause-stream) generator exit

* (feat:pause-stream) close request

* (feat:pause-stream) finally close; google anthropic

* (feat:task_status)communicate failure

* (clean:connector) unused routes

* (feat:file-table) missing skeletons

* (chore:fe) purge unused

* (fix:apiKeys) build err

* (chore:settings) clean unused

* merge from main

* (chore:purge) unused fe assets

* (clean:check_docs) unused logic

* (feat:selectedDocs) replace null type

---------

Co-authored-by: GH Action - Upstream Sync <action@github.com>
2025-10-13 19:11:24 +03:00
Siddhant Rai
d6c49bdbf0 test: add agent test coverage and standardize test suite (#2051)
- Add 104 comprehensive tests for agent system
- Integrate agent tests into CI/CD pipeline
- Standardize tests with @pytest.mark.unit markers
- Fix cross-platform path compatibility
- Clean up unused imports and dependencies
2025-10-13 14:43:35 +03:00
beKool.sh
1805292528 Update README.md (#2057) 2025-10-13 13:00:13 +03:00
Nirjas Jakilim
d09ce7e1f7 fixed broken link (#2054) 2025-10-12 15:26:05 +03:00
Marco Ponce
a8d2024791 Windows deployment powershell and renamed LLM_PROVIDER and runtime (#2050)
* Windows deployment powershell and  renamed LLM_PROVIDER and runtime

* added LLM_NAME back

* revert changes on docker-compose-hub.yaml
2025-10-12 15:25:42 +03:00
Manish Madan
f0b954dbfb Upload: communicate failure, minor frontend updates (#2048)
* (feat:pause-stream) generator exit

* (feat:pause-stream) close request

* (feat:pause-stream) finally close; google anthropic

* (feat:task_status)communicate failure

* (clean:connector) unused routes

* (feat:file-table) missing skeletons

* (fix:apiKeys) build err

---------

Co-authored-by: GH Action - Upstream Sync <action@github.com>
2025-10-10 17:34:02 +03:00
Rahul
50bee7c2b0 feat: Add button to cancel LLM response (#1978)
* feat: Add button to cancel LLM response
- Replace text area with cancel button when loading.
- Add useEffect to change elipsis in cancel button text.
- Add new SVG icon for cancel response.
- Button colors match Figma designs.

* fix: Cancel button UI matches new design
- Delete cancel-response svg.
- Change previous cancel button to match the new Figma design.
- Remove console log in handleCancel function.

* fix: Adjust cancel button rounding

* feat: Update UI for send button
- Add SendArrowIcon component, enables dynamic svg color changes
- Replace original icon
- Update colors and hover effects

* (fix:send-button) minor blink in transition

---------

Co-authored-by: Manish Madan <manishmadan321@gmail.com>
2025-10-09 12:01:25 +03:00
Mariam Saeed
e7b15b316e Feat: Notification section (#2033)
* Feature/Notification-section

* fix notification ui and add local storage variable to save the state

* add notification component to app.tsx
2025-10-09 01:26:10 +03:00
Manish Madan
a4507008c1 complete_stream: Stop response streaming (#2031)
* (feat:pause-stream) generator exit

* (feat:pause-stream) close request

* (feat:pause-stream) finally close; google anthropic

---------

Co-authored-by: GH Action - Upstream Sync <action@github.com>
2025-10-08 20:37:30 +03:00
Hanzalah Waheed
c5ba85f929 fix(ui): create a var to check for shared metadata obj (#2040) 2025-10-08 18:18:54 +03:00
Manish Madan
2e636bd67e Merge pull request #1970 from arc53/dependabot/npm_and_yarn/frontend/mermaid-11.12.0
chore(deps): bump mermaid from 11.6.0 to 11.12.0 in /frontend
2025-10-08 20:42:32 +05:30
dependabot[bot]
4a039f1abf chore(deps): bump mermaid from 11.6.0 to 11.12.0 in /frontend
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.6.0 to 11.12.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.6.0...mermaid@11.12.0)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 15:06:36 +00:00
JeevaRamanathan
434d8e2070 fix spinner to match theme (dark/light) in conversation (#2044) 2025-10-08 15:16:44 +03:00
Nihar
160ad2dc79 correct path for storing embeddings model (#2035) 2025-10-07 23:01:39 +03:00
Anshuman Payasi
0ec86c2c71 fix: adjust share modal size and spacing (#2027)
* fix/share-modal-spacing

* fix(share-modal): spacing adjusted for mobile view
2025-10-07 17:11:03 +03:00
Alex
03452ffd9f feat: add GitHub access token support and fix file content fetching logic (#2032) 2025-10-07 16:53:14 +03:00
138 changed files with 6140 additions and 2425 deletions

View File

@@ -16,15 +16,15 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
cd application
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
cd ../tests
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest and generate coverage report
run: |
python -m pytest --cov=application --cov-report=xml
python -m pytest --cov=application --cov-report=xml --cov-report=term-missing
- name: Upload coverage reports to Codecov
if: github.event_name == 'pull_request' && matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -3,6 +3,7 @@
Welcome, contributors! We're excited to announce that DocsGPT is participating in Hacktoberfest. Get involved by submitting meaningful pull requests.
All Meaningful contributors with accepted PRs that were created for issues with the `hacktoberfest` label (set by our maintainer team: dartpain, siiddhantt, pabik, ManishMadan2882) will receive a cool T-shirt! 🤩.
<img width="1331" height="678" alt="hacktoberfest-mocks-preview" src="https://github.com/user-attachments/assets/633f6377-38db-48f5-b519-a8b3855a9eb4" />
Fill in [this form](https://forms.gle/Npaba4n9Epfyx56S8
) after your PR was merged please

View File

@@ -68,7 +68,7 @@
- [x] MCP support (August 2025)
- [x] Google Drive integration (September 2025)
- [x] Add OAuth 2.0 authentication for MCP (September 2025)
- [ ] Sharepoint integration (October 2025)
- [ ] SharePoint integration (October 2025)
- [ ] Deep Agents (October 2025)
- [ ] Agent scheduling

View File

@@ -30,6 +30,10 @@ class BaseAgent(ABC):
decoded_token: Optional[Dict] = None,
attachments: Optional[List[Dict]] = None,
json_schema: Optional[Dict] = None,
limited_token_mode: Optional[bool] = False,
token_limit: Optional[int] = settings.DEFAULT_AGENT_LIMITS["token_limit"],
limited_request_mode: Optional[bool] = False,
request_limit: Optional[int] = settings.DEFAULT_AGENT_LIMITS["request_limit"],
):
self.endpoint = endpoint
self.llm_name = llm_name
@@ -38,7 +42,7 @@ class BaseAgent(ABC):
self.user_api_key = user_api_key
self.prompt = prompt
self.decoded_token = decoded_token or {}
self.user: str = decoded_token.get("sub")
self.user: str = self.decoded_token.get("sub")
self.tool_config: Dict = {}
self.tools: List[Dict] = []
self.tool_calls: List[Dict] = []
@@ -54,6 +58,10 @@ class BaseAgent(ABC):
)
self.attachments = attachments or []
self.json_schema = json_schema
self.limited_token_mode = limited_token_mode
self.token_limit = token_limit
self.limited_request_mode = limited_request_mode
self.request_limit = request_limit
@log_activity()
def gen(

View File

@@ -0,0 +1,321 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
import uuid
from .base import Tool
from application.core.mongo_db import MongoDB
from application.core.settings import settings
class TodoListTool(Tool):
"""Todo List
Manages todo items for users. Supports creating, viewing, updating, and deleting todos.
"""
def __init__(self, tool_config: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None) -> None:
"""Initialize the tool.
Args:
tool_config: Optional tool configuration. Should include:
- tool_id: Unique identifier for this todo list tool instance (from user_tools._id)
This ensures each user's tool configuration has isolated todos
user_id: The authenticated user's id (should come from decoded_token["sub"]).
"""
self.user_id: Optional[str] = user_id
# Get tool_id from configuration (passed from user_tools._id in production)
# In production, tool_id is the MongoDB ObjectId string from user_tools collection
if tool_config and "tool_id" in tool_config:
self.tool_id = tool_config["tool_id"]
elif user_id:
# Fallback for backward compatibility or testing
self.tool_id = f"default_{user_id}"
else:
# Last resort fallback (shouldn't happen in normal use)
self.tool_id = str(uuid.uuid4())
db = MongoDB.get_client()[settings.MONGO_DB_NAME]
self.collection = db["todos"]
# -----------------------------
# Action implementations
# -----------------------------
def execute_action(self, action_name: str, **kwargs: Any) -> str:
"""Execute an action by name.
Args:
action_name: One of list, create, get, update, complete, delete.
**kwargs: Parameters for the action.
Returns:
A human-readable string result.
"""
if not self.user_id:
return "Error: TodoListTool requires a valid user_id."
if action_name == "list":
return self._list()
if action_name == "create":
return self._create(kwargs.get("title", ""))
if action_name == "get":
return self._get(kwargs.get("todo_id"))
if action_name == "update":
return self._update(
kwargs.get("todo_id"),
kwargs.get("title", "")
)
if action_name == "complete":
return self._complete(kwargs.get("todo_id"))
if action_name == "delete":
return self._delete(kwargs.get("todo_id"))
return f"Unknown action: {action_name}"
def get_actions_metadata(self) -> List[Dict[str, Any]]:
"""Return JSON metadata describing supported actions for tool schemas."""
return [
{
"name": "list",
"description": "List all todos for the user.",
"parameters": {"type": "object", "properties": {}},
},
{
"name": "create",
"description": "Create a new todo item.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Title of the todo item."
}
},
"required": ["title"],
},
},
{
"name": "get",
"description": "Get a specific todo by ID.",
"parameters": {
"type": "object",
"properties": {
"todo_id": {
"type": "integer",
"description": "The ID of the todo to retrieve."
}
},
"required": ["todo_id"],
},
},
{
"name": "update",
"description": "Update a todo's title by ID.",
"parameters": {
"type": "object",
"properties": {
"todo_id": {
"type": "integer",
"description": "The ID of the todo to update."
},
"title": {
"type": "string",
"description": "The new title for the todo."
}
},
"required": ["todo_id", "title"],
},
},
{
"name": "complete",
"description": "Mark a todo as completed.",
"parameters": {
"type": "object",
"properties": {
"todo_id": {
"type": "integer",
"description": "The ID of the todo to mark as completed."
}
},
"required": ["todo_id"],
},
},
{
"name": "delete",
"description": "Delete a specific todo by ID.",
"parameters": {
"type": "object",
"properties": {
"todo_id": {
"type": "integer",
"description": "The ID of the todo to delete."
}
},
"required": ["todo_id"],
},
},
]
def get_config_requirements(self) -> Dict[str, Any]:
"""Return configuration requirements."""
return {}
# -----------------------------
# Internal helpers
# -----------------------------
def _coerce_todo_id(self, value: Optional[Any]) -> Optional[int]:
"""Convert todo identifiers to sequential integers."""
if value is None:
return None
if isinstance(value, int):
return value if value > 0 else None
if isinstance(value, str):
stripped = value.strip()
if stripped.isdigit():
numeric_value = int(stripped)
return numeric_value if numeric_value > 0 else None
return None
def _get_next_todo_id(self) -> int:
"""Get the next sequential todo_id for this user and tool.
Returns a simple integer (1, 2, 3, ...) scoped to this user/tool.
With 5-10 todos max, scanning is negligible.
"""
# Find all todos for this user/tool and get their IDs
todos = list(self.collection.find(
{"user_id": self.user_id, "tool_id": self.tool_id},
{"todo_id": 1}
))
# Find the maximum todo_id
max_id = 0
for todo in todos:
todo_id = self._coerce_todo_id(todo.get("todo_id"))
if todo_id is not None:
max_id = max(max_id, todo_id)
return max_id + 1
def _list(self) -> str:
"""List all todos for the user."""
cursor = self.collection.find({"user_id": self.user_id, "tool_id": self.tool_id})
todos = list(cursor)
if not todos:
return "No todos found."
result_lines = ["Todos:"]
for doc in todos:
todo_id = doc.get("todo_id")
title = doc.get("title", "Untitled")
status = doc.get("status", "open")
line = f"[{todo_id}] {title} ({status})"
result_lines.append(line)
return "\n".join(result_lines)
def _create(self, title: str) -> str:
"""Create a new todo item."""
title = (title or "").strip()
if not title:
return "Error: Title is required."
now = datetime.now()
todo_id = self._get_next_todo_id()
doc = {
"todo_id": todo_id,
"user_id": self.user_id,
"tool_id": self.tool_id,
"title": title,
"status": "open",
"created_at": now,
"updated_at": now,
}
self.collection.insert_one(doc)
return f"Todo created with ID {todo_id}: {title}"
def _get(self, todo_id: Optional[Any]) -> str:
"""Get a specific todo by ID."""
parsed_todo_id = self._coerce_todo_id(todo_id)
if parsed_todo_id is None:
return "Error: todo_id must be a positive integer."
doc = self.collection.find_one({
"user_id": self.user_id,
"tool_id": self.tool_id,
"todo_id": parsed_todo_id
})
if not doc:
return f"Error: Todo with ID {parsed_todo_id} not found."
title = doc.get("title", "Untitled")
status = doc.get("status", "open")
result = f"Todo [{parsed_todo_id}]:\nTitle: {title}\nStatus: {status}"
return result
def _update(self, todo_id: Optional[Any], title: str) -> str:
"""Update a todo's title by ID."""
parsed_todo_id = self._coerce_todo_id(todo_id)
if parsed_todo_id is None:
return "Error: todo_id must be a positive integer."
title = (title or "").strip()
if not title:
return "Error: Title is required."
result = self.collection.update_one(
{"user_id": self.user_id, "tool_id": self.tool_id, "todo_id": parsed_todo_id},
{"$set": {"title": title, "updated_at": datetime.now()}}
)
if result.matched_count == 0:
return f"Error: Todo with ID {parsed_todo_id} not found."
return f"Todo {parsed_todo_id} updated to: {title}"
def _complete(self, todo_id: Optional[Any]) -> str:
"""Mark a todo as completed."""
parsed_todo_id = self._coerce_todo_id(todo_id)
if parsed_todo_id is None:
return "Error: todo_id must be a positive integer."
result = self.collection.update_one(
{"user_id": self.user_id, "tool_id": self.tool_id, "todo_id": parsed_todo_id},
{"$set": {"status": "completed", "updated_at": datetime.now()}}
)
if result.matched_count == 0:
return f"Error: Todo with ID {parsed_todo_id} not found."
return f"Todo {parsed_todo_id} marked as completed."
def _delete(self, todo_id: Optional[Any]) -> str:
"""Delete a specific todo by ID."""
parsed_todo_id = self._coerce_todo_id(todo_id)
if parsed_todo_id is None:
return "Error: todo_id must be a positive integer."
result = self.collection.delete_one({
"user_id": self.user_id,
"tool_id": self.tool_id,
"todo_id": parsed_todo_id
})
if result.deleted_count == 0:
return f"Error: Todo with ID {parsed_todo_id} not found."
return f"Todo {parsed_todo_id} deleted."

View File

@@ -20,20 +20,24 @@ class ToolActionParser:
try:
call_args = json.loads(call.arguments)
tool_parts = call.name.split("_")
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
if len(tool_parts) < 2:
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
logger.warning(
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
)
return None, None, None
tool_id = tool_parts[-1]
action_name = "_".join(tool_parts[:-1])
# Validate that tool_id looks like a numerical ID
if not tool_id.isdigit():
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
except (AttributeError, TypeError) as e:
logger.warning(
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
)
except (AttributeError, TypeError, json.JSONDecodeError) as e:
logger.error(f"Error parsing OpenAI LLM call: {e}")
return None, None, None
return tool_id, action_name, call_args
@@ -42,19 +46,23 @@ class ToolActionParser:
try:
call_args = call.arguments
tool_parts = call.name.split("_")
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
if len(tool_parts) < 2:
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
logger.warning(
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
)
return None, None, None
tool_id = tool_parts[-1]
action_name = "_".join(tool_parts[:-1])
# Validate that tool_id looks like a numerical ID
if not tool_id.isdigit():
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
logger.warning(
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
)
except (AttributeError, TypeError) as e:
logger.error(f"Error parsing Google LLM call: {e}")
return None, None, None

View File

@@ -28,7 +28,7 @@ class ToolManager:
module = importlib.import_module(f"application.agents.tools.{tool_name}")
for member_name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Tool) and obj is not Tool:
if tool_name in {"mcp_tool", "notes", "memory"} and user_id:
if tool_name in {"mcp_tool", "notes", "memory", "todo_list"} and user_id:
return obj(tool_config, user_id)
else:
return obj(tool_config)
@@ -36,7 +36,7 @@ class ToolManager:
def execute_action(self, tool_name, action_name, user_id=None, **kwargs):
if tool_name not in self.tools:
raise ValueError(f"Tool '{tool_name}' not loaded")
if tool_name in {"mcp_tool", "memory"} and user_id:
if tool_name in {"mcp_tool", "memory", "todo_list"} and user_id:
tool_config = self.config.get(tool_name, {})
tool = self.load_tool(tool_name, tool_config, user_id)
return tool.execute_action(action_name, **kwargs)

View File

@@ -72,6 +72,9 @@ class AnswerResource(Resource, BaseAnswerResource):
agent = processor.create_agent()
retriever = processor.create_retriever()
if error := self.check_usage(processor.agent_config):
return error
stream = self.complete_stream(
question=data["question"],
agent=agent,

View File

@@ -3,7 +3,7 @@ import json
import logging
from typing import Any, Dict, Generator, List, Optional
from flask import Response
from flask import Response, make_response, jsonify
from flask_restx import Namespace
from application.api.answer.services.conversation_service import ConversationService
@@ -25,6 +25,7 @@ class BaseAnswerResource:
def __init__(self):
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
self.db = db
self.user_logs_collection = db["user_logs"]
self.gpt_model = get_gpt_model()
self.conversation_service = ConversationService()
@@ -40,6 +41,88 @@ class BaseAnswerResource:
return missing_fields
return None
def check_usage(
self, agent_config: Dict
) -> Optional[Response]:
"""Check if there is a usage limit and if it is exceeded
Args:
agent_config: The config dict of agent instance
Returns:
None or Response if either of limits exceeded.
"""
api_key = agent_config.get("user_api_key")
if not api_key:
return None
agents_collection = self.db["agents"]
agent = agents_collection.find_one({"key": api_key})
if not agent:
return make_response(
jsonify(
{
"success": False,
"message": "Invalid API key."
}
),
401
)
limited_token_mode = agent.get("limited_token_mode", False)
limited_request_mode = agent.get("limited_request_mode", False)
token_limit = int(agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]))
request_limit = int(agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]))
token_usage_collection = self.db["token_usage"]
end_date = datetime.datetime.now()
start_date = end_date - datetime.timedelta(hours=24)
match_query = {
"timestamp": {"$gte": start_date, "$lte": end_date},
"api_key": api_key
}
if limited_token_mode:
token_pipeline = [
{"$match": match_query},
{
"$group": {
"_id": None,
"total_tokens": {"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}}
}
}
]
token_result = list(token_usage_collection.aggregate(token_pipeline))
daily_token_usage = token_result[0]["total_tokens"] if token_result else 0
else:
daily_token_usage = 0
if limited_request_mode:
daily_request_usage = token_usage_collection.count_documents(match_query)
else:
daily_request_usage = 0
if not limited_token_mode and not limited_request_mode:
return None
elif limited_token_mode and token_limit > daily_token_usage:
return None
elif limited_request_mode and request_limit > daily_request_usage:
return None
return make_response(
jsonify(
{
"success": False,
"message": "Exceeding usage limit, please try again later."
}
),
429, # too many requests
)
def complete_stream(
self,
question: str,
@@ -193,6 +276,43 @@ class BaseAnswerResource:
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except GeneratorExit:
# Client aborted the connection
logger.info(
f"Stream aborted by client for question: {question[:50]}... "
)
# Save partial response to database before exiting
if should_save_conversation and response_full:
try:
if isNoneDoc:
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=user_api_key,
decoded_token=decoded_token,
)
self.conversation_service.save_conversation(
conversation_id,
question,
response_full,
thought,
source_log_docs,
tool_calls,
llm,
self.gpt_model,
decoded_token,
index=index,
api_key=user_api_key,
agent_id=agent_id,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
attachment_ids=attachment_ids,
)
except Exception as e:
logger.error(f"Error saving partial response: {str(e)}", exc_info=True)
raise
except Exception as e:
logger.error(f"Error in stream: {str(e)}", exc_info=True)
data = json.dumps(

View File

@@ -76,6 +76,9 @@ class StreamResource(Resource, BaseAnswerResource):
agent = processor.create_agent()
retriever = processor.create_retriever()
if error := self.check_usage(processor.agent_config):
return error
return Response(
self.complete_stream(
question=data["question"],

View File

@@ -23,15 +23,9 @@ from application.core.settings import settings
from application.api import api
from application.utils import (
check_required_fields
)
from application.parser.connectors.connector_creator import ConnectorCreator
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
sources_collection = db["sources"]
@@ -43,185 +37,6 @@ api.add_namespace(connectors_ns)
@connectors_ns.route("/api/connectors/upload")
class UploadConnector(Resource):
@api.expect(
api.model(
"ConnectorUploadModel",
{
"user": fields.String(required=True, description="User ID"),
"source": fields.String(
required=True, description="Source type (google_drive, github, etc.)"
),
"name": fields.String(required=True, description="Job name"),
"data": fields.String(required=True, description="Configuration data"),
"repo_url": fields.String(description="GitHub repository URL"),
},
)
)
@api.doc(
description="Uploads connector source for vectorization",
)
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
data = request.form
required_fields = ["user", "source", "name", "data"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
config = json.loads(data["data"])
source_data = None
sync_frequency = config.get("sync_frequency", "never")
if data["source"] == "github":
source_data = config.get("repo_url")
elif data["source"] in ["crawler", "url"]:
source_data = config.get("url")
elif data["source"] == "reddit":
source_data = config
elif data["source"] in ConnectorCreator.get_supported_connectors():
session_token = config.get("session_token")
if not session_token:
return make_response(jsonify({
"success": False,
"error": f"Missing session_token in {data['source']} configuration"
}), 400)
file_ids = config.get("file_ids", [])
if isinstance(file_ids, str):
file_ids = [id.strip() for id in file_ids.split(',') if id.strip()]
elif not isinstance(file_ids, list):
file_ids = []
folder_ids = config.get("folder_ids", [])
if isinstance(folder_ids, str):
folder_ids = [id.strip() for id in folder_ids.split(',') if id.strip()]
elif not isinstance(folder_ids, list):
folder_ids = []
config["file_ids"] = file_ids
config["folder_ids"] = folder_ids
task = ingest_connector_task.delay(
job_name=data["name"],
user=decoded_token.get("sub"),
source_type=data["source"],
session_token=session_token,
file_ids=file_ids,
folder_ids=folder_ids,
recursive=config.get("recursive", False),
retriever=config.get("retriever", "classic"),
sync_frequency=sync_frequency
)
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
task = ingest_connector_task.delay(
source_data=source_data,
job_name=data["name"],
user=decoded_token.get("sub"),
loader=data["source"],
sync_frequency=sync_frequency
)
except Exception as err:
current_app.logger.error(
f"Error uploading connector source: {err}", exc_info=True
)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
@connectors_ns.route("/api/connectors/task_status")
class ConnectorTaskStatus(Resource):
task_status_model = api.model(
"ConnectorTaskStatusModel",
{"task_id": fields.String(required=True, description="Task ID")},
)
@api.expect(task_status_model)
@api.doc(description="Get connector task status")
def get(self):
task_id = request.args.get("task_id")
if not task_id:
return make_response(
jsonify({"success": False, "message": "Task ID is required"}), 400
)
try:
from application.celery_init import celery
task = celery.AsyncResult(task_id)
task_meta = task.info
print(f"Task status: {task.status}")
if not isinstance(
task_meta, (dict, list, str, int, float, bool, type(None))
):
task_meta = str(task_meta)
except Exception as err:
current_app.logger.error(f"Error getting task status: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"status": task.status, "result": task_meta}), 200)
@connectors_ns.route("/api/connectors/sources")
class ConnectorSources(Resource):
@api.doc(description="Get connector sources")
def get(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
try:
sources = sources_collection.find({"user": user, "type": "connector:file"}).sort("date", -1)
connector_sources = []
for source in sources:
connector_sources.append({
"id": str(source["_id"]),
"name": source.get("name"),
"date": source.get("date"),
"type": source.get("type"),
"source": source.get("source"),
"tokens": source.get("tokens", ""),
"retriever": source.get("retriever", "classic"),
"syncFrequency": source.get("sync_frequency", ""),
})
except Exception as err:
current_app.logger.error(f"Error retrieving connector sources: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify(connector_sources), 200)
@connectors_ns.route("/api/connectors/delete")
class DeleteConnectorSource(Resource):
@api.doc(
description="Delete a connector source",
params={"source_id": "The source ID to delete"},
)
def delete(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
source_id = request.args.get("source_id")
if not source_id:
return make_response(
jsonify({"success": False, "message": "source_id is required"}), 400
)
try:
result = sources_collection.delete_one(
{"_id": ObjectId(source_id), "user": decoded_token.get("sub")}
)
if result.deleted_count == 0:
return make_response(
jsonify({"success": False, "message": "Source not found"}), 404
)
except Exception as err:
current_app.logger.error(
f"Error deleting connector source: {err}", exc_info=True
)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True}), 200)
@connectors_ns.route("/api/connectors/auth")
class ConnectorAuth(Resource):
@api.doc(description="Get connector OAuth authorization URL", params={"provider": "Connector provider (e.g., google_drive)"})
@@ -337,27 +152,6 @@ class ConnectorsCallback(Resource):
return redirect("/api/connectors/callback-status?status=error&message=Authentication+failed.+Please+try+again+and+make+sure+to+grant+all+requested+permissions.")
@connectors_ns.route("/api/connectors/refresh")
class ConnectorRefresh(Resource):
@api.expect(api.model("ConnectorRefreshModel", {"provider": fields.String(required=True), "refresh_token": fields.String(required=True)}))
@api.doc(description="Refresh connector access token")
def post(self):
try:
data = request.get_json()
provider = data.get('provider')
refresh_token = data.get('refresh_token')
if not provider or not refresh_token:
return make_response(jsonify({"success": False, "error": "provider and refresh_token are required"}), 400)
auth = ConnectorCreator.create_auth(provider)
token_info = auth.refresh_access_token(refresh_token)
return make_response(jsonify({"success": True, "token_info": token_info}), 200)
except Exception as e:
current_app.logger.error(f"Error refreshing token for connector: {e}")
return make_response(jsonify({"success": False, "error": str(e)}), 500)
@connectors_ns.route("/api/connectors/files")
class ConnectorFiles(Resource):
@api.expect(api.model("ConnectorFilesModel", {

View File

@@ -10,6 +10,7 @@ from flask import current_app, jsonify, make_response, request
from flask_restx import fields, Namespace, Resource
from application.api import api
from application.core.settings import settings
from application.api.user.base import (
agents_collection,
db,
@@ -74,6 +75,10 @@ class GetAgent(Resource):
"agent_type": agent.get("agent_type", ""),
"status": agent.get("status", ""),
"json_schema": agent.get("json_schema"),
"limited_token_mode": agent.get("limited_token_mode", False),
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
"limited_request_mode": agent.get("limited_request_mode", False),
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
"created_at": agent.get("createdAt", ""),
"updated_at": agent.get("updatedAt", ""),
"last_used_at": agent.get("lastUsedAt", ""),
@@ -143,6 +148,10 @@ class GetAgents(Resource):
"agent_type": agent.get("agent_type", ""),
"status": agent.get("status", ""),
"json_schema": agent.get("json_schema"),
"limited_token_mode": agent.get("limited_token_mode", False),
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
"limited_request_mode": agent.get("limited_request_mode", False),
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
"created_at": agent.get("createdAt", ""),
"updated_at": agent.get("updatedAt", ""),
"last_used_at": agent.get("lastUsedAt", ""),
@@ -199,6 +208,22 @@ class CreateAgent(Resource):
required=False,
description="JSON schema for enforcing structured output format",
),
"limited_token_mode": fields.Boolean(
required=False,
description="Whether the agent is in limited token mode"
),
"token_limit": fields.Integer(
required=False,
description="Token limit for the agent in limited mode"
),
"limited_request_mode": fields.Boolean(
required=False,
description="Whether the agent is in limited request mode"
),
"request_limit": fields.Integer(
required=False,
description="Request limit for the agent in limited mode"
)
},
)
@@ -344,6 +369,10 @@ class CreateAgent(Resource):
"agent_type": data.get("agent_type", ""),
"status": data.get("status"),
"json_schema": data.get("json_schema"),
"limited_token_mode": data.get("limited_token_mode", False),
"token_limit": data.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
"limited_request_mode": data.get("limited_request_mode", False),
"request_limit": data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
"createdAt": datetime.datetime.now(datetime.timezone.utc),
"updatedAt": datetime.datetime.now(datetime.timezone.utc),
"lastUsedAt": None,
@@ -399,6 +428,22 @@ class UpdateAgent(Resource):
required=False,
description="JSON schema for enforcing structured output format",
),
"limited_token_mode": fields.Boolean(
required=False,
description="Whether the agent is in limited token mode"
),
"token_limit": fields.Integer(
required=False,
description="Token limit for the agent in limited mode"
),
"limited_request_mode": fields.Boolean(
require=False,
description="Whether the agent is in limited request mode"
),
"request_limit": fields.Integer(
required=False,
description="Request limit for the agent in limited mode"
)
},
)
@@ -486,6 +531,10 @@ class UpdateAgent(Resource):
"agent_type",
"status",
"json_schema",
"limited_token_mode",
"token_limit",
"limited_request_mode",
"request_limit"
]
for field in allowed_fields:
@@ -602,6 +651,54 @@ class UpdateAgent(Resource):
update_fields[field] = json_schema
else:
update_fields[field] = None
elif field == "limited_token_mode":
is_mode_enabled = data.get("limited_token_mode", False)
if is_mode_enabled and data.get("token_limit") is None:
return make_response(
jsonify(
{
"success": False,
"message": "Token limit must be provided when limited token mode is enabled",
}
),
400,
)
elif field == "limited_request_mode":
is_mode_enabled = data.get("limited_request_mode", False)
if is_mode_enabled and data.get("request_limit") is None:
return make_response(
jsonify(
{
"success": False,
"message": "Request limit must be provided when limited request mode is enabled",
}
),
400,
)
elif field == "token_limit":
token_limit = data.get("token_limit")
if token_limit is not None and not data.get("limited_token_mode"):
return make_response(
jsonify(
{
"success": False,
"message": "Token limit cannot be set when limited token mode is disabled",
}
),
400,
)
elif field == "request_limit":
request_limit = data.get("request_limit")
if request_limit is not None and not data.get("limited_request_mode"):
return make_response(
jsonify(
{
"success": False,
"message": "Request limit cannot be set when limited request mode is disabled",
}
),
400,
)
else:
value = data[field]
if field in ["name", "description", "prompt_id", "agent_type"]:

View File

@@ -9,6 +9,7 @@ from flask import current_app, jsonify, make_response, request
from flask_restx import fields, Namespace, Resource
from application.api import api
from application.core.settings import settings
from application.api.user.base import (
agents_collection,
db,
@@ -75,6 +76,10 @@ class SharedAgent(Resource):
"agent_type": shared_agent.get("agent_type", ""),
"status": shared_agent.get("status", ""),
"json_schema": shared_agent.get("json_schema"),
"limited_token_mode": shared_agent.get("limited_token_mode", False),
"token_limit": shared_agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
"limited_request_mode": shared_agent.get("limited_request_mode", False),
"request_limit": shared_agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
"created_at": shared_agent.get("createdAt", ""),
"updated_at": shared_agent.get("updatedAt", ""),
"shared": shared_agent.get("shared_publicly", False),
@@ -149,6 +154,10 @@ class SharedAgents(Resource):
"agent_type": agent.get("agent_type", ""),
"status": agent.get("status", ""),
"json_schema": agent.get("json_schema"),
"limited_token_mode": agent.get("limited_token_mode", False),
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
"limited_request_mode": agent.get("limited_request_mode", False),
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
"created_at": agent.get("createdAt", ""),
"updated_at": agent.get("updatedAt", ""),
"pinned": str(agent["_id"]) in pinned_ids,

View File

@@ -10,7 +10,7 @@ from application.api import api
from application.api.user.base import agents_collection, storage
from application.api.user.tasks import store_attachment
from application.core.settings import settings
from application.tts.google_tts import GoogleTTS
from application.tts.tts_creator import TTSCreator
from application.utils import safe_filename
@@ -133,7 +133,7 @@ class TextToSpeech(Resource):
data = request.get_json()
text = data["text"]
try:
tts_instance = GoogleTTS()
tts_instance = TTSCreator.create_tts(settings.TTS_PROVIDER)
audio_base64, detected_language = tts_instance.text_to_speech(text)
return make_response(
jsonify(

View File

@@ -2,12 +2,10 @@
import json
import math
import os
from bson.objectid import ObjectId
from flask import current_app, jsonify, make_response, redirect, request
from flask_restx import fields, Namespace, Resource
from werkzeug.utils import secure_filename
from application.api import api
from application.api.user.base import sources_collection
@@ -136,31 +134,6 @@ class PaginatedSources(Resource):
return make_response(jsonify({"success": False}), 400)
@sources_ns.route("/docs_check")
class CheckDocs(Resource):
check_docs_model = api.model(
"CheckDocsModel",
{"docs": fields.String(required=True, description="Document name")},
)
@api.expect(check_docs_model)
@api.doc(description="Check if document exists")
def post(self):
data = request.get_json()
required_fields = ["docs"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
vectorstore = "vectors/" + secure_filename(data["docs"])
if os.path.exists(vectorstore) or data["docs"] == "default":
return {"status": "exists"}, 200
except Exception as err:
current_app.logger.error(f"Error checking document: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"status": "not found"}), 404)
@sources_ns.route("/delete_by_ids")
class DeleteByIds(Resource):
@api.doc(

View File

@@ -562,10 +562,21 @@ class TaskStatus(Resource):
task = celery.AsyncResult(task_id)
task_meta = task.info
print(f"Task status: {task.status}")
if task.status == "PENDING":
inspect = celery.control.inspect()
active_workers = inspect.ping()
if not active_workers:
raise ConnectionError("Service unavailable")
if not isinstance(
task_meta, (dict, list, str, int, float, bool, type(None))
):
task_meta = str(task_meta) # Convert to a string representation
except ConnectionError as err:
return make_response(
jsonify({"success": False, "message": str(err)}), 503
)
except Exception as err:
current_app.logger.error(f"Error getting task status: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)

View File

@@ -28,6 +28,10 @@ class Settings(BaseSettings):
"claude-2": 1e5,
"gemini-2.5-flash": 1e6,
}
DEFAULT_AGENT_LIMITS: dict = {
"token_limit": 50000,
"request_limit": 500,
}
UPLOAD_FOLDER: str = "inputs"
PARSE_PDF_AS_IMAGE: bool = False
PARSE_IMAGE_REMOTE: bool = False
@@ -126,6 +130,7 @@ class Settings(BaseSettings):
# Encryption settings
ENCRYPTION_SECRET_KEY: str = "default-docsgpt-encryption-key"
TTS_PROVIDER: str = "google_tts" # google_tts or elevenlabs
ELEVENLABS_API_KEY: Optional[str] = None
path = Path(__file__).parent.parent.absolute()

View File

@@ -46,5 +46,9 @@ class AnthropicLLM(BaseLLM):
stream=True,
)
for completion in stream_response:
yield completion.completion
try:
for completion in stream_response:
yield completion.completion
finally:
if hasattr(stream_response, 'close'):
stream_response.close()

View File

@@ -121,11 +121,19 @@ class DocsGPTAPILLM(BaseLLM):
model="docsgpt", messages=messages, stream=stream, **kwargs
)
for line in response:
if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0:
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
try:
for line in response:
if (
len(line.choices) > 0
and line.choices[0].delta.content is not None
and len(line.choices[0].delta.content) > 0
):
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
finally:
if hasattr(response, 'close'):
response.close()
def _supports_tools(self):
return True

View File

@@ -373,17 +373,21 @@ class GoogleLLM(BaseLLM):
config=config,
)
for chunk in response:
if hasattr(chunk, "candidates") and chunk.candidates:
for candidate in chunk.candidates:
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.function_call:
yield part
elif part.text:
yield part.text
elif hasattr(chunk, "text"):
yield chunk.text
try:
for chunk in response:
if hasattr(chunk, "candidates") and chunk.candidates:
for candidate in chunk.candidates:
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.function_call:
yield part
elif part.text:
yield part.text
elif hasattr(chunk, "text"):
yield chunk.text
finally:
if hasattr(response, 'close'):
response.close()
def _supports_tools(self):
"""Return whether this LLM supports function calling."""

View File

@@ -170,15 +170,19 @@ class OpenAILLM(BaseLLM):
response = self.client.chat.completions.create(**request_params)
for line in response:
if (
len(line.choices) > 0
and line.choices[0].delta.content is not None
and len(line.choices[0].delta.content) > 0
):
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
try:
for line in response:
if (
len(line.choices) > 0
and line.choices[0].delta.content is not None
and len(line.choices[0].delta.content) > 0
):
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
finally:
if hasattr(response, 'close'):
response.close()
def _supports_tools(self):
return True

View File

@@ -10,6 +10,7 @@ ebooklib==0.18
escodegen==1.0.11
esprima==4.0.1
esutils==1.0.1
elevenlabs==2.17.0
Flask==3.1.1
faiss-cpu==1.9.0.post1
fastmcp==2.11.0
@@ -29,7 +30,7 @@ jsonpatch==1.33
jsonpointer==3.0.0
kombu==5.4.2
langchain==0.3.20
langchain-community==0.3.19
langchain-community==0.4.1
langchain-core==0.3.59
langchain-openai==0.3.16
langchain-text-splitters==0.3.8

View File

@@ -15,10 +15,11 @@ class ElevenlabsTTS(BaseTTS):
def text_to_speech(self, text):
lang = "en"
audio = self.client.generate(
audio = self.client.text_to_speech.convert(
voice_id="nPczCjzI2devNBz1zQrb",
model_id="eleven_multilingual_v2",
text=text,
model="eleven_multilingual_v2",
voice="Brian",
output_format="mp3_44100_128"
)
audio_data = BytesIO()
for chunk in audio:

View File

@@ -0,0 +1,18 @@
from application.tts.google_tts import GoogleTTS
from application.tts.elevenlabs import ElevenlabsTTS
from application.tts.base import BaseTTS
class TTSCreator:
tts_providers = {
"google_tts": GoogleTTS,
"elevenlabs": ElevenlabsTTS,
}
@classmethod
def create_tts(cls, tts_type, *args, **kwargs)-> BaseTTS:
tts_class = cls.tts_providers.get(tts_type.lower())
if not tts_class:
raise ValueError(f"No tts class found for type {tts_type}")
return tts_class(*args, **kwargs)

View File

@@ -21,7 +21,7 @@ def get_encoding():
def get_gpt_model() -> str:
"""Get the appropriate GPT model based on provider"""
"""Get GPT model based on provider"""
model_map = {
"openai": "gpt-4o-mini",
"anthropic": "claude-2",
@@ -32,16 +32,7 @@ def get_gpt_model() -> str:
def safe_filename(filename):
"""
Creates a safe filename that preserves the original extension.
Uses secure_filename, but ensures a proper filename is returned even with non-Latin characters.
Args:
filename (str): The original filename
Returns:
str: A safe filename that can be used for storage
"""
"""Create safe filename, preserving extension. Handles non-Latin characters."""
if not filename:
return str(uuid.uuid4())
_, extension = os.path.splitext(filename)
@@ -83,8 +74,14 @@ def count_tokens_docs(docs):
return tokens
def get_missing_fields(data, required_fields):
"""Check for missing required fields. Returns list of missing field names."""
return [field for field in required_fields if field not in data]
def check_required_fields(data, required_fields):
missing_fields = [field for field in required_fields if field not in data]
"""Validate required fields. Returns Flask 400 response if validation fails, None otherwise."""
missing_fields = get_missing_fields(data, required_fields)
if missing_fields:
return make_response(
jsonify(
@@ -98,7 +95,8 @@ def check_required_fields(data, required_fields):
return None
def validate_required_fields(data, required_fields):
def get_field_validation_errors(data, required_fields):
"""Check for missing and empty fields. Returns dict with 'missing_fields' and 'empty_fields', or None."""
missing_fields = []
empty_fields = []
@@ -107,12 +105,24 @@ def validate_required_fields(data, required_fields):
missing_fields.append(field)
elif not data[field]:
empty_fields.append(field)
errors = []
if missing_fields:
errors.append(f"Missing required fields: {', '.join(missing_fields)}")
if empty_fields:
errors.append(f"Empty values in required fields: {', '.join(empty_fields)}")
if errors:
if missing_fields or empty_fields:
return {"missing_fields": missing_fields, "empty_fields": empty_fields}
return None
def validate_required_fields(data, required_fields):
"""Validate required fields (must exist and be non-empty). Returns Flask 400 response if validation fails, None otherwise."""
errors_dict = get_field_validation_errors(data, required_fields)
if errors_dict:
errors = []
if errors_dict["missing_fields"]:
errors.append(
f"Missing required fields: {', '.join(errors_dict['missing_fields'])}"
)
if errors_dict["empty_fields"]:
errors.append(
f"Empty values in required fields: {', '.join(errors_dict['empty_fields'])}"
)
return make_response(
jsonify({"success": False, "message": " | ".join(errors)}), 400
)
@@ -124,10 +134,7 @@ def get_hash(data):
def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"):
"""
Limits chat history based on token count.
Returns a list of messages that fit within the token limit.
"""
"""Limit chat history to fit within token limit."""
from application.core.settings import settings
max_token_limit = (
@@ -161,7 +168,7 @@ def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"):
def validate_function_name(function_name):
"""Validates if a function name matches the allowed pattern."""
"""Validate function name matches allowed pattern (alphanumeric, underscore, hyphen)."""
if not re.match(r"^[a-zA-Z0-9_-]+$", function_name):
return False
return True

View File

@@ -72,4 +72,4 @@ services:
- mongodb_data_container:/data/db
volumes:
mongodb_data_container:
mongodb_data_container:

View File

@@ -42,7 +42,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
* **Option 1: Using a `.env` file (Recommended):**
* If you haven't already, create a file named `.env` in the **root directory** of your DocsGPT project.
* Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](application/core/settings.py).
* Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](https://github.com/arc53/DocsGPT/blob/main/application/core/settings.py).
* **Option 2: Exporting Environment Variables:**
* Alternatively, you can export environment variables directly in your terminal. However, using a `.env` file is generally more organized for development.
@@ -67,7 +67,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
3. **Download Embedding Model:**
The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `model/` directory within the project root. You can use the following script:
The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `models/` directory within the project root. You can use the following script:
```bash
wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip
@@ -75,7 +75,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
rm mpnet-base-v2.zip
```
Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `model/`.
Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `models/`.
4. **Install Backend Dependencies:**
@@ -160,4 +160,4 @@ To run the DocsGPT frontend locally, you'll need Node.js and npm (Node Package M
This command will start the Vite development server. The frontend application will typically be accessible at [http://localhost:5173/](http://localhost:5173/). The terminal will display the exact URL where the frontend is running.
With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing!
With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing!

View File

@@ -1,4 +1,6 @@
# Please put appropriate value
VITE_BASE_URL=http://localhost:5173
VITE_API_HOST=http://127.0.0.1:7091
VITE_API_STREAMING=true
VITE_API_STREAMING=true
VITE_NOTIFICATION_TEXT="What's new in 0.14.0 — Changelog"
VITE_NOTIFICATION_LINK="#"

View File

@@ -15,7 +15,7 @@
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.0.2",
"lodash": "^4.17.21",
"mermaid": "^11.6.0",
"mermaid": "^11.12.0",
"prop-types": "^15.8.1",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
@@ -90,22 +90,22 @@
}
},
"node_modules/@antfu/install-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
"integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
"license": "MIT",
"dependencies": {
"package-manager-detector": "^0.2.8",
"tinyexec": "^0.3.2"
"package-manager-detector": "^1.3.0",
"tinyexec": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@antfu/utils": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
"integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz",
"integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -957,18 +957,18 @@
"license": "MIT"
},
"node_modules/@iconify/utils": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
"integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz",
"integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==",
"license": "MIT",
"dependencies": {
"@antfu/install-pkg": "^1.0.0",
"@antfu/utils": "^8.1.0",
"@antfu/install-pkg": "^1.1.0",
"@antfu/utils": "^9.2.0",
"@iconify/types": "^2.0.0",
"debug": "^4.4.0",
"globals": "^15.14.0",
"debug": "^4.4.1",
"globals": "^15.15.0",
"kolorist": "^1.8.0",
"local-pkg": "^1.0.0",
"local-pkg": "^1.1.1",
"mlly": "^1.7.4"
}
},
@@ -1053,9 +1053,9 @@
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@mermaid-js/parser": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.4.0.tgz",
"integrity": "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
"license": "MIT",
"dependencies": {
"langium": "3.3.1"
@@ -2856,9 +2856,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -4141,15 +4141,16 @@
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"version": "1.11.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
@@ -5326,9 +5327,9 @@
}
},
"node_modules/exsolve": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz",
"integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"license": "MIT"
},
"node_modules/extend": {
@@ -6909,13 +6910,14 @@
}
},
"node_modules/katex": {
"version": "0.16.21",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
"integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
"version": "0.16.23",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.23.tgz",
"integrity": "sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
@@ -7280,14 +7282,14 @@
}
},
"node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
"pkg-types": "^2.3.0",
"quansync": "^0.2.11"
},
"engines": {
"node": ">=14"
@@ -7483,15 +7485,15 @@
}
},
"node_modules/marked": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz",
"integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
@@ -7927,14 +7929,14 @@
}
},
"node_modules/mermaid": {
"version": "11.6.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz",
"integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==",
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.0.tgz",
"integrity": "sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^2.1.33",
"@mermaid-js/parser": "^0.4.0",
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.1",
"@mermaid-js/parser": "^0.6.2",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
"cytoscape-cose-bilkent": "^4.1.0",
@@ -7942,12 +7944,12 @@
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.13",
"dompurify": "^3.2.4",
"katex": "^0.16.9",
"dayjs": "^1.11.18",
"dompurify": "^3.2.5",
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.7",
"marked": "^16.2.1",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
@@ -8606,15 +8608,15 @@
}
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
"acorn": "^8.15.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.1"
}
},
"node_modules/mlly/node_modules/confbox": {
@@ -8923,13 +8925,10 @@
}
},
"node_modules/package-manager-detector": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
"license": "MIT",
"dependencies": {
"quansync": "^0.2.7"
}
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz",
"integrity": "sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
@@ -9084,13 +9083,13 @@
}
},
"node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
"integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"license": "MIT",
"dependencies": {
"confbox": "^0.2.1",
"exsolve": "^1.0.1",
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
@@ -9302,9 +9301,9 @@
}
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
"funding": [
{
"type": "individual",
@@ -10539,9 +10538,9 @@
"dev": true
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
"license": "MIT"
},
"node_modules/tinyglobby": {
@@ -10791,9 +10790,9 @@
}
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"license": "MIT"
},
"node_modules/unbox-primitive": {

View File

@@ -26,7 +26,7 @@
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.0.2",
"lodash": "^4.17.21",
"mermaid": "^11.6.0",
"mermaid": "^11.12.0",
"prop-types": "^15.8.1",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h480q33 0 56.5 23.5T800-800v640q0 33-23.5 56.5T720-80H240Zm0-80h480v-640H240v640Zm88-104 56-56-56-56-56 56 56 56Zm0-160 56-56-56-56-56 56 56 56Zm0-160 56-56-56-56-56 56 56 56Zm120 280h232v-80H448v80Zm0-160h232v-80H448v80Zm0-160h232v-80H448v80ZM240-160v-640 640Z"/></svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -15,6 +15,7 @@ import useTokenAuth from './hooks/useTokenAuth';
import Navigation from './Navigation';
import PageNotFound from './PageNotFound';
import Setting from './settings';
import Notification from './components/Notification';
function AuthWrapper({ children }: { children: React.ReactNode }) {
const { isAuthLoading } = useTokenAuth();
@@ -52,11 +53,27 @@ function MainLayout() {
}
export default function App() {
const [, , componentMounted] = useDarkTheme();
const [showNotification, setShowNotification] = useState<boolean>(() => {
const saved = localStorage.getItem('showNotification');
return saved ? JSON.parse(saved) : true;
});
const notificationText = import.meta.env.VITE_NOTIFICATION_TEXT;
const notificationLink = import.meta.env.VITE_NOTIFICATION_LINK;
if (!componentMounted) {
return <div />;
}
return (
<div className="relative h-full overflow-hidden">
{notificationLink && notificationText && showNotification && (
<Notification
notificationText={notificationText}
notificationLink={notificationLink}
handleCloseNotification={() => {
setShowNotification(false);
localStorage.setItem('showNotification', 'false');
}}
/>
)}
<Routes>
<Route
element={

View File

@@ -9,7 +9,8 @@ import userService from './api/services/userService';
import Add from './assets/add.svg';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
import Discord from './assets/discord.svg';
import Expand from './assets/expand.svg';
import PanelLeftClose from './assets/panel-left-close.svg';
import PanelLeftOpen from './assets/panel-left-open.svg';
import Github from './assets/git_nav.svg';
import Hamburger from './assets/hamburger.svg';
import openNewChat from './assets/openNewChat.svg';
@@ -302,18 +303,20 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
{
<div className="absolute top-3 left-3 z-20 hidden transition-all duration-300 ease-in-out lg:block">
<div className="flex items-center gap-3">
<button
onClick={() => {
setNavOpen(!navOpen);
}}
className="transition-transform duration-200 hover:scale-110"
>
<img
src={Expand}
alt="Toggle navigation menu"
className="m-auto transition-all duration-300 ease-in-out"
/>
</button>
{!navOpen && (
<button
onClick={() => {
setNavOpen(!navOpen);
}}
className="transition-transform duration-200 hover:scale-110"
>
<img
src={PanelLeftOpen}
alt="Open navigation menu"
className="m-auto transition-all duration-300 ease-in-out"
/>
</button>
)}
{queries?.length > 0 && (
<button
onClick={() => {
@@ -363,8 +366,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}}
>
<img
src={Expand}
alt="Toggle navigation menu"
src={navOpen ? PanelLeftClose : PanelLeftOpen}
alt={navOpen ? 'Collapse sidebar' : 'Expand sidebar'}
className="m-auto transition-all duration-300 ease-in-out hover:scale-110"
/>
</button>
@@ -399,7 +402,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
{conversations?.loading && !isDeletingConversation && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<img
src={isDarkTheme ? SpinnerDark : Spinner}
src={isDarkTheme ? Spinner : SpinnerDark}
className="animate-spin cursor-pointer bg-transparent"
alt="Loading conversations"
/>

View File

@@ -109,18 +109,18 @@ export default function AgentPreview() {
} else setLastQueryReturnedErr(false);
}, [queries]);
return (
<div>
<div className="dark:bg-raisin-black flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden">
<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">
<div className="relative h-full w-full">
<div className="scrollbar-thin absolute inset-0 bottom-[180px] overflow-hidden px-4 pt-4 [&>div>div]:!w-full [&>div>div]:!max-w-none">
<ConversationMessages
handleQuestion={handleQuestion}
handleQuestionSubmission={handleQuestionSubmission}
queries={queries}
status={status}
showHeroOnEmpty={false}
/>
</div>
<div className="absolute right-0 bottom-0 left-0 flex w-full flex-col gap-4 pb-2">
<div className="w-full px-4">
<MessageInput
onSubmit={(text) => handleQuestionSubmission(text)}
loading={status === 'loading'}
@@ -128,11 +128,11 @@ export default function AgentPreview() {
showToolButton={selectedAgent ? false : true}
autoFocus={false}
/>
<p className="text-gray-4000 dark:text-sonic-silver w-full self-center bg-transparent pt-2 text-center text-xs md:inline">
This is a preview of the agent. You can publish it to start using it
in conversations.
</p>
</div>
<p className="text-gray-4000 dark:text-sonic-silver w-full bg-transparent text-center text-xs md:inline">
This is a preview of the agent. You can publish it to start using it
in conversations.
</p>
</div>
</div>
);

View File

@@ -53,6 +53,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
agent_type: 'classic',
status: '',
json_schema: undefined,
limited_token_mode: false,
token_limit: undefined,
limited_request_mode: false,
request_limit: undefined,
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [prompts, setPrompts] = useState<
@@ -74,7 +78,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const [publishLoading, setPublishLoading] = useState(false);
const [jsonSchemaText, setJsonSchemaText] = useState('');
const [jsonSchemaValid, setJsonSchemaValid] = useState(true);
const [isJsonSchemaExpanded, setIsJsonSchemaExpanded] = useState(false);
const [isAdvancedSectionExpanded, setIsAdvancedSectionExpanded] =
useState(false);
const initialAgentRef = useRef<Agent | null>(null);
const sourceAnchorButtonRef = useRef<HTMLButtonElement>(null);
@@ -191,6 +196,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
formData.append('agent_type', agent.agent_type);
formData.append('status', 'draft');
if (agent.limited_token_mode && agent.token_limit) {
formData.append('limited_token_mode', 'True');
formData.append('token_limit', JSON.stringify(agent.token_limit));
} else formData.append('token_limit', '0');
if (agent.limited_request_mode && agent.request_limit) {
formData.append('limited_request_mode', 'True');
formData.append('request_limit', JSON.stringify(agent.request_limit));
} else formData.append('request_limit', '0');
if (imageFile) formData.append('image', imageFile);
if (agent.tools && agent.tools.length > 0)
@@ -280,6 +295,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
formData.append('json_schema', JSON.stringify(agent.json_schema));
}
if (agent.limited_token_mode && agent.token_limit) {
formData.append('limited_token_mode', 'True');
formData.append('token_limit', JSON.stringify(agent.token_limit));
} else formData.append('token_limit', '0');
if (agent.limited_request_mode && agent.request_limit) {
formData.append('limited_request_mode', 'True');
formData.append('request_limit', JSON.stringify(agent.request_limit));
} else formData.append('request_limit', '0');
try {
setPublishLoading(true);
const response =
@@ -509,7 +534,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
setHasChanges(isChanged);
}, [agent, dispatch, effectiveMode, imageFile, jsonSchemaText]);
return (
<div className="p-4 md:p-12">
<div className="flex flex-col px-4 pt-4 pb-2 max-[1179px]:min-h-[100dvh] min-[1180px]:h-[100dvh] md:px-12 md:pt-12 md:pb-3">
<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]"
@@ -590,9 +615,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</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]">
<div className="mt-3 flex w-full flex-1 grid-cols-5 flex-col gap-10 rounded-[30px] bg-[#F6F6F6] p-5 max-[1179px]:overflow-visible min-[1180px]:grid min-[1180px]:gap-5 min-[1180px]:overflow-hidden dark:bg-[#383838]">
<div className="scrollbar-thin col-span-2 flex flex-col gap-5 max-[1179px]:overflow-visible min-[1180px]:max-h-full min-[1180px]:overflow-y-auto min-[1180px]:pr-3">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Meta</h2>
<input
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
@@ -625,7 +650,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Source</h2>
<div className="mt-3">
<div className="flex flex-wrap items-center gap-1">
@@ -719,7 +744,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</div>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<div className="flex flex-wrap items-end gap-1">
<div className="min-w-20 grow basis-full sm:basis-0">
<Prompts
@@ -756,7 +781,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</button>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Tools</h2>
<div className="mt-3 flex flex-wrap items-center gap-1">
<button
@@ -798,7 +823,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Agent type</h2>
<div className="mt-3">
<Dropdown
@@ -823,9 +848,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<button
onClick={() => setIsJsonSchemaExpanded(!isJsonSchemaExpanded)}
onClick={() =>
setIsAdvancedSectionExpanded(!isAdvancedSectionExpanded)
}
className="flex w-full items-center justify-between text-left focus:outline-none"
>
<div>
@@ -834,7 +861,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<div className="ml-4 flex items-center">
<svg
className={`h-5 w-5 transform transition-transform duration-200 ${
isJsonSchemaExpanded ? 'rotate-180' : ''
isAdvancedSectionExpanded ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
@@ -849,7 +876,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</svg>
</div>
</button>
{isJsonSchemaExpanded && (
{isAdvancedSectionExpanded && (
<div className="mt-3">
<div>
<h2 className="text-sm font-medium">JSON response schema</h2>
@@ -892,13 +919,124 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
: 'Invalid JSON - fix to enable saving'}
</div>
)}
<div className="mt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Token limiting</h2>
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
Limit daily total tokens that can be used by this agent
</p>
</div>
<button
onClick={() => {
const newTokenMode = !agent.limited_token_mode;
setAgent({
...agent,
limited_token_mode: newTokenMode,
limited_request_mode: newTokenMode
? false
: agent.limited_request_mode,
});
}}
className={`relative h-6 w-11 rounded-full transition-colors ${
agent.limited_token_mode
? 'bg-purple-30'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
agent.limited_token_mode ? '' : '-translate-x-5'
}`}
/>
</button>
</div>
<input
type="number"
min="0"
value={agent.token_limit || ''}
onChange={(e) =>
setAgent({
...agent,
token_limit: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
disabled={!agent.limited_token_mode}
placeholder="Enter token limit"
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
!agent.limited_token_mode
? 'cursor-not-allowed opacity-50'
: ''
}`}
/>
</div>
<div className="mt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Request limiting</h2>
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
Limit daily total requests that can be made to this
agent
</p>
</div>
<button
onClick={() => {
const newRequestMode = !agent.limited_request_mode;
setAgent({
...agent,
limited_request_mode: newRequestMode,
limited_token_mode: newRequestMode
? false
: agent.limited_token_mode,
});
}}
className={`relative h-6 w-11 rounded-full transition-colors ${
agent.limited_request_mode
? 'bg-purple-30'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
agent.limited_request_mode ? '' : '-translate-x-5'
}`}
/>
</button>
</div>
<input
type="number"
min="0"
value={agent.request_limit || ''}
onChange={(e) =>
setAgent({
...agent,
request_limit: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
disabled={!agent.limited_request_mode}
placeholder="Enter request limit"
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
!agent.limited_request_mode
? 'cursor-not-allowed opacity-50'
: ''
}`}
/>
</div>
</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]">
<div className="col-span-3 flex flex-col gap-2 max-[1179px]:h-auto max-[1179px]:px-0 max-[1179px]:py-0 min-[1180px]:h-full min-[1180px]:py-2 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Preview</h2>
<AgentPreviewArea />
<div className="flex-1 max-[1179px]:overflow-visible min-[1180px]:min-h-0 min-[1180px]:overflow-hidden">
<AgentPreviewArea />
</div>
</div>
</div>
<ConfirmationModal
@@ -935,9 +1073,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
function AgentPreviewArea() {
const selectedAgent = useSelector(selectSelectedAgent);
return (
<div className="dark:bg-raisin-black h-full w-full rounded-[30px] border border-[#F6F6F6] bg-white max-[1180px]:h-192 dark:border-[#7E7E7E]">
<div className="dark:bg-raisin-black w-full rounded-[30px] border border-[#F6F6F6] bg-white max-[1179px]:h-[600px] min-[1180px]:h-full dark:border-[#7E7E7E]">
{selectedAgent?.status === 'published' ? (
<div className="flex h-full w-full flex-col justify-end overflow-auto rounded-[30px]">
<div className="flex h-full w-full flex-col overflow-hidden rounded-[30px]">
<AgentPreview />
</div>
) : (

View File

@@ -177,13 +177,15 @@ export default function SharedAgent() {
/>
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
onSubmit={(text) => handleQuestionSubmission(text)}
loading={status === 'loading'}
showSourceButton={sharedAgent ? false : true}
showToolButton={sharedAgent ? false : true}
autoFocus={false}
/>
<div className="w-full px-2">
<MessageInput
onSubmit={(text) => handleQuestionSubmission(text)}
loading={status === 'loading'}
showSourceButton={sharedAgent ? false : true}
showToolButton={sharedAgent ? false : true}
autoFocus={false}
/>
</div>
<p className="text-gray-4000 dark:text-sonic-silver hidden w-screen self-center bg-transparent py-2 text-center text-xs md:inline md:w-full">
{t('tagline')}
</p>

View File

@@ -2,6 +2,12 @@ import AgentImage from '../components/AgentImage';
import { Agent } from './types';
export default function SharedAgentCard({ agent }: { agent: Agent }) {
// Check if shared metadata exists and has properties (type is 'any' so we validate it's a non-empty object)
const hasSharedMetadata =
agent.shared_metadata &&
typeof agent.shared_metadata === 'object' &&
agent.shared_metadata !== null &&
Object.keys(agent.shared_metadata).length > 0;
return (
<div className="border-dark-gray dark:border-grey flex w-full max-w-[720px] flex-col rounded-3xl border p-6 shadow-xs sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
@@ -20,7 +26,7 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
</p>
</div>
</div>
{agent.shared_metadata && (
{hasSharedMetadata && (
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-eerie-black text-xs font-light sm:text-sm dark:text-[#E0E0E0]">

View File

@@ -57,7 +57,7 @@ export const fetchPreviewAnswer = createAsyncThunk<
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.preference.selectedDocs,
null, // No conversation ID for previews
state.preference.prompt.id,
state.preference.chunks,
@@ -128,7 +128,7 @@ export const fetchPreviewAnswer = createAsyncThunk<
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.preference.selectedDocs,
null, // No conversation ID for previews
state.preference.prompt.id,
state.preference.chunks,

View File

@@ -28,4 +28,8 @@ export type Agent = {
updated_at?: string;
last_used_at?: string;
json_schema?: object;
limited_token_mode?: boolean;
token_limit?: number;
limited_request_mode?: boolean;
request_limit?: number;
};

View File

@@ -3,7 +3,6 @@ const endpoints = {
CONFIG: '/api/config',
NEW_TOKEN: '/api/generate_token',
DOCS: '/api/sources',
DOCS_CHECK: '/api/docs_check',
DOCS_PAGINATED: '/api/sources/paginated',
API_KEYS: '/api/get_api_keys',
CREATE_API_KEY: '/api/create_api_key',

View File

@@ -10,8 +10,6 @@ const userService = {
apiClient.get(`${endpoints.USER.DOCS}`, token),
getDocsWithPagination: (query: string, token: string | null): Promise<any> =>
apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`, token),
checkDocs: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.DOCS_CHECK, data, token),
getAPIKeys: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.API_KEYS, token),
createAPIKey: (data: any, token: string | null): Promise<any> =>

View File

@@ -0,0 +1,3 @@
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0303 7.03033C13.3232 6.73744 13.3232 6.26256 13.0303 5.96967L8.25736 1.1967C7.96447 0.903806 7.48959 0.903806 7.1967 1.1967C6.90381 1.48959 6.90381 1.96447 7.1967 2.25736L11.4393 6.5L7.1967 10.7426C6.90381 11.0355 6.90381 11.5104 7.1967 11.8033C7.48959 12.0962 7.96447 12.0962 8.25736 11.8033L13.0303 7.03033ZM0.5 6.5V7.25H12.5V6.5V5.75H0.5V6.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

View File

@@ -1,3 +0,0 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@@ -1 +0,0 @@
<svg width="18" height="18" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.93179 5.43179C4.75605 5.60753 4.75605 5.89245 4.93179 6.06819C5.10753 6.24392 5.39245 6.24392 5.56819 6.06819L7.49999 4.13638L9.43179 6.06819C9.60753 6.24392 9.89245 6.24392 10.0682 6.06819C10.2439 5.89245 10.2439 5.60753 10.0682 5.43179L7.81819 3.18179C7.73379 3.0974 7.61933 3.04999 7.49999 3.04999C7.38064 3.04999 7.26618 3.0974 7.18179 3.18179L4.93179 5.43179ZM10.0682 9.56819C10.2439 9.39245 10.2439 9.10753 10.0682 8.93179C9.89245 8.75606 9.60753 8.75606 9.43179 8.93179L7.49999 10.8636L5.56819 8.93179C5.39245 8.75606 5.10753 8.75606 4.93179 8.93179C4.75605 9.10753 4.75605 9.39245 4.93179 9.56819L7.18179 11.8182C7.35753 11.9939 7.64245 11.9939 7.81819 11.8182L10.0682 9.56819Z" fill="gray" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 859 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00182 11.2502C7.23838 11.2502 6.50621 10.9552 5.96637 10.4301C5.42654 9.90499 5.12327 9.1928 5.12327 8.4502C5.12327 7.70759 5.42654 6.9954 5.96637 6.4703C6.50621 5.9452 7.23838 5.6502 8.00182 5.6502C8.76525 5.6502 9.49743 5.9452 10.0373 6.4703C10.5771 6.9954 10.8804 7.70759 10.8804 8.4502C10.8804 9.1928 10.5771 9.90499 10.0373 10.4301C9.49743 10.9552 8.76525 11.2502 8.00182 11.2502ZM14.1126 9.2262C14.1455 8.9702 14.1701 8.7142 14.1701 8.4502C14.1701 8.1862 14.1455 7.9222 14.1126 7.6502L15.8479 6.3462C16.0042 6.2262 16.0453 6.0102 15.9466 5.8342L14.3017 3.0662C14.203 2.8902 13.981 2.8182 13.8 2.8902L11.7522 3.6902C11.3245 3.3782 10.8804 3.1062 10.3622 2.9062L10.0579 0.786197C10.0412 0.69197 9.99076 0.606538 9.91549 0.545038C9.84022 0.483538 9.745 0.449939 9.6467 0.450197H6.35693C6.15132 0.450197 5.97861 0.594197 5.94571 0.786197L5.64141 2.9062C5.12327 3.1062 4.67915 3.3782 4.25148 3.6902L2.2036 2.8902C2.02266 2.8182 1.8006 2.8902 1.70191 3.0662L0.0570212 5.8342C-0.0498963 6.0102 -0.00054964 6.2262 0.155714 6.3462L1.89107 7.6502C1.85817 7.9222 1.8335 8.1862 1.8335 8.4502C1.8335 8.7142 1.85817 8.9702 1.89107 9.2262L0.155714 10.5542C-0.00054964 10.6742 -0.0498963 10.8902 0.0570212 11.0662L1.70191 13.8342C1.8006 14.0102 2.02266 14.0742 2.2036 14.0102L4.25148 13.2022C4.67915 13.5222 5.12327 13.7942 5.64141 13.9942L5.94571 16.1142C5.97861 16.3062 6.15132 16.4502 6.35693 16.4502H9.6467C9.85231 16.4502 10.025 16.3062 10.0579 16.1142L10.3622 13.9942C10.8804 13.7862 11.3245 13.5222 11.7522 13.2022L13.8 14.0102C13.981 14.0742 14.203 14.0102 14.3017 13.8342L15.9466 11.0662C16.0453 10.8902 16.0042 10.6742 15.8479 10.5542L14.1126 9.2262Z" fill="#747474"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 901 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="18" viewBox="0 0 22 18" fill="none">
<path d="M18.7359 2.29769C17.4059 1.66546 15.9659 1.20658 14.4659 0.941453C14.4528 0.941024 14.4397 0.943541 14.4276 0.948827C14.4155 0.954112 14.4047 0.962038 14.3959 0.972045C14.2159 1.30856 14.0059 1.74704 13.8659 2.08355C12.2749 1.83882 10.6569 1.83882 9.06594 2.08355C8.92594 1.73684 8.71594 1.30856 8.52594 0.972045C8.51594 0.951651 8.48594 0.941453 8.45594 0.941453C6.95594 1.20658 5.52594 1.66546 4.18594 2.29769C4.17594 2.29769 4.16594 2.30789 4.15594 2.31809C1.43594 6.46839 0.685937 10.5065 1.05594 14.5039C1.05594 14.5243 1.06594 14.5447 1.08594 14.5548C2.88594 15.9009 4.61594 16.7167 6.32594 17.2571C6.35594 17.2673 6.38594 17.2571 6.39594 17.2367C6.79594 16.6759 7.15594 16.0844 7.46594 15.4624C7.48594 15.4216 7.46594 15.3808 7.42594 15.3706C6.85594 15.1463 6.31594 14.8812 5.78594 14.5752C5.74594 14.5549 5.74594 14.4937 5.77594 14.4631C5.88594 14.3815 5.99594 14.2897 6.10594 14.2081C6.12594 14.1877 6.15594 14.1877 6.17594 14.1979C9.61594 15.7989 13.3259 15.7989 16.7259 14.1979C16.7459 14.1877 16.7759 14.1877 16.7959 14.2081C16.9059 14.2999 17.0159 14.3815 17.1259 14.4733C17.1659 14.5039 17.1659 14.565 17.1159 14.5854C16.5959 14.9016 16.0459 15.1565 15.4759 15.3808C15.4359 15.391 15.4259 15.442 15.4359 15.4726C15.7559 16.0946 16.1159 16.6861 16.5059 17.2469C16.5359 17.2571 16.5659 17.2673 16.5959 17.2571C18.3159 16.7167 20.0459 15.9009 21.8459 14.5548C21.8659 14.5447 21.8759 14.5243 21.8759 14.5039C22.3159 9.88449 21.1459 5.87695 18.7759 2.31809C18.7659 2.30789 18.7559 2.29769 18.7359 2.29769ZM7.98594 12.0667C6.95594 12.0667 6.09594 11.098 6.09594 9.90488C6.09594 8.7118 6.93594 7.74305 7.98594 7.74305C9.04594 7.74305 9.88594 8.72199 9.87594 9.90488C9.87594 11.098 9.03594 12.0667 7.98594 12.0667ZM14.9559 12.0667C13.9259 12.0667 13.0659 11.098 13.0659 9.90488C13.0659 8.7118 13.9059 7.74305 14.9559 7.74305C16.0159 7.74305 16.8559 8.72199 16.8459 9.90488C16.8459 11.098 16.0159 12.0667 14.9559 12.0667Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.04867 15.49H15.1775C16.5339 15.49 17.3172 14.7067 17.3172 13.1548V4.83755C17.3172 3.29309 16.5265 2.50977 14.9515 2.50977H2.82302C1.47463 2.50977 0.683594 3.28569 0.683594 4.83755V13.1545C0.683594 14.7138 1.48138 15.49 3.04867 15.49ZM8.10441 9.3803L2.41609 3.76816C2.58163 3.70034 2.77738 3.66273 3.01106 3.66273H14.9971C15.2305 3.66273 15.434 3.70034 15.6072 3.78327L9.92692 9.38062C9.60291 9.7043 9.31652 9.84766 9.01534 9.84766C8.71384 9.84766 8.42841 9.70398 8.10441 9.3803ZM1.83559 13.1548V4.76234L6.16717 9.01162L1.84331 13.2824C1.83592 13.2448 1.83559 13.1998 1.83559 13.1548ZM16.1646 4.84494V13.2599L11.8629 9.0113L16.1649 4.78516L16.1646 4.84494ZM3.01106 14.3377C2.79249 14.3377 2.61184 14.3075 2.4537 14.2397L6.95852 9.78723L7.44838 10.2694C7.97552 10.7891 8.48017 11.0077 9.01534 11.0077C9.54249 11.0077 10.0548 10.7891 10.5823 10.2694L11.0718 9.78723L15.5696 14.2319C15.4111 14.3075 15.2154 14.3374 14.9968 14.3374L3.01106 14.3377Z" fill="#ECECF1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.03371 5.27275L4.1915 20.9162C4.20021 21.7802 4.90766 22.4735 5.77162 22.4648L21.4151 22.307C22.2791 22.2983 22.9724 21.5909 22.9637 20.7269L22.8059 5.0834C22.7972 4.21944 22.0897 3.52612 21.2258 3.53483L5.58228 3.69262C4.71831 3.70134 4.02499 4.40878 4.03371 5.27275Z" stroke="#949494" stroke-width="2.08591" stroke-linejoin="round"/>
<path d="M9.42289 22.428L9.23354 3.65585M17.6924 15.0436L15.5856 12.9788L17.6504 10.872M6.29419 22.4596L12.5516 22.3965M6.10484 3.68741L12.3622 3.62429" stroke="#949494" stroke-width="2.08591" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 692 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none">
<path d="M19.8155 2.09139C19.0825 1.34392 18.2005 0.970703 17.1685 0.970703H4.68155C3.64955 0.970703 2.76755 1.34392 2.03455 2.09139C1.30155 2.83885 0.935547 3.73825 0.935547 4.79061V17.524C0.935547 18.5763 1.30155 19.4757 2.03455 20.2232C2.76755 20.9707 3.64955 21.3439 4.68155 21.3439H7.59555C7.78555 21.3439 7.92855 21.3368 8.02455 21.3235C8.13624 21.3007 8.23705 21.2399 8.31055 21.1512C8.40555 21.0492 8.45355 20.9013 8.45355 20.7076L8.44655 19.8051C8.44255 19.23 8.44055 18.7752 8.44055 18.4387L8.14055 18.4917C7.95055 18.5274 7.71055 18.5427 7.41955 18.5386C7.11624 18.5329 6.81389 18.5019 6.51555 18.4458C6.19795 18.386 5.89897 18.2497 5.64355 18.0481C5.37604 17.8418 5.17652 17.5572 5.07155 17.2323L4.94155 16.9264C4.83198 16.6851 4.69432 16.4581 4.53155 16.2503C4.34555 16.0025 4.15655 15.8353 3.96555 15.7466L3.87555 15.6803C3.81279 15.6345 3.75571 15.5811 3.70555 15.5212C3.65764 15.4657 3.6182 15.4031 3.58855 15.3356C3.56255 15.2734 3.58455 15.2224 3.65355 15.1827C3.72355 15.1419 3.84855 15.1225 4.03155 15.1225L4.29155 15.1633C4.46455 15.198 4.67955 15.304 4.93455 15.4804C5.19261 15.6598 5.40818 15.8957 5.56555 16.1708C5.76555 16.5328 6.00555 16.8091 6.28755 16.9998C6.56955 17.1895 6.85355 17.2854 7.13955 17.2854C7.42555 17.2854 7.67255 17.2629 7.88155 17.2191C8.08366 17.1765 8.28005 17.1094 8.46655 17.0192C8.54455 16.4278 8.75655 15.9709 9.10355 15.6528C8.65392 15.6078 8.2083 15.5281 7.77055 15.4142C7.34334 15.2945 6.93249 15.1208 6.54755 14.8972C6.14479 14.6736 5.78905 14.3714 5.50055 14.008C5.22355 13.6541 4.99555 13.1901 4.81755 12.616C4.64055 12.0409 4.55155 11.377 4.55155 10.6255C4.55155 9.55581 4.89355 8.64519 5.57855 7.89263C5.25855 7.08908 5.28855 6.18662 5.66955 5.18831C5.92155 5.10775 6.29455 5.16791 6.78855 5.36676C7.28255 5.56561 7.64455 5.7359 7.87455 5.87662C8.10455 6.01939 8.28855 6.13869 8.42755 6.23557C9.24052 6.00486 10.0807 5.88889 10.9245 5.8909C11.7835 5.8909 12.6155 6.00613 13.4225 6.23557L13.9165 5.91741C14.2965 5.68476 14.6973 5.48946 15.1135 5.33413C15.5735 5.15669 15.9235 5.10877 16.1675 5.18831C16.5575 6.18764 16.5915 7.08908 16.2705 7.89365C16.9555 8.64519 17.2985 9.55581 17.2985 10.6265C17.2985 11.3781 17.2095 12.044 17.0315 12.6221C16.8545 13.2013 16.6245 13.6653 16.3425 14.0151C16.049 14.3739 15.6917 14.6731 15.2895 14.8972C14.8695 15.1358 14.4615 15.3081 14.0665 15.4142C13.6288 15.5284 13.1832 15.6085 12.7335 15.6538C13.1835 16.0515 13.4095 16.6786 13.4095 17.5362V20.7076C13.4095 20.8575 13.4305 20.9788 13.4745 21.0716C13.4948 21.1163 13.5236 21.1564 13.5593 21.1895C13.5951 21.2226 13.637 21.2481 13.6825 21.2643C13.7785 21.299 13.8625 21.3215 13.9365 21.3296C14.0105 21.3398 14.1165 21.3429 14.2545 21.3429H17.1685C18.2005 21.3429 19.0825 20.9696 19.8155 20.2222C20.5475 19.4757 20.9145 18.5753 20.9145 17.523V4.79061C20.9145 3.73825 20.5485 2.83885 19.8155 2.09139Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,3 +0,0 @@
<svg width="600" height="450" viewBox="0 0 600 450" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25 25H575M25 225H575M25 425H575" stroke="#949494" stroke-opacity="0.54" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" fill="none">
<path d="M10.9355 0.442139C5.41555 0.442139 0.935547 5.01053 0.935547 10.6394C0.935547 16.2683 5.41555 20.8367 10.9355 20.8367C16.4555 20.8367 20.9355 16.2683 20.9355 10.6394C20.9355 5.01053 16.4555 0.442139 10.9355 0.442139ZM11.9355 15.7381H9.93555V9.61971H11.9355V15.7381ZM11.9355 7.58025H9.93555V5.54079H11.9355V7.58025Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 446 B

View File

@@ -1,3 +0,0 @@
<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.65 4C10.83 1.67 8.61 0 6 0C2.69 0 0 2.69 0 6C0 9.31 2.69 12 6 12C8.61 12 10.83 10.33 11.65 8H16V12H20V8H22V4H11.65ZM6 8C4.9 8 4 7.1 4 6C4 4.9 4.9 4 6 4C7.1 4 8 4.9 8 6C8 7.1 7.1 8 6 8Z" fill="black" fill-opacity="0.54"/>
</svg>

Before

Width:  |  Height:  |  Size: 337 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M20 4H4C2.9 4 2 4.9 2 6V24L6 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4ZM20 18H6L4 20V6H20V18Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 232 B

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM18 14H4L2 16V2H18V14Z" fill="gray" fill-opacity="0.80"/>
</svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -1,5 +1,5 @@
<svg width="113" height="124" viewBox="0 0 113 124" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="55.5" cy="71" r="53" fill="#F1F1F1" fill-opacity="0.5"/>
<circle cx="55.5" cy="71" r="53" fill="#E8E3F3" fill-opacity="0.6"/>
<rect x="-0.599797" y="0.654564" width="43.9445" height="61.5222" rx="4.39444" transform="matrix(-0.999048 0.0436194 0.0436194 0.999048 68.9873 43.3176)" fill="#EEEEEE" stroke="#999999" stroke-width="1.25556"/>
<rect x="0.704349" y="-0.540466" width="46.4556" height="64.0333" rx="5.65" transform="matrix(-0.991445 -0.130526 -0.130526 0.991445 96.3673 40.893)" fill="#FAFAFA" stroke="#999999" stroke-width="1.25556"/>
<path d="M94.3796 45.7849C94.7417 43.0349 92.8059 40.5122 90.0559 40.1501L55.2011 35.5614C52.4511 35.1994 49.9284 37.1352 49.5663 39.8851L48.3372 49.2212L93.1505 55.121L94.3796 45.7849Z" fill="#EEEEEE"/>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,3 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.25 5.25H12.25L10.5 3.5H5.25C4.2875 3.5 3.50875 4.2875 3.50875 5.25L3.5 15.75C3.5 16.7125 4.2875 17.5 5.25 17.5H19.25C20.2125 17.5 21 16.7125 21 15.75V7C21 6.0375 20.2125 5.25 19.25 5.25ZM19.25 15.75H5.25V5.25H9.77375L11.5238 7H19.25V15.75ZM17.5 10.5H7V8.75H17.5V10.5ZM14 14H7V12.25H14V14Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 423 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left-close-icon lucide-panel-left-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left-open-icon lucide-panel-left-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/></svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.6294 0.371048C15.4618 0.204652 15.2516 0.0876771 15.0218 0.0329776C14.7921 -0.0217218 14.5517 -0.012028 14.3271 0.0609937L1.36688 4.38403C1.12652 4.4602 0.914122 4.60588 0.756509 4.80269C0.598897 4.9995 0.503143 5.2386 0.481341 5.48979C0.459539 5.74099 0.512667 5.99301 0.634015 6.21403C0.755364 6.43505 0.939488 6.61515 1.16313 6.73159L6.54036 9.38919L9.19796 14.7841C9.30478 14.9953 9.46824 15.1726 9.67006 15.2962C9.87187 15.4198 10.1041 15.4848 10.3407 15.484H10.4293C10.6828 15.4653 10.9247 15.3708 11.1238 15.2129C11.3228 15.0549 11.4698 14.8407 11.5455 14.5981L15.9306 1.67327C16.0089 1.44998 16.0221 1.20903 15.9688 0.978485C15.9155 0.747942 15.7978 0.537288 15.6294 0.371048ZM1.91612 5.60653L13.2287 1.83273L6.94786 8.11354L1.91612 5.60653ZM10.4027 14.0843L7.88688 9.05256L14.1677 2.77175L10.4027 14.0843Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 947 B

View File

@@ -1,3 +0,0 @@
<svg width="21" height="18" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.00999999 18L21 9L0.00999999 0L0 7L15 9L0 11L0.00999999 18Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 192 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none">
<path d="M10.5024 14.1916C9.50037 14.1916 8.5394 13.8044 7.83087 13.1152C7.12234 12.426 6.72429 11.4913 6.72429 10.5166C6.72429 9.54193 7.12234 8.60718 7.83087 7.91799C8.5394 7.22879 9.50037 6.8416 10.5024 6.8416C11.5044 6.8416 12.4654 7.22879 13.1739 7.91799C13.8824 8.60718 14.2805 9.54193 14.2805 10.5166C14.2805 11.4913 13.8824 12.426 13.1739 13.1152C12.4654 13.8044 11.5044 14.1916 10.5024 14.1916ZM18.5227 11.5351C18.5659 11.1991 18.5983 10.8631 18.5983 10.5166C18.5983 10.1701 18.5659 9.8236 18.5227 9.4666L20.8004 7.7551C21.0055 7.5976 21.0595 7.3141 20.9299 7.0831L18.771 3.4501C18.6415 3.2191 18.35 3.1246 18.1125 3.2191L15.4247 4.2691C14.8634 3.8596 14.2805 3.5026 13.6004 3.2401L13.201 0.457604C13.1791 0.333931 13.1129 0.221802 13.0141 0.141083C12.9153 0.0603633 12.7903 0.0162658 12.6613 0.0166035H8.34347C8.07361 0.0166035 7.84692 0.205604 7.80374 0.457604L7.40435 3.2401C6.72429 3.5026 6.14138 3.8596 5.58007 4.2691L2.89222 3.2191C2.65474 3.1246 2.36329 3.2191 2.23375 3.4501L0.0748404 7.0831C-0.0654889 7.3141 -0.000721403 7.5976 0.204375 7.7551L2.48203 9.4666C2.43885 9.8236 2.40647 10.1701 2.40647 10.5166C2.40647 10.8631 2.43885 11.1991 2.48203 11.5351L0.204375 13.2781C-0.000721403 13.4356 -0.0654889 13.7191 0.0748404 13.9501L2.23375 17.5831C2.36329 17.8141 2.65474 17.8981 2.89222 17.8141L5.58007 16.7536C6.14138 17.1736 6.72429 17.5306 7.40435 17.7931L7.80374 20.5756C7.84692 20.8276 8.07361 21.0166 8.34347 21.0166H12.6613C12.9312 21.0166 13.1578 20.8276 13.201 20.5756L13.6004 17.7931C14.2805 17.5201 14.8634 17.1736 15.4247 16.7536L18.1125 17.8141C18.35 17.8981 18.6415 17.8141 18.771 17.5831L20.9299 13.9501C21.0595 13.7191 21.0055 13.4356 20.8004 13.2781L18.5227 11.5351Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,6 +0,0 @@
<svg width="58" height="49" viewBox="0 0 58 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M33.0002 45.3337H6.3335C5.27263 45.3337 4.25521 44.9122 3.50507 44.1621C2.75492 43.4119 2.3335 42.3945 2.3335 41.3337V6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V24.0003M42.3335 40.0003L49.0002 46.667M49.0002 46.667L55.6668 40.0003M49.0002 46.667V30.667" stroke="#949494" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.3335 6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V18.667H2.3335V6.66699Z" stroke="#949494" stroke-width="4"/>
<path d="M7.66667 10.6673C7.66667 9.19456 8.86057 8.00065 10.3333 8.00065C11.8061 8.00065 13 9.19456 13 10.6673C13 12.1401 11.8061 13.334 10.3333 13.334C8.86057 13.334 7.66667 12.1401 7.66667 10.6673Z" fill="#949494"/>
<path d="M15.6667 10.6673C15.6667 9.19456 16.8606 8.00065 18.3333 8.00065C19.8061 8.00065 21 9.19456 21 10.6673C21 12.1401 19.8061 13.334 18.3333 13.334C16.8606 13.334 15.6667 12.1401 15.6667 10.6673Z" fill="#949494"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -136,33 +136,34 @@ const Chunks: React.FC<ChunksProps> = ({
const pathParts = path ? path.split('/') : [];
const fetchChunks = () => {
const fetchChunks = async () => {
setLoading(true);
try {
userService
.getDocumentChunks(documentId, page, perPage, token, path, searchTerm)
.then((response) => {
if (!response.ok) {
setLoading(false);
setPaginatedChunks([]);
throw new Error('Failed to fetch chunks data');
}
return response.json();
})
.then((data) => {
setPage(data.page);
setPerPage(data.per_page);
setTotalChunks(data.total);
setPaginatedChunks(data.chunks);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setPaginatedChunks([]);
});
} catch (e) {
setLoading(false);
const response = await userService.getDocumentChunks(
documentId,
page,
perPage,
token,
path,
searchTerm,
);
if (!response.ok) {
throw new Error('Failed to fetch chunks data');
}
const data = await response.json();
setPage(data.page);
setPerPage(data.per_page);
setTotalChunks(data.total);
setPaginatedChunks(data.chunks);
} catch (error) {
setPaginatedChunks([]);
console.error(error);
} finally {
// ✅ always runs, success or failure
setLoading(false);
}
};

View File

@@ -6,6 +6,7 @@ import { selectToken } from '../preferences/preferenceSlice';
import { ActiveState } from '../models/misc';
import Chunks from './Chunks';
import ContextMenu, { MenuOption } from './ContextMenu';
import SkeletonLoader from './SkeletonLoader';
import ConfirmationModal from '../modals/ConfirmationModal';
import userService from '../api/services/userService';
import FileIcon from '../assets/file.svg';
@@ -15,7 +16,7 @@ import ThreeDots from '../assets/three-dots.svg';
import EyeView from '../assets/eye-view.svg';
import SyncIcon from '../assets/sync.svg';
import CheckmarkIcon from '../assets/checkMark2.svg';
import { useOutsideAlerter } from '../hooks';
import { useOutsideAlerter, useLoaderState } from '../hooks';
import {
Table,
TableContainer,
@@ -55,7 +56,7 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
onBackToDocuments,
}) => {
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(true);
const [loading, setLoading] = useLoaderState(true, 500);
const [error, setError] = useState<string | null>(null);
const [directoryStructure, setDirectoryStructure] =
useState<DirectoryStructure | null>(null);
@@ -716,7 +717,13 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
</TableHeader>
</TableRow>
</TableHead>
<TableBody>{renderFileTree(getCurrentDirectory())}</TableBody>
<TableBody>
{loading ? (
<SkeletonLoader component="fileTable" />
) : (
renderFileTree(getCurrentDirectory())
)}
</TableBody>
</Table>
</TableContainer>
</div>

View File

@@ -20,14 +20,9 @@ type CopyButtonProps = {
const DEFAULT_ICON_SIZE = 'w-4 h-4';
const DEFAULT_PADDING = 'p-2';
const DEFAULT_COPIED_DURATION = 2000;
const DEFAULT_BG_LIGHT = '#FFFFFF';
const DEFAULT_BG_DARK = 'transparent';
const DEFAULT_HOVER_BG_LIGHT = '#EEEEEE';
const DEFAULT_HOVER_BG_DARK = '#464152';
export default function CopyButton({
textToCopy,
iconSize = DEFAULT_ICON_SIZE,
padding = DEFAULT_PADDING,
showText = false,
@@ -43,9 +38,8 @@ export default function CopyButton({
const iconWrapperClasses = clsx(
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
padding,
`bg-[${DEFAULT_BG_LIGHT}] dark:bg-[${DEFAULT_BG_DARK}]`,
{
[`hover:bg-[${DEFAULT_HOVER_BG_LIGHT}] dark:hover:bg-[${DEFAULT_HOVER_BG_DARK}]`]:
[`bg-[#FFFFFF}] dark:bg-transparent hover:bg-[#EEEEEE] dark:hover:bg-purple-taupe`]:
!isCopied,
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
isCopied,

View File

@@ -5,6 +5,7 @@ import { selectToken } from '../preferences/preferenceSlice';
import { formatBytes } from '../utils/stringUtils';
import Chunks from './Chunks';
import ContextMenu, { MenuOption } from './ContextMenu';
import SkeletonLoader from './SkeletonLoader';
import userService from '../api/services/userService';
import FileIcon from '../assets/file.svg';
import FolderIcon from '../assets/folder.svg';
@@ -12,7 +13,7 @@ import ArrowLeft from '../assets/arrow-left.svg';
import ThreeDots from '../assets/three-dots.svg';
import EyeView from '../assets/eye-view.svg';
import Trash from '../assets/red-trash.svg';
import { useOutsideAlerter } from '../hooks';
import { useOutsideAlerter, useLoaderState } from '../hooks';
import ConfirmationModal from '../modals/ConfirmationModal';
import {
Table,
@@ -53,7 +54,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
onBackToDocuments,
}) => {
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(true);
const [loading, setLoading] = useLoaderState(true, 500);
const [error, setError] = useState<string | null>(null);
const [directoryStructure, setDirectoryStructure] =
useState<DirectoryStructure | null>(null);
@@ -839,7 +840,13 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
</TableHeader>
</TableRow>
</TableHead>
<TableBody>{renderFileTree(currentDirectory)}</TableBody>
<TableBody>
{loading ? (
<SkeletonLoader component="fileTable" />
) : (
renderFileTree(currentDirectory)
)}
</TableBody>
</Table>
</TableContainer>
</div>

View File

@@ -101,7 +101,7 @@ export const FileUpload = ({
const filesToUpload = multiple ? acceptedFiles : [acceptedFiles[0]];
onUpload(filesToUpload);
const file = multiple ? acceptedFiles[0] : acceptedFiles[0];
const file = acceptedFiles[0];
setCurrentFile(file);
if (showPreview && file.type.startsWith('image/')) {

View File

@@ -1,4 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -6,12 +8,11 @@ import endpoints from '../api/endpoints';
import userService from '../api/services/userService';
import AlertIcon from '../assets/alert.svg';
import ClipIcon from '../assets/clip.svg';
import DragFileUpload from '../assets/DragFileUpload.svg';
import ExitIcon from '../assets/exit.svg';
import PaperPlane from '../assets/paper_plane.svg';
import SendArrowIcon from './SendArrowIcon';
import SourceIcon from '../assets/source.svg';
import DocumentationDark from '../assets/documentation-dark.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
import ToolIcon from '../assets/tool.svg';
import {
addAttachment,
@@ -19,7 +20,8 @@ import {
selectAttachments,
updateAttachment,
} from '../upload/uploadSlice';
import { useDarkTheme } from '../hooks';
import { reorderAttachments } from '../upload/uploadSlice';
import { ActiveState } from '../models/misc';
import {
selectSelectedDocs,
@@ -29,6 +31,7 @@ import Upload from '../upload/Upload';
import { getOS, isTouchDevice } from '../utils/browserUtils';
import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
import { handleAbort } from '../conversation/conversationSlice';
type MessageInputProps = {
onSubmit: (text: string) => void;
@@ -46,7 +49,6 @@ export default function MessageInput({
autoFocus = true,
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const [value, setValue] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);
const sourceButtonRef = useRef<HTMLButtonElement>(null);
@@ -55,6 +57,7 @@ export default function MessageInput({
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
const selectedDocs = useSelector(selectSelectedDocs);
const token = useSelector(selectToken);
@@ -84,77 +87,134 @@ export default function MessageInput({
};
}, [browserOS]);
const uploadFiles = useCallback(
(files: File[]) => {
const apiHost = import.meta.env.VITE_API_HOST;
files.forEach((file) => {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
const uniqueId = crypto.randomUUID();
const newAttachment = {
id: uniqueId,
fileName: file.name,
progress: 0,
status: 'uploading' as const,
taskId: '',
};
dispatch(addAttachment(newAttachment));
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
dispatch(
updateAttachment({
id: uniqueId,
updates: { progress },
}),
);
}
});
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.task_id) {
dispatch(
updateAttachment({
id: uniqueId,
updates: {
taskId: response.task_id,
status: 'processing',
progress: 10,
},
}),
);
}
} else {
dispatch(
updateAttachment({
id: uniqueId,
updates: { status: 'failed' },
}),
);
}
};
xhr.onerror = () => {
dispatch(
updateAttachment({
id: uniqueId,
updates: { status: 'failed' },
}),
);
};
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
});
},
[dispatch, token],
);
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || e.target.files.length === 0) return;
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
const files = Array.from(e.target.files);
uploadFiles(files);
const apiHost = import.meta.env.VITE_API_HOST;
const xhr = new XMLHttpRequest();
const newAttachment = {
fileName: file.name,
progress: 0,
status: 'uploading' as const,
taskId: '',
};
dispatch(addAttachment(newAttachment));
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { progress },
}),
);
}
});
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.task_id) {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: {
taskId: response.task_id,
status: 'processing',
progress: 10,
},
}),
);
}
} else {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { status: 'failed' },
}),
);
}
};
xhr.onerror = () => {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { status: 'failed' },
}),
);
};
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
// clear input so same file can be selected again
e.target.value = '';
};
// Drag and drop handler
const onDrop = useCallback(
(acceptedFiles: File[]) => {
uploadFiles(acceptedFiles);
setHandleDragActive(false);
},
[uploadFiles],
);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
noClick: true,
noKeyboard: true,
multiple: true,
onDragEnter: () => {
setHandleDragActive(true);
},
onDragLeave: () => {
setHandleDragActive(false);
},
maxSize: 25000000,
accept: {
'application/pdf': ['.pdf'],
'text/plain': ['.txt'],
'text/x-rst': ['.rst'],
'text/x-markdown': ['.md'],
'application/zip': ['.zip'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
['.docx'],
'application/json': ['.json'],
'text/csv': ['.csv'],
'text/html': ['.html'],
'application/epub+zip': ['.epub'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
'.xlsx',
],
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
['.pptx'],
'image/png': ['.png'],
'image/jpeg': ['.jpeg'],
'image/jpg': ['.jpg'],
},
});
useEffect(() => {
const checkTaskStatus = () => {
const processingAttachments = attachments.filter(
@@ -169,7 +229,7 @@ export default function MessageInput({
if (data.status === 'SUCCESS') {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
id: attachment.id,
updates: {
status: 'completed',
progress: 100,
@@ -181,14 +241,14 @@ export default function MessageInput({
} else if (data.status === 'FAILURE') {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
id: attachment.id,
updates: { status: 'failed' },
}),
);
} else if (data.status === 'PROGRESS' && data.result?.current) {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
id: attachment.id,
updates: { progress: data.result.current },
}),
);
@@ -197,7 +257,7 @@ export default function MessageInput({
.catch(() => {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
id: attachment.id,
updates: { status: 'failed' },
}),
);
@@ -256,90 +316,136 @@ export default function MessageInput({
setValue('');
}
};
const handleCancel = () => {
handleAbort();
};
// Drag state for reordering
const [draggingId, setDraggingId] = useState<string | null>(null);
// no preview object URLs to revoke (preview removed per reviewer request)
const findIndexById = (id: string) =>
attachments.findIndex((a) => a.id === id);
const handleDragStart = (e: React.DragEvent, id: string) => {
setDraggingId(id);
try {
e.dataTransfer.setData('text/plain', id);
e.dataTransfer.effectAllowed = 'move';
} catch (err) {
// ignore
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDropOn = (e: React.DragEvent, targetId: string) => {
e.preventDefault();
const sourceId = e.dataTransfer.getData('text/plain');
if (!sourceId || sourceId === targetId) return;
const sourceIndex = findIndexById(sourceId);
const destIndex = findIndexById(targetId);
if (sourceIndex === -1 || destIndex === -1) return;
dispatch(reorderAttachments({ sourceIndex, destinationIndex: destIndex }));
setDraggingId(null);
};
return (
<div className="mx-2 flex w-full flex-col">
<div {...getRootProps()} className="flex w-full flex-col">
<input {...getInputProps()} />
<div className="border-dark-gray bg-lotion dark:border-grey relative flex w-full flex-col rounded-[23px] border dark:bg-transparent">
<div className="flex flex-wrap gap-1.5 px-2 py-2 sm:gap-2 sm:px-3">
{attachments.map((attachment, index) => (
<div
key={index}
className={`group dark:text-bright-gray relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] sm:px-3 sm:py-1.5 sm:text-[14px] dark:bg-[#393B3D] ${
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
}`}
title={attachment.fileName}
>
<div className="bg-purple-30 mr-2 items-center justify-center rounded-lg p-[5.5px]">
{attachment.status === 'completed' && (
<img
src={DocumentationDark}
alt="Attachment"
className="h-[15px] w-[15px] object-fill"
/>
)}
{attachment.status === 'failed' && (
<img
src={AlertIcon}
alt="Failed"
className="h-[15px] w-[15px] object-fill"
/>
)}
{(attachment.status === 'uploading' ||
attachment.status === 'processing') && (
<div className="flex h-[15px] w-[15px] items-center justify-center">
<svg className="h-[15px] w-[15px]" viewBox="0 0 24 24">
<circle
className="opacity-0"
cx="12"
cy="12"
r="10"
stroke="transparent"
strokeWidth="4"
fill="none"
/>
<circle
className="text-[#ECECF1]"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray="62.83"
strokeDashoffset={
62.83 * (1 - attachment.progress / 100)
}
transform="rotate(-90 12 12)"
/>
</svg>
</div>
)}
</div>
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
{attachment.fileName}
</span>
<button
className="ml-1.5 flex items-center justify-center rounded-full p-1"
onClick={() => {
if (attachment.id) {
dispatch(removeAttachment(attachment.id));
} else if (attachment.taskId) {
dispatch(removeAttachment(attachment.taskId));
}
}}
aria-label={t('conversation.attachments.remove')}
{attachments.map((attachment) => {
return (
<div
key={attachment.id}
draggable={true}
onDragStart={(e) => handleDragStart(e, attachment.id)}
onDragOver={handleDragOver}
onDrop={(e) => handleDropOn(e, attachment.id)}
className={`group dark:text-bright-gray relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] sm:px-3 sm:py-1.5 sm:text-[14px] dark:bg-[#393B3D] ${
attachment.status !== 'completed'
? 'opacity-70'
: 'opacity-100'
} ${draggingId === attachment.id ? 'ring-dashed opacity-60 ring-2 ring-purple-200' : ''}`}
title={attachment.fileName}
>
<img
src={ExitIcon}
alt={t('conversation.attachments.remove')}
className="h-2.5 w-2.5 filter dark:invert"
/>
</button>
</div>
))}
<div className="bg-purple-30 mr-2 flex h-8 w-8 items-center justify-center rounded-md p-1">
{attachment.status === 'completed' && (
<img
src={DocumentationDark}
alt="Attachment"
className="h-[15px] w-[15px] object-fill"
/>
)}
{attachment.status === 'failed' && (
<img
src={AlertIcon}
alt="Failed"
className="h-[15px] w-[15px] object-fill"
/>
)}
{(attachment.status === 'uploading' ||
attachment.status === 'processing') && (
<div className="flex h-[15px] w-[15px] items-center justify-center">
<svg className="h-[15px] w-[15px]" viewBox="0 0 24 24">
<circle
className="opacity-0"
cx="12"
cy="12"
r="10"
stroke="transparent"
strokeWidth="4"
fill="none"
/>
<circle
className="text-[#ECECF1]"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray="62.83"
strokeDashoffset={
62.83 * (1 - attachment.progress / 100)
}
transform="rotate(-90 12 12)"
/>
</svg>
</div>
)}
</div>
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
{attachment.fileName}
</span>
<button
className="ml-1.5 flex items-center justify-center rounded-full p-1"
onClick={() => {
dispatch(removeAttachment(attachment.id));
}}
aria-label={t('conversation.attachments.remove')}
>
<img
src={ExitIcon}
alt={t('conversation.attachments.remove')}
className="h-2.5 w-2.5 filter dark:invert"
/>
</button>
</div>
);
})}
</div>
<div className="w-full">
@@ -421,32 +527,40 @@ export default function MessageInput({
<input
type="file"
className="hidden"
multiple
onChange={handleFileAttachment}
/>
</label>
{/* Additional badges can be added here in the future */}
</div>
<button
onClick={loading ? undefined : handleSubmit}
aria-label={loading ? t('loading') : t('send')}
className={`flex h-7 w-7 items-center justify-center rounded-full sm:h-9 sm:w-9 ${loading || !value.trim() ? 'bg-black opacity-60 dark:bg-[#F0F3F4] dark:opacity-80' : 'bg-black opacity-100 dark:bg-[#F0F3F4]'} ml-auto shrink-0`}
disabled={loading}
>
{loading ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="mx-auto my-auto block h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4"
alt={t('loading')}
{loading ? (
<button
onClick={handleCancel}
aria-label={t('cancel')}
className={`ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#7F54D6] text-white sm:h-9 sm:w-9`}
disabled={!loading}
>
<div className="flex h-3 w-3 items-center justify-center rounded-[3px] bg-white sm:h-3.5 sm:w-3.5" />
</button>
) : (
<button
onClick={handleSubmit}
aria-label={t('send')}
className={`ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition-colors duration-300 ease-in-out sm:h-9 sm:w-9 ${
value.trim() && !loading
? 'bg-purple-30 text-white'
: 'bg-[#EDEDED] text-[#959595] dark:bg-[#37383D] dark:text-[#77787D]'
}`}
disabled={!value.trim() || loading}
>
<SendArrowIcon
className="mx-auto my-auto block h-3.5 w-3.5 sm:h-4 sm:w-4"
aria-label={t('send')}
role="img"
/>
) : (
<img
className={`mx-auto my-auto block h-3.5 w-3.5 translate-x-[-0.9px] translate-y-[1.1px] sm:h-4 sm:w-4 ${isDarkTheme ? 'invert filter' : ''}`}
src={PaperPlane}
alt={t('send')}
/>
)}
</button>
</button>
)}
</div>
</div>
@@ -473,6 +587,20 @@ export default function MessageInput({
close={() => setUploadModalState('INACTIVE')}
/>
)}
{handleDragActive &&
createPortal(
<div className="dark:bg-gray-alpha/50 pointer-events-none fixed top-0 left-0 z-50 flex size-full flex-col items-center justify-center bg-white/85">
<img className="filter dark:invert" src={DragFileUpload} />
<span className="text-outer-space dark:text-silver px-2 text-2xl font-bold">
{t('modals.uploadDoc.drag.title')}
</span>
<span className="text-s text-outer-space dark:text-silver w-48 p-2 text-center">
{t('modals.uploadDoc.drag.description')}
</span>
</div>,
document.body,
)}
</div>
);
}

View File

@@ -0,0 +1,45 @@
import close from '../assets/cross.svg';
import rightArrow from '../assets/arrow-full-right.svg';
import bg from '../assets/notification-bg.jpg';
interface NotificationProps {
notificationText: string;
notificationLink: string;
handleCloseNotification: () => void;
}
export default function Notification({
notificationText,
notificationLink,
handleCloseNotification,
}: NotificationProps) {
return (
<a
className="absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 rounded-lg bg-cover bg-center bg-no-repeat px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
style={{ backgroundImage: `url(${bg})` }}
href={notificationLink}
target="_blank"
aria-label="Notification"
rel="noreferrer"
>
<p className="text-white-3000 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
{notificationText}
</p>
<span>
<img className="w-full" src={rightArrow} alt="" />
</span>
<button
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
aria-label="Close notification"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleCloseNotification();
}}
>
<img className="w-full" src={close} alt="Close notification" />
</button>
</a>
);
}

View File

@@ -0,0 +1,17 @@
import { SVGProps } from 'react';
const SendArrowIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width="11"
height="14"
viewBox="0 0 11 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0.292786 6.20676C0.105315 6.01923 -3.59956e-07 5.76492 -3.71547e-07 5.49976C-3.83138e-07 5.23459 0.105315 4.98028 0.292786 4.79276L4.79279 0.292756C4.98031 0.105284 5.23462 -3.07464e-05 5.49979 -3.0758e-05C5.76495 -3.07696e-05 6.01926 0.105284 6.20679 0.292756L10.7068 4.79276C10.8889 4.98136 10.9897 5.23396 10.9875 5.49616C10.9852 5.75835 10.88 6.00917 10.6946 6.19457C10.5092 6.37998 10.2584 6.48515 9.99619 6.48743C9.73399 6.48971 9.48139 6.38891 9.29279 6.20676L6.49979 3.49976L6.49979 12.9998C6.49979 13.265 6.39443 13.5193 6.20689 13.7069C6.01936 13.8944 5.765 13.9998 5.49979 13.9998C5.23457 13.9998 4.98022 13.8944 4.79268 13.7069C4.60514 13.5193 4.49979 13.265 4.49979 12.9998L4.49979 3.49976L1.70679 6.20676C1.51926 6.39423 1.26495 6.49954 0.999786 6.49954C0.734622 6.49954 0.480314 6.39423 0.292786 6.20676Z"
fill="currentColor"
/>
</svg>
);
export default SendArrowIcon;

View File

@@ -1,36 +0,0 @@
import { useState } from 'react';
import ShareIcon from '../assets/share.svg';
import { ShareConversationModal } from '../modals/ShareConversationModal';
type ShareButtonProps = {
conversationId: string;
};
export default function ShareButton({ conversationId }: ShareButtonProps) {
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
return (
<>
<button
title="Share"
onClick={() => {
setShareModalState(true);
}}
className="hover:bg-bright-gray absolute top-4 right-20 z-20 rounded-full dark:hover:bg-[#28292E]"
>
<img
className="m-2 h-5 w-5 filter dark:invert"
alt="share"
src={ShareIcon}
/>
</button>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</>
);
}

View File

@@ -33,7 +33,7 @@ export default function Sidebar({
return (
<div ref={sidebarRef} className="h-vh relative">
<div
className={`dark:bg-chinese-black fixed top-0 right-0 z-50 h-full w-72 transform bg-white shadow-xl transition-all duration-300 sm:w-96 ${
className={`dark:bg-chinese-black fixed top-0 right-0 z-50 h-full w-64 transform bg-white shadow-xl transition-all duration-300 sm:w-80 ${
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
} border-l border-[#9ca3af]/10`}
>

View File

@@ -6,7 +6,7 @@ interface SkeletonLoaderProps {
| 'default'
| 'analysis'
| 'logs'
| 'table'
| 'fileTable'
| 'chatbot'
| 'dropdown'
| 'chunkCards'
@@ -44,15 +44,15 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
<>
{[...Array(4)].map((_, idx) => (
<tr key={idx} className="animate-pulse">
<td className="w-[45%] px-4 py-4">
<td className="w-[40%] px-4 py-4">
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
</td>
<td className="w-[30%] px-4 py-4">
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
</td>
<td className="w-[20%] px-4 py-4">
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
</td>
<td className="w-[25%] px-4 py-4">
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
</td>
<td className="w-[10%] px-4 py-4">
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
</td>
@@ -241,7 +241,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
);
const componentMap = {
table: renderTable,
fileTable: renderTable,
chatbot: renderChatbot,
dropdown: renderDropdown,
logs: renderLogs,

View File

@@ -1,175 +0,0 @@
import React, { useState } from 'react';
import Trash from '../assets/trash.svg';
import Arrow2 from '../assets/dropdown-arrow.svg';
import { Doc, ActiveState } from '../models/misc';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import ConfirmationModal from '../modals/ConfirmationModal';
type Props = {
options: Doc[] | null;
selectedDocs: Doc | null;
setSelectedDocs: any;
isDocsListOpen: boolean;
setIsDocsListOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteClick: any;
handlePostDocumentSelect: any;
};
function SourceDropdown({
options,
setSelectedDocs,
selectedDocs,
setIsDocsListOpen,
isDocsListOpen,
handleDeleteClick,
handlePostDocumentSelect, // Callback function fired after a document is selected
}: Props) {
const dispatch = useDispatch();
const { t } = useTranslation();
const dropdownRef = React.useRef<HTMLDivElement>(null);
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const [deleteModalState, setDeleteModalState] =
useState<ActiveState>('INACTIVE');
const [documentToDelete, setDocumentToDelete] = useState<Doc | null>(null);
const handleEmptyDocumentSelect = () => {
dispatch(setSelectedDocs(null));
setIsDocsListOpen(false);
};
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDocsListOpen(false);
}
};
React.useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const confirmDelete = (option: Doc) => {
setDocumentToDelete(option);
setDeleteModalState('ACTIVE');
};
const handleConfirmedDelete = () => {
if (documentToDelete) {
handleDeleteClick(documentToDelete);
setDeleteModalState('INACTIVE');
setDocumentToDelete(null);
}
};
const handleCancelDelete = () => {
setDeleteModalState('INACTIVE');
setDocumentToDelete(null);
};
return (
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
<button
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
className={`border-silver flex w-full cursor-pointer items-center border bg-white p-[11px] dark:bg-transparent ${
isDocsListOpen
? 'dark:border-silver/40 rounded-t-3xl'
: 'dark:border-purple-taupe rounded-3xl'
}`}
>
<span className="dark:text-bright-gray mr-2 ml-1 flex-1 overflow-hidden text-left text-ellipsis">
<div className="flex flex-row gap-2">
<p className="max-w-3/4 truncate whitespace-nowrap">
{selectedDocs?.name || 'None'}
</p>
</div>
</span>
<img
src={Arrow2}
alt="arrow"
className={`transform ${
isDocsListOpen ? 'rotate-180' : 'rotate-0'
} h-3 w-3 transition-transform`}
/>
</button>
{isDocsListOpen && (
<div className="border-silver dark:border-silver/40 dark:bg-dark-charcoal absolute right-0 left-0 z-20 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border bg-white shadow-lg">
{options ? (
options.map((option: any, index: number) => {
if (option.model === embeddingsName) {
return (
<div
key={index}
className="dark:text-bright-gray flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:hover:bg-[#545561]"
onClick={() => {
dispatch(setSelectedDocs(option));
setIsDocsListOpen(false);
handlePostDocumentSelect(option);
}}
>
<span
onClick={() => {
setIsDocsListOpen(false);
}}
className="ml-4 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap"
>
{option.name}
</span>
{option.location === 'local' && (
<img
src={Trash}
alt="Delete"
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
id={`img-${index}`}
onClick={(event) => {
event.stopPropagation();
confirmDelete(option);
}}
/>
)}
</div>
);
}
})
) : (
<></>
)}
<div
className="dark:text-bright-gray dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-between hover:bg-gray-100"
onClick={handleEmptyDocumentSelect}
>
<span
className="ml-4 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap"
onClick={() => {
handlePostDocumentSelect(null);
}}
>
{t('none')}
</span>
</div>
</div>
)}
<ConfirmationModal
message={t('settings.sources.deleteWarning', {
name: documentToDelete?.name,
})}
modalState={deleteModalState}
setModalState={setDeleteModalState}
handleSubmit={handleConfirmedDelete}
handleCancel={handleCancelDelete}
submitLabel={t('convTile.delete')}
variant="danger"
/>
</div>
);
}
export default SourceDropdown;

View File

@@ -1,4 +1,5 @@
import React, { useRef, useEffect, useState, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Doc } from '../models/misc';
@@ -107,7 +108,7 @@ export default function SourcesPopup({
onClose();
};
return (
const popupContent = (
<div
ref={popupRef}
className="bg-lotion dark:bg-charleston-green-2 fixed z-50 flex flex-col rounded-xl shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
@@ -218,7 +219,7 @@ export default function SourcesPopup({
</>
) : (
<div className="dark:text-bright-gray p-4 text-center text-gray-500 dark:text-[14px]">
{t('noSourcesAvailable')}
{t('conversation.sources.noSourcesAvailable')}
</div>
)}
</div>
@@ -245,4 +246,6 @@ export default function SourcesPopup({
</div>
</div>
);
return createPortal(popupContent, document.body);
}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { selectToken } from '../preferences/preferenceSlice';
@@ -133,10 +134,10 @@ export default function ToolsPopup({
tool.displayName.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
const popupContent = (
<div
ref={popupRef}
className="border-light-silver bg-lotion dark:border-dim-gray dark:bg-charleston-green-2 fixed z-9999 rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
className="border-light-silver bg-lotion dark:border-dim-gray dark:bg-charleston-green-2 fixed z-50 rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
style={{
top: popupPosition.showAbove ? popupPosition.top : undefined,
bottom: popupPosition.showAbove
@@ -242,4 +243,6 @@ export default function ToolsPopup({
</div>
</div>
);
return createPortal(popupContent, document.body);
}

View File

@@ -37,14 +37,17 @@ export default function UploadToast() {
case 'completed':
return t('modals.uploadDoc.progress.completed');
case 'failed':
return t('attachments.uploadFailed');
return t('modals.uploadDoc.progress.failed');
default:
return t('modals.uploadDoc.progress.preparing');
}
};
return (
<div className="fixed right-4 bottom-4 z-50 flex max-w-md flex-col gap-2">
<div
className="fixed right-4 bottom-4 z-50 flex max-w-md flex-col gap-2"
onMouseDown={(e) => e.stopPropagation()}
>
{uploadTasks
.filter((task) => !task.dismissed)
.map((task) => {

View File

@@ -1,20 +1,16 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import SharedAgentCard from '../agents/SharedAgentCard';
import DragFileUpload from '../assets/DragFileUpload.svg';
import MessageInput from '../components/MessageInput';
import { useMediaQuery } from '../hooks';
import { ActiveState } from '../models/misc';
import {
selectConversationId,
selectSelectedAgent,
selectToken,
} from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import Upload from '../upload/Upload';
import { handleSendFeedback } from './conversationHandlers';
import ConversationMessages from './ConversationMessages';
import { FEEDBACK, Query } from './conversationModels';
@@ -45,53 +41,12 @@ export default function Conversation() {
const selectedAgent = useSelector(selectSelectedAgent);
const completedAttachments = useSelector(selectCompletedAttachments);
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const [files, setFiles] = useState<File[]>([]);
const [lastQueryReturnedErr, setLastQueryReturnedErr] =
useState<boolean>(false);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
const fetchStream = useRef<any>(null);
const onDrop = useCallback((acceptedFiles: File[]) => {
setUploadModalState('ACTIVE');
setFiles(acceptedFiles);
setHandleDragActive(false);
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
noClick: true,
multiple: true,
onDragEnter: () => {
setHandleDragActive(true);
},
onDragLeave: () => {
setHandleDragActive(false);
},
maxSize: 25000000,
accept: {
'application/pdf': ['.pdf'],
'text/plain': ['.txt'],
'text/x-rst': ['.rst'],
'text/x-markdown': ['.md'],
'application/zip': ['.zip'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
['.docx'],
'application/json': ['.json'],
'text/csv': ['.csv'],
'text/html': ['.html'],
'application/epub+zip': ['.epub'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
'.xlsx',
],
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
['.pptx'],
},
});
const handleFetchAnswer = useCallback(
({ question, index }: { question: string; index?: number }) => {
fetchStream.current = dispatch(fetchAnswer({ question, indx: index }));
@@ -202,6 +157,7 @@ export default function Conversation() {
queries[queries.length - 1].response && setLastQueryReturnedErr(false);
}
}, [queries[queries.length - 1]]);
return (
<div className="flex h-full flex-col justify-end gap-1">
<ConversationMessages
@@ -221,14 +177,7 @@ export default function Conversation() {
/>
<div className="bg-opacity-0 z-3 flex h-auto w-full max-w-[1300px] flex-col items-end self-center rounded-2xl py-1 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<div
{...getRootProps()}
className="flex w-full items-center rounded-[40px]"
>
<label htmlFor="file-upload" className="sr-only">
{t('modals.uploadDoc.label')}
</label>
<input {...getInputProps()} id="file-upload" />
<div className="flex w-full items-center rounded-[40px] px-2">
<MessageInput
onSubmit={(text) => {
handleQuestionSubmission(text);
@@ -243,26 +192,6 @@ export default function Conversation() {
{t('tagline')}
</p>
</div>
{handleDragActive && (
<div className="bg-opacity-50 dark:bg-gray-alpha pointer-events-none fixed top-0 left-0 z-30 flex size-full flex-col items-center justify-center bg-white">
<img className="filter dark:invert" src={DragFileUpload} />
<span className="text-outer-space dark:text-silver px-2 text-2xl font-bold">
{t('modals.uploadDoc.drag.title')}
</span>
<span className="text-s text-outer-space dark:text-silver w-48 p-2 text-center">
{t('modals.uploadDoc.drag.description')}
</span>
</div>
)}
{uploadModalState === 'ACTIVE' && (
<Upload
receivedFile={files}
setModalState={setUploadModalState}
isOnboarding={false}
renderTab={'file'}
close={() => setUploadModalState('INACTIVE')}
></Upload>
)}
</div>
);
}

View File

@@ -3,9 +3,9 @@
}
.list li:not(:first-child) {
margin-top: 1em;
margin-top: 0.5em;
}
.list li > .list {
margin-top: 1em;
margin-top: 0.5em;
}

View File

@@ -86,10 +86,7 @@ const ConversationBubble = forwardRef<
// const bubbleRef = useRef<HTMLDivElement | null>(null);
const chunks = useSelector(selectChunks);
const selectedDocs = useSelector(selectSelectedDocs);
const [isLikeHovered, setIsLikeHovered] = useState(false);
const [isEditClicked, setIsEditClicked] = useState(false);
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
const [isQuestionHovered, setIsQuestionHovered] = useState(false);
const [editInputBox, setEditInputBox] = useState<string>('');
const messageRef = useRef<HTMLDivElement>(null);
const [shouldShowToggle, setShouldShowToggle] = useState(false);
@@ -115,11 +112,7 @@ const ConversationBubble = forwardRef<
let bubble;
if (type === 'QUESTION') {
bubble = (
<div
onMouseEnter={() => setIsQuestionHovered(true)}
onMouseLeave={() => setIsQuestionHovered(false)}
className={className}
>
<div className={`group ${className}`}>
<div className="flex flex-col items-end">
{filesAttached && filesAttached.length > 0 && (
<div className="mr-12 mb-4 flex flex-wrap justify-end gap-2">
@@ -188,7 +181,7 @@ const ConversationBubble = forwardRef<
setIsEditClicked(true);
setEditInputBox(message ?? '');
}}
className={`hover:bg-light-silver mt-3 flex h-fit shrink-0 cursor-pointer items-center rounded-full p-2 pt-1.5 pl-1.5 dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
className={`hover:bg-light-silver mt-3 flex h-fit shrink-0 cursor-pointer items-center rounded-full p-2 pt-1.5 pl-1.5 dark:hover:bg-[#35363B] ${isEditClicked ? 'visible' : 'invisible group-hover:visible'}`}
>
<img src={Edit} alt="Edit" className="cursor-pointer" />
</button>
@@ -421,7 +414,7 @@ const ConversationBubble = forwardRef<
<Fragment key={index}>
{segment.type === 'text' ? (
<ReactMarkdown
className="fade-in leading-normal break-words whitespace-pre-wrap"
className="fade-in flex flex-col gap-3 leading-normal break-words whitespace-pre-wrap"
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
@@ -556,63 +549,51 @@ const ConversationBubble = forwardRef<
</div>
) : (
<>
<div className="relative mr-2 block items-center justify-center">
<CopyButton textToCopy={message} />
</div>
<div className="relative mr-2 block items-center justify-center">
<SpeakButton text={message} />
</div>
{handleFeedback && (
{!isStreaming && (
<>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isLikeHovered
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
: 'bg-white-3000 dark:bg-transparent'
}`}
>
<Like
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'LIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('LIKE');
}
}}
onMouseEnter={() => setIsLikeHovered(true)}
onMouseLeave={() => setIsLikeHovered(false)}
></Like>
</div>
</div>
<div className="relative mr-2 block items-center justify-center">
<CopyButton textToCopy={message} />
</div>
<div className="relative mr-2 block items-center justify-center">
<SpeakButton text={message} />
</div>
{handleFeedback && (
<>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
<Like
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'LIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('LIKE');
}
}}
></Like>
</div>
</div>
</div>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isDislikeHovered
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
: 'bg-white-3000 dark:bg-transparent'
}`}
>
<Dislike
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'DISLIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('DISLIKE');
}
}}
onMouseEnter={() => setIsDislikeHovered(true)}
onMouseLeave={() => setIsDislikeHovered(false)}
></Dislike>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
<Dislike
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'DISLIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('DISLIKE');
}
}}
></Dislike>
</div>
</div>
</div>
</div>
</div>
</>
)}
</>
)}
</>
@@ -654,7 +635,7 @@ function AllSources(sources: AllSourcesProps) {
<p className="text-left text-xl">{`${sources.sources.length} ${t('conversation.sources.title')}`}</p>
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%]"></div>
</div>
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
<div className="scrollbar-thin mt-6 flex h-[90%] w-52 flex-col gap-4 overflow-y-auto pr-3 sm:w-64">
{sources.sources.map((source, index) => {
const isExternalSource = source.link && source.link !== 'local';
return (

View File

@@ -161,14 +161,16 @@ export const SharedConversation = () => {
/>
<div className="flex w-full max-w-[1200px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
{apiKey ? (
<MessageInput
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
loading={status === 'loading'}
showSourceButton={false}
showToolButton={false}
/>
<div className="w-full px-2">
<MessageInput
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
loading={status === 'loading'}
showSourceButton={false}
showToolButton={false}
/>
</div>
) : (
<button
onClick={() => navigate('/')}

View File

@@ -7,7 +7,7 @@ export function handleFetchAnswer(
question: string,
signal: AbortSignal,
token: string | null,
selectedDocs: Doc[] | null,
selectedDocs: Doc[],
conversationId: string | null,
promptId: string | null,
chunks: string,
@@ -42,7 +42,7 @@ export function handleFetchAnswer(
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
isNoneDoc: selectedDocs.length === 0,
agent_id: agentId,
save_conversation: save_conversation,
};
@@ -52,7 +52,7 @@ export function handleFetchAnswer(
payload.attachments = attachments;
}
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 0) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);
@@ -91,7 +91,7 @@ export function handleFetchAnswerSteaming(
question: string,
signal: AbortSignal,
token: string | null,
selectedDocs: Doc[] | null,
selectedDocs: Doc[],
conversationId: string | null,
promptId: string | null,
chunks: string,
@@ -108,7 +108,7 @@ export function handleFetchAnswerSteaming(
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
isNoneDoc: selectedDocs.length === 0,
index: indx,
agent_id: agentId,
save_conversation: save_conversation,
@@ -119,7 +119,7 @@ export function handleFetchAnswerSteaming(
payload.attachments = attachments;
}
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 0) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);
@@ -185,7 +185,7 @@ export function handleFetchAnswerSteaming(
export function handleSearch(
question: string,
token: string | null,
selectedDocs: Doc[] | null,
selectedDocs: Doc[],
conversation_id: string | null,
chunks: string,
token_limit: number,
@@ -195,9 +195,9 @@ export function handleSearch(
conversation_id: conversation_id,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
isNoneDoc: selectedDocs.length === 0,
};
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 0) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);

View File

@@ -56,7 +56,7 @@ export const fetchAnswer = createAsyncThunk<
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.preference.selectedDocs || [],
currentConversationId,
state.preference.prompt.id,
state.preference.chunks,
@@ -163,7 +163,7 @@ export const fetchAnswer = createAsyncThunk<
question,
signal,
state.preference.token,
state.preference.selectedDocs!,
state.preference.selectedDocs || [],
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.chunks,

View File

@@ -1,168 +0,0 @@
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,
};
};

View File

@@ -118,18 +118,34 @@ layer(base);
background: transparent;
}
/* Light theme scrollbar */
&::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.5);
background: rgba(215, 215, 215, 1);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.7);
background: rgba(195, 195, 195, 1);
}
/* For Firefox */
/* Dark theme scrollbar */
.dark &::-webkit-scrollbar-thumb {
background: rgba(77, 78, 88, 1);
border-radius: 3px;
}
.dark &::-webkit-scrollbar-thumb:hover {
background: rgba(97, 98, 108, 1);
}
/* For Firefox - Light theme */
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
scrollbar-color: rgba(215, 215, 215, 1) transparent;
/* For Firefox - Dark theme */
.dark & {
scrollbar-color: rgba(77, 78, 88, 1) transparent;
}
}
@utility table-default {

View File

@@ -255,13 +255,14 @@
"addQuery": "Add Query"
},
"drag": {
"title": "Upload a source file",
"description": "Drop your file here to add it as a source"
"title": "Drop attachments here",
"description": "Release to upload your attachments"
},
"progress": {
"upload": "Upload is in progress",
"training": "Upload is in progress",
"completed": "Upload completed",
"failed": "Upload failed",
"wait": "This may take several minutes",
"preparing": "Preparing upload",
"tokenLimit": "Over the token limit, please consider uploading smaller document",
@@ -420,12 +421,12 @@
"title": "Sources",
"text": "Choose Your Sources",
"link": "Source link",
"view_more": "{{count}} more sources"
"view_more": "{{count}} more sources",
"noSourcesAvailable": "No sources available"
},
"attachments": {
"attach": "Attach",
"remove": "Remove attachment",
"uploadFailed": "Upload failed"
"remove": "Remove attachment"
},
"retry": "Retry"
}

View File

@@ -218,13 +218,14 @@
"addQuery": "Agregar Consulta"
},
"drag": {
"title": "Subir archivo fuente",
"description": "Arrastra tu archivo aquí para agregarlo como fuente"
"title": "Suelta los archivos adjuntos aquí",
"description": "Suelta para subir tus archivos adjuntos"
},
"progress": {
"upload": "Subida en progreso",
"training": "Subida en progreso",
"completed": "Subida completada",
"failed": "Error al subir",
"wait": "Esto puede tardar varios minutos",
"preparing": "Preparando subida",
"tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño",
@@ -383,12 +384,12 @@
"title": "Fuentes",
"link": "Enlace fuente",
"view_more": "Ver {{count}} más fuentes",
"text": "Elegir tus fuentes"
"text": "Elegir tus fuentes",
"noSourcesAvailable": "No hay fuentes disponibles"
},
"attachments": {
"attach": "Adjuntar",
"remove": "Eliminar adjunto",
"uploadFailed": "Error al subir"
"remove": "Eliminar adjunto"
},
"retry": "Reintentar"
}

View File

@@ -218,13 +218,14 @@
"addQuery": "クエリを追加"
},
"drag": {
"title": "ソースファイルをアップロード",
"description": "ファイルをここにドロップしてソースとして追加してください"
"title": "添付ファイルをここにドロップ",
"description": "リリースして添付ファイルをアップロード"
},
"progress": {
"upload": "アップロード中",
"training": "アップロード中",
"completed": "アップロード完了",
"failed": "アップロード失敗",
"wait": "数分かかる場合があります",
"preparing": "アップロードを準備中",
"tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください",
@@ -383,12 +384,12 @@
"title": "ソース",
"text": "ソーステキスト",
"link": "ソースリンク",
"view_more": "さらに{{count}}個のソース"
"view_more": "さらに{{count}}個のソース",
"noSourcesAvailable": "利用可能なソースがありません"
},
"attachments": {
"attach": "添付",
"remove": "添付ファイルを削除",
"uploadFailed": "アップロード失敗"
"remove": "添付ファイルを削除"
},
"retry": "再試行"
}

View File

@@ -218,13 +218,14 @@
"addQuery": "Добавить запрос"
},
"drag": {
"title": "Загрузить исходный файл",
"description": "Перетащите файл сюда, чтобы добавить его как источник"
"title": "Перетащите вложения сюда",
"description": "Отпустите, чтобы загрузить ваши вложения"
},
"progress": {
"upload": "Идет загрузка",
"training": "Идет загрузка",
"completed": "Загрузка завершена",
"failed": "Ошибка загрузки",
"wait": "Это может занять несколько минут",
"preparing": "Подготовка загрузки",
"tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера",
@@ -383,12 +384,12 @@
"title": "Источники",
"text": "Выберите ваши источники",
"link": "Ссылка на источник",
"view_more": "ещё {{count}} источников"
"view_more": "ещё {{count}} источников",
"noSourcesAvailable": "Нет доступных источников"
},
"attachments": {
"attach": "Прикрепить",
"remove": "Удалить вложение",
"uploadFailed": "Ошибка загрузки"
"remove": "Удалить вложение"
},
"retry": "Повторить"
}

View File

@@ -218,13 +218,14 @@
"addQuery": "新增查詢"
},
"drag": {
"title": "上傳來源檔案",
"description": "將檔案拖放到此處以新增為來源"
"title": "將附件拖放到此處",
"description": "釋放以上傳您的附件"
},
"progress": {
"upload": "正在上傳",
"training": "正在上傳",
"completed": "上傳完成",
"failed": "上傳失敗",
"wait": "這可能需要幾分鐘",
"preparing": "準備上傳",
"tokenLimit": "超出令牌限制,請考慮上傳較小的文檔",
@@ -383,12 +384,12 @@
"title": "來源",
"text": "來源文字",
"link": "來源連結",
"view_more": "查看更多 {{count}} 個來源"
"view_more": "查看更多 {{count}} 個來源",
"noSourcesAvailable": "沒有可用的來源"
},
"attachments": {
"attach": "附件",
"remove": "刪除附件",
"uploadFailed": "上傳失敗"
"remove": "刪除附件"
},
"retry": "重試"
}

View File

@@ -218,13 +218,14 @@
"addQuery": "添加查询"
},
"drag": {
"title": "上传源文件",
"description": "将文件拖放到此处以添加为源"
"title": "将附件拖放到此处",
"description": "释放以上传您的附件"
},
"progress": {
"upload": "正在上传",
"training": "正在上传",
"completed": "上传完成",
"failed": "上传失败",
"wait": "这可能需要几分钟",
"preparing": "准备上传",
"tokenLimit": "超出令牌限制,请考虑上传较小的文档",
@@ -383,12 +384,12 @@
"title": "来源",
"text": "来源文本",
"link": "来源链接",
"view_more": "还有{{count}}个来源"
"view_more": "还有{{count}}个来源",
"noSourcesAvailable": "没有可用的来源"
},
"attachments": {
"attach": "附件",
"remove": "删除附件",
"uploadFailed": "上传失败"
"remove": "删除附件"
},
"retry": "重试"
}

View File

@@ -1,158 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
import ConfirmationModal from './ConfirmationModal';
import WrapperModal from './WrapperModal';
export default function ChunkModal({
type,
modalState,
setModalState,
handleSubmit,
originalTitle,
originalText,
handleDelete,
}: {
type: 'ADD' | 'EDIT';
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
handleSubmit: (title: string, text: string) => void;
originalTitle?: string;
originalText?: string;
handleDelete?: () => void;
}) {
const { t } = useTranslation();
const [title, setTitle] = React.useState('');
const [chunkText, setChunkText] = React.useState('');
const [deleteModal, setDeleteModal] = React.useState<ActiveState>('INACTIVE');
React.useEffect(() => {
setTitle(originalTitle || '');
setChunkText(originalText || '');
}, [originalTitle, originalText]);
const resetForm = () => {
setTitle('');
setChunkText('');
};
const handleDeleteConfirmed = () => {
if (handleDelete) {
handleDelete();
}
setDeleteModal('INACTIVE');
setModalState('INACTIVE');
};
if (modalState !== 'ACTIVE') return null;
const content = (
<div>
<h2 className="text-jet dark:text-bright-gray px-3 text-xl font-semibold">
{t(`modals.chunk.${type === 'ADD' ? 'add' : 'edit'}`)}
</h2>
<div className="relative mt-6 px-3">
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={t('modals.chunk.title')}
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
<div className="relative mt-6 px-3">
<div className="border-silver dark:border-silver/40 rounded-lg border pt-3 pb-1">
<span className="text-gray-4000 dark:text-silver absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs dark:bg-[#26272E]">
{t('modals.chunk.bodyText')}
</span>
<textarea
id="chunk-body-text"
className="h-60 max-h-60 w-full resize-none px-3 outline-hidden dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label={t('modals.chunk.promptText')}
></textarea>
</div>
</div>
{type === 'ADD' ? (
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
resetForm();
}}
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
>
{t('modals.chunk.add')}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
resetForm();
}}
className="dark:text-light-gray cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
>
{t('modals.chunk.close')}
</button>
</div>
) : (
<div className="mt-8 flex w-full items-center justify-between px-3">
<button
className="rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-nowrap text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => {
setDeleteModal('ACTIVE');
}}
>
{t('modals.chunk.delete')}
</button>
<div className="flex flex-row-reverse gap-1">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
>
{t('modals.chunk.update')}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="dark:text-light-gray cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
>
{t('modals.chunk.close')}
</button>
</div>
</div>
)}
</div>
);
return (
<>
<WrapperModal
close={() => setModalState('INACTIVE')}
className="sm:w-[620px]"
isPerformingTask={true}
>
{content}
</WrapperModal>
{type === 'EDIT' && (
<ConfirmationModal
message={t('modals.chunk.deleteConfirmation')}
modalState={deleteModal}
setModalState={setDeleteModal}
handleSubmit={handleDeleteConfirmed}
submitLabel={t('modals.chunk.delete')}
/>
)}
</>
);
}

View File

@@ -103,12 +103,12 @@ export const ShareConversationModal = ({
};
return (
<WrapperModal close={close}>
<div className="flex flex-col gap-2">
<WrapperModal close={close} contentClassName="!overflow-visible">
<div className="flex w-[600px] max-w-[80vw] flex-col gap-2">
<h2 className="text-eerie-black dark:text-chinese-white text-xl font-medium">
{t('modals.shareConv.label')}
</h2>
<p className="text-eerie-black dark:text-silver/60 text-sm">
<p className="text-eerie-black dark:text-silver/60 text-sm leading-relaxed">
{t('modals.shareConv.note')}
</p>
<div className="flex items-center justify-between">

View File

@@ -111,16 +111,6 @@ export function setLocalPrompt(prompt: string): void {
export function setLocalRecentDocs(docs: Doc[] | null): void {
if (docs && docs.length > 0) {
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(docs));
docs.forEach((doc) => {
let docPath = 'default';
if (doc.type === 'local') {
docPath = 'local' + '/' + doc.name + '/';
}
userService
.checkDocs({ docs: docPath }, null)
.then((response) => response.json());
});
} else {
localStorage.removeItem('DocsGPTRecentDocs');
}

View File

@@ -15,7 +15,7 @@ export interface Preference {
prompt: { name: string; id: string; type: string };
chunks: string;
token_limit: number;
selectedDocs: Doc[] | null;
selectedDocs: Doc[];
sourceDocs: Doc[] | null;
conversations: {
data: { name: string; id: string }[] | null;
@@ -137,8 +137,11 @@ prefListenerMiddleware.startListening({
prefListenerMiddleware.startListening({
matcher: isAnyOf(setSelectedDocs),
effect: (action, listenerApi) => {
const state = listenerApi.getState() as RootState;
setLocalRecentDocs(
(listenerApi.getState() as RootState).preference.selectedDocs ?? null,
state.preference.selectedDocs.length > 0
? state.preference.selectedDocs
: null,
);
},
});
@@ -179,7 +182,7 @@ export const selectApiKey = (state: RootState) => state.preference.apiKey;
export const selectApiKeyStatus = (state: RootState) =>
!!state.preference.apiKey;
export const selectSelectedDocsStatus = (state: RootState) =>
!!state.preference.selectedDocs;
state.preference.selectedDocs.length > 0;
export const selectSourceDocs = (state: RootState) =>
state.preference.sourceDocs;
export const selectModalStateDeleteConv = (state: RootState) =>

View File

@@ -1,233 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Trash from '../assets/trash.svg';
import SkeletonLoader from '../components/SkeletonLoader';
import { useLoaderState } from '../hooks';
import ConfirmationModal from '../modals/ConfirmationModal';
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
import { selectToken } from '../preferences/preferenceSlice';
import { APIKeyData } from './types';
export default function APIKeys() {
const { t } = useTranslation();
const token = useSelector(selectToken);
const [isCreateModalOpen, setCreateModal] = useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false);
const [newKey, setNewKey] = useState('');
const [apiKeys, setApiKeys] = useState<APIKeyData[]>([]);
const [loading, setLoading] = useLoaderState(true);
const [keyToDelete, setKeyToDelete] = useState<{
id: string;
name: string;
} | null>(null);
const handleFetchKeys = async () => {
setLoading(true);
try {
const response = await userService.getAPIKeys(token);
if (!response.ok) {
throw new Error('Failed to fetch API Keys');
}
const apiKeys = await response.json();
setApiKeys(apiKeys);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
const handleDeleteKey = (id: string) => {
setLoading(true);
userService
.deleteAPIKey({ id }, token)
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete API Key');
}
return response.json();
})
.then((data) => {
if (data.success === true) {
setApiKeys((previous) => previous.filter((elem) => elem.id !== id));
}
setKeyToDelete(null);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
setLoading(false);
});
};
const handleCreateKey = (payload: {
name: string;
source?: string;
retriever?: string;
prompt_id: string;
chunks: string;
}) => {
setLoading(true);
userService
.createAPIKey(payload, token)
.then((response) => {
if (!response.ok) {
throw new Error('Failed to create API Key');
}
return response.json();
})
.then((data) => {
setApiKeys([...apiKeys, data]);
setCreateModal(false);
setNewKey(data.key);
setSaveKeyModal(true);
handleFetchKeys();
})
.catch((error) => {
console.error(error);
})
.finally(() => {
setLoading(false);
});
};
React.useEffect(() => {
handleFetchKeys();
}, []);
return (
<div className="mt-8 flex w-full max-w-full flex-col overflow-hidden">
<div className="relative flex grow flex-col">
<div className="mb-6">
<h2 className="text-sonic-silver text-base font-medium">
{t('settings.apiKeys.description')}
</h2>
</div>
<div className="mb-6 flex flex-col items-start justify-end gap-3 sm:flex-row sm:items-center">
<button
onClick={() => setCreateModal(true)}
className="bg-purple-30 hover:bg-violets-are-blue flex h-[30px] w-[108px] items-center justify-center rounded-full text-sm text-white"
title={t('settings.apiKeys.createNew')}
>
{t('settings.apiKeys.createNew')}
</button>
</div>
<div className="relative w-full">
<div className="dark:border-silver/40 overflow-hidden rounded-md border border-gray-300">
<div className="table-scroll overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr className="dark:border-silver/40 border-b border-gray-300">
<th className="text-sonic-silver w-[35%] px-4 py-3 text-left text-xs font-medium">
{t('settings.apiKeys.name')}
</th>
<th className="text-sonic-silver w-[35%] px-4 py-3 text-left text-xs font-medium">
{t('settings.apiKeys.sourceDoc')}
</th>
<th className="text-sonic-silver w-[25%] px-4 py-3 text-left text-xs font-medium">
<span className="hidden sm:inline">
{t('settings.apiKeys.key')}
</span>
<span className="sm:hidden">
{t('settings.apiKeys.key')}
</span>
</th>
<th className="w-[5%] px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0]">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="dark:divide-silver/40 divide-y divide-gray-300">
{loading ? (
<SkeletonLoader component="table" />
) : !apiKeys?.length ? (
<tr>
<td
colSpan={4}
className="bg-transparent py-4 text-center text-gray-700 dark:text-neutral-200"
>
{t('settings.apiKeys.noData')}
</td>
</tr>
) : (
Array.isArray(apiKeys) &&
apiKeys.map((element) => (
<tr
key={element.id}
className="group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
>
<td className="w-[35%] max-w-0 min-w-48 px-4 py-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0]">
<div className="truncate" title={element.name}>
{element.name}
</div>
</td>
<td className="w-[35%] max-w-0 min-w-48 px-4 py-4 text-sm text-gray-700 dark:text-[#E0E0E0]">
<div className="truncate" title={element.source}>
{element.source}
</div>
</td>
<td className="w-[25%] px-4 py-4 font-mono text-sm text-gray-700 dark:text-[#E0E0E0]">
<div className="truncate" title={element.key}>
{element.key}
</div>
</td>
<td className="w-[5%] px-4 py-4 text-right">
<div className="flex justify-end">
<button
onClick={() =>
setKeyToDelete({
id: element.id,
name: element.name,
})
}
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
>
<img
src={Trash}
alt={t('convTile.delete')}
className="h-4 w-4 opacity-60 hover:opacity-100"
/>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
{isCreateModalOpen && (
<CreateAPIKeyModal
createAPIKey={handleCreateKey}
close={() => setCreateModal(false)}
/>
)}
{isSaveKeyModalOpen && (
<SaveAPIKeyModal apiKey={newKey} close={() => setSaveKeyModal(false)} />
)}
{keyToDelete && (
<ConfirmationModal
message={t('settings.apiKeys.deleteConfirmation', {
name: keyToDelete.name,
})}
modalState="ACTIVE"
setModalState={() => setKeyToDelete(null)}
submitLabel={t('modals.deleteConv.delete')}
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
handleCancel={() => setKeyToDelete(null)}
variant="danger"
/>
)}
</div>
);
}

View File

@@ -1,112 +0,0 @@
import React from 'react';
import Dropdown from '../components/Dropdown';
const Widgets: React.FC<{
widgetScreenshot: File | null;
onWidgetScreenshotChange: (screenshot: File | null) => void;
}> = ({ widgetScreenshot, onWidgetScreenshotChange }) => {
const widgetSources = ['Source 1', 'Source 2', 'Source 3'];
const widgetMethods = ['Method 1', 'Method 2', 'Method 3'];
const widgetTypes = ['Type 1', 'Type 2', 'Type 3'];
const [selectedWidgetSource, setSelectedWidgetSource] = React.useState(
widgetSources[0],
);
const [selectedWidgetMethod, setSelectedWidgetMethod] = React.useState(
widgetMethods[0],
);
const [selectedWidgetType, setSelectedWidgetType] = React.useState(
widgetTypes[0],
);
// const [widgetScreenshot, setWidgetScreenshot] = useState<File | null>(null);
const [widgetCode, setWidgetCode] = React.useState<string>(''); // Your widget code state
const handleScreenshotChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const files = event.target.files;
if (files && files.length > 0) {
const selectedScreenshot = files[0];
onWidgetScreenshotChange(selectedScreenshot); // Update the screenshot in the parent component
}
};
const handleCopyToClipboard = () => {
// Create a new textarea element to select the text
const textArea = document.createElement('textarea');
textArea.value = widgetCode;
document.body.appendChild(textArea);
// Select and copy the text
textArea.select();
document.execCommand('copy');
// Clean up the textarea element
document.body.removeChild(textArea);
};
return (
<div>
<div className="mt-[59px]">
<p className="text-jet font-bold">Widget Source</p>
<Dropdown
options={widgetSources}
selectedValue={selectedWidgetSource}
onSelect={setSelectedWidgetSource}
/>
</div>
<div className="mt-5">
<p className="text-jet font-bold">Widget Method</p>
<Dropdown
options={widgetMethods}
selectedValue={selectedWidgetMethod}
onSelect={setSelectedWidgetMethod}
/>
</div>
<div className="mt-5">
<p className="text-jet font-bold">Widget Type</p>
<Dropdown
options={widgetTypes}
selectedValue={selectedWidgetType}
onSelect={setSelectedWidgetType}
/>
</div>
<div className="mt-6">
<p className="text-jet font-bold">Widget Code Snippet</p>
<textarea
rows={4}
value={widgetCode}
onChange={(e) => setWidgetCode(e.target.value)}
className="mt-3 w-full rounded-lg border-2 p-2"
/>
</div>
<div className="mt-1">
<button
onClick={handleCopyToClipboard}
className="rounded-lg bg-blue-400 px-2 py-2 font-bold text-white transition-all hover:bg-blue-600"
>
Copy
</button>
</div>
<div className="mt-4">
<p className="text-lg font-semibold">Widget Screenshot</p>
<input type="file" accept="image/*" onChange={handleScreenshotChange} />
</div>
{widgetScreenshot && (
<div className="mt-4">
<img
src={URL.createObjectURL(widgetScreenshot)}
alt="Widget Screenshot"
className="max-w-full rounded-lg border border-gray-300"
/>
</div>
)}
</div>
);
};
export default Widgets;

View File

@@ -25,16 +25,12 @@ import Sources from './Sources';
import General from './General';
import Logs from './Logs';
import Tools from './Tools';
import Widgets from './Widgets';
export default function Settings() {
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
null,
);
const getActiveTabFromPath = () => {
const path = location.pathname;
@@ -43,7 +39,6 @@ export default function Settings() {
return t('settings.analytics.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/widgets')) return 'Widgets';
return t('settings.general.label');
};
@@ -57,7 +52,6 @@ export default function Settings() {
navigate('/settings/analytics');
else if (tab === t('settings.logs.label')) navigate('/settings/logs');
else if (tab === t('settings.tools.label')) navigate('/settings/tools');
else if (tab === 'Widgets') navigate('/settings/widgets');
};
React.useEffect(() => {
@@ -72,9 +66,6 @@ export default function Settings() {
const token = useSelector(selectToken);
const documents = useSelector(selectSourceDocs);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const updateWidgetScreenshot = (screenshot: File | null) => {
setWidgetScreenshot(screenshot);
};
const updateDocumentsList = (documents: Doc[], index: number) => [
...documents.slice(0, index),
@@ -122,15 +113,6 @@ export default function Settings() {
<Route path="analytics" element={<Analytics />} />
<Route path="logs" element={<Logs />} />
<Route path="tools" element={<Tools />} />
<Route
path="widgets"
element={
<Widgets
widgetScreenshot={widgetScreenshot}
onWidgetScreenshotChange={updateWidgetScreenshot}
/>
}
/>
<Route path="*" element={<Navigate to="/settings" replace />} />
</Routes>
</div>

Some files were not shown because too many files have changed in this diff Show More