mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-05-10 04:21:31 +00:00
Compare commits
63 Commits
tests-util
...
fast-ebook
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcdb4fb5e8 | ||
|
|
e787c896eb | ||
|
|
23aeaff5db | ||
|
|
689dd79597 | ||
|
|
0c15af90b1 | ||
|
|
cdd6ff6557 | ||
|
|
72b3d94453 | ||
|
|
7e88d09e5d | ||
|
|
74a4a237dc | ||
|
|
c3f01c6619 | ||
|
|
6b408823d4 | ||
|
|
3fc81ac5d8 | ||
|
|
2652f8a5b0 | ||
|
|
d711eefe96 | ||
|
|
79206f3919 | ||
|
|
de971d9452 | ||
|
|
1b4d5ca0dd | ||
|
|
81989e8258 | ||
|
|
dc262d1698 | ||
|
|
69f9c93869 | ||
|
|
74bf80b25c | ||
|
|
d9a92a7208 | ||
|
|
02e93d993d | ||
|
|
6b6495f48c | ||
|
|
249dd9ce37 | ||
|
|
9134ab0478 | ||
|
|
10ef68c9d0 | ||
|
|
7d65cf1c2b | ||
|
|
13c6cc59c1 | ||
|
|
6381f7dd4e | ||
|
|
e6ac4008fe | ||
|
|
1af09f114d | ||
|
|
be7da983e7 | ||
|
|
8b9e595d85 | ||
|
|
398f3acc8d | ||
|
|
e04baa7ed8 | ||
|
|
e5586b6f20 | ||
|
|
addf57cab7 | ||
|
|
648b3f1d20 | ||
|
|
a75a9e23f9 | ||
|
|
73256389cf | ||
|
|
d609efca49 | ||
|
|
772860b667 | ||
|
|
ea2fd8b04a | ||
|
|
2c73deac20 | ||
|
|
47f3907e5e | ||
|
|
727495c553 | ||
|
|
a3b08a5b44 | ||
|
|
81532ada2a | ||
|
|
43f71374e5 | ||
|
|
d5c0322e2a | ||
|
|
3b66a3176c | ||
|
|
dc6db847ca | ||
|
|
ed0063aada | ||
|
|
9a6a55b6da | ||
|
|
12a8368216 | ||
|
|
3f6d6f15ea | ||
|
|
193ca6fd63 | ||
|
|
174dee0fe6 | ||
|
|
844167ba06 | ||
|
|
6fa3acb1ca | ||
|
|
9fd063266b | ||
|
|
324a8cd4cf |
18
SECURITY.md
18
SECURITY.md
@@ -2,13 +2,21 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Supported Versions:
|
Security patches target the latest release and the `main` branch. We recommend always running the most recent version.
|
||||||
|
|
||||||
Currently, we support security patches by committing changes and bumping the version published on Github.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Found a vulnerability? Please email us:
|
Preferred method: use GitHub's private vulnerability reporting flow:
|
||||||
|
https://github.com/arc53/DocsGPT/security
|
||||||
|
|
||||||
security@arc53.com
|
Then click **Report a vulnerability**.
|
||||||
|
|
||||||
|
|
||||||
|
Alternatively, email us at: security@arc53.com
|
||||||
|
|
||||||
|
We aim to acknowledge reports within 48 hours.
|
||||||
|
|
||||||
|
## Incident Handling
|
||||||
|
|
||||||
|
Arc53 maintains internal incident response procedures. If you believe an active exploit is occurring, include **URGENT** in your report subject line.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Generator, List, Optional
|
from typing import Any, Dict, Generator, List, Optional
|
||||||
|
|
||||||
from application.agents.tool_executor import ToolExecutor
|
from application.agents.tool_executor import ToolExecutor
|
||||||
from application.core.json_schema_utils import (
|
from application.core.json_schema_utils import (
|
||||||
@@ -9,6 +10,7 @@ from application.core.json_schema_utils import (
|
|||||||
normalize_json_schema_payload,
|
normalize_json_schema_payload,
|
||||||
)
|
)
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
|
from application.llm.handlers.base import ToolCall
|
||||||
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
||||||
from application.llm.llm_creator import LLMCreator
|
from application.llm.llm_creator import LLMCreator
|
||||||
from application.logging import build_stack_data, log_activity, LogContext
|
from application.logging import build_stack_data, log_activity, LogContext
|
||||||
@@ -113,6 +115,153 @@ class BaseAgent(ABC):
|
|||||||
) -> Generator[Dict, None, None]:
|
) -> Generator[Dict, None, None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def gen_continuation(
|
||||||
|
self,
|
||||||
|
messages: List[Dict],
|
||||||
|
tools_dict: Dict,
|
||||||
|
pending_tool_calls: List[Dict],
|
||||||
|
tool_actions: List[Dict],
|
||||||
|
) -> Generator[Dict, None, None]:
|
||||||
|
"""Resume generation after tool actions are resolved.
|
||||||
|
|
||||||
|
Processes the client-provided *tool_actions* (approvals, denials,
|
||||||
|
or client-side results), appends the resulting messages, then
|
||||||
|
hands back to the LLM to continue the conversation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: The saved messages array from the pause point.
|
||||||
|
tools_dict: The saved tools dictionary.
|
||||||
|
pending_tool_calls: The pending tool call descriptors from the pause.
|
||||||
|
tool_actions: Client-provided actions resolving the pending calls.
|
||||||
|
"""
|
||||||
|
self._prepare_tools(tools_dict)
|
||||||
|
|
||||||
|
actions_by_id = {a["call_id"]: a for a in tool_actions}
|
||||||
|
|
||||||
|
# Build a single assistant message containing all tool calls so
|
||||||
|
# the message history matches the format LLM providers expect
|
||||||
|
# (one assistant message with N tool_calls, followed by N tool results).
|
||||||
|
tc_objects: List[Dict[str, Any]] = []
|
||||||
|
for pending in pending_tool_calls:
|
||||||
|
call_id = pending["call_id"]
|
||||||
|
args = pending["arguments"]
|
||||||
|
args_str = (
|
||||||
|
json.dumps(args) if isinstance(args, dict) else (args or "{}")
|
||||||
|
)
|
||||||
|
tc_obj: Dict[str, Any] = {
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": pending["name"],
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if pending.get("thought_signature"):
|
||||||
|
tc_obj["thought_signature"] = pending["thought_signature"]
|
||||||
|
tc_objects.append(tc_obj)
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": tc_objects,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Now process each pending call and append tool result messages
|
||||||
|
for pending in pending_tool_calls:
|
||||||
|
call_id = pending["call_id"]
|
||||||
|
args = pending["arguments"]
|
||||||
|
action = actions_by_id.get(call_id)
|
||||||
|
if not action:
|
||||||
|
action = {
|
||||||
|
"call_id": call_id,
|
||||||
|
"decision": "denied",
|
||||||
|
"comment": "No response provided",
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.get("decision") == "approved":
|
||||||
|
# Execute the tool server-side
|
||||||
|
tc = ToolCall(
|
||||||
|
id=call_id,
|
||||||
|
name=pending["name"],
|
||||||
|
arguments=(
|
||||||
|
json.dumps(args) if isinstance(args, dict) else args
|
||||||
|
),
|
||||||
|
)
|
||||||
|
tool_gen = self._execute_tool_action(tools_dict, tc)
|
||||||
|
tool_response = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
event = next(tool_gen)
|
||||||
|
yield event
|
||||||
|
except StopIteration as e:
|
||||||
|
tool_response, _ = e.value
|
||||||
|
break
|
||||||
|
messages.append(
|
||||||
|
self.llm_handler.create_tool_message(tc, tool_response)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action.get("decision") == "denied":
|
||||||
|
comment = action.get("comment", "")
|
||||||
|
denial = (
|
||||||
|
f"Tool execution denied by user. Reason: {comment}"
|
||||||
|
if comment
|
||||||
|
else "Tool execution denied by user."
|
||||||
|
)
|
||||||
|
tc = ToolCall(
|
||||||
|
id=call_id, name=pending["name"], arguments=args
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
self.llm_handler.create_tool_message(tc, denial)
|
||||||
|
)
|
||||||
|
yield {
|
||||||
|
"type": "tool_call",
|
||||||
|
"data": {
|
||||||
|
"tool_name": pending.get("tool_name", "unknown"),
|
||||||
|
"call_id": call_id,
|
||||||
|
"action_name": pending.get("llm_name", pending["name"]),
|
||||||
|
"arguments": args,
|
||||||
|
"status": "denied",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
elif "result" in action:
|
||||||
|
result = action["result"]
|
||||||
|
result_str = (
|
||||||
|
json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else result
|
||||||
|
)
|
||||||
|
tc = ToolCall(
|
||||||
|
id=call_id, name=pending["name"], arguments=args
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
self.llm_handler.create_tool_message(tc, result_str)
|
||||||
|
)
|
||||||
|
yield {
|
||||||
|
"type": "tool_call",
|
||||||
|
"data": {
|
||||||
|
"tool_name": pending.get("tool_name", "unknown"),
|
||||||
|
"call_id": call_id,
|
||||||
|
"action_name": pending.get("llm_name", pending["name"]),
|
||||||
|
"arguments": args,
|
||||||
|
"result": (
|
||||||
|
result_str[:50] + "..."
|
||||||
|
if len(result_str) > 50
|
||||||
|
else result_str
|
||||||
|
),
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resume the LLM loop with the updated messages
|
||||||
|
llm_response = self._llm_gen(messages)
|
||||||
|
yield from self._handle_response(
|
||||||
|
llm_response, tools_dict, messages, None
|
||||||
|
)
|
||||||
|
|
||||||
|
yield {"sources": self.retrieved_docs}
|
||||||
|
yield {"tool_calls": self._get_truncated_tool_calls()}
|
||||||
|
|
||||||
# ---- Tool delegation (thin wrappers around ToolExecutor) ----
|
# ---- Tool delegation (thin wrappers around ToolExecutor) ----
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -267,28 +416,35 @@ class BaseAgent(ABC):
|
|||||||
if "tool_calls" in i:
|
if "tool_calls" in i:
|
||||||
for tool_call in i["tool_calls"]:
|
for tool_call in i["tool_calls"]:
|
||||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||||
|
args = tool_call.get("arguments")
|
||||||
function_call_dict = {
|
args_str = (
|
||||||
"function_call": {
|
json.dumps(args)
|
||||||
"name": tool_call.get("action_name"),
|
if isinstance(args, dict)
|
||||||
"args": tool_call.get("arguments"),
|
else (args or "{}")
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.get("action_name"),
|
|
||||||
"response": {"result": tool_call.get("result")},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
)
|
||||||
messages.append(
|
messages.append({
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_call.get("action_name", ""),
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
result = tool_call.get("result")
|
||||||
|
result_str = (
|
||||||
|
json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else (result or "")
|
||||||
)
|
)
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"content": result_str,
|
||||||
|
})
|
||||||
messages.append({"role": "user", "content": query})
|
messages.append({"role": "user", "content": query})
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|||||||
@@ -593,16 +593,22 @@ class ResearchAgent(BaseAgent):
|
|||||||
)
|
)
|
||||||
result = result_str
|
result = result_str
|
||||||
|
|
||||||
function_call_content = {
|
import json as _json
|
||||||
"function_call": {
|
|
||||||
"name": call.name,
|
args_str = (
|
||||||
"args": call.arguments,
|
_json.dumps(call.arguments)
|
||||||
"call_id": call_id,
|
if isinstance(call.arguments, dict)
|
||||||
}
|
else call.arguments
|
||||||
}
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_content]}
|
|
||||||
)
|
)
|
||||||
|
messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": call.name, "arguments": args_str},
|
||||||
|
}],
|
||||||
|
})
|
||||||
tool_message = self.llm_handler.create_tool_message(call, result)
|
tool_message = self.llm_handler.create_tool_message(call, result)
|
||||||
messages.append(tool_message)
|
messages.append(tool_message)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, List, Optional
|
from collections import Counter
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
@@ -31,12 +32,23 @@ class ToolExecutor:
|
|||||||
self.tool_calls: List[Dict] = []
|
self.tool_calls: List[Dict] = []
|
||||||
self._loaded_tools: Dict[str, object] = {}
|
self._loaded_tools: Dict[str, object] = {}
|
||||||
self.conversation_id: Optional[str] = None
|
self.conversation_id: Optional[str] = None
|
||||||
|
self.client_tools: Optional[List[Dict]] = None
|
||||||
|
self._name_to_tool: Dict[str, Tuple[str, str]] = {}
|
||||||
|
self._tool_to_name: Dict[Tuple[str, str], str] = {}
|
||||||
|
|
||||||
def get_tools(self) -> Dict[str, Dict]:
|
def get_tools(self) -> Dict[str, Dict]:
|
||||||
"""Load tool configs from DB based on user context."""
|
"""Load tool configs from DB based on user context.
|
||||||
|
|
||||||
|
If *client_tools* have been set on this executor, they are
|
||||||
|
automatically merged into the returned dict.
|
||||||
|
"""
|
||||||
if self.user_api_key:
|
if self.user_api_key:
|
||||||
return self._get_tools_by_api_key(self.user_api_key)
|
tools = self._get_tools_by_api_key(self.user_api_key)
|
||||||
return self._get_user_tools(self.user or "local")
|
else:
|
||||||
|
tools = self._get_user_tools(self.user or "local")
|
||||||
|
if self.client_tools:
|
||||||
|
self.merge_client_tools(tools, self.client_tools)
|
||||||
|
return tools
|
||||||
|
|
||||||
def _get_tools_by_api_key(self, api_key: str) -> Dict[str, Dict]:
|
def _get_tools_by_api_key(self, api_key: str) -> Dict[str, Dict]:
|
||||||
mongo = MongoDB.get_client()
|
mongo = MongoDB.get_client()
|
||||||
@@ -65,29 +77,123 @@ class ToolExecutor:
|
|||||||
user_tools = list(user_tools)
|
user_tools = list(user_tools)
|
||||||
return {str(i): tool for i, tool in enumerate(user_tools)}
|
return {str(i): tool for i, tool in enumerate(user_tools)}
|
||||||
|
|
||||||
def prepare_tools_for_llm(self, tools_dict: Dict) -> List[Dict]:
|
def merge_client_tools(
|
||||||
"""Convert tool configs to LLM function schemas."""
|
self, tools_dict: Dict, client_tools: List[Dict]
|
||||||
return [
|
) -> Dict:
|
||||||
{
|
"""Merge client-provided tool definitions into tools_dict.
|
||||||
"type": "function",
|
|
||||||
"function": {
|
Client tools use the standard function-calling format::
|
||||||
"name": f"{action['name']}_{tool_id}",
|
|
||||||
"description": action["description"],
|
[{"type": "function", "function": {"name": "get_weather",
|
||||||
"parameters": self._build_tool_parameters(action),
|
"description": "...", "parameters": {...}}}]
|
||||||
},
|
|
||||||
|
They are stored in *tools_dict* with ``client_side: True`` so that
|
||||||
|
:meth:`check_pause` returns a pause signal instead of trying to
|
||||||
|
execute them server-side.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tools_dict: The mutable server tools dict (will be modified in place).
|
||||||
|
client_tools: List of tool definitions in function-calling format.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated *tools_dict* (same reference, for convenience).
|
||||||
|
"""
|
||||||
|
for i, ct in enumerate(client_tools):
|
||||||
|
func = ct.get("function", ct) # tolerate bare {"name":..} too
|
||||||
|
name = func.get("name", f"clienttool{i}")
|
||||||
|
tool_id = f"ct{i}"
|
||||||
|
|
||||||
|
tools_dict[tool_id] = {
|
||||||
|
"name": name,
|
||||||
|
"client_side": True,
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"description": func.get("description", ""),
|
||||||
|
"active": True,
|
||||||
|
"parameters": func.get("parameters", {}),
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
for tool_id, tool in tools_dict.items()
|
return tools_dict
|
||||||
if (
|
|
||||||
(tool["name"] == "api_tool" and "actions" in tool.get("config", {}))
|
def prepare_tools_for_llm(self, tools_dict: Dict) -> List[Dict]:
|
||||||
or (tool["name"] != "api_tool" and "actions" in tool)
|
"""Convert tool configs to LLM function schemas.
|
||||||
)
|
|
||||||
for action in (
|
Action names are kept clean for the LLM:
|
||||||
|
- Unique action names appear as-is (e.g. ``get_weather``).
|
||||||
|
- Duplicate action names get numbered suffixes (e.g. ``search_1``,
|
||||||
|
``search_2``).
|
||||||
|
|
||||||
|
A reverse mapping is stored in ``_name_to_tool`` so that tool calls
|
||||||
|
can be routed back to the correct ``(tool_id, action_name)`` without
|
||||||
|
brittle string splitting.
|
||||||
|
"""
|
||||||
|
# Pass 1: collect entries and count action name occurrences
|
||||||
|
entries: List[Tuple[str, str, Dict, bool]] = [] # (tool_id, action_name, action, is_client)
|
||||||
|
name_counts: Counter = Counter()
|
||||||
|
|
||||||
|
for tool_id, tool in tools_dict.items():
|
||||||
|
is_api = tool["name"] == "api_tool"
|
||||||
|
is_client = tool.get("client_side", False)
|
||||||
|
|
||||||
|
if is_api and "actions" not in tool.get("config", {}):
|
||||||
|
continue
|
||||||
|
if not is_api and "actions" not in tool:
|
||||||
|
continue
|
||||||
|
|
||||||
|
actions = (
|
||||||
tool["config"]["actions"].values()
|
tool["config"]["actions"].values()
|
||||||
if tool["name"] == "api_tool"
|
if is_api
|
||||||
else tool["actions"]
|
else tool["actions"]
|
||||||
)
|
)
|
||||||
if action.get("active", True)
|
|
||||||
]
|
for action in actions:
|
||||||
|
if not action.get("active", True):
|
||||||
|
continue
|
||||||
|
entries.append((tool_id, action["name"], action, is_client))
|
||||||
|
name_counts[action["name"]] += 1
|
||||||
|
|
||||||
|
# Pass 2: assign LLM-visible names and build mappings
|
||||||
|
self._name_to_tool = {}
|
||||||
|
self._tool_to_name = {}
|
||||||
|
collision_counters: Dict[str, int] = {}
|
||||||
|
all_llm_names: set = set()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for tool_id, action_name, action, is_client in entries:
|
||||||
|
if name_counts[action_name] == 1:
|
||||||
|
llm_name = action_name
|
||||||
|
else:
|
||||||
|
counter = collision_counters.get(action_name, 1)
|
||||||
|
candidate = f"{action_name}_{counter}"
|
||||||
|
# Skip if candidate collides with a unique action name
|
||||||
|
while candidate in all_llm_names or (
|
||||||
|
candidate in name_counts and name_counts[candidate] == 1
|
||||||
|
):
|
||||||
|
counter += 1
|
||||||
|
candidate = f"{action_name}_{counter}"
|
||||||
|
collision_counters[action_name] = counter + 1
|
||||||
|
llm_name = candidate
|
||||||
|
|
||||||
|
all_llm_names.add(llm_name)
|
||||||
|
self._name_to_tool[llm_name] = (tool_id, action_name)
|
||||||
|
self._tool_to_name[(tool_id, action_name)] = llm_name
|
||||||
|
|
||||||
|
if is_client:
|
||||||
|
params = action.get("parameters", {})
|
||||||
|
else:
|
||||||
|
params = self._build_tool_parameters(action)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": llm_name,
|
||||||
|
"description": action.get("description", ""),
|
||||||
|
"parameters": params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
def _build_tool_parameters(self, action: Dict) -> Dict:
|
def _build_tool_parameters(self, action: Dict) -> Dict:
|
||||||
params = {"type": "object", "properties": {}, "required": []}
|
params = {"type": "object", "properties": {}, "required": []}
|
||||||
@@ -104,23 +210,81 @@ class ToolExecutor:
|
|||||||
params["required"].append(k)
|
params["required"].append(k)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def check_pause(
|
||||||
|
self, tools_dict: Dict, call, llm_class_name: str
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""Check if a tool call requires pausing for approval or client execution.
|
||||||
|
|
||||||
|
Returns a dict describing the pending action if pause is needed, None otherwise.
|
||||||
|
"""
|
||||||
|
parser = ToolActionParser(llm_class_name, name_mapping=self._name_to_tool)
|
||||||
|
tool_id, action_name, call_args = parser.parse_args(call)
|
||||||
|
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
||||||
|
llm_name = getattr(call, "name", "")
|
||||||
|
|
||||||
|
if tool_id is None or action_name is None or tool_id not in tools_dict:
|
||||||
|
return None # Will be handled as error by execute()
|
||||||
|
|
||||||
|
tool_data = tools_dict[tool_id]
|
||||||
|
|
||||||
|
# Client-side tools
|
||||||
|
if tool_data.get("client_side"):
|
||||||
|
return {
|
||||||
|
"call_id": call_id,
|
||||||
|
"name": llm_name,
|
||||||
|
"tool_name": tool_data.get("name", "unknown"),
|
||||||
|
"tool_id": tool_id,
|
||||||
|
"action_name": action_name,
|
||||||
|
"llm_name": llm_name,
|
||||||
|
"arguments": call_args if isinstance(call_args, dict) else {},
|
||||||
|
"pause_type": "requires_client_execution",
|
||||||
|
"thought_signature": getattr(call, "thought_signature", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Approval required
|
||||||
|
if tool_data["name"] == "api_tool":
|
||||||
|
action_data = tool_data.get("config", {}).get("actions", {}).get(
|
||||||
|
action_name, {}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
action_data = next(
|
||||||
|
(a for a in tool_data.get("actions", []) if a["name"] == action_name),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if action_data.get("require_approval"):
|
||||||
|
return {
|
||||||
|
"call_id": call_id,
|
||||||
|
"name": llm_name,
|
||||||
|
"tool_name": tool_data.get("name", "unknown"),
|
||||||
|
"tool_id": tool_id,
|
||||||
|
"action_name": action_name,
|
||||||
|
"llm_name": llm_name,
|
||||||
|
"arguments": call_args if isinstance(call_args, dict) else {},
|
||||||
|
"pause_type": "awaiting_approval",
|
||||||
|
"thought_signature": getattr(call, "thought_signature", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def execute(self, tools_dict: Dict, call, llm_class_name: str):
|
def execute(self, tools_dict: Dict, call, llm_class_name: str):
|
||||||
"""Execute a tool call. Yields status events, returns (result, call_id)."""
|
"""Execute a tool call. Yields status events, returns (result, call_id)."""
|
||||||
parser = ToolActionParser(llm_class_name)
|
parser = ToolActionParser(llm_class_name, name_mapping=self._name_to_tool)
|
||||||
tool_id, action_name, call_args = parser.parse_args(call)
|
tool_id, action_name, call_args = parser.parse_args(call)
|
||||||
|
llm_name = getattr(call, "name", "unknown")
|
||||||
|
|
||||||
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
||||||
|
|
||||||
if tool_id is None or action_name is None:
|
if tool_id is None or action_name is None:
|
||||||
error_message = f"Error: Failed to parse LLM tool call. Tool name: {getattr(call, 'name', 'unknown')}"
|
error_message = f"Error: Failed to parse LLM tool call. Tool name: {llm_name}"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
|
|
||||||
tool_call_data = {
|
tool_call_data = {
|
||||||
"tool_name": "unknown",
|
"tool_name": "unknown",
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
"action_name": getattr(call, "name", "unknown"),
|
"action_name": llm_name,
|
||||||
"arguments": call_args or {},
|
"arguments": call_args or {},
|
||||||
"result": f"Failed to parse tool call. Invalid tool name format: {getattr(call, 'name', 'unknown')}",
|
"result": f"Failed to parse tool call. Invalid tool name format: {llm_name}",
|
||||||
}
|
}
|
||||||
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
|
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
|
||||||
self.tool_calls.append(tool_call_data)
|
self.tool_calls.append(tool_call_data)
|
||||||
@@ -133,7 +297,7 @@ class ToolExecutor:
|
|||||||
tool_call_data = {
|
tool_call_data = {
|
||||||
"tool_name": "unknown",
|
"tool_name": "unknown",
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
"action_name": f"{action_name}_{tool_id}",
|
"action_name": llm_name,
|
||||||
"arguments": call_args,
|
"arguments": call_args,
|
||||||
"result": f"Tool with ID {tool_id} not found. Available tools: {list(tools_dict.keys())}",
|
"result": f"Tool with ID {tool_id} not found. Available tools: {list(tools_dict.keys())}",
|
||||||
}
|
}
|
||||||
@@ -144,7 +308,7 @@ class ToolExecutor:
|
|||||||
tool_call_data = {
|
tool_call_data = {
|
||||||
"tool_name": tools_dict[tool_id]["name"],
|
"tool_name": tools_dict[tool_id]["name"],
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
"action_name": f"{action_name}_{tool_id}",
|
"action_name": llm_name,
|
||||||
"arguments": call_args,
|
"arguments": call_args,
|
||||||
}
|
}
|
||||||
yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}}
|
yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
|
|||||||
|
|
||||||
|
|
||||||
class Tool(ABC):
|
class Tool(ABC):
|
||||||
|
internal: bool = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute_action(self, action_name: str, **kwargs):
|
def execute_action(self, action_name: str, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class InternalSearchTool(Tool):
|
|||||||
- list_files action: browse the file/folder structure
|
- list_files action: browse the file/folder structure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
internal = True
|
||||||
|
|
||||||
def __init__(self, config: Dict):
|
def __init__(self, config: Dict):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.retrieved_docs: List[Dict] = []
|
self.retrieved_docs: List[Dict] = []
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from application.api.user.tasks import mcp_oauth_status_task, mcp_oauth_task
|
|||||||
from application.cache import get_redis_instance
|
from application.cache import get_redis_instance
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
|
from application.core.url_validation import SSRFError, validate_url
|
||||||
from application.security.encryption import decrypt_credentials
|
from application.security.encryption import decrypt_credentials
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -61,7 +62,8 @@ class MCPTool(Tool):
|
|||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.server_url = config.get("server_url", "")
|
raw_url = config.get("server_url", "")
|
||||||
|
self.server_url = self._validate_server_url(raw_url) if raw_url else ""
|
||||||
self.transport_type = config.get("transport_type", "auto")
|
self.transport_type = config.get("transport_type", "auto")
|
||||||
self.auth_type = config.get("auth_type", "none")
|
self.auth_type = config.get("auth_type", "none")
|
||||||
self.timeout = config.get("timeout", 30)
|
self.timeout = config.get("timeout", 30)
|
||||||
@@ -87,6 +89,18 @@ class MCPTool(Tool):
|
|||||||
if self.server_url and self.auth_type != "oauth":
|
if self.server_url and self.auth_type != "oauth":
|
||||||
self._setup_client()
|
self._setup_client()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_server_url(server_url: str) -> str:
|
||||||
|
"""Validate server_url to prevent SSRF to internal networks.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the URL points to a private/internal address.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return validate_url(server_url)
|
||||||
|
except SSRFError as exc:
|
||||||
|
raise ValueError(f"Invalid MCP server URL: {exc}") from exc
|
||||||
|
|
||||||
def _resolve_redirect_uri(self, configured_redirect_uri: Optional[str]) -> str:
|
def _resolve_redirect_uri(self, configured_redirect_uri: Optional[str]) -> str:
|
||||||
if configured_redirect_uri:
|
if configured_redirect_uri:
|
||||||
return configured_redirect_uri.rstrip("/")
|
return configured_redirect_uri.rstrip("/")
|
||||||
@@ -108,8 +122,9 @@ class MCPTool(Tool):
|
|||||||
auth_key = ""
|
auth_key = ""
|
||||||
if self.auth_type == "oauth":
|
if self.auth_type == "oauth":
|
||||||
scopes_str = ",".join(self.oauth_scopes) if self.oauth_scopes else "none"
|
scopes_str = ",".join(self.oauth_scopes) if self.oauth_scopes else "none"
|
||||||
|
oauth_identity = self.user_id or self.oauth_task_id or "anonymous"
|
||||||
auth_key = (
|
auth_key = (
|
||||||
f"oauth:{self.oauth_client_name}:{scopes_str}:{self.redirect_uri}"
|
f"oauth:{oauth_identity}:{self.oauth_client_name}:{scopes_str}:{self.redirect_uri}"
|
||||||
)
|
)
|
||||||
elif self.auth_type in ["bearer"]:
|
elif self.auth_type in ["bearer"]:
|
||||||
token = self.auth_credentials.get(
|
token = self.auth_credentials.get(
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class ThinkTool(Tool):
|
|||||||
The reasoning content is captured in tool_call data for transparency.
|
The reasoning content is captured in tool_call data for transparency.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
internal = True
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ToolActionParser:
|
class ToolActionParser:
|
||||||
def __init__(self, llm_type):
|
def __init__(self, llm_type, name_mapping=None):
|
||||||
self.llm_type = llm_type
|
self.llm_type = llm_type
|
||||||
|
self.name_mapping = name_mapping
|
||||||
self.parsers = {
|
self.parsers = {
|
||||||
"OpenAILLM": self._parse_openai_llm,
|
"OpenAILLM": self._parse_openai_llm,
|
||||||
"GoogleLLM": self._parse_google_llm,
|
"GoogleLLM": self._parse_google_llm,
|
||||||
@@ -16,22 +17,33 @@ class ToolActionParser:
|
|||||||
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
|
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
|
||||||
return parser(call)
|
return parser(call)
|
||||||
|
|
||||||
|
def _resolve_via_mapping(self, call_name):
|
||||||
|
"""Look up (tool_id, action_name) from the name mapping if available."""
|
||||||
|
if self.name_mapping and call_name in self.name_mapping:
|
||||||
|
return self.name_mapping[call_name]
|
||||||
|
return None
|
||||||
|
|
||||||
def _parse_openai_llm(self, call):
|
def _parse_openai_llm(self, call):
|
||||||
try:
|
try:
|
||||||
call_args = json.loads(call.arguments)
|
call_args = json.loads(call.arguments)
|
||||||
|
|
||||||
|
resolved = self._resolve_via_mapping(call.name)
|
||||||
|
if resolved:
|
||||||
|
return resolved[0], resolved[1], call_args
|
||||||
|
|
||||||
|
# Fallback: legacy split on "_" for backward compatibility
|
||||||
tool_parts = call.name.split("_")
|
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:
|
if len(tool_parts) < 2:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
f"Invalid tool name format: {call.name}. "
|
||||||
|
"Could not resolve via mapping or legacy parsing."
|
||||||
)
|
)
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
tool_id = tool_parts[-1]
|
tool_id = tool_parts[-1]
|
||||||
action_name = "_".join(tool_parts[:-1])
|
action_name = "_".join(tool_parts[:-1])
|
||||||
|
|
||||||
# Validate that tool_id looks like a numerical ID
|
|
||||||
if not tool_id.isdigit():
|
if not tool_id.isdigit():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||||
@@ -45,19 +57,24 @@ class ToolActionParser:
|
|||||||
def _parse_google_llm(self, call):
|
def _parse_google_llm(self, call):
|
||||||
try:
|
try:
|
||||||
call_args = call.arguments
|
call_args = call.arguments
|
||||||
|
|
||||||
|
resolved = self._resolve_via_mapping(call.name)
|
||||||
|
if resolved:
|
||||||
|
return resolved[0], resolved[1], call_args
|
||||||
|
|
||||||
|
# Fallback: legacy split on "_" for backward compatibility
|
||||||
tool_parts = call.name.split("_")
|
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:
|
if len(tool_parts) < 2:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
f"Invalid tool name format: {call.name}. "
|
||||||
|
"Could not resolve via mapping or legacy parsing."
|
||||||
)
|
)
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
tool_id = tool_parts[-1]
|
tool_id = tool_parts[-1]
|
||||||
action_name = "_".join(tool_parts[:-1])
|
action_name = "_".join(tool_parts[:-1])
|
||||||
|
|
||||||
# Validate that tool_id looks like a numerical ID
|
|
||||||
if not tool_id.isdigit():
|
if not tool_id.isdigit():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ToolManager:
|
|||||||
continue
|
continue
|
||||||
module = importlib.import_module(f"application.agents.tools.{name}")
|
module = importlib.import_module(f"application.agents.tools.{name}")
|
||||||
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if issubclass(obj, Tool) and obj is not Tool:
|
if issubclass(obj, Tool) and obj is not Tool and not obj.internal:
|
||||||
tool_config = self.config.get(name, {})
|
tool_config = self.config.get(name, {})
|
||||||
self.tools[name] = obj(tool_config)
|
self.tools[name] = obj(tool_config)
|
||||||
|
|
||||||
|
|||||||
@@ -74,57 +74,76 @@ class AnswerResource(Resource, BaseAnswerResource):
|
|||||||
decoded_token = getattr(request, "decoded_token", None)
|
decoded_token = getattr(request, "decoded_token", None)
|
||||||
processor = StreamProcessor(data, decoded_token)
|
processor = StreamProcessor(data, decoded_token)
|
||||||
try:
|
try:
|
||||||
agent = processor.build_agent(data.get("question", ""))
|
# ---- Continuation mode ----
|
||||||
if not processor.decoded_token:
|
if data.get("tool_actions"):
|
||||||
return make_response({"error": "Unauthorized"}, 401)
|
(
|
||||||
|
agent,
|
||||||
|
messages,
|
||||||
|
tools_dict,
|
||||||
|
pending_tool_calls,
|
||||||
|
tool_actions,
|
||||||
|
) = processor.resume_from_tool_actions(
|
||||||
|
data["tool_actions"], data["conversation_id"]
|
||||||
|
)
|
||||||
|
if not processor.decoded_token:
|
||||||
|
return make_response({"error": "Unauthorized"}, 401)
|
||||||
|
if error := self.check_usage(processor.agent_config):
|
||||||
|
return error
|
||||||
|
stream = self.complete_stream(
|
||||||
|
question="",
|
||||||
|
agent=agent,
|
||||||
|
conversation_id=processor.conversation_id,
|
||||||
|
user_api_key=processor.agent_config.get("user_api_key"),
|
||||||
|
decoded_token=processor.decoded_token,
|
||||||
|
agent_id=processor.agent_id,
|
||||||
|
model_id=processor.model_id,
|
||||||
|
_continuation={
|
||||||
|
"messages": messages,
|
||||||
|
"tools_dict": tools_dict,
|
||||||
|
"pending_tool_calls": pending_tool_calls,
|
||||||
|
"tool_actions": tool_actions,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# ---- Normal mode ----
|
||||||
|
agent = processor.build_agent(data.get("question", ""))
|
||||||
|
if not processor.decoded_token:
|
||||||
|
return make_response({"error": "Unauthorized"}, 401)
|
||||||
|
|
||||||
if error := self.check_usage(processor.agent_config):
|
if error := self.check_usage(processor.agent_config):
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
stream = self.complete_stream(
|
||||||
|
question=data["question"],
|
||||||
|
agent=agent,
|
||||||
|
conversation_id=processor.conversation_id,
|
||||||
|
user_api_key=processor.agent_config.get("user_api_key"),
|
||||||
|
decoded_token=processor.decoded_token,
|
||||||
|
isNoneDoc=data.get("isNoneDoc"),
|
||||||
|
index=None,
|
||||||
|
should_save_conversation=data.get("save_conversation", True),
|
||||||
|
agent_id=processor.agent_id,
|
||||||
|
is_shared_usage=processor.is_shared_usage,
|
||||||
|
shared_token=processor.shared_token,
|
||||||
|
model_id=processor.model_id,
|
||||||
|
)
|
||||||
|
|
||||||
stream = self.complete_stream(
|
|
||||||
question=data["question"],
|
|
||||||
agent=agent,
|
|
||||||
conversation_id=processor.conversation_id,
|
|
||||||
user_api_key=processor.agent_config.get("user_api_key"),
|
|
||||||
decoded_token=processor.decoded_token,
|
|
||||||
isNoneDoc=data.get("isNoneDoc"),
|
|
||||||
index=None,
|
|
||||||
should_save_conversation=data.get("save_conversation", True),
|
|
||||||
agent_id=processor.agent_id,
|
|
||||||
is_shared_usage=processor.is_shared_usage,
|
|
||||||
shared_token=processor.shared_token,
|
|
||||||
model_id=processor.model_id,
|
|
||||||
)
|
|
||||||
stream_result = self.process_response_stream(stream)
|
stream_result = self.process_response_stream(stream)
|
||||||
|
|
||||||
if len(stream_result) == 7:
|
if stream_result["error"]:
|
||||||
(
|
return make_response({"error": stream_result["error"]}, 400)
|
||||||
conversation_id,
|
|
||||||
response,
|
|
||||||
sources,
|
|
||||||
tool_calls,
|
|
||||||
thought,
|
|
||||||
error,
|
|
||||||
structured_info,
|
|
||||||
) = stream_result
|
|
||||||
else:
|
|
||||||
conversation_id, response, sources, tool_calls, thought, error = (
|
|
||||||
stream_result
|
|
||||||
)
|
|
||||||
structured_info = None
|
|
||||||
|
|
||||||
if error:
|
|
||||||
return make_response({"error": error}, 400)
|
|
||||||
result = {
|
result = {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": stream_result["conversation_id"],
|
||||||
"answer": response,
|
"answer": stream_result["answer"],
|
||||||
"sources": sources,
|
"sources": stream_result["sources"],
|
||||||
"tool_calls": tool_calls,
|
"tool_calls": stream_result["tool_calls"],
|
||||||
"thought": thought,
|
"thought": stream_result["thought"],
|
||||||
}
|
}
|
||||||
|
|
||||||
if structured_info:
|
extra_info = stream_result.get("extra")
|
||||||
result.update(structured_info)
|
if extra_info:
|
||||||
|
result.update(extra_info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any, Dict, Generator, List, Optional
|
|||||||
from flask import jsonify, make_response, Response
|
from flask import jsonify, make_response, Response
|
||||||
from flask_restx import Namespace
|
from flask_restx import Namespace
|
||||||
|
|
||||||
|
from application.api.answer.services.continuation_service import ContinuationService
|
||||||
from application.api.answer.services.conversation_service import ConversationService
|
from application.api.answer.services.conversation_service import ConversationService
|
||||||
from application.core.model_utils import (
|
from application.core.model_utils import (
|
||||||
get_api_key_for_provider,
|
get_api_key_for_provider,
|
||||||
@@ -39,7 +40,16 @@ class BaseAnswerResource:
|
|||||||
def validate_request(
|
def validate_request(
|
||||||
self, data: Dict[str, Any], require_conversation_id: bool = False
|
self, data: Dict[str, Any], require_conversation_id: bool = False
|
||||||
) -> Optional[Response]:
|
) -> Optional[Response]:
|
||||||
"""Common request validation"""
|
"""Common request validation.
|
||||||
|
|
||||||
|
Continuation requests (``tool_actions`` present) require
|
||||||
|
``conversation_id`` but not ``question``.
|
||||||
|
"""
|
||||||
|
if data.get("tool_actions"):
|
||||||
|
# Continuation mode — question is not required
|
||||||
|
if missing := check_required_fields(data, ["conversation_id"]):
|
||||||
|
return missing
|
||||||
|
return None
|
||||||
required_fields = ["question"]
|
required_fields = ["question"]
|
||||||
if require_conversation_id:
|
if require_conversation_id:
|
||||||
required_fields.append("conversation_id")
|
required_fields.append("conversation_id")
|
||||||
@@ -177,6 +187,7 @@ class BaseAnswerResource:
|
|||||||
is_shared_usage: bool = False,
|
is_shared_usage: bool = False,
|
||||||
shared_token: Optional[str] = None,
|
shared_token: Optional[str] = None,
|
||||||
model_id: Optional[str] = None,
|
model_id: Optional[str] = None,
|
||||||
|
_continuation: Optional[Dict] = None,
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
"""
|
"""
|
||||||
Generator function that streams the complete conversation response.
|
Generator function that streams the complete conversation response.
|
||||||
@@ -207,8 +218,19 @@ class BaseAnswerResource:
|
|||||||
schema_info = None
|
schema_info = None
|
||||||
structured_chunks = []
|
structured_chunks = []
|
||||||
query_metadata = {}
|
query_metadata = {}
|
||||||
|
paused = False
|
||||||
|
|
||||||
for line in agent.gen(query=question):
|
if _continuation:
|
||||||
|
gen_iter = agent.gen_continuation(
|
||||||
|
messages=_continuation["messages"],
|
||||||
|
tools_dict=_continuation["tools_dict"],
|
||||||
|
pending_tool_calls=_continuation["pending_tool_calls"],
|
||||||
|
tool_actions=_continuation["tool_actions"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
gen_iter = agent.gen(query=question)
|
||||||
|
|
||||||
|
for line in gen_iter:
|
||||||
if "metadata" in line:
|
if "metadata" in line:
|
||||||
query_metadata.update(line["metadata"])
|
query_metadata.update(line["metadata"])
|
||||||
elif "answer" in line:
|
elif "answer" in line:
|
||||||
@@ -244,15 +266,21 @@ class BaseAnswerResource:
|
|||||||
data = json.dumps({"type": "thought", "thought": line["thought"]})
|
data = json.dumps({"type": "thought", "thought": line["thought"]})
|
||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
elif "type" in line:
|
elif "type" in line:
|
||||||
if line.get("type") == "error":
|
if line.get("type") == "tool_calls_pending":
|
||||||
|
# Save continuation state and end the stream
|
||||||
|
paused = True
|
||||||
|
data = json.dumps(line)
|
||||||
|
yield f"data: {data}\n\n"
|
||||||
|
elif line.get("type") == "error":
|
||||||
sanitized_error = {
|
sanitized_error = {
|
||||||
"type": "error",
|
"type": "error",
|
||||||
"error": sanitize_api_error(line.get("error", "An error occurred"))
|
"error": sanitize_api_error(line.get("error", "An error occurred"))
|
||||||
}
|
}
|
||||||
data = json.dumps(sanitized_error)
|
data = json.dumps(sanitized_error)
|
||||||
|
yield f"data: {data}\n\n"
|
||||||
else:
|
else:
|
||||||
data = json.dumps(line)
|
data = json.dumps(line)
|
||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
if is_structured and structured_chunks:
|
if is_structured and structured_chunks:
|
||||||
structured_data = {
|
structured_data = {
|
||||||
"type": "structured_answer",
|
"type": "structured_answer",
|
||||||
@@ -262,6 +290,93 @@ class BaseAnswerResource:
|
|||||||
}
|
}
|
||||||
data = json.dumps(structured_data)
|
data = json.dumps(structured_data)
|
||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
|
|
||||||
|
# ---- Paused: save continuation state and end stream early ----
|
||||||
|
if paused:
|
||||||
|
continuation = getattr(agent, "_pending_continuation", None)
|
||||||
|
if continuation:
|
||||||
|
# Ensure we have a conversation_id — create a partial
|
||||||
|
# conversation if this is the first turn.
|
||||||
|
if not conversation_id and should_save_conversation:
|
||||||
|
try:
|
||||||
|
provider = (
|
||||||
|
get_provider_from_model_id(model_id)
|
||||||
|
if model_id
|
||||||
|
else settings.LLM_PROVIDER
|
||||||
|
)
|
||||||
|
sys_api_key = get_api_key_for_provider(
|
||||||
|
provider or settings.LLM_PROVIDER
|
||||||
|
)
|
||||||
|
llm = LLMCreator.create_llm(
|
||||||
|
provider or settings.LLM_PROVIDER,
|
||||||
|
api_key=sys_api_key,
|
||||||
|
user_api_key=user_api_key,
|
||||||
|
decoded_token=decoded_token,
|
||||||
|
model_id=model_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
)
|
||||||
|
conversation_id = (
|
||||||
|
self.conversation_service.save_conversation(
|
||||||
|
None,
|
||||||
|
question,
|
||||||
|
response_full,
|
||||||
|
thought,
|
||||||
|
source_log_docs,
|
||||||
|
tool_calls,
|
||||||
|
llm,
|
||||||
|
model_id or self.default_model_id,
|
||||||
|
decoded_token,
|
||||||
|
api_key=user_api_key,
|
||||||
|
agent_id=agent_id,
|
||||||
|
is_shared_usage=is_shared_usage,
|
||||||
|
shared_token=shared_token,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to create conversation for continuation: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if conversation_id:
|
||||||
|
try:
|
||||||
|
cont_service = ContinuationService()
|
||||||
|
cont_service.save_state(
|
||||||
|
conversation_id=str(conversation_id),
|
||||||
|
user=decoded_token.get("sub", "local"),
|
||||||
|
messages=continuation["messages"],
|
||||||
|
pending_tool_calls=continuation["pending_tool_calls"],
|
||||||
|
tools_dict=continuation["tools_dict"],
|
||||||
|
tool_schemas=getattr(agent, "tools", []),
|
||||||
|
agent_config={
|
||||||
|
"model_id": model_id or self.default_model_id,
|
||||||
|
"llm_name": getattr(agent, "llm_name", settings.LLM_PROVIDER),
|
||||||
|
"api_key": getattr(agent, "api_key", None),
|
||||||
|
"user_api_key": user_api_key,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_type": agent.__class__.__name__,
|
||||||
|
"prompt": getattr(agent, "prompt", ""),
|
||||||
|
"json_schema": getattr(agent, "json_schema", None),
|
||||||
|
"retriever_config": getattr(agent, "retriever_config", None),
|
||||||
|
},
|
||||||
|
client_tools=getattr(
|
||||||
|
agent.tool_executor, "client_tools", None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to save continuation state: {str(e)}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
id_data = {"type": "id", "id": str(conversation_id)}
|
||||||
|
data = json.dumps(id_data)
|
||||||
|
yield f"data: {data}\n\n"
|
||||||
|
|
||||||
|
data = json.dumps({"type": "end"})
|
||||||
|
yield f"data: {data}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
if isNoneDoc:
|
if isNoneDoc:
|
||||||
for doc in source_log_docs:
|
for doc in source_log_docs:
|
||||||
doc["source"] = "None"
|
doc["source"] = "None"
|
||||||
@@ -425,8 +540,13 @@ class BaseAnswerResource:
|
|||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
def process_response_stream(self, stream):
|
def process_response_stream(self, stream) -> Dict[str, Any]:
|
||||||
"""Process the stream response for non-streaming endpoint"""
|
"""Process the stream response for non-streaming endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: conversation_id, answer, sources, tool_calls,
|
||||||
|
thought, error, and optional extra.
|
||||||
|
"""
|
||||||
conversation_id = ""
|
conversation_id = ""
|
||||||
response_full = ""
|
response_full = ""
|
||||||
source_log_docs = []
|
source_log_docs = []
|
||||||
@@ -435,6 +555,7 @@ class BaseAnswerResource:
|
|||||||
stream_ended = False
|
stream_ended = False
|
||||||
is_structured = False
|
is_structured = False
|
||||||
schema_info = None
|
schema_info = None
|
||||||
|
pending_tool_calls = None
|
||||||
|
|
||||||
for line in stream:
|
for line in stream:
|
||||||
try:
|
try:
|
||||||
@@ -453,11 +574,22 @@ class BaseAnswerResource:
|
|||||||
source_log_docs = event["source"]
|
source_log_docs = event["source"]
|
||||||
elif event["type"] == "tool_calls":
|
elif event["type"] == "tool_calls":
|
||||||
tool_calls = event["tool_calls"]
|
tool_calls = event["tool_calls"]
|
||||||
|
elif event["type"] == "tool_calls_pending":
|
||||||
|
pending_tool_calls = event.get("data", {}).get(
|
||||||
|
"pending_tool_calls", []
|
||||||
|
)
|
||||||
elif event["type"] == "thought":
|
elif event["type"] == "thought":
|
||||||
thought = event["thought"]
|
thought = event["thought"]
|
||||||
elif event["type"] == "error":
|
elif event["type"] == "error":
|
||||||
logger.error(f"Error from stream: {event['error']}")
|
logger.error(f"Error from stream: {event['error']}")
|
||||||
return None, None, None, None, event["error"], None
|
return {
|
||||||
|
"conversation_id": None,
|
||||||
|
"answer": None,
|
||||||
|
"sources": None,
|
||||||
|
"tool_calls": None,
|
||||||
|
"thought": None,
|
||||||
|
"error": event["error"],
|
||||||
|
}
|
||||||
elif event["type"] == "end":
|
elif event["type"] == "end":
|
||||||
stream_ended = True
|
stream_ended = True
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
@@ -465,18 +597,30 @@ class BaseAnswerResource:
|
|||||||
continue
|
continue
|
||||||
if not stream_ended:
|
if not stream_ended:
|
||||||
logger.error("Stream ended unexpectedly without an 'end' event.")
|
logger.error("Stream ended unexpectedly without an 'end' event.")
|
||||||
return None, None, None, None, "Stream ended unexpectedly", None
|
return {
|
||||||
result = (
|
"conversation_id": None,
|
||||||
conversation_id,
|
"answer": None,
|
||||||
response_full,
|
"sources": None,
|
||||||
source_log_docs,
|
"tool_calls": None,
|
||||||
tool_calls,
|
"thought": None,
|
||||||
thought,
|
"error": "Stream ended unexpectedly",
|
||||||
None,
|
}
|
||||||
)
|
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"answer": response_full,
|
||||||
|
"sources": source_log_docs,
|
||||||
|
"tool_calls": tool_calls,
|
||||||
|
"thought": thought,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_tool_calls is not None:
|
||||||
|
result["extra"] = {"pending_tool_calls": pending_tool_calls}
|
||||||
|
|
||||||
if is_structured:
|
if is_structured:
|
||||||
result = result + ({"structured": True, "schema": schema_info},)
|
result["extra"] = {"structured": True, "schema": schema_info}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def error_stream_generate(self, err_response):
|
def error_stream_generate(self, err_response):
|
||||||
|
|||||||
@@ -79,7 +79,47 @@ class StreamResource(Resource, BaseAnswerResource):
|
|||||||
return error
|
return error
|
||||||
decoded_token = getattr(request, "decoded_token", None)
|
decoded_token = getattr(request, "decoded_token", None)
|
||||||
processor = StreamProcessor(data, decoded_token)
|
processor = StreamProcessor(data, decoded_token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# ---- Continuation mode ----
|
||||||
|
if data.get("tool_actions"):
|
||||||
|
(
|
||||||
|
agent,
|
||||||
|
messages,
|
||||||
|
tools_dict,
|
||||||
|
pending_tool_calls,
|
||||||
|
tool_actions,
|
||||||
|
) = processor.resume_from_tool_actions(
|
||||||
|
data["tool_actions"], data["conversation_id"]
|
||||||
|
)
|
||||||
|
if not processor.decoded_token:
|
||||||
|
return Response(
|
||||||
|
self.error_stream_generate("Unauthorized"),
|
||||||
|
status=401,
|
||||||
|
mimetype="text/event-stream",
|
||||||
|
)
|
||||||
|
if error := self.check_usage(processor.agent_config):
|
||||||
|
return error
|
||||||
|
return Response(
|
||||||
|
self.complete_stream(
|
||||||
|
question="",
|
||||||
|
agent=agent,
|
||||||
|
conversation_id=processor.conversation_id,
|
||||||
|
user_api_key=processor.agent_config.get("user_api_key"),
|
||||||
|
decoded_token=processor.decoded_token,
|
||||||
|
agent_id=processor.agent_id,
|
||||||
|
model_id=processor.model_id,
|
||||||
|
_continuation={
|
||||||
|
"messages": messages,
|
||||||
|
"tools_dict": tools_dict,
|
||||||
|
"pending_tool_calls": pending_tool_calls,
|
||||||
|
"tool_actions": tool_actions,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
mimetype="text/event-stream",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Normal mode ----
|
||||||
agent = processor.build_agent(data["question"])
|
agent = processor.build_agent(data["question"])
|
||||||
if not processor.decoded_token:
|
if not processor.decoded_token:
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Message reconstruction utilities for compression."""
|
"""Message reconstruction utilities for compression."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
@@ -49,28 +50,35 @@ class MessageBuilder:
|
|||||||
if include_tool_calls and "tool_calls" in query:
|
if include_tool_calls and "tool_calls" in query:
|
||||||
for tool_call in query["tool_calls"]:
|
for tool_call in query["tool_calls"]:
|
||||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||||
|
args = tool_call.get("arguments")
|
||||||
function_call_dict = {
|
args_str = (
|
||||||
"function_call": {
|
json.dumps(args)
|
||||||
"name": tool_call.get("action_name"),
|
if isinstance(args, dict)
|
||||||
"args": tool_call.get("arguments"),
|
else (args or "{}")
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.get("action_name"),
|
|
||||||
"response": {"result": tool_call.get("result")},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
)
|
||||||
messages.append(
|
messages.append({
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_call.get("action_name", ""),
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
result = tool_call.get("result")
|
||||||
|
result_str = (
|
||||||
|
json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else (result or "")
|
||||||
)
|
)
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"content": result_str,
|
||||||
|
})
|
||||||
|
|
||||||
# If no recent queries (everything was compressed), add a continuation user message
|
# If no recent queries (everything was compressed), add a continuation user message
|
||||||
if len(recent_queries) == 0 and compressed_summary:
|
if len(recent_queries) == 0 and compressed_summary:
|
||||||
@@ -180,28 +188,35 @@ class MessageBuilder:
|
|||||||
if include_tool_calls and "tool_calls" in query:
|
if include_tool_calls and "tool_calls" in query:
|
||||||
for tool_call in query["tool_calls"]:
|
for tool_call in query["tool_calls"]:
|
||||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||||
|
args = tool_call.get("arguments")
|
||||||
function_call_dict = {
|
args_str = (
|
||||||
"function_call": {
|
json.dumps(args)
|
||||||
"name": tool_call.get("action_name"),
|
if isinstance(args, dict)
|
||||||
"args": tool_call.get("arguments"),
|
else (args or "{}")
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.get("action_name"),
|
|
||||||
"response": {"result": tool_call.get("result")},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuilt_messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
)
|
||||||
rebuilt_messages.append(
|
rebuilt_messages.append({
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_call.get("action_name", ""),
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
result = tool_call.get("result")
|
||||||
|
result_str = (
|
||||||
|
json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else (result or "")
|
||||||
)
|
)
|
||||||
|
rebuilt_messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"content": result_str,
|
||||||
|
})
|
||||||
|
|
||||||
# If no recent queries (everything was compressed), add a continuation user message
|
# If no recent queries (everything was compressed), add a continuation user message
|
||||||
if len(recent_queries) == 0 and compressed_summary:
|
if len(recent_queries) == 0 and compressed_summary:
|
||||||
|
|||||||
141
application/api/answer/services/continuation_service.py
Normal file
141
application/api/answer/services/continuation_service.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""Service for saving and restoring tool-call continuation state.
|
||||||
|
|
||||||
|
When a stream pauses (tool needs approval or client-side execution),
|
||||||
|
the full execution state is persisted to MongoDB so the client can
|
||||||
|
resume later by sending tool_actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
from application.core.mongo_db import MongoDB
|
||||||
|
from application.core.settings import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TTL for pending states — auto-cleaned after this period
|
||||||
|
PENDING_STATE_TTL_SECONDS = 30 * 60 # 30 minutes
|
||||||
|
|
||||||
|
|
||||||
|
def _make_serializable(obj: Any) -> Any:
|
||||||
|
"""Recursively convert MongoDB ObjectIds and other non-JSON types."""
|
||||||
|
if isinstance(obj, ObjectId):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {str(k): _make_serializable(v) for k, v in obj.items()}
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [_make_serializable(v) for v in obj]
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return obj.decode("utf-8", errors="replace")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuationService:
|
||||||
|
"""Manages pending tool-call state in MongoDB."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo[settings.MONGO_DB_NAME]
|
||||||
|
self.collection = db["pending_tool_state"]
|
||||||
|
self._ensure_indexes()
|
||||||
|
|
||||||
|
def _ensure_indexes(self):
|
||||||
|
try:
|
||||||
|
self.collection.create_index(
|
||||||
|
"expires_at", expireAfterSeconds=0
|
||||||
|
)
|
||||||
|
self.collection.create_index(
|
||||||
|
[("conversation_id", 1), ("user", 1)], unique=True
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Indexes may already exist or mongomock doesn't support TTL
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save_state(
|
||||||
|
self,
|
||||||
|
conversation_id: str,
|
||||||
|
user: str,
|
||||||
|
messages: List[Dict],
|
||||||
|
pending_tool_calls: List[Dict],
|
||||||
|
tools_dict: Dict,
|
||||||
|
tool_schemas: List[Dict],
|
||||||
|
agent_config: Dict,
|
||||||
|
client_tools: Optional[List[Dict]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Save execution state for later continuation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: The conversation this state belongs to.
|
||||||
|
user: Owner user ID.
|
||||||
|
messages: Full messages array at the pause point.
|
||||||
|
pending_tool_calls: Tool calls awaiting client action.
|
||||||
|
tools_dict: Serializable tools configuration dict.
|
||||||
|
tool_schemas: LLM-formatted tool schemas (agent.tools).
|
||||||
|
agent_config: Config needed to recreate the agent on resume.
|
||||||
|
client_tools: Client-provided tool schemas for client-side execution.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The string ID of the saved state document.
|
||||||
|
"""
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
expires_at = now + datetime.timedelta(seconds=PENDING_STATE_TTL_SECONDS)
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"user": user,
|
||||||
|
"messages": _make_serializable(messages),
|
||||||
|
"pending_tool_calls": _make_serializable(pending_tool_calls),
|
||||||
|
"tools_dict": _make_serializable(tools_dict),
|
||||||
|
"tool_schemas": _make_serializable(tool_schemas),
|
||||||
|
"agent_config": _make_serializable(agent_config),
|
||||||
|
"client_tools": _make_serializable(client_tools) if client_tools else None,
|
||||||
|
"created_at": now,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upsert — only one pending state per conversation per user
|
||||||
|
result = self.collection.replace_one(
|
||||||
|
{"conversation_id": conversation_id, "user": user},
|
||||||
|
doc,
|
||||||
|
upsert=True,
|
||||||
|
)
|
||||||
|
state_id = str(result.upserted_id) if result.upserted_id else conversation_id
|
||||||
|
logger.info(
|
||||||
|
f"Saved continuation state for conversation {conversation_id} "
|
||||||
|
f"with {len(pending_tool_calls)} pending tool call(s)"
|
||||||
|
)
|
||||||
|
return state_id
|
||||||
|
|
||||||
|
def load_state(
|
||||||
|
self, conversation_id: str, user: str
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Load pending continuation state.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The state dict, or None if no pending state exists.
|
||||||
|
"""
|
||||||
|
doc = self.collection.find_one(
|
||||||
|
{"conversation_id": conversation_id, "user": user}
|
||||||
|
)
|
||||||
|
if not doc:
|
||||||
|
return None
|
||||||
|
doc["_id"] = str(doc["_id"])
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def delete_state(self, conversation_id: str, user: str) -> bool:
|
||||||
|
"""Delete pending state after successful resumption.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if a document was deleted.
|
||||||
|
"""
|
||||||
|
result = self.collection.delete_one(
|
||||||
|
{"conversation_id": conversation_id, "user": user}
|
||||||
|
)
|
||||||
|
if result.deleted_count:
|
||||||
|
logger.info(
|
||||||
|
f"Deleted continuation state for conversation {conversation_id}"
|
||||||
|
)
|
||||||
|
return result.deleted_count > 0
|
||||||
@@ -112,6 +112,7 @@ class StreamProcessor:
|
|||||||
self._required_tool_actions: Optional[Dict[str, Set[Optional[str]]]] = None
|
self._required_tool_actions: Optional[Dict[str, Set[Optional[str]]]] = None
|
||||||
self.compressed_summary: Optional[str] = None
|
self.compressed_summary: Optional[str] = None
|
||||||
self.compressed_summary_tokens: int = 0
|
self.compressed_summary_tokens: int = 0
|
||||||
|
self._agent_data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize all required components for processing"""
|
"""Initialize all required components for processing"""
|
||||||
@@ -359,22 +360,29 @@ class StreamProcessor:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def _configure_source(self):
|
def _configure_source(self):
|
||||||
"""Configure the source based on agent data"""
|
"""Configure the source based on agent data.
|
||||||
api_key = self.data.get("api_key") or self.agent_key
|
|
||||||
|
|
||||||
if api_key:
|
The literal string ``"default"`` is a placeholder meaning "no
|
||||||
agent_data = self._get_data_from_api_key(api_key)
|
ingested source" and is normalized to an empty source so that no
|
||||||
|
retrieval is attempted.
|
||||||
|
"""
|
||||||
|
if self._agent_data:
|
||||||
|
agent_data = self._agent_data
|
||||||
|
|
||||||
if agent_data.get("sources") and len(agent_data["sources"]) > 0:
|
if agent_data.get("sources") and len(agent_data["sources"]) > 0:
|
||||||
source_ids = [
|
source_ids = [
|
||||||
source["id"] for source in agent_data["sources"] if source.get("id")
|
source["id"]
|
||||||
|
for source in agent_data["sources"]
|
||||||
|
if source.get("id") and source["id"] != "default"
|
||||||
]
|
]
|
||||||
if source_ids:
|
if source_ids:
|
||||||
self.source = {"active_docs": source_ids}
|
self.source = {"active_docs": source_ids}
|
||||||
else:
|
else:
|
||||||
self.source = {}
|
self.source = {}
|
||||||
self.all_sources = agent_data["sources"]
|
self.all_sources = [
|
||||||
elif agent_data.get("source"):
|
s for s in agent_data["sources"] if s.get("id") != "default"
|
||||||
|
]
|
||||||
|
elif agent_data.get("source") and agent_data["source"] != "default":
|
||||||
self.source = {"active_docs": agent_data["source"]}
|
self.source = {"active_docs": agent_data["source"]}
|
||||||
self.all_sources = [
|
self.all_sources = [
|
||||||
{
|
{
|
||||||
@@ -387,11 +395,24 @@ class StreamProcessor:
|
|||||||
self.all_sources = []
|
self.all_sources = []
|
||||||
return
|
return
|
||||||
if "active_docs" in self.data:
|
if "active_docs" in self.data:
|
||||||
self.source = {"active_docs": self.data["active_docs"]}
|
active_docs = self.data["active_docs"]
|
||||||
|
if active_docs and active_docs != "default":
|
||||||
|
self.source = {"active_docs": active_docs}
|
||||||
|
else:
|
||||||
|
self.source = {}
|
||||||
return
|
return
|
||||||
self.source = {}
|
self.source = {}
|
||||||
self.all_sources = []
|
self.all_sources = []
|
||||||
|
|
||||||
|
def _has_active_docs(self) -> bool:
|
||||||
|
"""Return True if a real document source is configured for retrieval."""
|
||||||
|
active_docs = self.source.get("active_docs") if self.source else None
|
||||||
|
if not active_docs:
|
||||||
|
return False
|
||||||
|
if active_docs == "default":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _resolve_agent_id(self) -> Optional[str]:
|
def _resolve_agent_id(self) -> Optional[str]:
|
||||||
"""Resolve agent_id from request, then fall back to conversation context."""
|
"""Resolve agent_id from request, then fall back to conversation context."""
|
||||||
request_agent_id = self.data.get("agent_id")
|
request_agent_id = self.data.get("agent_id")
|
||||||
@@ -433,48 +454,39 @@ class StreamProcessor:
|
|||||||
effective_key = self.data.get("api_key") or self.agent_key
|
effective_key = self.data.get("api_key") or self.agent_key
|
||||||
|
|
||||||
if effective_key:
|
if effective_key:
|
||||||
data_key = self._get_data_from_api_key(effective_key)
|
self._agent_data = self._get_data_from_api_key(effective_key)
|
||||||
if data_key.get("_id"):
|
if self._agent_data.get("_id"):
|
||||||
self.agent_id = str(data_key.get("_id"))
|
self.agent_id = str(self._agent_data.get("_id"))
|
||||||
|
|
||||||
self.agent_config.update(
|
self.agent_config.update(
|
||||||
{
|
{
|
||||||
"prompt_id": data_key.get("prompt_id", "default"),
|
"prompt_id": self._agent_data.get("prompt_id", "default"),
|
||||||
"agent_type": data_key.get("agent_type", settings.AGENT_NAME),
|
"agent_type": self._agent_data.get("agent_type", settings.AGENT_NAME),
|
||||||
"user_api_key": effective_key,
|
"user_api_key": effective_key,
|
||||||
"json_schema": data_key.get("json_schema"),
|
"json_schema": self._agent_data.get("json_schema"),
|
||||||
"default_model_id": data_key.get("default_model_id", ""),
|
"default_model_id": self._agent_data.get("default_model_id", ""),
|
||||||
"models": data_key.get("models", []),
|
"models": self._agent_data.get("models", []),
|
||||||
|
"allow_system_prompt_override": self._agent_data.get(
|
||||||
|
"allow_system_prompt_override", False
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set identity context
|
# Set identity context
|
||||||
if self.data.get("api_key"):
|
if self.data.get("api_key"):
|
||||||
# External API key: use the key owner's identity
|
# External API key: use the key owner's identity
|
||||||
self.initial_user_id = data_key.get("user")
|
self.initial_user_id = self._agent_data.get("user")
|
||||||
self.decoded_token = {"sub": data_key.get("user")}
|
self.decoded_token = {"sub": self._agent_data.get("user")}
|
||||||
elif self.is_shared_usage:
|
elif self.is_shared_usage:
|
||||||
# Shared agent: keep the caller's identity
|
# Shared agent: keep the caller's identity
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Owner using their own agent
|
# Owner using their own agent
|
||||||
self.decoded_token = {"sub": data_key.get("user")}
|
self.decoded_token = {"sub": self._agent_data.get("user")}
|
||||||
|
|
||||||
if data_key.get("source"):
|
if self._agent_data.get("workflow"):
|
||||||
self.source = {"active_docs": data_key["source"]}
|
self.agent_config["workflow"] = self._agent_data["workflow"]
|
||||||
if data_key.get("workflow"):
|
self.agent_config["workflow_owner"] = self._agent_data.get("user")
|
||||||
self.agent_config["workflow"] = data_key["workflow"]
|
|
||||||
self.agent_config["workflow_owner"] = data_key.get("user")
|
|
||||||
if data_key.get("retriever"):
|
|
||||||
self.retriever_config["retriever_name"] = data_key["retriever"]
|
|
||||||
if data_key.get("chunks") is not None:
|
|
||||||
try:
|
|
||||||
self.retriever_config["chunks"] = int(data_key["chunks"])
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
logger.warning(
|
|
||||||
f"Invalid chunks value: {data_key['chunks']}, using default value 2"
|
|
||||||
)
|
|
||||||
self.retriever_config["chunks"] = 2
|
|
||||||
else:
|
else:
|
||||||
# No API key — default/workflow configuration
|
# No API key — default/workflow configuration
|
||||||
agent_type = settings.AGENT_NAME
|
agent_type = settings.AGENT_NAME
|
||||||
@@ -497,14 +509,45 @@ class StreamProcessor:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _configure_retriever(self):
|
def _configure_retriever(self):
|
||||||
|
"""Assemble retriever config with precedence: request > agent > default."""
|
||||||
doc_token_limit = calculate_doc_token_budget(model_id=self.model_id)
|
doc_token_limit = calculate_doc_token_budget(model_id=self.model_id)
|
||||||
|
|
||||||
|
# Start with defaults
|
||||||
|
retriever_name = "classic"
|
||||||
|
chunks = 2
|
||||||
|
|
||||||
|
# Layer agent-level config (if present)
|
||||||
|
if self._agent_data:
|
||||||
|
if self._agent_data.get("retriever"):
|
||||||
|
retriever_name = self._agent_data["retriever"]
|
||||||
|
if self._agent_data.get("chunks") is not None:
|
||||||
|
try:
|
||||||
|
chunks = int(self._agent_data["chunks"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid agent chunks value: {self._agent_data['chunks']}, "
|
||||||
|
"using default value 2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explicit request values win over agent config
|
||||||
|
if "retriever" in self.data:
|
||||||
|
retriever_name = self.data["retriever"]
|
||||||
|
if "chunks" in self.data:
|
||||||
|
try:
|
||||||
|
chunks = int(self.data["chunks"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid request chunks value: {self.data['chunks']}, "
|
||||||
|
"using default value 2"
|
||||||
|
)
|
||||||
|
|
||||||
self.retriever_config = {
|
self.retriever_config = {
|
||||||
"retriever_name": self.data.get("retriever", "classic"),
|
"retriever_name": retriever_name,
|
||||||
"chunks": int(self.data.get("chunks", 2)),
|
"chunks": chunks,
|
||||||
"doc_token_limit": doc_token_limit,
|
"doc_token_limit": doc_token_limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# isNoneDoc without an API key forces no retrieval
|
||||||
api_key = self.data.get("api_key") or self.agent_key
|
api_key = self.data.get("api_key") or self.agent_key
|
||||||
if not api_key and "isNoneDoc" in self.data and self.data["isNoneDoc"]:
|
if not api_key and "isNoneDoc" in self.data and self.data["isNoneDoc"]:
|
||||||
self.retriever_config["chunks"] = 0
|
self.retriever_config["chunks"] = 0
|
||||||
@@ -528,6 +571,9 @@ class StreamProcessor:
|
|||||||
if self.data.get("isNoneDoc", False) and not self.agent_id:
|
if self.data.get("isNoneDoc", False) and not self.agent_id:
|
||||||
logger.info("Pre-fetch skipped: isNoneDoc=True")
|
logger.info("Pre-fetch skipped: isNoneDoc=True")
|
||||||
return None, None
|
return None, None
|
||||||
|
if not self._has_active_docs():
|
||||||
|
logger.info("Pre-fetch skipped: no active docs configured")
|
||||||
|
return None, None
|
||||||
try:
|
try:
|
||||||
retriever = self.create_retriever()
|
retriever = self.create_retriever()
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -771,6 +817,121 @@ class StreamProcessor:
|
|||||||
logger.warning(f"Failed to fetch memory tool data: {str(e)}")
|
logger.warning(f"Failed to fetch memory tool data: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def resume_from_tool_actions(
|
||||||
|
self,
|
||||||
|
tool_actions: list,
|
||||||
|
conversation_id: str,
|
||||||
|
):
|
||||||
|
"""Resume a paused agent from saved continuation state.
|
||||||
|
|
||||||
|
Loads the pending state from MongoDB, recreates the agent with
|
||||||
|
the saved configuration, and returns an agent ready to call
|
||||||
|
``gen_continuation()``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_actions: Client-provided actions (approvals / results).
|
||||||
|
conversation_id: The conversation being resumed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (agent, messages, tools_dict, pending_tool_calls, tool_actions).
|
||||||
|
"""
|
||||||
|
from application.api.answer.services.continuation_service import (
|
||||||
|
ContinuationService,
|
||||||
|
)
|
||||||
|
from application.agents.agent_creator import AgentCreator
|
||||||
|
from application.agents.tool_executor import ToolExecutor
|
||||||
|
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
||||||
|
from application.llm.llm_creator import LLMCreator
|
||||||
|
|
||||||
|
cont_service = ContinuationService()
|
||||||
|
state = cont_service.load_state(conversation_id, self.initial_user_id)
|
||||||
|
if not state:
|
||||||
|
raise ValueError("No pending tool state found for this conversation")
|
||||||
|
|
||||||
|
messages = state["messages"]
|
||||||
|
pending_tool_calls = state["pending_tool_calls"]
|
||||||
|
tools_dict = state["tools_dict"]
|
||||||
|
tool_schemas = state.get("tool_schemas", [])
|
||||||
|
agent_config = state["agent_config"]
|
||||||
|
|
||||||
|
model_id = agent_config.get("model_id")
|
||||||
|
llm_name = agent_config.get("llm_name", settings.LLM_PROVIDER)
|
||||||
|
api_key = agent_config.get("api_key")
|
||||||
|
user_api_key = agent_config.get("user_api_key")
|
||||||
|
agent_id = agent_config.get("agent_id")
|
||||||
|
prompt = agent_config.get("prompt", "")
|
||||||
|
json_schema = agent_config.get("json_schema")
|
||||||
|
retriever_config = agent_config.get("retriever_config")
|
||||||
|
|
||||||
|
# Recreate dependencies
|
||||||
|
system_api_key = api_key or get_api_key_for_provider(llm_name)
|
||||||
|
llm = LLMCreator.create_llm(
|
||||||
|
llm_name,
|
||||||
|
api_key=system_api_key,
|
||||||
|
user_api_key=user_api_key,
|
||||||
|
decoded_token=self.decoded_token,
|
||||||
|
model_id=model_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
)
|
||||||
|
llm_handler = LLMHandlerCreator.create_handler(llm_name or "default")
|
||||||
|
tool_executor = ToolExecutor(
|
||||||
|
user_api_key=user_api_key,
|
||||||
|
user=self.initial_user_id,
|
||||||
|
decoded_token=self.decoded_token,
|
||||||
|
)
|
||||||
|
tool_executor.conversation_id = conversation_id
|
||||||
|
# Restore client tools so they stay available for subsequent LLM calls
|
||||||
|
saved_client_tools = state.get("client_tools")
|
||||||
|
if saved_client_tools:
|
||||||
|
tool_executor.client_tools = saved_client_tools
|
||||||
|
# Re-merge into tools_dict (they may have been stripped during serialization)
|
||||||
|
tool_executor.merge_client_tools(tools_dict, saved_client_tools)
|
||||||
|
|
||||||
|
agent_type = agent_config.get("agent_type", "ClassicAgent")
|
||||||
|
# Map class names back to agent creator keys
|
||||||
|
type_map = {
|
||||||
|
"ClassicAgent": "classic",
|
||||||
|
"AgenticAgent": "agentic",
|
||||||
|
"ResearchAgent": "research",
|
||||||
|
"WorkflowAgent": "workflow",
|
||||||
|
}
|
||||||
|
agent_key = type_map.get(agent_type, "classic")
|
||||||
|
|
||||||
|
agent_kwargs = {
|
||||||
|
"endpoint": "stream",
|
||||||
|
"llm_name": llm_name,
|
||||||
|
"model_id": model_id,
|
||||||
|
"api_key": system_api_key,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"user_api_key": user_api_key,
|
||||||
|
"prompt": prompt,
|
||||||
|
"chat_history": [],
|
||||||
|
"decoded_token": self.decoded_token,
|
||||||
|
"json_schema": json_schema,
|
||||||
|
"llm": llm,
|
||||||
|
"llm_handler": llm_handler,
|
||||||
|
"tool_executor": tool_executor,
|
||||||
|
}
|
||||||
|
|
||||||
|
if agent_key in ("agentic", "research") and retriever_config:
|
||||||
|
agent_kwargs["retriever_config"] = retriever_config
|
||||||
|
|
||||||
|
agent = AgentCreator.create_agent(agent_key, **agent_kwargs)
|
||||||
|
agent.conversation_id = conversation_id
|
||||||
|
agent.initial_user_id = self.initial_user_id
|
||||||
|
agent.tools = tool_schemas
|
||||||
|
|
||||||
|
# Store config for the route layer
|
||||||
|
self.model_id = model_id
|
||||||
|
self.agent_id = agent_id
|
||||||
|
self.agent_config["user_api_key"] = user_api_key
|
||||||
|
self.conversation_id = conversation_id
|
||||||
|
|
||||||
|
# Delete state so it can't be replayed
|
||||||
|
cont_service.delete_state(conversation_id, self.initial_user_id)
|
||||||
|
|
||||||
|
return agent, messages, tools_dict, pending_tool_calls, tool_actions
|
||||||
|
|
||||||
def create_agent(
|
def create_agent(
|
||||||
self,
|
self,
|
||||||
docs_together: Optional[str] = None,
|
docs_together: Optional[str] = None,
|
||||||
@@ -795,15 +956,23 @@ class StreamProcessor:
|
|||||||
raw_prompt = get_prompt(prompt_id, self.prompts_collection)
|
raw_prompt = get_prompt(prompt_id, self.prompts_collection)
|
||||||
self._prompt_content = raw_prompt
|
self._prompt_content = raw_prompt
|
||||||
|
|
||||||
rendered_prompt = self.prompt_renderer.render_prompt(
|
# Allow API callers to override the system prompt when the agent
|
||||||
prompt_content=raw_prompt,
|
# has opted in via allow_system_prompt_override.
|
||||||
user_id=self.initial_user_id,
|
if (
|
||||||
request_id=self.data.get("request_id"),
|
self.agent_config.get("allow_system_prompt_override", False)
|
||||||
passthrough_data=self.data.get("passthrough"),
|
and self.data.get("system_prompt_override")
|
||||||
docs=docs,
|
):
|
||||||
docs_together=docs_together,
|
rendered_prompt = self.data["system_prompt_override"]
|
||||||
tools_data=tools_data,
|
else:
|
||||||
)
|
rendered_prompt = self.prompt_renderer.render_prompt(
|
||||||
|
prompt_content=raw_prompt,
|
||||||
|
user_id=self.initial_user_id,
|
||||||
|
request_id=self.data.get("request_id"),
|
||||||
|
passthrough_data=self.data.get("passthrough"),
|
||||||
|
docs=docs,
|
||||||
|
docs_together=docs_together,
|
||||||
|
tools_data=tools_data,
|
||||||
|
)
|
||||||
|
|
||||||
provider = (
|
provider = (
|
||||||
get_provider_from_model_id(self.model_id)
|
get_provider_from_model_id(self.model_id)
|
||||||
@@ -841,6 +1010,10 @@ class StreamProcessor:
|
|||||||
decoded_token=self.decoded_token,
|
decoded_token=self.decoded_token,
|
||||||
)
|
)
|
||||||
tool_executor.conversation_id = self.conversation_id
|
tool_executor.conversation_id = self.conversation_id
|
||||||
|
# Pass client-side tools so they get merged in get_tools()
|
||||||
|
client_tools = self.data.get("client_tools")
|
||||||
|
if client_tools:
|
||||||
|
tool_executor.client_tools = client_tools
|
||||||
|
|
||||||
# Base agent kwargs
|
# Base agent kwargs
|
||||||
agent_kwargs = {
|
agent_kwargs = {
|
||||||
|
|||||||
@@ -26,12 +26,20 @@ internal = Blueprint("internal", __name__)
|
|||||||
|
|
||||||
@internal.before_request
|
@internal.before_request
|
||||||
def verify_internal_key():
|
def verify_internal_key():
|
||||||
"""Verify INTERNAL_KEY for all internal endpoint requests."""
|
"""Verify INTERNAL_KEY for all internal endpoint requests.
|
||||||
if settings.INTERNAL_KEY:
|
|
||||||
internal_key = request.headers.get("X-Internal-Key")
|
Deny by default: if INTERNAL_KEY is not configured, reject all requests.
|
||||||
if not internal_key or internal_key != settings.INTERNAL_KEY:
|
"""
|
||||||
logger.warning(f"Unauthorized internal API access attempt from {request.remote_addr}")
|
if not settings.INTERNAL_KEY:
|
||||||
return jsonify({"error": "Unauthorized", "message": "Invalid or missing internal key"}), 401
|
logger.warning(
|
||||||
|
f"Internal API request rejected from {request.remote_addr}: "
|
||||||
|
"INTERNAL_KEY is not configured"
|
||||||
|
)
|
||||||
|
return jsonify({"error": "Unauthorized", "message": "Internal API is not configured"}), 401
|
||||||
|
internal_key = request.headers.get("X-Internal-Key")
|
||||||
|
if not internal_key or internal_key != settings.INTERNAL_KEY:
|
||||||
|
logger.warning(f"Unauthorized internal API access attempt from {request.remote_addr}")
|
||||||
|
return jsonify({"error": "Unauthorized", "message": "Invalid or missing internal key"}), 401
|
||||||
|
|
||||||
|
|
||||||
@internal.route("/api/download", methods=["get"])
|
@internal.route("/api/download", methods=["get"])
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ AGENT_TYPE_SCHEMAS = {
|
|||||||
"token_limit",
|
"token_limit",
|
||||||
"limited_request_mode",
|
"limited_request_mode",
|
||||||
"request_limit",
|
"request_limit",
|
||||||
|
"allow_system_prompt_override",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
"lastUsedAt",
|
"lastUsedAt",
|
||||||
@@ -96,6 +97,7 @@ AGENT_TYPE_SCHEMAS = {
|
|||||||
"token_limit",
|
"token_limit",
|
||||||
"limited_request_mode",
|
"limited_request_mode",
|
||||||
"request_limit",
|
"request_limit",
|
||||||
|
"allow_system_prompt_override",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
"lastUsedAt",
|
"lastUsedAt",
|
||||||
@@ -220,6 +222,12 @@ def build_agent_document(
|
|||||||
base_doc["request_limit"] = int(
|
base_doc["request_limit"] = int(
|
||||||
data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"])
|
data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"])
|
||||||
)
|
)
|
||||||
|
if "allow_system_prompt_override" in allowed_fields:
|
||||||
|
base_doc["allow_system_prompt_override"] = (
|
||||||
|
data.get("allow_system_prompt_override") == "True"
|
||||||
|
if isinstance(data.get("allow_system_prompt_override"), str)
|
||||||
|
else bool(data.get("allow_system_prompt_override", False))
|
||||||
|
)
|
||||||
return {k: v for k, v in base_doc.items() if k in allowed_fields}
|
return {k: v for k, v in base_doc.items() if k in allowed_fields}
|
||||||
|
|
||||||
|
|
||||||
@@ -292,6 +300,9 @@ class GetAgent(Resource):
|
|||||||
"default_model_id": agent.get("default_model_id", ""),
|
"default_model_id": agent.get("default_model_id", ""),
|
||||||
"folder_id": agent.get("folder_id"),
|
"folder_id": agent.get("folder_id"),
|
||||||
"workflow": agent.get("workflow"),
|
"workflow": agent.get("workflow"),
|
||||||
|
"allow_system_prompt_override": agent.get(
|
||||||
|
"allow_system_prompt_override", False
|
||||||
|
),
|
||||||
}
|
}
|
||||||
return make_response(jsonify(data), 200)
|
return make_response(jsonify(data), 200)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -373,6 +384,9 @@ class GetAgents(Resource):
|
|||||||
"default_model_id": agent.get("default_model_id", ""),
|
"default_model_id": agent.get("default_model_id", ""),
|
||||||
"folder_id": agent.get("folder_id"),
|
"folder_id": agent.get("folder_id"),
|
||||||
"workflow": agent.get("workflow"),
|
"workflow": agent.get("workflow"),
|
||||||
|
"allow_system_prompt_override": agent.get(
|
||||||
|
"allow_system_prompt_override", False
|
||||||
|
),
|
||||||
}
|
}
|
||||||
for agent in agents
|
for agent in agents
|
||||||
if "source" in agent
|
if "source" in agent
|
||||||
@@ -450,6 +464,10 @@ class CreateAgent(Resource):
|
|||||||
"folder_id": fields.String(
|
"folder_id": fields.String(
|
||||||
required=False, description="Folder ID to organize the agent"
|
required=False, description="Folder ID to organize the agent"
|
||||||
),
|
),
|
||||||
|
"allow_system_prompt_override": fields.Boolean(
|
||||||
|
required=False,
|
||||||
|
description="Allow API callers to override the system prompt via the v1 endpoint",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -491,9 +509,9 @@ class CreateAgent(Resource):
|
|||||||
data["json_schema"] = normalize_json_schema_payload(
|
data["json_schema"] = normalize_json_schema_payload(
|
||||||
data.get("json_schema")
|
data.get("json_schema")
|
||||||
)
|
)
|
||||||
except JsonSchemaValidationError as exc:
|
except JsonSchemaValidationError:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": f"JSON schema {exc}"}),
|
jsonify({"success": False, "message": "Invalid JSON schema"}),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
if data.get("status") not in ["draft", "published"]:
|
if data.get("status") not in ["draft", "published"]:
|
||||||
@@ -674,6 +692,10 @@ class UpdateAgent(Resource):
|
|||||||
"folder_id": fields.String(
|
"folder_id": fields.String(
|
||||||
required=False, description="Folder ID to organize the agent"
|
required=False, description="Folder ID to organize the agent"
|
||||||
),
|
),
|
||||||
|
"allow_system_prompt_override": fields.Boolean(
|
||||||
|
required=False,
|
||||||
|
description="Allow API callers to override the system prompt via the v1 endpoint",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -765,6 +787,7 @@ class UpdateAgent(Resource):
|
|||||||
"default_model_id",
|
"default_model_id",
|
||||||
"folder_id",
|
"folder_id",
|
||||||
"workflow",
|
"workflow",
|
||||||
|
"allow_system_prompt_override",
|
||||||
]
|
]
|
||||||
|
|
||||||
for field in allowed_fields:
|
for field in allowed_fields:
|
||||||
@@ -872,9 +895,9 @@ class UpdateAgent(Resource):
|
|||||||
update_fields[field] = normalize_json_schema_payload(
|
update_fields[field] = normalize_json_schema_payload(
|
||||||
json_schema
|
json_schema
|
||||||
)
|
)
|
||||||
except JsonSchemaValidationError as exc:
|
except JsonSchemaValidationError:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": f"JSON schema {exc}"}),
|
jsonify({"success": False, "message": "Invalid JSON schema"}),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -983,6 +1006,13 @@ class UpdateAgent(Resource):
|
|||||||
if workflow_error:
|
if workflow_error:
|
||||||
return workflow_error
|
return workflow_error
|
||||||
update_fields[field] = workflow_id
|
update_fields[field] = workflow_id
|
||||||
|
elif field == "allow_system_prompt_override":
|
||||||
|
raw_value = data.get("allow_system_prompt_override", False)
|
||||||
|
update_fields[field] = (
|
||||||
|
raw_value == "True"
|
||||||
|
if isinstance(raw_value, str)
|
||||||
|
else bool(raw_value)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
value = data[field]
|
value = data[field]
|
||||||
if field in ["name", "description", "prompt_id", "agent_type"]:
|
if field in ["name", "description", "prompt_id", "agent_type"]:
|
||||||
|
|||||||
@@ -612,6 +612,10 @@ class LiveSpeechToTextFinish(Resource):
|
|||||||
class ServeImage(Resource):
|
class ServeImage(Resource):
|
||||||
@api.doc(description="Serve an image from storage")
|
@api.doc(description="Serve an image from storage")
|
||||||
def get(self, image_path):
|
def get(self, image_path):
|
||||||
|
if ".." in image_path or image_path.startswith("/") or "\x00" in image_path:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid image path"}), 400
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
from application.api.user.base import storage
|
from application.api.user.base import storage
|
||||||
|
|
||||||
@@ -629,6 +633,10 @@ class ServeImage(Resource):
|
|||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Image not found"}), 404
|
jsonify({"success": False, "message": "Image not found"}), 404
|
||||||
)
|
)
|
||||||
|
except ValueError:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid image path"}), 400
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error serving image: {e}")
|
current_app.logger.error(f"Error serving image: {e}")
|
||||||
return make_response(
|
return make_response(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ShareConversation(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
conversation = conversations_collection.find_one(
|
conversation = conversations_collection.find_one(
|
||||||
{"_id": ObjectId(conversation_id)}
|
{"_id": ObjectId(conversation_id), "user": user}
|
||||||
)
|
)
|
||||||
if conversation is None:
|
if conversation is None:
|
||||||
return make_response(
|
return make_response(
|
||||||
|
|||||||
@@ -463,6 +463,16 @@ class ManageSourceFiles(Resource):
|
|||||||
removed_files = []
|
removed_files = []
|
||||||
map_updated = False
|
map_updated = False
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
|
if ".." in str(file_path) or str(file_path).startswith("/"):
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Invalid file path",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
full_path = f"{source_file_path}/{file_path}"
|
full_path = f"{source_file_path}/{file_path}"
|
||||||
|
|
||||||
# Remove from storage
|
# Remove from storage
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from application.api.user.tools.routes import transform_actions
|
|||||||
from application.cache import get_redis_instance
|
from application.cache import get_redis_instance
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
|
from application.core.url_validation import SSRFError, validate_url
|
||||||
from application.security.encryption import decrypt_credentials, encrypt_credentials
|
from application.security.encryption import decrypt_credentials, encrypt_credentials
|
||||||
from application.utils import check_required_fields
|
from application.utils import check_required_fields
|
||||||
|
|
||||||
@@ -63,6 +64,21 @@ def _extract_auth_credentials(config):
|
|||||||
return auth_credentials
|
return auth_credentials
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_mcp_server_url(config: dict) -> None:
|
||||||
|
"""Validate the server_url in an MCP config to prevent SSRF.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the URL is missing or points to a blocked address.
|
||||||
|
"""
|
||||||
|
server_url = (config.get("server_url") or "").strip()
|
||||||
|
if not server_url:
|
||||||
|
raise ValueError("server_url is required")
|
||||||
|
try:
|
||||||
|
validate_url(server_url)
|
||||||
|
except SSRFError as exc:
|
||||||
|
raise ValueError(f"Invalid server URL: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
@tools_mcp_ns.route("/mcp_server/test")
|
@tools_mcp_ns.route("/mcp_server/test")
|
||||||
class TestMCPServerConfig(Resource):
|
class TestMCPServerConfig(Resource):
|
||||||
@api.expect(
|
@api.expect(
|
||||||
@@ -97,6 +113,8 @@ class TestMCPServerConfig(Resource):
|
|||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_validate_mcp_server_url(config)
|
||||||
|
|
||||||
auth_credentials = _extract_auth_credentials(config)
|
auth_credentials = _extract_auth_credentials(config)
|
||||||
test_config = config.copy()
|
test_config = config.copy()
|
||||||
test_config["auth_credentials"] = auth_credentials
|
test_config["auth_credentials"] = auth_credentials
|
||||||
@@ -105,15 +123,41 @@ class TestMCPServerConfig(Resource):
|
|||||||
result = mcp_tool.test_connection()
|
result = mcp_tool.test_connection()
|
||||||
|
|
||||||
if result.get("requires_oauth"):
|
if result.get("requires_oauth"):
|
||||||
return make_response(jsonify(result), 200)
|
safe_result = {
|
||||||
|
k: v
|
||||||
|
for k, v in result.items()
|
||||||
|
if k in ("success", "requires_oauth", "auth_url")
|
||||||
|
}
|
||||||
|
return make_response(jsonify(safe_result), 200)
|
||||||
|
|
||||||
if not result.get("success") and "message" in result:
|
if not result.get("success"):
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
f"MCP connection test failed: {result.get('message')}"
|
f"MCP connection test failed: {result.get('message')}"
|
||||||
)
|
)
|
||||||
result["message"] = "Connection test failed"
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Connection test failed",
|
||||||
|
"tools_count": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
return make_response(jsonify(result), 200)
|
safe_result = {
|
||||||
|
"success": True,
|
||||||
|
"message": result.get("message", "Connection successful"),
|
||||||
|
"tools_count": result.get("tools_count", 0),
|
||||||
|
"tools": result.get("tools", []),
|
||||||
|
}
|
||||||
|
return make_response(jsonify(safe_result), 200)
|
||||||
|
except ValueError as e:
|
||||||
|
current_app.logger.warning(f"Invalid MCP server test request: {e}")
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "error": "Invalid MCP server configuration"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error testing MCP server: {e}", exc_info=True)
|
current_app.logger.error(f"Error testing MCP server: {e}", exc_info=True)
|
||||||
return make_response(
|
return make_response(
|
||||||
@@ -165,6 +209,8 @@ class MCPServerSave(Resource):
|
|||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_validate_mcp_server_url(config)
|
||||||
|
|
||||||
auth_credentials = _extract_auth_credentials(config)
|
auth_credentials = _extract_auth_credentials(config)
|
||||||
auth_type = config.get("auth_type", "none")
|
auth_type = config.get("auth_type", "none")
|
||||||
mcp_config = config.copy()
|
mcp_config = config.copy()
|
||||||
@@ -279,6 +325,12 @@ class MCPServerSave(Resource):
|
|||||||
"tools_count": len(transformed_actions),
|
"tools_count": len(transformed_actions),
|
||||||
}
|
}
|
||||||
return make_response(jsonify(response_data), 200)
|
return make_response(jsonify(response_data), 200)
|
||||||
|
except ValueError as e:
|
||||||
|
current_app.logger.warning(f"Invalid MCP server save request: {e}")
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "error": "Invalid MCP server configuration"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error saving MCP server: {e}", exc_info=True)
|
current_app.logger.error(f"Error saving MCP server: {e}", exc_info=True)
|
||||||
return make_response(
|
return make_response(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from application.agents.tools.spec_parser import parse_spec
|
|||||||
from application.agents.tools.tool_manager import ToolManager
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
from application.api import api
|
from application.api import api
|
||||||
from application.api.user.base import user_tools_collection
|
from application.api.user.base import user_tools_collection
|
||||||
|
from application.core.url_validation import SSRFError, validate_url
|
||||||
from application.security.encryption import decrypt_credentials, encrypt_credentials
|
from application.security.encryption import decrypt_credentials, encrypt_credentials
|
||||||
from application.utils import check_required_fields, validate_function_name
|
from application.utils import check_required_fields, validate_function_name
|
||||||
|
|
||||||
@@ -130,6 +131,8 @@ tools_ns = Namespace("tools", description="Tool management operations", path="/a
|
|||||||
class AvailableTools(Resource):
|
class AvailableTools(Resource):
|
||||||
@api.doc(description="Get available tools for a user")
|
@api.doc(description="Get available tools for a user")
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not request.decoded_token:
|
||||||
|
return make_response(jsonify({"success": False}), 401)
|
||||||
try:
|
try:
|
||||||
tools_metadata = []
|
tools_metadata = []
|
||||||
for tool_name, tool_instance in tool_manager.tools.items():
|
for tool_name, tool_instance in tool_manager.tools.items():
|
||||||
@@ -236,6 +239,16 @@ class CreateTool(Resource):
|
|||||||
if missing_fields:
|
if missing_fields:
|
||||||
return missing_fields
|
return missing_fields
|
||||||
try:
|
try:
|
||||||
|
if data["name"] == "mcp_tool":
|
||||||
|
server_url = (data.get("config", {}).get("server_url") or "").strip()
|
||||||
|
if server_url:
|
||||||
|
try:
|
||||||
|
validate_url(server_url)
|
||||||
|
except SSRFError:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid server URL"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
tool_instance = tool_manager.tools.get(data["name"])
|
tool_instance = tool_manager.tools.get(data["name"])
|
||||||
if not tool_instance:
|
if not tool_instance:
|
||||||
return make_response(
|
return make_response(
|
||||||
@@ -421,6 +434,16 @@ class UpdateToolConfig(Resource):
|
|||||||
return make_response(jsonify({"success": False}), 404)
|
return make_response(jsonify({"success": False}), 404)
|
||||||
|
|
||||||
tool_name = tool_doc.get("name")
|
tool_name = tool_doc.get("name")
|
||||||
|
if tool_name == "mcp_tool":
|
||||||
|
server_url = (data["config"].get("server_url") or "").strip()
|
||||||
|
if server_url:
|
||||||
|
try:
|
||||||
|
validate_url(server_url)
|
||||||
|
except SSRFError:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid server URL"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
tool_instance = tool_manager.tools.get(tool_name)
|
tool_instance = tool_manager.tools.get(tool_name)
|
||||||
config_requirements = (
|
config_requirements = (
|
||||||
tool_instance.get_config_requirements() if tool_instance else {}
|
tool_instance.get_config_requirements() if tool_instance else {}
|
||||||
|
|||||||
3
application/api/v1/__init__.py
Normal file
3
application/api/v1/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from application.api.v1.routes import v1_bp
|
||||||
|
|
||||||
|
__all__ = ["v1_bp"]
|
||||||
333
application/api/v1/routes.py
Normal file
333
application/api/v1/routes.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
"""Standard chat completions API routes.
|
||||||
|
|
||||||
|
Exposes ``/v1/chat/completions`` and ``/v1/models`` endpoints that
|
||||||
|
follow the widely-adopted chat completions protocol so external tools
|
||||||
|
(opencode, continue, etc.) can connect to DocsGPT agents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from typing import Any, Dict, Generator, Optional
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, make_response, request, Response
|
||||||
|
|
||||||
|
from application.api.answer.routes.base import BaseAnswerResource
|
||||||
|
from application.api.answer.services.stream_processor import StreamProcessor
|
||||||
|
from application.api.v1.translator import (
|
||||||
|
translate_request,
|
||||||
|
translate_response,
|
||||||
|
translate_stream_event,
|
||||||
|
)
|
||||||
|
from application.core.mongo_db import MongoDB
|
||||||
|
from application.core.settings import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
v1_bp = Blueprint("v1", __name__, url_prefix="/v1")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_bearer_token() -> Optional[str]:
|
||||||
|
"""Extract API key from Authorization: Bearer header."""
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
if auth.startswith("Bearer "):
|
||||||
|
return auth[7:].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_agent(api_key: str) -> Optional[Dict]:
|
||||||
|
"""Look up the agent document for this API key."""
|
||||||
|
try:
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo[settings.MONGO_DB_NAME]
|
||||||
|
return db["agents"].find_one({"key": api_key})
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to look up agent for API key", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_model_name(agent: Optional[Dict], api_key: str) -> str:
|
||||||
|
"""Return agent name for display as model name."""
|
||||||
|
if agent:
|
||||||
|
return agent.get("name", api_key)
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
class _V1AnswerHelper(BaseAnswerResource):
|
||||||
|
"""Thin wrapper to access complete_stream / process_response_stream."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@v1_bp.route("/chat/completions", methods=["POST"])
|
||||||
|
def chat_completions():
|
||||||
|
"""Handle POST /v1/chat/completions."""
|
||||||
|
api_key = _extract_bearer_token()
|
||||||
|
if not api_key:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Missing Authorization header", "type": "auth_error"}}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or not data.get("messages"):
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "messages field is required", "type": "invalid_request"}}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_stream = data.get("stream", False)
|
||||||
|
agent_doc = _lookup_agent(api_key)
|
||||||
|
model_name = _get_model_name(agent_doc, api_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_data = translate_request(data, api_key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"/v1/chat/completions translate error: {e}", exc_info=True)
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Failed to process request", "type": "invalid_request"}}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Link decoded_token to the agent's owner so continuation state,
|
||||||
|
# logs, and tool execution use the correct user identity.
|
||||||
|
agent_user = agent_doc.get("user") if agent_doc else None
|
||||||
|
decoded_token = {"sub": agent_user or "api_key_user"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
processor = StreamProcessor(internal_data, decoded_token)
|
||||||
|
|
||||||
|
if internal_data.get("tool_actions"):
|
||||||
|
# Continuation mode
|
||||||
|
conversation_id = internal_data.get("conversation_id")
|
||||||
|
if not conversation_id:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "conversation_id required for tool continuation", "type": "invalid_request"}}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
(
|
||||||
|
agent,
|
||||||
|
messages,
|
||||||
|
tools_dict,
|
||||||
|
pending_tool_calls,
|
||||||
|
tool_actions,
|
||||||
|
) = processor.resume_from_tool_actions(
|
||||||
|
internal_data["tool_actions"], conversation_id
|
||||||
|
)
|
||||||
|
continuation = {
|
||||||
|
"messages": messages,
|
||||||
|
"tools_dict": tools_dict,
|
||||||
|
"pending_tool_calls": pending_tool_calls,
|
||||||
|
"tool_actions": tool_actions,
|
||||||
|
}
|
||||||
|
question = ""
|
||||||
|
else:
|
||||||
|
# Normal mode
|
||||||
|
question = internal_data.get("question", "")
|
||||||
|
agent = processor.build_agent(question)
|
||||||
|
continuation = None
|
||||||
|
|
||||||
|
if not processor.decoded_token:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Unauthorized", "type": "auth_error"}}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
|
||||||
|
helper = _V1AnswerHelper()
|
||||||
|
usage_error = helper.check_usage(processor.agent_config)
|
||||||
|
if usage_error:
|
||||||
|
return usage_error
|
||||||
|
|
||||||
|
should_save_conversation = bool(internal_data.get("save_conversation", False))
|
||||||
|
|
||||||
|
if is_stream:
|
||||||
|
return Response(
|
||||||
|
_stream_response(
|
||||||
|
helper,
|
||||||
|
question,
|
||||||
|
agent,
|
||||||
|
processor,
|
||||||
|
model_name,
|
||||||
|
continuation,
|
||||||
|
should_save_conversation,
|
||||||
|
),
|
||||||
|
mimetype="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return _non_stream_response(
|
||||||
|
helper,
|
||||||
|
question,
|
||||||
|
agent,
|
||||||
|
processor,
|
||||||
|
model_name,
|
||||||
|
continuation,
|
||||||
|
should_save_conversation,
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(
|
||||||
|
f"/v1/chat/completions error: {e} - {traceback.format_exc()}",
|
||||||
|
extra={"error": str(e)},
|
||||||
|
)
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Failed to process request", "type": "invalid_request"}}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"/v1/chat/completions error: {e} - {traceback.format_exc()}",
|
||||||
|
extra={"error": str(e)},
|
||||||
|
)
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Internal server error", "type": "server_error"}}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_response(
|
||||||
|
helper: _V1AnswerHelper,
|
||||||
|
question: str,
|
||||||
|
agent: Any,
|
||||||
|
processor: StreamProcessor,
|
||||||
|
model_name: str,
|
||||||
|
continuation: Optional[Dict],
|
||||||
|
should_save_conversation: bool,
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
|
"""Generate translated SSE chunks for streaming response."""
|
||||||
|
completion_id = f"chatcmpl-{int(time.time())}"
|
||||||
|
|
||||||
|
internal_stream = helper.complete_stream(
|
||||||
|
question=question,
|
||||||
|
agent=agent,
|
||||||
|
conversation_id=processor.conversation_id,
|
||||||
|
user_api_key=processor.agent_config.get("user_api_key"),
|
||||||
|
decoded_token=processor.decoded_token,
|
||||||
|
agent_id=processor.agent_id,
|
||||||
|
model_id=processor.model_id,
|
||||||
|
should_save_conversation=should_save_conversation,
|
||||||
|
_continuation=continuation,
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in internal_stream:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
# Parse the internal SSE event
|
||||||
|
event_str = line.replace("data: ", "").strip()
|
||||||
|
try:
|
||||||
|
event_data = json.loads(event_str)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update completion_id when we get the conversation id
|
||||||
|
if event_data.get("type") == "id":
|
||||||
|
conv_id = event_data.get("id", "")
|
||||||
|
if conv_id:
|
||||||
|
completion_id = f"chatcmpl-{conv_id}"
|
||||||
|
|
||||||
|
# Translate to standard format
|
||||||
|
translated = translate_stream_event(event_data, completion_id, model_name)
|
||||||
|
for chunk in translated:
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
|
||||||
|
def _non_stream_response(
|
||||||
|
helper: _V1AnswerHelper,
|
||||||
|
question: str,
|
||||||
|
agent: Any,
|
||||||
|
processor: StreamProcessor,
|
||||||
|
model_name: str,
|
||||||
|
continuation: Optional[Dict],
|
||||||
|
should_save_conversation: bool,
|
||||||
|
) -> Response:
|
||||||
|
"""Collect full response and return as single JSON."""
|
||||||
|
stream = helper.complete_stream(
|
||||||
|
question=question,
|
||||||
|
agent=agent,
|
||||||
|
conversation_id=processor.conversation_id,
|
||||||
|
user_api_key=processor.agent_config.get("user_api_key"),
|
||||||
|
decoded_token=processor.decoded_token,
|
||||||
|
agent_id=processor.agent_id,
|
||||||
|
model_id=processor.model_id,
|
||||||
|
should_save_conversation=should_save_conversation,
|
||||||
|
_continuation=continuation,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = helper.process_response_stream(stream)
|
||||||
|
|
||||||
|
if result["error"]:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": result["error"], "type": "server_error"}}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
extra = result.get("extra")
|
||||||
|
pending = extra.get("pending_tool_calls") if isinstance(extra, dict) else None
|
||||||
|
|
||||||
|
response = translate_response(
|
||||||
|
conversation_id=result["conversation_id"],
|
||||||
|
answer=result["answer"] or "",
|
||||||
|
sources=result["sources"],
|
||||||
|
tool_calls=result["tool_calls"],
|
||||||
|
thought=result["thought"] or "",
|
||||||
|
model_name=model_name,
|
||||||
|
pending_tool_calls=pending,
|
||||||
|
)
|
||||||
|
return make_response(jsonify(response), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@v1_bp.route("/models", methods=["GET"])
|
||||||
|
def list_models():
|
||||||
|
"""Handle GET /v1/models — return agents as models."""
|
||||||
|
api_key = _extract_bearer_token()
|
||||||
|
if not api_key:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Missing Authorization header", "type": "auth_error"}}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mongo = MongoDB.get_client()
|
||||||
|
db = mongo[settings.MONGO_DB_NAME]
|
||||||
|
agents_collection = db["agents"]
|
||||||
|
|
||||||
|
# Find the agent for this api_key
|
||||||
|
agent = agents_collection.find_one({"key": api_key})
|
||||||
|
if not agent:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Invalid API key", "type": "auth_error"}}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = agent.get("user")
|
||||||
|
|
||||||
|
# Return all agents belonging to this user
|
||||||
|
user_agents = list(agents_collection.find({"user": user}))
|
||||||
|
|
||||||
|
models = []
|
||||||
|
for ag in user_agents:
|
||||||
|
created = ag.get("createdAt")
|
||||||
|
created_ts = int(created.timestamp()) if created else int(time.time())
|
||||||
|
model_id = str(ag.get("_id") or ag.get("id") or "")
|
||||||
|
models.append({
|
||||||
|
"id": model_id,
|
||||||
|
"object": "model",
|
||||||
|
"created": created_ts,
|
||||||
|
"owned_by": "docsgpt",
|
||||||
|
"name": ag.get("name", ""),
|
||||||
|
"description": ag.get("description", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
jsonify({"object": "list", "data": models}),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"/v1/models error: {e}", exc_info=True)
|
||||||
|
return make_response(
|
||||||
|
jsonify({"error": {"message": "Internal server error", "type": "server_error"}}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
433
application/api/v1/translator.py
Normal file
433
application/api/v1/translator.py
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
"""Translate between standard chat completions format and DocsGPT internals.
|
||||||
|
|
||||||
|
This module handles:
|
||||||
|
- Request translation (chat completions -> DocsGPT internal format)
|
||||||
|
- Response translation (DocsGPT response -> chat completions format)
|
||||||
|
- Streaming event translation (DocsGPT SSE -> standard SSE chunks)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
def _get_client_tool_name(tc: Dict) -> str:
|
||||||
|
"""Return the original tool name for client-facing responses.
|
||||||
|
|
||||||
|
For client-side tools the ``tool_name`` field carries the name the
|
||||||
|
client originally registered. Fall back to ``action_name`` (which
|
||||||
|
is now the clean LLM-visible name) or ``name``.
|
||||||
|
"""
|
||||||
|
return tc.get("tool_name", tc.get("action_name", tc.get("name", "")))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Request translation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def is_continuation(messages: List[Dict]) -> bool:
|
||||||
|
"""Check if messages represent a tool-call continuation.
|
||||||
|
|
||||||
|
A continuation is detected when the last message(s) have ``role: "tool"``
|
||||||
|
immediately after an assistant message with ``tool_calls``.
|
||||||
|
"""
|
||||||
|
if not messages:
|
||||||
|
return False
|
||||||
|
# Walk backwards: if we see tool messages before hitting a non-tool, non-assistant message
|
||||||
|
# and there's an assistant message with tool_calls, it's a continuation.
|
||||||
|
i = len(messages) - 1
|
||||||
|
while i >= 0 and messages[i].get("role") == "tool":
|
||||||
|
i -= 1
|
||||||
|
if i < 0:
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
messages[i].get("role") == "assistant"
|
||||||
|
and bool(messages[i].get("tool_calls"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_tool_results(messages: List[Dict]) -> List[Dict]:
|
||||||
|
"""Extract tool results from trailing tool messages for continuation.
|
||||||
|
|
||||||
|
Returns a list of ``tool_actions`` dicts with ``call_id`` and ``result``.
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for msg in reversed(messages):
|
||||||
|
if msg.get("role") != "tool":
|
||||||
|
break
|
||||||
|
call_id = msg.get("tool_call_id", "")
|
||||||
|
content = msg.get("content", "")
|
||||||
|
if isinstance(content, str):
|
||||||
|
try:
|
||||||
|
content = json.loads(content)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
results.append({"call_id": call_id, "result": content})
|
||||||
|
results.reverse()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def extract_conversation_id(messages: List[Dict]) -> Optional[str]:
|
||||||
|
"""Try to extract conversation_id from the assistant message before tool results.
|
||||||
|
|
||||||
|
The conversation_id may be stored in a custom field on the assistant message
|
||||||
|
from a previous response cycle.
|
||||||
|
"""
|
||||||
|
for msg in reversed(messages):
|
||||||
|
if msg.get("role") == "assistant":
|
||||||
|
# Check docsgpt extension
|
||||||
|
return msg.get("docsgpt", {}).get("conversation_id")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_system_prompt(messages: List[Dict]) -> Optional[str]:
|
||||||
|
"""Extract the first system message content from the messages array.
|
||||||
|
|
||||||
|
Returns None if no system message is present.
|
||||||
|
"""
|
||||||
|
for msg in messages:
|
||||||
|
if msg.get("role") == "system":
|
||||||
|
return msg.get("content", "")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_history(messages: List[Dict]) -> List[Dict]:
|
||||||
|
"""Convert chat completions messages array to DocsGPT history format.
|
||||||
|
|
||||||
|
DocsGPT history is a list of ``{prompt, response}`` dicts.
|
||||||
|
Excludes the last user message (that becomes the ``question``).
|
||||||
|
"""
|
||||||
|
history = []
|
||||||
|
i = 0
|
||||||
|
while i < len(messages):
|
||||||
|
msg = messages[i]
|
||||||
|
if msg.get("role") == "system":
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if msg.get("role") == "user":
|
||||||
|
# Look ahead for assistant response
|
||||||
|
if i + 1 < len(messages) and messages[i + 1].get("role") == "assistant":
|
||||||
|
content = messages[i + 1].get("content") or ""
|
||||||
|
history.append({
|
||||||
|
"prompt": msg.get("content", ""),
|
||||||
|
"response": content,
|
||||||
|
})
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
# Last user message without response — skip (it's the question)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
return history
|
||||||
|
|
||||||
|
|
||||||
|
def translate_request(
|
||||||
|
data: Dict[str, Any], api_key: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Translate a chat completions request to DocsGPT internal format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The incoming request body.
|
||||||
|
api_key: Agent API key from the Authorization header.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict suitable for passing to ``StreamProcessor``.
|
||||||
|
"""
|
||||||
|
messages = data.get("messages", [])
|
||||||
|
|
||||||
|
# Check for continuation (tool results after assistant tool_calls)
|
||||||
|
if is_continuation(messages):
|
||||||
|
tool_actions = extract_tool_results(messages)
|
||||||
|
conversation_id = extract_conversation_id(messages)
|
||||||
|
if not conversation_id:
|
||||||
|
conversation_id = data.get("conversation_id")
|
||||||
|
result = {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"tool_actions": tool_actions,
|
||||||
|
"api_key": api_key,
|
||||||
|
}
|
||||||
|
# Carry tools forward for next iteration
|
||||||
|
if data.get("tools"):
|
||||||
|
result["client_tools"] = data["tools"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Normal request — extract question from last user message
|
||||||
|
question = ""
|
||||||
|
for msg in reversed(messages):
|
||||||
|
if msg.get("role") == "user":
|
||||||
|
question = msg.get("content", "")
|
||||||
|
break
|
||||||
|
|
||||||
|
history = convert_history(messages)
|
||||||
|
system_prompt_override = extract_system_prompt(messages)
|
||||||
|
|
||||||
|
docsgpt = data.get("docsgpt", {})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"question": question,
|
||||||
|
"api_key": api_key,
|
||||||
|
"history": json.dumps(history),
|
||||||
|
# Conversations are NOT persisted by default on the v1 endpoint.
|
||||||
|
# Callers opt in via ``docsgpt.save_conversation: true``.
|
||||||
|
"save_conversation": bool(docsgpt.get("save_conversation", False)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if system_prompt_override is not None:
|
||||||
|
result["system_prompt_override"] = system_prompt_override
|
||||||
|
|
||||||
|
# Client tools
|
||||||
|
if data.get("tools"):
|
||||||
|
result["client_tools"] = data["tools"]
|
||||||
|
|
||||||
|
# DocsGPT extensions
|
||||||
|
if docsgpt.get("attachments"):
|
||||||
|
result["attachments"] = docsgpt["attachments"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Response translation (non-streaming)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def translate_response(
|
||||||
|
conversation_id: str,
|
||||||
|
answer: str,
|
||||||
|
sources: Optional[List[Dict]],
|
||||||
|
tool_calls: Optional[List[Dict]],
|
||||||
|
thought: str,
|
||||||
|
model_name: str,
|
||||||
|
pending_tool_calls: Optional[List[Dict]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Translate DocsGPT response to chat completions format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: The DocsGPT conversation ID.
|
||||||
|
answer: The assistant's text response.
|
||||||
|
sources: RAG retrieval sources.
|
||||||
|
tool_calls: Completed tool call results.
|
||||||
|
thought: Reasoning/thinking tokens.
|
||||||
|
model_name: Model/agent identifier.
|
||||||
|
pending_tool_calls: Pending client-side tool calls (if paused).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict in the standard chat completions response format.
|
||||||
|
"""
|
||||||
|
created = int(time.time())
|
||||||
|
completion_id = f"chatcmpl-{conversation_id}" if conversation_id else f"chatcmpl-{created}"
|
||||||
|
|
||||||
|
# Build message
|
||||||
|
message: Dict[str, Any] = {"role": "assistant"}
|
||||||
|
|
||||||
|
if pending_tool_calls:
|
||||||
|
# Tool calls pending — return them for client execution
|
||||||
|
message["content"] = None
|
||||||
|
message["tool_calls"] = [
|
||||||
|
{
|
||||||
|
"id": tc.get("call_id", ""),
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": _get_client_tool_name(tc),
|
||||||
|
"arguments": (
|
||||||
|
json.dumps(tc["arguments"])
|
||||||
|
if isinstance(tc.get("arguments"), dict)
|
||||||
|
else tc.get("arguments", "{}")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for tc in pending_tool_calls
|
||||||
|
]
|
||||||
|
finish_reason = "tool_calls"
|
||||||
|
else:
|
||||||
|
message["content"] = answer
|
||||||
|
if thought:
|
||||||
|
message["reasoning_content"] = thought
|
||||||
|
finish_reason = "stop"
|
||||||
|
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"id": completion_id,
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": created,
|
||||||
|
"model": model_name,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": message,
|
||||||
|
"finish_reason": finish_reason,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"total_tokens": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# DocsGPT extensions
|
||||||
|
docsgpt: Dict[str, Any] = {}
|
||||||
|
if conversation_id:
|
||||||
|
docsgpt["conversation_id"] = conversation_id
|
||||||
|
if sources:
|
||||||
|
docsgpt["sources"] = sources
|
||||||
|
if tool_calls:
|
||||||
|
docsgpt["tool_calls"] = tool_calls
|
||||||
|
if docsgpt:
|
||||||
|
result["docsgpt"] = docsgpt
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Streaming event translation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_chunk(
|
||||||
|
completion_id: str,
|
||||||
|
model_name: str,
|
||||||
|
delta: Dict[str, Any],
|
||||||
|
finish_reason: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Build a single SSE chunk in the standard streaming format."""
|
||||||
|
chunk = {
|
||||||
|
"id": completion_id,
|
||||||
|
"object": "chat.completion.chunk",
|
||||||
|
"created": int(time.time()),
|
||||||
|
"model": model_name,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"delta": delta,
|
||||||
|
"finish_reason": finish_reason,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return f"data: {json.dumps(chunk)}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _make_docsgpt_chunk(data: Dict[str, Any]) -> str:
|
||||||
|
"""Build a DocsGPT extension SSE chunk."""
|
||||||
|
return f"data: {json.dumps({'docsgpt': data})}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def translate_stream_event(
|
||||||
|
event_data: Dict[str, Any],
|
||||||
|
completion_id: str,
|
||||||
|
model_name: str,
|
||||||
|
) -> List[str]:
|
||||||
|
"""Translate a DocsGPT SSE event dict to standard streaming chunks.
|
||||||
|
|
||||||
|
May return 0, 1, or 2 chunks per input event. For example, a completed
|
||||||
|
tool call produces both a docsgpt extension chunk and nothing on the
|
||||||
|
standard side (since server-side tool calls aren't surfaced in standard
|
||||||
|
format).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_data: Parsed DocsGPT event dict.
|
||||||
|
completion_id: The completion ID for this response.
|
||||||
|
model_name: Model/agent identifier.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of SSE-formatted strings to send to the client.
|
||||||
|
"""
|
||||||
|
event_type = event_data.get("type")
|
||||||
|
chunks: List[str] = []
|
||||||
|
|
||||||
|
if event_type == "answer":
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(completion_id, model_name, {"content": event_data.get("answer", "")})
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type == "thought":
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(
|
||||||
|
completion_id, model_name,
|
||||||
|
{"reasoning_content": event_data.get("thought", "")},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type == "source":
|
||||||
|
chunks.append(
|
||||||
|
_make_docsgpt_chunk({
|
||||||
|
"type": "source",
|
||||||
|
"sources": event_data.get("source", []),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type == "tool_call":
|
||||||
|
tc_data = event_data.get("data", {})
|
||||||
|
status = tc_data.get("status")
|
||||||
|
|
||||||
|
if status == "requires_client_execution":
|
||||||
|
# Standard: stream as tool_calls delta
|
||||||
|
args = tc_data.get("arguments", {})
|
||||||
|
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(completion_id, model_name, {
|
||||||
|
"tool_calls": [{
|
||||||
|
"index": 0,
|
||||||
|
"id": tc_data.get("call_id", ""),
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": _get_client_tool_name(tc_data),
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
elif status == "awaiting_approval":
|
||||||
|
# Extension: approval needed
|
||||||
|
chunks.append(_make_docsgpt_chunk({"type": "tool_call", "data": tc_data}))
|
||||||
|
elif status in ("completed", "pending", "error", "denied", "skipped"):
|
||||||
|
# Extension: tool call progress
|
||||||
|
chunks.append(_make_docsgpt_chunk({"type": "tool_call", "data": tc_data}))
|
||||||
|
|
||||||
|
elif event_type == "tool_calls_pending":
|
||||||
|
# Standard: finish_reason = tool_calls
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(completion_id, model_name, {}, finish_reason="tool_calls")
|
||||||
|
)
|
||||||
|
# Also emit as docsgpt extension
|
||||||
|
chunks.append(
|
||||||
|
_make_docsgpt_chunk({
|
||||||
|
"type": "tool_calls_pending",
|
||||||
|
"pending_tool_calls": event_data.get("data", {}).get("pending_tool_calls", []),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type == "end":
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(completion_id, model_name, {}, finish_reason="stop")
|
||||||
|
)
|
||||||
|
chunks.append("data: [DONE]\n\n")
|
||||||
|
|
||||||
|
elif event_type == "id":
|
||||||
|
chunks.append(
|
||||||
|
_make_docsgpt_chunk({
|
||||||
|
"type": "id",
|
||||||
|
"conversation_id": event_data.get("id", ""),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type == "error":
|
||||||
|
# Emit as standard error (non-standard but widely supported)
|
||||||
|
error_data = {
|
||||||
|
"error": {
|
||||||
|
"message": event_data.get("error", "An error occurred"),
|
||||||
|
"type": "server_error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks.append(f"data: {json.dumps(error_data)}\n\n")
|
||||||
|
|
||||||
|
elif event_type == "structured_answer":
|
||||||
|
chunks.append(
|
||||||
|
_make_chunk(
|
||||||
|
completion_id, model_name,
|
||||||
|
{"content": event_data.get("answer", "")},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip: tool_calls (redundant), research_plan, research_progress
|
||||||
|
|
||||||
|
return chunks
|
||||||
@@ -17,6 +17,7 @@ from application.api.answer import answer # noqa: E402
|
|||||||
from application.api.internal.routes import internal # noqa: E402
|
from application.api.internal.routes import internal # noqa: E402
|
||||||
from application.api.user.routes import user # noqa: E402
|
from application.api.user.routes import user # noqa: E402
|
||||||
from application.api.connector.routes import connector # noqa: E402
|
from application.api.connector.routes import connector # noqa: E402
|
||||||
|
from application.api.v1 import v1_bp # noqa: E402
|
||||||
from application.celery_init import celery # noqa: E402
|
from application.celery_init import celery # noqa: E402
|
||||||
from application.core.settings import settings # noqa: E402
|
from application.core.settings import settings # noqa: E402
|
||||||
from application.stt.upload_limits import ( # noqa: E402
|
from application.stt.upload_limits import ( # noqa: E402
|
||||||
@@ -36,6 +37,7 @@ app.register_blueprint(user)
|
|||||||
app.register_blueprint(answer)
|
app.register_blueprint(answer)
|
||||||
app.register_blueprint(internal)
|
app.register_blueprint(internal)
|
||||||
app.register_blueprint(connector)
|
app.register_blueprint(connector)
|
||||||
|
app.register_blueprint(v1_bp)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
UPLOAD_FOLDER="inputs",
|
UPLOAD_FOLDER="inputs",
|
||||||
CELERY_BROKER_URL=settings.CELERY_BROKER_URL,
|
CELERY_BROKER_URL=settings.CELERY_BROKER_URL,
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ class GoogleLLM(BaseLLM):
|
|||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
import json as _json
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
role = message.get("role")
|
role = message.get("role")
|
||||||
content = message.get("content")
|
content = message.get("content")
|
||||||
@@ -180,9 +182,66 @@ class GoogleLLM(BaseLLM):
|
|||||||
|
|
||||||
if role == "assistant":
|
if role == "assistant":
|
||||||
role = "model"
|
role = "model"
|
||||||
elif role == "tool":
|
|
||||||
role = "model"
|
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
|
# Standard format: assistant message with tool_calls array
|
||||||
|
msg_tool_calls = message.get("tool_calls")
|
||||||
|
if msg_tool_calls and role == "model":
|
||||||
|
for tc in msg_tool_calls:
|
||||||
|
func = tc.get("function", {})
|
||||||
|
args = func.get("arguments", "{}")
|
||||||
|
if isinstance(args, str):
|
||||||
|
try:
|
||||||
|
args = _json.loads(args)
|
||||||
|
except (_json.JSONDecodeError, TypeError):
|
||||||
|
args = {}
|
||||||
|
cleaned_args = self._remove_null_values(args)
|
||||||
|
thought_sig = tc.get("thought_signature")
|
||||||
|
if thought_sig:
|
||||||
|
parts.append(
|
||||||
|
types.Part(
|
||||||
|
functionCall=types.FunctionCall(
|
||||||
|
name=func.get("name", ""),
|
||||||
|
args=cleaned_args,
|
||||||
|
),
|
||||||
|
thoughtSignature=thought_sig,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parts.append(
|
||||||
|
types.Part.from_function_call(
|
||||||
|
name=func.get("name", ""),
|
||||||
|
args=cleaned_args,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if parts:
|
||||||
|
cleaned_messages.append(types.Content(role=role, parts=parts))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Standard format: tool message with tool_call_id
|
||||||
|
tool_call_id = message.get("tool_call_id")
|
||||||
|
if role == "tool" and tool_call_id is not None:
|
||||||
|
result_content = content
|
||||||
|
if isinstance(result_content, str):
|
||||||
|
try:
|
||||||
|
result_content = _json.loads(result_content)
|
||||||
|
except (_json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
# Google expects function_response name — extract from tool_call_id context
|
||||||
|
# We use a placeholder name since Google API doesn't require exact match
|
||||||
|
parts.append(
|
||||||
|
types.Part.from_function_response(
|
||||||
|
name="tool_result",
|
||||||
|
response={"result": result_content},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cleaned_messages.append(types.Content(role="model", parts=parts))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if role == "tool":
|
||||||
|
role = "model"
|
||||||
|
|
||||||
if role and content is not None:
|
if role and content is not None:
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
parts = [types.Part.from_text(text=content)]
|
parts = [types.Part.from_text(text=content)]
|
||||||
@@ -191,15 +250,11 @@ class GoogleLLM(BaseLLM):
|
|||||||
if "text" in item:
|
if "text" in item:
|
||||||
parts.append(types.Part.from_text(text=item["text"]))
|
parts.append(types.Part.from_text(text=item["text"]))
|
||||||
elif "function_call" in item:
|
elif "function_call" in item:
|
||||||
# Remove null values from args to avoid API errors
|
# Legacy format support
|
||||||
|
|
||||||
cleaned_args = self._remove_null_values(
|
cleaned_args = self._remove_null_values(
|
||||||
item["function_call"]["args"]
|
item["function_call"]["args"]
|
||||||
)
|
)
|
||||||
# Create function call part with thought_signature if present
|
|
||||||
# For Gemini 3 models, we need to include thought_signature
|
|
||||||
if "thought_signature" in item:
|
if "thought_signature" in item:
|
||||||
# Use Part constructor with functionCall and thoughtSignature
|
|
||||||
parts.append(
|
parts.append(
|
||||||
types.Part(
|
types.Part(
|
||||||
functionCall=types.FunctionCall(
|
functionCall=types.FunctionCall(
|
||||||
@@ -210,7 +265,6 @@ class GoogleLLM(BaseLLM):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Use helper method when no thought_signature
|
|
||||||
parts.append(
|
parts.append(
|
||||||
types.Part.from_function_call(
|
types.Part.from_function_call(
|
||||||
name=item["function_call"]["name"],
|
name=item["function_call"]["name"],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@@ -315,10 +316,34 @@ class LLMHandler(ABC):
|
|||||||
current_prompt = self._extract_text_from_content(content)
|
current_prompt = self._extract_text_from_content(content)
|
||||||
|
|
||||||
elif role in {"assistant", "model"}:
|
elif role in {"assistant", "model"}:
|
||||||
# If this assistant turn contains tool calls, collect them; otherwise commit a response.
|
# Standard format: tool_calls array on assistant message
|
||||||
|
msg_tool_calls = message.get("tool_calls")
|
||||||
|
if msg_tool_calls:
|
||||||
|
for tc in msg_tool_calls:
|
||||||
|
call_id = tc.get("id") or str(uuid.uuid4())
|
||||||
|
func = tc.get("function", {})
|
||||||
|
args = func.get("arguments")
|
||||||
|
if isinstance(args, str):
|
||||||
|
try:
|
||||||
|
args = json.loads(args)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
current_tool_calls[call_id] = {
|
||||||
|
"tool_name": "unknown_tool",
|
||||||
|
"action_name": func.get("name"),
|
||||||
|
"arguments": args,
|
||||||
|
"result": None,
|
||||||
|
"status": "called",
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Legacy format: function_call/function_response in content list
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
|
has_fc = False
|
||||||
for item in content:
|
for item in content:
|
||||||
if "function_call" in item:
|
if "function_call" in item:
|
||||||
|
has_fc = True
|
||||||
fc = item["function_call"]
|
fc = item["function_call"]
|
||||||
call_id = fc.get("call_id") or str(uuid.uuid4())
|
call_id = fc.get("call_id") or str(uuid.uuid4())
|
||||||
current_tool_calls[call_id] = {
|
current_tool_calls[call_id] = {
|
||||||
@@ -329,37 +354,30 @@ class LLMHandler(ABC):
|
|||||||
"status": "called",
|
"status": "called",
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
}
|
}
|
||||||
elif "function_response" in item:
|
if has_fc:
|
||||||
fr = item["function_response"]
|
continue
|
||||||
call_id = fr.get("call_id") or str(uuid.uuid4())
|
|
||||||
current_tool_calls[call_id] = {
|
|
||||||
"tool_name": "unknown_tool",
|
|
||||||
"action_name": fr.get("name"),
|
|
||||||
"arguments": None,
|
|
||||||
"result": fr.get("response", {}).get("result"),
|
|
||||||
"status": "completed",
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
# No direct assistant text here; continue to next message
|
|
||||||
continue
|
|
||||||
|
|
||||||
response_text = self._extract_text_from_content(content)
|
response_text = self._extract_text_from_content(content)
|
||||||
_commit_query(response_text)
|
_commit_query(response_text)
|
||||||
|
|
||||||
elif role == "tool":
|
elif role == "tool":
|
||||||
# Attach tool outputs to the latest pending tool call if possible
|
# Standard format: tool_call_id on tool message
|
||||||
|
call_id = message.get("tool_call_id")
|
||||||
tool_text = self._extract_text_from_content(content)
|
tool_text = self._extract_text_from_content(content)
|
||||||
# Attempt to parse function_response style
|
|
||||||
call_id = None
|
|
||||||
if isinstance(content, list):
|
|
||||||
for item in content:
|
|
||||||
if "function_response" in item and item["function_response"].get("call_id"):
|
|
||||||
call_id = item["function_response"]["call_id"]
|
|
||||||
break
|
|
||||||
if call_id and call_id in current_tool_calls:
|
if call_id and call_id in current_tool_calls:
|
||||||
current_tool_calls[call_id]["result"] = tool_text
|
current_tool_calls[call_id]["result"] = tool_text
|
||||||
current_tool_calls[call_id]["status"] = "completed"
|
current_tool_calls[call_id]["status"] = "completed"
|
||||||
elif queries:
|
# Legacy: function_response in content list
|
||||||
|
elif isinstance(content, list):
|
||||||
|
for item in content:
|
||||||
|
if "function_response" in item:
|
||||||
|
legacy_id = item["function_response"].get("call_id")
|
||||||
|
if legacy_id and legacy_id in current_tool_calls:
|
||||||
|
current_tool_calls[legacy_id]["result"] = tool_text
|
||||||
|
current_tool_calls[legacy_id]["status"] = "completed"
|
||||||
|
break
|
||||||
|
elif call_id is None and queries:
|
||||||
queries[-1].setdefault("tool_calls", []).append(
|
queries[-1].setdefault("tool_calls", []).append(
|
||||||
{
|
{
|
||||||
"tool_name": "unknown_tool",
|
"tool_name": "unknown_tool",
|
||||||
@@ -648,6 +666,13 @@ class LLMHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
Execute tool calls and update conversation history.
|
Execute tool calls and update conversation history.
|
||||||
|
|
||||||
|
When a tool requires approval or client-side execution, it is
|
||||||
|
collected as a pending action instead of being executed. The
|
||||||
|
generator returns ``(updated_messages, pending_actions)`` where
|
||||||
|
*pending_actions* is ``None`` when every tool was executed
|
||||||
|
normally, or a list of dicts describing actions the client must
|
||||||
|
resolve before the LLM loop can continue.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent: The agent instance
|
agent: The agent instance
|
||||||
tool_calls: List of tool calls to execute
|
tool_calls: List of tool calls to execute
|
||||||
@@ -655,9 +680,11 @@ class LLMHandler(ABC):
|
|||||||
messages: Current conversation history
|
messages: Current conversation history
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated messages list
|
Tuple of (updated_messages, pending_actions).
|
||||||
|
pending_actions is None if all tools executed, otherwise a list.
|
||||||
"""
|
"""
|
||||||
updated_messages = messages.copy()
|
updated_messages = messages.copy()
|
||||||
|
pending_actions: List[Dict] = []
|
||||||
|
|
||||||
for i, call in enumerate(tool_calls):
|
for i, call in enumerate(tool_calls):
|
||||||
# Check context limit before executing tool call
|
# Check context limit before executing tool call
|
||||||
@@ -763,6 +790,29 @@ class LLMHandler(ABC):
|
|||||||
# Set flag on agent
|
# Set flag on agent
|
||||||
agent.context_limit_reached = True
|
agent.context_limit_reached = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# ---- Pause check: approval / client-side execution ----
|
||||||
|
llm_class = agent.llm.__class__.__name__
|
||||||
|
pause_info = agent.tool_executor.check_pause(
|
||||||
|
tools_dict, call, llm_class
|
||||||
|
)
|
||||||
|
if pause_info:
|
||||||
|
# Yield pause event so the client knows this tool is waiting
|
||||||
|
yield {
|
||||||
|
"type": "tool_call",
|
||||||
|
"data": {
|
||||||
|
"tool_name": pause_info["tool_name"],
|
||||||
|
"call_id": pause_info["call_id"],
|
||||||
|
"action_name": pause_info.get("llm_name", pause_info["name"]),
|
||||||
|
"arguments": pause_info["arguments"],
|
||||||
|
"status": pause_info["pause_type"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pending_actions.append(pause_info)
|
||||||
|
# Do NOT add messages for pending tools here.
|
||||||
|
# They will be added on resume to keep call/result pairs together.
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.tool_calls.append(call)
|
self.tool_calls.append(call)
|
||||||
tool_executor_gen = agent._execute_tool_action(tools_dict, call)
|
tool_executor_gen = agent._execute_tool_action(tools_dict, call)
|
||||||
@@ -772,25 +822,30 @@ class LLMHandler(ABC):
|
|||||||
except StopIteration as e:
|
except StopIteration as e:
|
||||||
tool_response, call_id = e.value
|
tool_response, call_id = e.value
|
||||||
break
|
break
|
||||||
|
|
||||||
function_call_content = {
|
|
||||||
"function_call": {
|
|
||||||
"name": call.name,
|
|
||||||
"args": call.arguments,
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Include thought_signature for Google Gemini 3 models
|
|
||||||
# It should be at the same level as function_call, not inside it
|
|
||||||
if call.thought_signature:
|
|
||||||
function_call_content["thought_signature"] = call.thought_signature
|
|
||||||
updated_messages.append(
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [function_call_content],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Standard internal format: assistant message with tool_calls array
|
||||||
|
args_str = (
|
||||||
|
json.dumps(call.arguments)
|
||||||
|
if isinstance(call.arguments, dict)
|
||||||
|
else call.arguments
|
||||||
|
)
|
||||||
|
tool_call_obj = {
|
||||||
|
"id": call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": call.name,
|
||||||
|
"arguments": args_str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Preserve thought_signature for Google Gemini 3 models
|
||||||
|
if call.thought_signature:
|
||||||
|
tool_call_obj["thought_signature"] = call.thought_signature
|
||||||
|
|
||||||
|
updated_messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [tool_call_obj],
|
||||||
|
})
|
||||||
|
|
||||||
updated_messages.append(self.create_tool_message(call, tool_response))
|
updated_messages.append(self.create_tool_message(call, tool_response))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -802,16 +857,15 @@ class LLMHandler(ABC):
|
|||||||
error_message = self.create_tool_message(error_call, error_response)
|
error_message = self.create_tool_message(error_call, error_response)
|
||||||
updated_messages.append(error_message)
|
updated_messages.append(error_message)
|
||||||
|
|
||||||
call_parts = call.name.split("_")
|
mapping = agent.tool_executor._name_to_tool
|
||||||
if len(call_parts) >= 2:
|
if call.name in mapping:
|
||||||
tool_id = call_parts[-1] # Last part is tool ID (e.g., "1")
|
resolved_tool_id, _ = mapping[call.name]
|
||||||
action_name = "_".join(call_parts[:-1])
|
tool_name = tools_dict.get(resolved_tool_id, {}).get(
|
||||||
tool_name = tools_dict.get(tool_id, {}).get("name", "unknown_tool")
|
"name", "unknown_tool"
|
||||||
full_action_name = f"{action_name}_{tool_id}"
|
)
|
||||||
else:
|
else:
|
||||||
tool_name = "unknown_tool"
|
tool_name = "unknown_tool"
|
||||||
action_name = call.name
|
full_action_name = call.name
|
||||||
full_action_name = call.name
|
|
||||||
yield {
|
yield {
|
||||||
"type": "tool_call",
|
"type": "tool_call",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -823,7 +877,7 @@ class LLMHandler(ABC):
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return updated_messages
|
return updated_messages, pending_actions if pending_actions else None
|
||||||
|
|
||||||
def handle_non_streaming(
|
def handle_non_streaming(
|
||||||
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
|
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
|
||||||
@@ -851,8 +905,22 @@ class LLMHandler(ABC):
|
|||||||
try:
|
try:
|
||||||
yield next(tool_handler_gen)
|
yield next(tool_handler_gen)
|
||||||
except StopIteration as e:
|
except StopIteration as e:
|
||||||
messages = e.value
|
messages, pending_actions = e.value
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# If tools need approval or client execution, pause the loop
|
||||||
|
if pending_actions:
|
||||||
|
agent._pending_continuation = {
|
||||||
|
"messages": messages,
|
||||||
|
"pending_tool_calls": pending_actions,
|
||||||
|
"tools_dict": tools_dict,
|
||||||
|
}
|
||||||
|
yield {
|
||||||
|
"type": "tool_calls_pending",
|
||||||
|
"data": {"pending_tool_calls": pending_actions},
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
|
||||||
response = agent.llm.gen(
|
response = agent.llm.gen(
|
||||||
model=agent.model_id, messages=messages, tools=agent.tools
|
model=agent.model_id, messages=messages, tools=agent.tools
|
||||||
)
|
)
|
||||||
@@ -913,10 +981,23 @@ class LLMHandler(ABC):
|
|||||||
try:
|
try:
|
||||||
yield next(tool_handler_gen)
|
yield next(tool_handler_gen)
|
||||||
except StopIteration as e:
|
except StopIteration as e:
|
||||||
messages = e.value
|
messages, pending_actions = e.value
|
||||||
break
|
break
|
||||||
tool_calls = {}
|
tool_calls = {}
|
||||||
|
|
||||||
|
# If tools need approval or client execution, pause the loop
|
||||||
|
if pending_actions:
|
||||||
|
agent._pending_continuation = {
|
||||||
|
"messages": messages,
|
||||||
|
"pending_tool_calls": pending_actions,
|
||||||
|
"tools_dict": tools_dict,
|
||||||
|
}
|
||||||
|
yield {
|
||||||
|
"type": "tool_calls_pending",
|
||||||
|
"data": {"pending_tool_calls": pending_actions},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
# Check if context limit was reached during tool execution
|
# Check if context limit was reached during tool execution
|
||||||
if hasattr(agent, 'context_limit_reached') and agent.context_limit_reached:
|
if hasattr(agent, 'context_limit_reached') and agent.context_limit_reached:
|
||||||
# Add system message warning about context limit
|
# Add system message warning about context limit
|
||||||
|
|||||||
@@ -67,18 +67,18 @@ class GoogleLLMHandler(LLMHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
||||||
"""Create Google-style tool message."""
|
"""Create a tool result message in the standard internal format."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
content = (
|
||||||
|
_json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else result
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"role": "model",
|
"role": "tool",
|
||||||
"content": [
|
"tool_call_id": tool_call.id,
|
||||||
{
|
"content": content,
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.name,
|
|
||||||
"response": {"result": result},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _iterate_stream(self, response: Any) -> Generator:
|
def _iterate_stream(self, response: Any) -> Generator:
|
||||||
|
|||||||
@@ -37,18 +37,18 @@ class OpenAILLMHandler(LLMHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
||||||
"""Create OpenAI-style tool message."""
|
"""Create a tool result message in the standard internal format."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
content = (
|
||||||
|
_json.dumps(result)
|
||||||
|
if not isinstance(result, str)
|
||||||
|
else result
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"content": [
|
"tool_call_id": tool_call.id,
|
||||||
{
|
"content": content,
|
||||||
"function_response": {
|
|
||||||
"name": tool_call.name,
|
|
||||||
"response": {"result": result},
|
|
||||||
"call_id": tool_call.id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _iterate_stream(self, response: Any) -> Generator:
|
def _iterate_stream(self, response: Any) -> Generator:
|
||||||
|
|||||||
@@ -91,16 +91,52 @@ class OpenAILLM(BaseLLM):
|
|||||||
|
|
||||||
if role == "model":
|
if role == "model":
|
||||||
role = "assistant"
|
role = "assistant"
|
||||||
|
|
||||||
|
# Standard format: assistant message with tool_calls (passthrough)
|
||||||
|
tool_calls = message.get("tool_calls")
|
||||||
|
if tool_calls and role == "assistant":
|
||||||
|
cleaned_tcs = []
|
||||||
|
for tc in tool_calls:
|
||||||
|
func = tc.get("function", {})
|
||||||
|
args = func.get("arguments", "{}")
|
||||||
|
if isinstance(args, dict):
|
||||||
|
args = json.dumps(self._remove_null_values(args))
|
||||||
|
elif isinstance(args, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(args)
|
||||||
|
args = json.dumps(self._remove_null_values(parsed))
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
cleaned_tcs.append({
|
||||||
|
"id": tc.get("id", ""),
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": func.get("name", ""), "arguments": args},
|
||||||
|
})
|
||||||
|
cleaned_messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": cleaned_tcs,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Standard format: tool message with tool_call_id (passthrough)
|
||||||
|
tool_call_id = message.get("tool_call_id")
|
||||||
|
if role == "tool" and tool_call_id is not None:
|
||||||
|
cleaned_messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
"content": content if isinstance(content, str) else json.dumps(content),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
if role and content is not None:
|
if role and content is not None:
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
cleaned_messages.append({"role": role, "content": content})
|
cleaned_messages.append({"role": role, "content": content})
|
||||||
elif isinstance(content, list):
|
elif isinstance(content, list):
|
||||||
# Collect all content parts into a single message
|
|
||||||
content_parts = []
|
content_parts = []
|
||||||
|
|
||||||
for item in content:
|
for item in content:
|
||||||
|
# Legacy format support: function_call / function_response
|
||||||
if "function_call" in item:
|
if "function_call" in item:
|
||||||
# Function calls need their own message
|
|
||||||
args = item["function_call"]["args"]
|
args = item["function_call"]["args"]
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
try:
|
try:
|
||||||
@@ -116,28 +152,20 @@ class OpenAILLM(BaseLLM):
|
|||||||
"arguments": json.dumps(cleaned_args),
|
"arguments": json.dumps(cleaned_args),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cleaned_messages.append(
|
cleaned_messages.append({
|
||||||
{
|
"role": "assistant",
|
||||||
"role": "assistant",
|
"content": None,
|
||||||
"content": None,
|
"tool_calls": [tool_call],
|
||||||
"tool_calls": [tool_call],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
elif "function_response" in item:
|
elif "function_response" in item:
|
||||||
# Function responses need their own message
|
cleaned_messages.append({
|
||||||
cleaned_messages.append(
|
"role": "tool",
|
||||||
{
|
"tool_call_id": item["function_response"]["call_id"],
|
||||||
"role": "tool",
|
"content": json.dumps(
|
||||||
"tool_call_id": item["function_response"][
|
item["function_response"]["response"]["result"]
|
||||||
"call_id"
|
),
|
||||||
],
|
})
|
||||||
"content": json.dumps(
|
|
||||||
item["function_response"]["response"]["result"]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
# Collect content parts (text, images, files) into a single message
|
|
||||||
if "type" in item and item["type"] == "text" and "text" in item:
|
if "type" in item and item["type"] == "text" and "text" in item:
|
||||||
content_parts.append(item)
|
content_parts.append(item)
|
||||||
elif "type" in item and item["type"] == "file" and "file" in item:
|
elif "type" in item and item["type"] == "file" and "file" in item:
|
||||||
@@ -145,10 +173,7 @@ class OpenAILLM(BaseLLM):
|
|||||||
elif "type" in item and item["type"] == "image_url" and "image_url" in item:
|
elif "type" in item and item["type"] == "image_url" and "image_url" in item:
|
||||||
content_parts.append(item)
|
content_parts.append(item)
|
||||||
elif "text" in item and "type" not in item:
|
elif "text" in item and "type" not in item:
|
||||||
# Legacy format: {"text": "..."} without type
|
|
||||||
content_parts.append({"type": "text", "text": item["text"]})
|
content_parts.append({"type": "text", "text": item["text"]})
|
||||||
|
|
||||||
# Add the collected content parts as a single message
|
|
||||||
if content_parts:
|
if content_parts:
|
||||||
cleaned_messages.append({"role": role, "content": content_parts})
|
cleaned_messages.append({"role": role, "content": content_parts})
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -19,25 +19,10 @@ class EpubParser(BaseParser):
|
|||||||
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
||||||
"""Parse file."""
|
"""Parse file."""
|
||||||
try:
|
try:
|
||||||
import ebooklib
|
from fast_ebook import epub
|
||||||
from ebooklib import epub
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ValueError("`EbookLib` is required to read Epub files.")
|
raise ValueError("`fast-ebook` is required to read Epub files.")
|
||||||
try:
|
|
||||||
import html2text
|
|
||||||
except ImportError:
|
|
||||||
raise ValueError("`html2text` is required to parse Epub files.")
|
|
||||||
|
|
||||||
text_list = []
|
book = epub.read_epub(file)
|
||||||
book = epub.read_epub(file, options={"ignore_ncx": True})
|
text = book.to_markdown()
|
||||||
|
|
||||||
# Iterate through all chapters.
|
|
||||||
for item in book.get_items():
|
|
||||||
# Chapters are typically located in epub documents items.
|
|
||||||
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
|
||||||
text_list.append(
|
|
||||||
html2text.html2text(item.get_content().decode("utf-8"))
|
|
||||||
)
|
|
||||||
|
|
||||||
text = "\n".join(text_list)
|
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
anthropic==0.75.0
|
anthropic==0.88.0
|
||||||
boto3==1.42.17
|
boto3==1.42.83
|
||||||
beautifulsoup4==4.14.3
|
beautifulsoup4==4.14.3
|
||||||
cel-python==0.5.0
|
cel-python==0.5.0
|
||||||
celery==5.6.0
|
celery==5.6.3
|
||||||
cryptography==46.0.3
|
cryptography==46.0.6
|
||||||
dataclasses-json==0.6.7
|
dataclasses-json==0.6.7
|
||||||
defusedxml==0.7.1
|
defusedxml==0.7.1
|
||||||
docling>=2.16.0
|
docling>=2.16.0
|
||||||
@@ -11,89 +11,83 @@ rapidocr>=1.4.0
|
|||||||
onnxruntime>=1.19.0
|
onnxruntime>=1.19.0
|
||||||
docx2txt==0.9
|
docx2txt==0.9
|
||||||
ddgs>=8.0.0
|
ddgs>=8.0.0
|
||||||
ebooklib==0.20
|
fast-ebook
|
||||||
escodegen==1.0.11
|
elevenlabs==2.41.0
|
||||||
esprima==4.0.1
|
Flask==3.1.3
|
||||||
esutils==1.0.1
|
|
||||||
elevenlabs==2.27.0
|
|
||||||
Flask==3.1.2
|
|
||||||
faiss-cpu==1.13.2
|
faiss-cpu==1.13.2
|
||||||
fastmcp==2.14.1
|
fastmcp==3.2.0
|
||||||
flask-restx==1.3.2
|
flask-restx==1.3.2
|
||||||
google-genai==1.54.0
|
google-genai==1.69.0
|
||||||
google-api-python-client==2.187.0
|
google-api-python-client==2.193.0
|
||||||
google-auth-httplib2==0.3.0
|
google-auth-httplib2==0.3.1
|
||||||
google-auth-oauthlib==1.2.3
|
google-auth-oauthlib==1.3.1
|
||||||
gTTS==2.5.4
|
gTTS==2.5.4
|
||||||
gunicorn==23.0.0
|
gunicorn==25.3.0
|
||||||
html2text==2025.4.15
|
|
||||||
javalang==0.13.0
|
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
jiter==0.12.0
|
jiter==0.13.0
|
||||||
jmespath==1.0.1
|
jmespath==1.1.0
|
||||||
joblib==1.5.3
|
joblib==1.5.3
|
||||||
jsonpatch==1.33
|
jsonpatch==1.33
|
||||||
jsonpointer==3.0.0
|
jsonpointer==3.0.0
|
||||||
kombu==5.6.1
|
kombu==5.6.2
|
||||||
langchain==1.2.0
|
langchain==1.2.3
|
||||||
langchain-community==0.4.1
|
langchain-community==0.4.1
|
||||||
langchain-core==1.2.5
|
langchain-core==1.2.23
|
||||||
langchain-openai==1.1.6
|
langchain-openai==1.1.12
|
||||||
langchain-text-splitters==1.1.0
|
langchain-text-splitters==1.1.1
|
||||||
langsmith==0.5.1
|
langsmith==0.7.23
|
||||||
lazy-object-proxy==1.12.0
|
lazy-object-proxy==1.12.0
|
||||||
lxml==6.0.2
|
lxml==6.0.2
|
||||||
markupsafe==3.0.3
|
markupsafe==3.0.3
|
||||||
marshmallow>=3.18.0,<5.0.0
|
marshmallow>=3.18.0,<5.0.0
|
||||||
mpmath==1.3.0
|
mpmath==1.3.0
|
||||||
multidict==6.7.0
|
multidict==6.7.1
|
||||||
msal==1.34.0
|
msal==1.35.1
|
||||||
mypy-extensions==1.1.0
|
mypy-extensions==1.1.0
|
||||||
networkx==3.6.1
|
networkx==3.6.1
|
||||||
numpy==2.4.0
|
numpy==2.4.4
|
||||||
openai==2.14.0
|
openai==2.30.0
|
||||||
openapi3-parser==1.1.22
|
openapi3-parser==1.1.22
|
||||||
orjson==3.11.5
|
orjson==3.11.7
|
||||||
packaging==24.2
|
packaging==26.0
|
||||||
pandas==2.3.3
|
pandas==3.0.2
|
||||||
openpyxl==3.1.5
|
openpyxl==3.1.5
|
||||||
pathable==0.4.4
|
pathable==0.5.0
|
||||||
pdf2image>=1.17.0
|
pdf2image>=1.17.0
|
||||||
pillow
|
pillow
|
||||||
portalocker>=2.7.0,<3.0.0
|
portalocker>=2.7.0,<4.0.0
|
||||||
prance==25.4.8.0
|
|
||||||
prompt-toolkit==3.0.52
|
prompt-toolkit==3.0.52
|
||||||
protobuf==6.33.2
|
protobuf==7.34.1
|
||||||
psycopg2-binary==2.9.11
|
psycopg2-binary==2.9.11
|
||||||
py==1.11.0
|
py==1.11.0
|
||||||
pydantic
|
pydantic
|
||||||
pydantic-core
|
pydantic-core
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
pymongo==4.15.5
|
pymongo==4.16.0
|
||||||
pypdf==6.5.0
|
pypdf==6.9.2
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
python-dotenv
|
python-dotenv
|
||||||
python-jose==3.5.0
|
python-jose==3.5.0
|
||||||
python-pptx==1.0.2
|
python-pptx==1.0.2
|
||||||
redis==7.1.0
|
redis==7.4.0
|
||||||
referencing>=0.28.0,<0.38.0
|
referencing>=0.28.0,<0.38.0
|
||||||
regex==2025.11.3
|
regex==2026.4.4
|
||||||
requests==2.32.5
|
requests==2.33.1
|
||||||
retry==0.9.2
|
retry==0.9.2
|
||||||
sentence-transformers==5.2.0
|
sentence-transformers==5.3.0
|
||||||
tiktoken==0.12.0
|
tiktoken==0.12.0
|
||||||
tokenizers==0.22.1
|
tokenizers==0.22.2
|
||||||
torch==2.9.1
|
torch==2.11.0
|
||||||
tqdm==4.67.1
|
tqdm==4.67.3
|
||||||
transformers==4.57.3
|
transformers==5.4.0
|
||||||
typing-extensions==4.15.0
|
typing-extensions==4.15.0
|
||||||
typing-inspect==0.9.0
|
typing-inspect==0.9.0
|
||||||
tzdata==2025.3
|
tzdata==2025.3
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
vine==5.1.0
|
vine==5.1.0
|
||||||
wcwidth==0.2.14
|
wcwidth==0.6.0
|
||||||
werkzeug>=3.1.0
|
werkzeug>=3.1.0
|
||||||
yarl==1.22.0
|
yarl==1.23.0
|
||||||
markdownify==1.2.2
|
markdownify==1.2.2
|
||||||
tldextract==5.3.0
|
tldextract==5.3.1
|
||||||
websockets==15.0.1
|
websockets==16.0
|
||||||
@@ -21,10 +21,19 @@ class LocalStorage(BaseStorage):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_full_path(self, path: str) -> str:
|
def _get_full_path(self, path: str) -> str:
|
||||||
"""Get absolute path by combining base_dir and path."""
|
"""Get absolute path by combining base_dir and path.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the resolved path escapes base_dir (path traversal).
|
||||||
|
"""
|
||||||
if os.path.isabs(path):
|
if os.path.isabs(path):
|
||||||
return path
|
resolved = os.path.realpath(path)
|
||||||
return os.path.join(self.base_dir, path)
|
else:
|
||||||
|
resolved = os.path.realpath(os.path.join(self.base_dir, path))
|
||||||
|
base = os.path.realpath(self.base_dir)
|
||||||
|
if not resolved.startswith(base + os.sep) and resolved != base:
|
||||||
|
raise ValueError(f"Path traversal detected: {path}")
|
||||||
|
return resolved
|
||||||
|
|
||||||
def save_file(self, file_data: BinaryIO, path: str, **kwargs) -> dict:
|
def save_file(self, file_data: BinaryIO, path: str, **kwargs) -> dict:
|
||||||
"""Save a file to local storage."""
|
"""Save a file to local storage."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import posixpath
|
||||||
from typing import BinaryIO, Callable, List
|
from typing import BinaryIO, Callable, List
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
@@ -14,6 +15,20 @@ from botocore.exceptions import ClientError
|
|||||||
class S3Storage(BaseStorage):
|
class S3Storage(BaseStorage):
|
||||||
"""AWS S3 storage implementation."""
|
"""AWS S3 storage implementation."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_path(path: str) -> str:
|
||||||
|
"""Validate and normalize an S3 key to prevent path traversal.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the path contains traversal sequences or is absolute.
|
||||||
|
"""
|
||||||
|
if "\x00" in path:
|
||||||
|
raise ValueError(f"Null byte in path: {path}")
|
||||||
|
normalized = posixpath.normpath(path)
|
||||||
|
if normalized.startswith("/") or normalized.startswith(".."):
|
||||||
|
raise ValueError(f"Path traversal detected: {path}")
|
||||||
|
return normalized
|
||||||
|
|
||||||
def __init__(self, bucket_name=None):
|
def __init__(self, bucket_name=None):
|
||||||
"""
|
"""
|
||||||
Initialize S3 storage.
|
Initialize S3 storage.
|
||||||
@@ -46,6 +61,7 @@ class S3Storage(BaseStorage):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Save a file to S3 storage."""
|
"""Save a file to S3 storage."""
|
||||||
|
path = self._validate_path(path)
|
||||||
self.s3.upload_fileobj(
|
self.s3.upload_fileobj(
|
||||||
file_data, self.bucket_name, path, ExtraArgs={"StorageClass": storage_class}
|
file_data, self.bucket_name, path, ExtraArgs={"StorageClass": storage_class}
|
||||||
)
|
)
|
||||||
@@ -61,6 +77,7 @@ class S3Storage(BaseStorage):
|
|||||||
|
|
||||||
def get_file(self, path: str) -> BinaryIO:
|
def get_file(self, path: str) -> BinaryIO:
|
||||||
"""Get a file from S3 storage."""
|
"""Get a file from S3 storage."""
|
||||||
|
path = self._validate_path(path)
|
||||||
if not self.file_exists(path):
|
if not self.file_exists(path):
|
||||||
raise FileNotFoundError(f"File not found: {path}")
|
raise FileNotFoundError(f"File not found: {path}")
|
||||||
file_obj = io.BytesIO()
|
file_obj = io.BytesIO()
|
||||||
@@ -70,6 +87,7 @@ class S3Storage(BaseStorage):
|
|||||||
|
|
||||||
def delete_file(self, path: str) -> bool:
|
def delete_file(self, path: str) -> bool:
|
||||||
"""Delete a file from S3 storage."""
|
"""Delete a file from S3 storage."""
|
||||||
|
path = self._validate_path(path)
|
||||||
try:
|
try:
|
||||||
self.s3.delete_object(Bucket=self.bucket_name, Key=path)
|
self.s3.delete_object(Bucket=self.bucket_name, Key=path)
|
||||||
return True
|
return True
|
||||||
@@ -78,6 +96,7 @@ class S3Storage(BaseStorage):
|
|||||||
|
|
||||||
def file_exists(self, path: str) -> bool:
|
def file_exists(self, path: str) -> bool:
|
||||||
"""Check if a file exists in S3 storage."""
|
"""Check if a file exists in S3 storage."""
|
||||||
|
path = self._validate_path(path)
|
||||||
try:
|
try:
|
||||||
self.s3.head_object(Bucket=self.bucket_name, Key=path)
|
self.s3.head_object(Bucket=self.bucket_name, Key=path)
|
||||||
return True
|
return True
|
||||||
@@ -115,6 +134,7 @@ class S3Storage(BaseStorage):
|
|||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
path = self._validate_path(path)
|
||||||
if not self.file_exists(path):
|
if not self.file_exists(path):
|
||||||
raise FileNotFoundError(f"File not found in S3: {path}")
|
raise FileNotFoundError(f"File not found in S3: {path}")
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
|
|||||||
@@ -11,11 +11,33 @@ from application.storage.storage_creator import StorageCreator
|
|||||||
|
|
||||||
|
|
||||||
def get_vectorstore(path: str) -> str:
|
def get_vectorstore(path: str) -> str:
|
||||||
if path:
|
"""Build a safe local path for a FAISS index.
|
||||||
vectorstore = f"indexes/{path}"
|
|
||||||
else:
|
Args:
|
||||||
vectorstore = "indexes"
|
path: Source identifier provided by the caller.
|
||||||
return vectorstore
|
|
||||||
|
Returns:
|
||||||
|
The validated vectorstore path rooted under ``indexes``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If ``path`` escapes the ``indexes`` directory.
|
||||||
|
"""
|
||||||
|
base_dir = "indexes"
|
||||||
|
if not path:
|
||||||
|
return base_dir
|
||||||
|
|
||||||
|
normalized = str(path).strip()
|
||||||
|
if "\\" in normalized:
|
||||||
|
raise ValueError("Invalid source_id path")
|
||||||
|
|
||||||
|
candidate = os.path.normpath(os.path.join(base_dir, normalized))
|
||||||
|
base_abs = os.path.abspath(base_dir)
|
||||||
|
candidate_abs = os.path.abspath(candidate)
|
||||||
|
|
||||||
|
if not candidate_abs.startswith(base_abs + os.sep) and candidate_abs != base_abs:
|
||||||
|
raise ValueError("Invalid source_id path")
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
class FaissStore(BaseVectorStore):
|
class FaissStore(BaseVectorStore):
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export default {
|
|||||||
"title": "🔌 Agent API",
|
"title": "🔌 Agent API",
|
||||||
"href": "/Agents/api"
|
"href": "/Agents/api"
|
||||||
},
|
},
|
||||||
|
"openai-compatible": {
|
||||||
|
"title": "🔄 OpenAI-Compatible API",
|
||||||
|
"href": "/Agents/openai-compatible"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"title": "🪝 Agent Webhooks",
|
"title": "🪝 Agent Webhooks",
|
||||||
"href": "/Agents/webhooks"
|
"href": "/Agents/webhooks"
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ DocsGPT Agents can be accessed programmatically through API endpoints. This page
|
|||||||
|
|
||||||
When you use an agent `api_key`, DocsGPT loads that agent's configuration automatically (prompt, tools, sources, default model). You usually only need to send `question` and `api_key`.
|
When you use an agent `api_key`, DocsGPT loads that agent's configuration automatically (prompt, tools, sources, default model). You usually only need to send `question` and `api_key`.
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Looking to connect an existing OpenAI-compatible client (opencode, aider, the OpenAI SDKs, etc.) to a DocsGPT Agent? Use the [OpenAI-Compatible Chat Completions API](/Agents/openai-compatible) — it speaks the standard chat completions protocol so no adapter code is required.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
## Base URL
|
## Base URL
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ Once an agent is created, you can:
|
|||||||
* Modify any of its configuration settings (name, description, source, prompt, tools, type).
|
* Modify any of its configuration settings (name, description, source, prompt, tools, type).
|
||||||
* **Generate a Public Link:** From the edit screen, you can create a shareable public link that allows others to import and use your agent.
|
* **Generate a Public Link:** From the edit screen, you can create a shareable public link that allows others to import and use your agent.
|
||||||
* **Get a Webhook URL:** You can also obtain a Webhook URL for the agent. This allows external applications or services to trigger the agent and receive responses programmatically, enabling powerful integrations and automations.
|
* **Get a Webhook URL:** You can also obtain a Webhook URL for the agent. This allows external applications or services to trigger the agent and receive responses programmatically, enabling powerful integrations and automations.
|
||||||
|
* **Use it via API:** Every agent exposes an API key that can be used with the native [Agent API](/Agents/api) or the [OpenAI-Compatible API](/Agents/openai-compatible) so you can drop DocsGPT Agents into any tool that already speaks the chat completions protocol.
|
||||||
|
|
||||||
## Seeding Premade Agents from YAML
|
## Seeding Premade Agents from YAML
|
||||||
|
|
||||||
|
|||||||
93
docs/content/Agents/openai-compatible.mdx
Normal file
93
docs/content/Agents/openai-compatible.mdx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: OpenAI-Compatible API
|
||||||
|
description: Connect any OpenAI-compatible client to DocsGPT Agents via /v1/chat/completions.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout, Tabs } from 'nextra/components';
|
||||||
|
|
||||||
|
# OpenAI-Compatible API
|
||||||
|
|
||||||
|
DocsGPT exposes `/v1/chat/completions` following the standard chat completions protocol. Point any compatible client — **opencode**, **Aider**, **LibreChat** or the OpenAI SDKs — at your DocsGPT Agent by changing only the base URL and API key.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
<Tabs items={['Python', 'cURL']}>
|
||||||
|
<Tabs.Tab>
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
base_url="http://localhost:7091/v1", # or https://gptcloud.arc53.com/v1
|
||||||
|
api_key="your_agent_api_key",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="docsgpt-agent",
|
||||||
|
messages=[{"role": "user", "content": "Summarize our refund policy"}],
|
||||||
|
)
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab>
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:7091/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer your_agent_api_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"docsgpt-agent","messages":[{"role":"user","content":"Summarize our refund policy"}]}'
|
||||||
|
```
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
The `model` field is accepted but ignored — the agent bound to your API key determines the model. The agent's prompt, sources, tools, and default model are loaded automatically.
|
||||||
|
|
||||||
|
## Base URL & Auth
|
||||||
|
|
||||||
|
| Environment | Base URL |
|
||||||
|
| --- | --- |
|
||||||
|
| Local | `http://localhost:7091/v1` |
|
||||||
|
| Cloud | `https://gptcloud.arc53.com/v1` |
|
||||||
|
|
||||||
|
Authenticate with `Authorization: Bearer <agent_api_key>`.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/v1/chat/completions` | Chat request (streaming or non-streaming) |
|
||||||
|
| `GET` | `/v1/models` | List agents available to your key |
|
||||||
|
|
||||||
|
## Streaming
|
||||||
|
|
||||||
|
Set `"stream": true`. You'll receive SSE chunks with `choices[0].delta.content`. DocsGPT-specific events (sources, tool calls) arrive as extra frames with a `docsgpt` key — standard clients ignore them.
|
||||||
|
|
||||||
|
```python
|
||||||
|
stream = client.chat.completions.create(
|
||||||
|
model="docsgpt-agent",
|
||||||
|
stream=True,
|
||||||
|
messages=[{"role": "user", "content": "Explain vector search"}],
|
||||||
|
)
|
||||||
|
for chunk in stream:
|
||||||
|
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Prompt Override
|
||||||
|
|
||||||
|
System messages are **dropped by default** — the agent's configured prompt is used. To allow callers to override it, enable **Allow prompt override** in the agent's Advanced settings.
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
When an override is active, the agent's prompt template is replaced wholesale — template variables like `{summaries}` are not substituted.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Conversation Persistence
|
||||||
|
|
||||||
|
Conversations are **not persisted by default** (stateless, like most OpenAI clients expect). Opt in per request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "docsgpt": { "save_conversation": true } }
|
||||||
|
```
|
||||||
|
|
||||||
|
The response will include `docsgpt.conversation_id`.
|
||||||
|
|
||||||
|
## When to Use Native Endpoints Instead
|
||||||
|
|
||||||
|
Use [`/api/answer` or `/stream`](/Agents/api) if you need server-side attachments, `passthrough` template variables, explicit `conversation_id` reuse, or persistence by default.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ docsgpt_url = os.getenv("docsgpt_url")
|
|||||||
chatwoot_url = os.getenv("chatwoot_url")
|
chatwoot_url = os.getenv("chatwoot_url")
|
||||||
docsgpt_key = os.getenv("docsgpt_key")
|
docsgpt_key = os.getenv("docsgpt_key")
|
||||||
chatwoot_token = os.getenv("chatwoot_token")
|
chatwoot_token = os.getenv("chatwoot_token")
|
||||||
|
chatwoot_webhook_secret = os.getenv("chatwoot_webhook_secret", "")
|
||||||
# account_id = os.getenv("account_id")
|
# account_id = os.getenv("account_id")
|
||||||
# assignee_id = os.getenv("assignee_id")
|
# assignee_id = os.getenv("assignee_id")
|
||||||
label_stop = "human-requested"
|
label_stop = "human-requested"
|
||||||
@@ -45,12 +48,35 @@ def send_to_chatwoot(account, conversation, message):
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_chatwoot_signature(raw_body: bytes, signature_header: str | None) -> bool:
|
||||||
|
"""Validate Chatwoot webhook signature using shared secret."""
|
||||||
|
if not chatwoot_webhook_secret or not signature_header:
|
||||||
|
return False
|
||||||
|
|
||||||
|
expected = hmac.new(
|
||||||
|
chatwoot_webhook_secret.encode("utf-8"), raw_body, hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
provided = signature_header.strip()
|
||||||
|
if provided.startswith("sha256="):
|
||||||
|
provided = provided.split("=", maxsplit=1)[1]
|
||||||
|
|
||||||
|
return hmac.compare_digest(provided, expected)
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docsgpt', methods=['POST'])
|
@app.route('/docsgpt', methods=['POST'])
|
||||||
def docsgpt():
|
def docsgpt():
|
||||||
data = request.get_json()
|
raw_body = request.get_data()
|
||||||
|
signature = request.headers.get("X-Chatwoot-Signature")
|
||||||
|
if not is_valid_chatwoot_signature(raw_body, signature):
|
||||||
|
return "Unauthorized", 401
|
||||||
|
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return "Invalid payload", 400
|
||||||
pp = pprint.PrettyPrinter(indent=4)
|
pp = pprint.PrettyPrinter(indent=4)
|
||||||
pp.pprint(data)
|
pp.pprint(data)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function MainLayout() {
|
|||||||
const [navOpen, setNavOpen] = useState(!(isMobile || isTablet));
|
const [navOpen, setNavOpen] = useState(!(isMobile || isTablet));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-raisin-black relative h-screen overflow-hidden">
|
<div className="bg-background relative h-screen overflow-hidden">
|
||||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
<ActionButtons showNewChat={true} showShare={true} />
|
<ActionButtons showNewChat={true} showShare={true} />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export default function Hero({
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-black-1000 dark:text-bright-gray flex h-full w-full flex-col items-center justify-between">
|
<div className="text-black-1000 dark:text-foreground flex h-full w-full flex-col items-center justify-between">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="flex grow flex-col items-center justify-center pt-8 md:pt-0">
|
<div className="flex grow flex-col items-center justify-center pt-8 md:pt-0">
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-px flex items-center">
|
||||||
<span className="text-4xl font-semibold">DocsGPT</span>
|
<span className="text-4xl font-semibold">DocsGPT</span>
|
||||||
<img className="mb-1 inline w-14" src={DocsGPT3} alt="docsgpt" />
|
<img className="mb-1 inline w-14" src={DocsGPT3} alt="docsgpt" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,9 +44,9 @@ export default function Hero({
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => handleQuestion({ question: demo.query })}
|
onClick={() => handleQuestion({ question: demo.query })}
|
||||||
className={`border-dark-gray text-just-black hover:bg-cultured dark:border-dim-gray dark:text-chinese-white dark:hover:bg-charleston-green w-full rounded-[66px] border bg-transparent px-6 py-[14px] text-left transition-colors ${key >= 2 ? 'hidden md:block' : ''}`}
|
className={`border-border text-foreground hover:bg-muted dark:hover:bg-muted/50 bg-card w-full rounded-[66px] border px-6 py-3.5 text-left transition-colors dark:bg-transparent ${key >= 2 ? 'hidden md:block' : ''}`}
|
||||||
>
|
>
|
||||||
<p className="text-black-1000 dark:text-bright-gray mb-2 font-semibold">
|
<p className="text-black-1000 dark:text-foreground mb-2 font-semibold">
|
||||||
{demo.header}
|
{demo.header}
|
||||||
</p>
|
</p>
|
||||||
<span className="line-clamp-2 text-gray-700 opacity-60 dark:text-gray-300">
|
<span className="line-clamp-2 text-gray-700 opacity-60 dark:text-gray-300">
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="text-gray-4000 text-[20px] font-medium">
|
<div className="text-muted-foreground text-[20px] font-medium">
|
||||||
DocsGPT
|
DocsGPT
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,7 +338,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
ref={navRef}
|
ref={navRef}
|
||||||
className={`${
|
className={`${
|
||||||
!navOpen && '-ml-96 md:-ml-72'
|
!navOpen && '-ml-96 md:-ml-72'
|
||||||
} bg-lotion dark:border-r-purple-taupe dark:bg-chinese-black fixed top-0 z-20 flex h-full w-72 flex-col border-r border-b-0 transition-all duration-300 ease-in-out dark:text-white`}
|
} bg-sidebar dark:border-r-sidebar-border fixed top-0 z-20 flex h-full w-72 flex-col border-r border-b-0 transition-all duration-300 ease-in-out dark:text-white`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
|
||||||
@@ -380,7 +380,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`${
|
`${
|
||||||
isActive ? 'bg-transparent' : ''
|
isActive ? 'bg-transparent' : ''
|
||||||
} group border-silver hover:border-rainy-gray dark:border-purple-taupe sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border p-3 hover:bg-transparent dark:text-white`
|
} group border-sidebar-border hover:border-sidebar-border sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border p-3 hover:bg-transparent dark:text-white`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -388,13 +388,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
alt="Create new chat"
|
alt="Create new chat"
|
||||||
className="opacity-80 group-hover:opacity-100"
|
className="opacity-80 group-hover:opacity-100"
|
||||||
/>
|
/>
|
||||||
<p className="text-dove-gray dark:text-chinese-silver dark:group-hover:text-bright-gray text-sm group-hover:text-neutral-600">
|
<p className="text-muted-foreground dark:text-foreground dark:group-hover:text-foreground text-sm group-hover:text-neutral-600">
|
||||||
{t('newChat')}
|
{t('newChat')}
|
||||||
</p>
|
</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div
|
<div
|
||||||
id="conversationsMainDiv"
|
id="conversationsMainDiv"
|
||||||
className="mb-auto h-[78vh] overflow-x-hidden overflow-y-auto scrollbar-overlay dark:text-white"
|
className="scrollbar-overlay mb-auto h-[78vh] overflow-x-hidden overflow-y-auto dark:text-white"
|
||||||
>
|
>
|
||||||
{conversations?.loading && !isDeletingConversation && (
|
{conversations?.loading && !isDeletingConversation && (
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
@@ -417,9 +417,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
{recentAgents.map((agent, idx) => (
|
{recentAgents.map((agent, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`group hover:bg-bright-gray dark:hover:bg-dark-charcoal mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between rounded-3xl pl-4 ${
|
className={`group hover:bg-sidebar-accent mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between rounded-3xl pl-4 ${
|
||||||
agent.id === selectedAgent?.id && !conversationId
|
agent.id === selectedAgent?.id && !conversationId
|
||||||
? 'bg-bright-gray dark:bg-dark-charcoal'
|
? 'bg-sidebar-accent'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleAgentClick(agent)}
|
onClick={() => handleAgentClick(agent)}
|
||||||
@@ -432,7 +432,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="h-6 w-6 rounded-full object-contain"
|
className="h-6 w-6 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-eerie-black dark:text-bright-gray overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
<p className="text-foreground dark:text-foreground overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
||||||
{agent.name}
|
{agent.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,7 +456,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="hover:bg-bright-gray dark:hover:bg-dark-charcoal mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
className="hover:bg-sidebar-accent mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(setSelectedAgent(null));
|
dispatch(setSelectedAgent(null));
|
||||||
if (isMobile || isTablet) {
|
if (isMobile || isTablet) {
|
||||||
@@ -472,7 +472,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="h-[18px] w-[18px]"
|
className="h-[18px] w-[18px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-eerie-black dark:text-bright-gray overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
<p className="text-foreground dark:text-foreground overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
||||||
{t('manageAgents')}
|
{t('manageAgents')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -480,7 +480,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="hover:bg-bright-gray dark:hover:bg-dark-charcoal mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
className="hover:bg-sidebar-accent mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile || isTablet) {
|
if (isMobile || isTablet) {
|
||||||
setNavOpen(false);
|
setNavOpen(false);
|
||||||
@@ -496,7 +496,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="h-[18px] w-[18px]"
|
className="h-[18px] w-[18px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-eerie-black dark:text-bright-gray overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
<p className="text-foreground dark:text-foreground overflow-hidden text-sm leading-6 text-ellipsis whitespace-nowrap">
|
||||||
{t('manageAgents')}
|
{t('manageAgents')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -529,8 +529,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-eerie-black flex h-auto flex-col justify-end dark:text-white">
|
<div className="text-foreground flex h-auto flex-col justify-end dark:text-white">
|
||||||
<div className="dark:border-b-purple-taupe flex flex-col gap-2 border-b py-2">
|
<div className="dark:border-b-sidebar-border flex flex-col gap-2 border-b py-2">
|
||||||
<NavLink
|
<NavLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile || isTablet) {
|
if (isMobile || isTablet) {
|
||||||
@@ -540,8 +540,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
}}
|
}}
|
||||||
to="/settings"
|
to="/settings"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`mx-4 my-auto flex h-9 cursor-pointer items-center gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
`hover:bg-sidebar-accent mx-4 my-auto flex h-9 cursor-pointer items-center gap-4 rounded-3xl ${
|
||||||
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
|
isActive ? 'bg-sidebar-accent' : ''
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -552,12 +552,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
height={21}
|
height={21}
|
||||||
className="my-auto ml-2 filter dark:invert"
|
className="my-auto ml-2 filter dark:invert"
|
||||||
/>
|
/>
|
||||||
<p className="text-eerie-black text-sm dark:text-white">
|
<p className="text-foreground text-sm dark:text-white">
|
||||||
{t('settings.label')}
|
{t('settings.label')}
|
||||||
</p>
|
</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-eerie-black flex flex-col justify-end dark:text-white">
|
<div className="text-foreground flex flex-col justify-end dark:text-white">
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<Help />
|
<Help />
|
||||||
|
|
||||||
@@ -565,9 +565,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<NavLink
|
<NavLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to={'https://discord.gg/vN7YFfdMpj'}
|
to={'https://discord.gg/vN7YFfdMpj'}
|
||||||
className={
|
className={'hover:bg-sidebar-accent rounded-full'}
|
||||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={Discord}
|
src={Discord}
|
||||||
@@ -580,9 +578,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<NavLink
|
<NavLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to={'https://x.com/docsgptai'}
|
to={'https://x.com/docsgptai'}
|
||||||
className={
|
className={'hover:bg-sidebar-accent rounded-full'}
|
||||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={Twitter}
|
src={Twitter}
|
||||||
@@ -595,9 +591,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<NavLink
|
<NavLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to={'https://github.com/arc53/docsgpt'}
|
to={'https://github.com/arc53/docsgpt'}
|
||||||
className={
|
className={'hover:bg-sidebar-accent rounded-full'}
|
||||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={Github}
|
src={Github}
|
||||||
@@ -612,7 +606,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:border-b-purple-taupe dark:bg-chinese-black sticky z-10 h-16 w-full border-b-2 bg-gray-50 lg:hidden">
|
<div className="dark:border-b-sidebar-border bg-sidebar sticky z-10 h-16 w-full border-b-2 lg:hidden">
|
||||||
<div className="ml-6 flex h-full items-center gap-6">
|
<div className="ml-6 flex h-full items-center gap-6">
|
||||||
<button
|
<button
|
||||||
className="h-6 w-6 lg:hidden"
|
className="h-6 w-6 lg:hidden"
|
||||||
@@ -624,7 +618,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
className="w-7 filter dark:invert"
|
className="w-7 filter dark:invert"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="text-gray-4000 text-[20px] font-medium">DocsGPT</div>
|
<div className="text-muted-foreground text-[20px] font-medium">
|
||||||
|
DocsGPT
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DeleteConvModal
|
<DeleteConvModal
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export default function PageNotFound() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-raisin-black grid min-h-screen">
|
<div className="bg-background grid min-h-screen">
|
||||||
<p className="text-jet dark:bg-outer-space mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 lg:p-10 xl:p-16 dark:text-gray-100">
|
<p className="text-foreground dark:bg-card mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 lg:p-10 xl:p-16">
|
||||||
<h1>{t('pageNotFound.title')}</h1>
|
<h1>{t('pageNotFound.title')}</h1>
|
||||||
<p>{t('pageNotFound.message')}</p>
|
<p>{t('pageNotFound.message')}</p>
|
||||||
<button className="pointer-cursor bg-blue-1000 hover:bg-blue-3000 mr-4 flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-white transition-colors duration-100">
|
<button className="pointer-cursor bg-blue-1000 hover:bg-blue-3000 mr-4 flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-white transition-colors duration-100">
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export default function AgentCard({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex h-44 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-4 py-5 hover:bg-[#ECECEC] sm:w-48 sm:px-6 dark:bg-[#383838] dark:hover:bg-[#383838]/80 ${agent.status === 'published' && 'cursor-pointer'}`}
|
className={`bg-muted hover:bg-accent relative flex h-44 flex-col justify-between rounded-[1.2rem] px-4 py-5 sm:w-48 sm:px-6 ${agent.status === 'published' && 'cursor-pointer'}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleClick();
|
handleClick();
|
||||||
@@ -283,17 +283,17 @@ export default function AgentCard({
|
|||||||
className="h-7 w-7 rounded-full object-contain"
|
className="h-7 w-7 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
{agent.status === 'draft' && (
|
{agent.status === 'draft' && (
|
||||||
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>
|
<p className="text-foreground text-xs opacity-50">{`(Draft)`}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p
|
<p
|
||||||
title={agent.name}
|
title={agent.name}
|
||||||
className="truncate px-1 text-[13px] leading-relaxed font-semibold text-[#020617] capitalize dark:text-[#E0E0E0]"
|
className="text-foreground truncate px-1 text-[13px] leading-relaxed font-semibold capitalize"
|
||||||
>
|
>
|
||||||
{agent.name}
|
{agent.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="dark:text-sonic-silver-light mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-[#64748B]">
|
<p className="dark:text-muted-foreground text-muted-foreground mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed">
|
||||||
{agent.description}
|
{agent.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,4 +320,4 @@ export default function AgentCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,25 +41,25 @@ export default function AgentLogs() {
|
|||||||
<div className="p-4 md:p-12">
|
<div className="p-4 md:p-12">
|
||||||
<div className="flex items-center gap-3 px-4">
|
<div className="flex items-center gap-3 px-4">
|
||||||
<button
|
<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]"
|
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||||
onClick={() => navigate('/agents')}
|
onClick={() => navigate('/agents')}
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
|
<p className="text-foreground dark:text-foreground mt-px text-sm font-semibold">
|
||||||
{t('agents.backToAll')}
|
{t('agents.backToAll')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||||
<h1 className="text-eerie-black m-0 text-[32px] font-bold md:text-[40px] dark:text-white">
|
<h1 className="text-foreground m-0 text-[32px] font-bold md:text-[40px] dark:text-white">
|
||||||
{t('agents.logs.title')}
|
{t('agents.logs.title')}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col gap-3 px-4">
|
<div className="mt-6 flex flex-col gap-3 px-4">
|
||||||
{agent && (
|
{agent && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
|
<p className="text-foreground">{agent.name}</p>
|
||||||
<p className="text-xs text-[#28292E] dark:text-[#E0E0E0]/40">
|
<p className="text-muted-foreground text-xs">
|
||||||
{agent.last_used_at
|
{agent.last_used_at
|
||||||
? t('agents.logs.lastUsedAt') +
|
? t('agents.logs.lastUsedAt') +
|
||||||
' ' +
|
' ' +
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function AgentPreview() {
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-4000 dark:text-sonic-silver w-full bg-transparent text-center text-xs md:inline">
|
<p className="text-muted-foreground w-full bg-transparent text-center text-xs md:inline">
|
||||||
{t('agents.preview.testMessage')}
|
{t('agents.preview.testMessage')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,10 +159,10 @@ export default function AgentsList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-12">
|
<div className="p-4 md:p-12">
|
||||||
<h1 className="text-eerie-black mb-0 text-[32px] font-bold lg:text-[40px] dark:text-[#E0E0E0]">
|
<h1 className="text-foreground mb-0 text-[32px] font-bold lg:text-[40px]">
|
||||||
{t('agents.title')}
|
{t('agents.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="dark:text-gray-4000 mt-5 text-[15px] leading-6 text-[#71717A]">
|
<p className="text-muted-foreground mt-5 text-[15px] leading-6">
|
||||||
{t('agents.description')}
|
{t('agents.description')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ export default function AgentsList() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder={t('agents.searchPlaceholder')}
|
placeholder={t('agents.searchPlaceholder')}
|
||||||
className="h-11 w-full rounded-full border border-[#E5E5E5] bg-white py-2 pr-5 pl-11 text-sm shadow-[0_1px_4px_rgba(0,0,0,0.06)] transition-shadow outline-none placeholder:text-[#9CA3AF] focus:shadow-[0_2px_8px_rgba(0,0,0,0.1)] dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white dark:shadow-none dark:placeholder:text-[#6B7280]"
|
className="border-border bg-card text-foreground placeholder:text-muted-foreground h-11 w-full rounded-full border py-2 pr-5 pl-11 text-sm shadow-[0_1px_4px_rgba(0,0,0,0.06)] transition-shadow outline-none focus:shadow-[0_2px_8px_rgba(0,0,0,0.1)] dark:shadow-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,8 +189,8 @@ export default function AgentsList() {
|
|||||||
onClick={() => setActiveFilter(tab.id)}
|
onClick={() => setActiveFilter(tab.id)}
|
||||||
className={`rounded-full px-4 py-2 text-sm transition-colors ${
|
className={`rounded-full px-4 py-2 text-sm transition-colors ${
|
||||||
activeFilter === tab.id
|
activeFilter === tab.id
|
||||||
? 'bg-[#E0E0E0] text-[#18181B] dark:bg-[#4A4A4A] dark:text-white'
|
? 'bg-border text-foreground dark:bg-accent dark:text-white'
|
||||||
: 'dark:text-gray bg-transparent text-[#71717A] hover:bg-[#F5F5F5] dark:hover:bg-[#383838]/50'
|
: 'dark:text-gray text-muted-foreground hover:bg-accent/50 bg-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t(tab.labelKey)}
|
{t(tab.labelKey)}
|
||||||
@@ -224,7 +224,7 @@ export default function AgentsList() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{showSearchEmptyState && (
|
{showSearchEmptyState && (
|
||||||
<div className="mt-12 flex flex-col items-center justify-center gap-2 text-[#71717A]">
|
<div className="text-muted-foreground mt-12 flex flex-col items-center justify-center gap-2">
|
||||||
<p className="text-lg">{t('agents.noSearchResults')}</p>
|
<p className="text-lg">{t('agents.noSearchResults')}</p>
|
||||||
<p className="text-sm">{t('agents.tryDifferentSearch')}</p>
|
<p className="text-sm">{t('agents.tryDifferentSearch')}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,7 +399,7 @@ function AgentSection({
|
|||||||
|
|
||||||
if (isFilteredView && isSearchingWithNoResults) {
|
if (isFilteredView && isSearchingWithNoResults) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 flex flex-col items-center justify-center gap-2 text-[#71717A]">
|
<div className="text-muted-foreground mt-12 flex flex-col items-center justify-center gap-2">
|
||||||
<p className="text-lg">{t('agents.noSearchResults')}</p>
|
<p className="text-lg">{t('agents.noSearchResults')}</p>
|
||||||
<p className="text-sm">{t('agents.tryDifferentSearch')}</p>
|
<p className="text-sm">{t('agents.tryDifferentSearch')}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -408,11 +408,11 @@ function AgentSection({
|
|||||||
|
|
||||||
if (isFilteredView && hasNoAgentsAtAll) {
|
if (isFilteredView && hasNoAgentsAtAll) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 flex flex-col items-center justify-center gap-3 text-[#71717A]">
|
<div className="text-muted-foreground mt-12 flex flex-col items-center justify-center gap-3">
|
||||||
<p>{t(`agents.sections.${config.id}.emptyState`)}</p>
|
<p>{t(`agents.sections.${config.id}.emptyState`)}</p>
|
||||||
{config.showNewAgentButton && (
|
{config.showNewAgentButton && (
|
||||||
<button
|
<button
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-4 py-2 text-sm text-white"
|
className="bg-primary hover:bg-primary/90 rounded-full px-4 py-2 text-sm text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalFolderId(null);
|
setModalFolderId(null);
|
||||||
setShowAgentTypeModal(true);
|
setShowAgentTypeModal(true);
|
||||||
@@ -456,12 +456,12 @@ function AgentSection({
|
|||||||
<div className="mt-8 flex flex-col gap-4">
|
<div className="mt-8 flex flex-col gap-4">
|
||||||
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="flex flex-wrap items-center gap-2 text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
<h2 className="text-foreground flex flex-wrap items-center gap-2 text-[18px] font-semibold">
|
||||||
{config.id === 'user' && folderPath.length > 0 ? (
|
{config.id === 'user' && folderPath.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigateToPath(-1)}
|
onClick={() => handleNavigateToPath(-1)}
|
||||||
className="text-[#71717A] hover:text-[#18181B] dark:hover:text-white"
|
className="text-muted-foreground hover:text-foreground dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{t(`agents.sections.${config.id}.title`)}
|
{t(`agents.sections.${config.id}.title`)}
|
||||||
</button>
|
</button>
|
||||||
@@ -473,7 +473,7 @@ function AgentSection({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigateToPath(index)}
|
onClick={() => handleNavigateToPath(index)}
|
||||||
className="text-[#71717A] hover:text-[#18181B] dark:hover:text-white"
|
className="text-muted-foreground hover:text-foreground dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -485,7 +485,7 @@ function AgentSection({
|
|||||||
t(`agents.sections.${config.id}.title`)
|
t(`agents.sections.${config.id}.title`)
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[13px] text-[#71717A]">
|
<p className="text-muted-foreground text-[13px]">
|
||||||
{t(`agents.sections.${config.id}.description`)}
|
{t(`agents.sections.${config.id}.description`)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -513,12 +513,12 @@ function AgentSection({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={t('agents.folders.newFolder')}
|
placeholder={t('agents.folders.newFolder')}
|
||||||
className="w-28 rounded-full border border-[#E5E5E5] bg-white px-4 py-2 text-sm text-[#18181B] outline-none placeholder:text-[#9CA3AF] sm:w-auto dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white dark:placeholder:text-[#6B7280]"
|
className="border-border bg-card text-foreground placeholder:text-muted-foreground w-28 rounded-full border px-4 py-2 text-sm outline-none sm:w-auto"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="shrink-0 rounded-full border border-[#E5E5E5] bg-white px-4 py-2 text-sm whitespace-nowrap text-[#18181B] hover:bg-[#F5F5F5] dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white dark:hover:bg-[#383838]"
|
className="border-border bg-card text-foreground hover:bg-accent shrink-0 rounded-full border px-4 py-2 text-sm whitespace-nowrap"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCreatingFolder(true);
|
setIsCreatingFolder(true);
|
||||||
setTimeout(() => newFolderInputRef.current?.focus(), 0);
|
setTimeout(() => newFolderInputRef.current?.focus(), 0);
|
||||||
@@ -529,7 +529,7 @@ function AgentSection({
|
|||||||
))}
|
))}
|
||||||
{config.showNewAgentButton && (
|
{config.showNewAgentButton && (
|
||||||
<button
|
<button
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue shrink-0 rounded-full px-4 py-2 text-sm whitespace-nowrap text-white"
|
className="bg-primary hover:bg-primary/90 shrink-0 rounded-full px-4 py-2 text-sm whitespace-nowrap text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalFolderId(currentFolderId);
|
setModalFolderId(currentFolderId);
|
||||||
setShowAgentTypeModal(true);
|
setShowAgentTypeModal(true);
|
||||||
@@ -579,7 +579,7 @@ function AgentSection({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : hasNoAgentsAtAll && currentLevelFolders.length === 0 ? (
|
) : hasNoAgentsAtAll && currentLevelFolders.length === 0 ? (
|
||||||
<div className="flex h-40 w-full flex-col items-center justify-center gap-3 text-[#71717A]">
|
<div className="text-muted-foreground flex h-40 w-full flex-col items-center justify-center gap-3">
|
||||||
<p>
|
<p>
|
||||||
{currentFolderId
|
{currentFolderId
|
||||||
? t('agents.folders.empty')
|
? t('agents.folders.empty')
|
||||||
@@ -587,7 +587,7 @@ function AgentSection({
|
|||||||
</p>
|
</p>
|
||||||
{config.showNewAgentButton && !currentFolderId && (
|
{config.showNewAgentButton && !currentFolderId && (
|
||||||
<button
|
<button
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue ml-2 rounded-full px-4 py-2 text-sm text-white"
|
className="bg-primary hover:bg-primary/90 ml-2 rounded-full px-4 py-2 text-sm text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalFolderId(currentFolderId);
|
setModalFolderId(currentFolderId);
|
||||||
setShowAgentTypeModal(true);
|
setShowAgentTypeModal(true);
|
||||||
@@ -603,4 +603,4 @@ function AgentSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,17 +70,15 @@ export default function FolderCard({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`relative flex cursor-pointer items-center justify-between rounded-[1.2rem] px-4 py-3 sm:w-48 ${
|
className={`relative flex cursor-pointer items-center justify-between rounded-[1.2rem] px-4 py-3 sm:w-48 ${
|
||||||
isExpanded
|
isExpanded ? 'bg-accent' : 'bg-muted hover:bg-accent'
|
||||||
? 'bg-[#E5E5E5] dark:bg-[#454545]'
|
|
||||||
: 'bg-[#F6F6F6] hover:bg-[#ECECEC] dark:bg-[#383838] dark:hover:bg-[#383838]/80'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onToggleExpand(folder.id)}
|
onClick={() => onToggleExpand(folder.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
<span className="truncate text-sm font-medium text-[#18181B] dark:text-[#E0E0E0]">
|
<span className="text-foreground truncate text-sm font-medium">
|
||||||
{folder.name}
|
{folder.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="shrink-0 text-xs text-[#71717A]">
|
<span className="text-muted-foreground shrink-0 text-xs">
|
||||||
({agentCount})
|
({agentCount})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
token_limit: undefined,
|
token_limit: undefined,
|
||||||
limited_request_mode: false,
|
limited_request_mode: false,
|
||||||
request_limit: undefined,
|
request_limit: undefined,
|
||||||
|
allow_system_prompt_override: false,
|
||||||
models: [],
|
models: [],
|
||||||
default_model_id: '',
|
default_model_id: '',
|
||||||
});
|
});
|
||||||
@@ -241,6 +242,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
formData.append('request_limit', '0');
|
formData.append('request_limit', '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formData.append(
|
||||||
|
'allow_system_prompt_override',
|
||||||
|
agent.allow_system_prompt_override ? 'True' : 'False',
|
||||||
|
);
|
||||||
|
|
||||||
if (imageFile) formData.append('image', imageFile);
|
if (imageFile) formData.append('image', imageFile);
|
||||||
|
|
||||||
if (agent.tools && agent.tools.length > 0)
|
if (agent.tools && agent.tools.length > 0)
|
||||||
@@ -361,6 +367,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
formData.append('request_limit', '0');
|
formData.append('request_limit', '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formData.append(
|
||||||
|
'allow_system_prompt_override',
|
||||||
|
agent.allow_system_prompt_override ? 'True' : 'False',
|
||||||
|
);
|
||||||
|
|
||||||
if (agent.models && agent.models.length > 0) {
|
if (agent.models && agent.models.length > 0) {
|
||||||
formData.append('models', JSON.stringify(agent.models));
|
formData.append('models', JSON.stringify(agent.models));
|
||||||
}
|
}
|
||||||
@@ -677,17 +688,17 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<div className="flex flex-col px-4 pt-4 pb-2 max-[1179px]:min-h-dvh min-[1180px]:h-dvh md:px-12 md:pt-12 md:pb-3">
|
<div className="flex flex-col px-4 pt-4 pb-2 max-[1179px]:min-h-dvh min-[1180px]:h-dvh md:px-12 md:pt-12 md:pb-3">
|
||||||
<div className="flex items-center gap-3 px-4">
|
<div className="flex items-center gap-3 px-4">
|
||||||
<button
|
<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]"
|
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
|
<p className="text-foreground dark:text-foreground mt-px text-sm font-semibold">
|
||||||
{t('agents.backToAll')}
|
{t('agents.backToAll')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||||
<h1 className="text-eerie-black m-0 text-[32px] font-bold lg:text-[40px] dark:text-white">
|
<h1 className="text-foreground m-0 text-[32px] font-bold lg:text-[40px] dark:text-white">
|
||||||
{modeConfig[effectiveMode].heading}
|
{modeConfig[effectiveMode].heading}
|
||||||
</h1>
|
</h1>
|
||||||
{agent.agent_type === 'workflow' && (
|
{agent.agent_type === 'workflow' && (
|
||||||
@@ -697,14 +708,14 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
<button
|
<button
|
||||||
className="text-purple-30 dark:text-light-gray mr-4 rounded-3xl py-2 text-sm font-medium dark:bg-transparent"
|
className="text-primary dark:text-foreground mr-4 rounded-3xl py-2 text-sm font-medium"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
>
|
>
|
||||||
{t('agents.form.buttons.cancel')}
|
{t('agents.form.buttons.cancel')}
|
||||||
</button>
|
</button>
|
||||||
{modeConfig[effectiveMode].showDelete && agent.id && (
|
{modeConfig[effectiveMode].showDelete && agent.id && (
|
||||||
<button
|
<button
|
||||||
className="group border-red-2000 text-red-2000 hover:bg-red-2000 flex items-center gap-2 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
className="group border-destructive text-destructive hover:bg-destructive flex items-center gap-2 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
||||||
onClick={() => setDeleteConfirmation('ACTIVE')}
|
onClick={() => setDeleteConfirmation('ACTIVE')}
|
||||||
>
|
>
|
||||||
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
|
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
|
||||||
@@ -714,7 +725,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
{modeConfig[effectiveMode].showSaveDraft && (
|
{modeConfig[effectiveMode].showSaveDraft && (
|
||||||
<button
|
<button
|
||||||
disabled={isJsonSchemaInvalid()}
|
disabled={isJsonSchemaInvalid()}
|
||||||
className={`border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue flex min-w-28 items-center justify-center rounded-3xl border border-solid px-5 py-2 text-sm font-medium whitespace-nowrap transition-colors hover:text-white ${
|
className={`border-primary text-primary hover:bg-primary/90 flex min-w-28 items-center justify-center rounded-3xl border border-solid px-5 py-2 text-sm font-medium whitespace-nowrap transition-colors hover:text-white ${
|
||||||
isJsonSchemaInvalid() ? 'cursor-not-allowed opacity-30' : ''
|
isJsonSchemaInvalid() ? 'cursor-not-allowed opacity-30' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={handleSaveDraft}
|
onClick={handleSaveDraft}
|
||||||
@@ -730,7 +741,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
)}
|
)}
|
||||||
{modeConfig[effectiveMode].showAccessDetails && (
|
{modeConfig[effectiveMode].showAccessDetails && (
|
||||||
<button
|
<button
|
||||||
className="group border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue flex items-center gap-2 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
className="group border-primary text-primary hover:bg-primary/90 flex items-center gap-2 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
||||||
onClick={() => navigate(`/agents/logs/${agent.id}`)}
|
onClick={() => navigate(`/agents/logs/${agent.id}`)}
|
||||||
>
|
>
|
||||||
<span className="block h-5 w-5 bg-[url('/src/assets/monitoring-purple.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/monitoring-white.svg')]" />
|
<span className="block h-5 w-5 bg-[url('/src/assets/monitoring-purple.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/monitoring-white.svg')]" />
|
||||||
@@ -739,7 +750,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
)}
|
)}
|
||||||
{modeConfig[effectiveMode].showAccessDetails && (
|
{modeConfig[effectiveMode].showAccessDetails && (
|
||||||
<button
|
<button
|
||||||
className="hover:bg-vi</button>olets-are-blue border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
className="border-primary text-primary hover:bg-primary/90 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
||||||
onClick={() => setAgentDetails('ACTIVE')}
|
onClick={() => setAgentDetails('ACTIVE')}
|
||||||
>
|
>
|
||||||
{t('agents.form.buttons.accessDetails')}
|
{t('agents.form.buttons.accessDetails')}
|
||||||
@@ -747,7 +758,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
disabled={!isPublishable() || !hasChanges}
|
disabled={!isPublishable() || !hasChanges}
|
||||||
className={`${!isPublishable() || !hasChanges ? 'cursor-not-allowed opacity-30' : ''} bg-purple-30 hover:bg-violets-are-blue flex min-w-28 items-center justify-center rounded-3xl px-5 py-2 text-sm font-medium whitespace-nowrap text-white`}
|
className={`${!isPublishable() || !hasChanges ? 'cursor-not-allowed opacity-30' : ''} bg-primary hover:bg-primary/90 flex min-w-28 items-center justify-center rounded-3xl px-5 py-2 text-sm font-medium whitespace-nowrap text-white`}
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center transition-all duration-200">
|
<span className="flex items-center justify-center transition-all duration-200">
|
||||||
@@ -760,21 +771,21 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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="bg-muted dark:bg-background mt-3 flex w-full flex-1 grid-cols-5 flex-col gap-10 rounded-[30px] p-5 max-[1179px]:overflow-visible min-[1180px]:grid min-[1180px]:gap-5 min-[1180px]:overflow-hidden">
|
||||||
<div className="scrollbar-overlay 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="scrollbar-overlay 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]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.meta')}
|
{t('agents.form.sections.meta')}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<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]"
|
className="border-border text-foreground dark:text-foreground dark:placeholder:text-silver bg-card dark:border-border mt-3 w-full rounded-3xl border px-5 py-3 text-sm outline-hidden placeholder:text-gray-400"
|
||||||
type="text"
|
type="text"
|
||||||
value={agent.name}
|
value={agent.name}
|
||||||
placeholder={t('agents.form.placeholders.agentName')}
|
placeholder={t('agents.form.placeholders.agentName')}
|
||||||
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
|
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 h-32 w-full rounded-xl border bg-white px-5 py-4 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
|
className="border-border text-foreground dark:text-foreground dark:placeholder:text-silver bg-card dark:border-border mt-3 h-32 w-full rounded-xl border px-5 py-4 text-sm outline-hidden placeholder:text-gray-400"
|
||||||
placeholder={t('agents.form.placeholders.describeAgent')}
|
placeholder={t('agents.form.placeholders.describeAgent')}
|
||||||
value={agent.description}
|
value={agent.description}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -784,7 +795,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<FileUpload
|
<FileUpload
|
||||||
showPreview
|
showPreview
|
||||||
className="dark:bg-raisin-black"
|
className="bg-card"
|
||||||
onUpload={handleUpload}
|
onUpload={handleUpload}
|
||||||
onRemove={() => setImageFile(null)}
|
onRemove={() => setImageFile(null)}
|
||||||
uploadText={[
|
uploadText={[
|
||||||
@@ -800,7 +811,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.source')}
|
{t('agents.form.sections.source')}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -809,10 +820,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<button
|
<button
|
||||||
ref={sourceAnchorButtonRef}
|
ref={sourceAnchorButtonRef}
|
||||||
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
|
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
|
||||||
className={`border-silver dark:bg-raisin-black w-full truncate rounded-3xl border bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] ${
|
className={`border-border bg-card dark:border-border w-full truncate rounded-3xl border px-5 py-3 text-left text-sm ${
|
||||||
selectedSourceIds.size > 0
|
selectedSourceIds.size > 0
|
||||||
? 'text-jet dark:text-bright-gray'
|
? 'text-foreground dark:text-foreground'
|
||||||
: 'dark:text-silver text-gray-400'
|
: 'dark:text-muted-foreground text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedSourceIds.size > 0
|
{selectedSourceIds.size > 0
|
||||||
@@ -892,17 +903,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}
|
}
|
||||||
size="w-full"
|
size="w-full"
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
|
||||||
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
|
|
||||||
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
|
|
||||||
placeholder={t('agents.form.placeholders.chunksPerQuery')}
|
placeholder={t('agents.form.placeholders.chunksPerQuery')}
|
||||||
placeholderClassName="text-gray-400 dark:text-silver"
|
|
||||||
contentSize="text-sm"
|
contentSize="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<div className="flex flex-wrap items-end gap-1">
|
<div className="flex flex-wrap items-end gap-1">
|
||||||
<div className="min-w-20 grow basis-full sm:basis-0">
|
<div className="min-w-20 grow basis-full sm:basis-0">
|
||||||
<Prompts
|
<Prompts
|
||||||
@@ -920,30 +927,24 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}
|
}
|
||||||
setPrompts={(newPrompts) => dispatch(setPrompts(newPrompts))}
|
setPrompts={(newPrompts) => dispatch(setPrompts(newPrompts))}
|
||||||
title={t('agents.form.sections.prompt')}
|
title={t('agents.form.sections.prompt')}
|
||||||
titleClassName="text-lg font-semibold dark:text-[#E0E0E0]"
|
titleClassName="text-lg font-semibold"
|
||||||
showAddButton={false}
|
showAddButton={false}
|
||||||
dropdownProps={{
|
dropdownProps={{
|
||||||
size: 'w-full',
|
size: 'w-full',
|
||||||
rounded: '3xl',
|
rounded: '3xl',
|
||||||
border: 'border',
|
|
||||||
buttonClassName:
|
|
||||||
'bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]',
|
|
||||||
optionsClassName:
|
|
||||||
'bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]',
|
|
||||||
placeholderClassName: 'text-gray-400 dark:text-silver',
|
|
||||||
contentSize: 'text-sm',
|
contentSize: 'text-sm',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue min-w-20 shrink-0 basis-full rounded-3xl border-2 border-solid px-5 py-[11px] text-sm whitespace-nowrap transition-colors hover:text-white sm:basis-auto"
|
className="border-primary text-primary hover:bg-primary/90 min-w-20 shrink-0 basis-full rounded-3xl border border-solid px-5 py-3 text-sm whitespace-nowrap transition-colors hover:text-white sm:basis-auto"
|
||||||
onClick={() => setAddPromptModal('ACTIVE')}
|
onClick={() => setAddPromptModal('ACTIVE')}
|
||||||
>
|
>
|
||||||
{t('agents.form.buttons.add')}
|
{t('agents.form.buttons.add')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.tools')}
|
{t('agents.form.sections.tools')}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -951,10 +952,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<button
|
<button
|
||||||
ref={toolAnchorButtonRef}
|
ref={toolAnchorButtonRef}
|
||||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||||
className={`border-silver dark:bg-raisin-black w-full truncate rounded-3xl border bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] ${
|
className={`border-border bg-card dark:border-border w-full truncate rounded-3xl border px-5 py-3 text-left text-sm ${
|
||||||
selectedTools.length > 0
|
selectedTools.length > 0
|
||||||
? 'text-jet dark:text-bright-gray'
|
? 'text-foreground dark:text-foreground'
|
||||||
: 'dark:text-silver text-gray-400'
|
: 'dark:text-muted-foreground text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedTools.length > 0
|
{selectedTools.length > 0
|
||||||
@@ -992,7 +993,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.agentType')}
|
{t('agents.form.sections.agentType')}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1010,16 +1011,12 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}
|
}
|
||||||
size="w-full"
|
size="w-full"
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
|
||||||
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
|
|
||||||
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
|
|
||||||
placeholder={t('agents.form.placeholders.selectType')}
|
placeholder={t('agents.form.placeholders.selectType')}
|
||||||
placeholderClassName="text-gray-400 dark:text-silver"
|
|
||||||
contentSize="text-sm"
|
contentSize="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.models')}
|
{t('agents.form.sections.models')}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1027,10 +1024,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<button
|
<button
|
||||||
ref={modelAnchorButtonRef}
|
ref={modelAnchorButtonRef}
|
||||||
onClick={() => setIsModelsPopupOpen(!isModelsPopupOpen)}
|
onClick={() => setIsModelsPopupOpen(!isModelsPopupOpen)}
|
||||||
className={`border-silver dark:bg-raisin-black w-full truncate rounded-3xl border bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] ${
|
className={`border-border bg-card dark:border-border w-full truncate rounded-3xl border px-5 py-3 text-left text-sm ${
|
||||||
selectedModelIds.size > 0
|
selectedModelIds.size > 0
|
||||||
? 'text-jet dark:text-bright-gray'
|
? 'text-foreground dark:text-foreground'
|
||||||
: 'dark:text-silver text-gray-400'
|
: 'dark:text-muted-foreground text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedModelIds.size > 0
|
{selectedModelIds.size > 0
|
||||||
@@ -1082,20 +1079,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}
|
}
|
||||||
size="w-full"
|
size="w-full"
|
||||||
rounded="3xl"
|
rounded="3xl"
|
||||||
border="border"
|
|
||||||
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
|
|
||||||
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
|
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
'agents.form.placeholders.selectDefaultModel',
|
'agents.form.placeholders.selectDefaultModel',
|
||||||
)}
|
)}
|
||||||
placeholderClassName="text-gray-400 dark:text-silver"
|
|
||||||
contentSize="text-sm"
|
contentSize="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
<div className="bg-card rounded-[30px] px-6 py-3">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setIsAdvancedSectionExpanded(!isAdvancedSectionExpanded)
|
setIsAdvancedSectionExpanded(!isAdvancedSectionExpanded)
|
||||||
@@ -1148,7 +1141,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}`}
|
}`}
|
||||||
rows={9}
|
rows={9}
|
||||||
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray mt-2 w-full rounded-2xl border bg-white px-4 py-3 font-mono text-sm outline-hidden dark:border-[#7E7E7E]`}
|
className={`border-border text-foreground dark:text-foreground bg-card dark:border-border mt-2 w-full rounded-2xl border px-4 py-3 font-mono text-sm outline-hidden`}
|
||||||
/>
|
/>
|
||||||
{jsonSchemaText.trim() !== '' && (
|
{jsonSchemaText.trim() !== '' && (
|
||||||
<div
|
<div
|
||||||
@@ -1194,7 +1187,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}}
|
}}
|
||||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||||
agent.limited_token_mode
|
agent.limited_token_mode
|
||||||
? 'bg-purple-30'
|
? 'bg-primary'
|
||||||
: 'bg-gray-300 dark:bg-gray-600'
|
: 'bg-gray-300 dark:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1219,7 +1212,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}
|
}
|
||||||
disabled={!agent.limited_token_mode}
|
disabled={!agent.limited_token_mode}
|
||||||
placeholder={t('agents.form.placeholders.enterTokenLimit')}
|
placeholder={t('agents.form.placeholders.enterTokenLimit')}
|
||||||
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] ${
|
className={`border-border text-foreground dark:text-foreground dark:placeholder:text-silver bg-card dark:border-border mt-2 w-full rounded-3xl border px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 ${
|
||||||
!agent.limited_token_mode
|
!agent.limited_token_mode
|
||||||
? 'cursor-not-allowed opacity-50'
|
? 'cursor-not-allowed opacity-50'
|
||||||
: ''
|
: ''
|
||||||
@@ -1250,7 +1243,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
}}
|
}}
|
||||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||||
agent.limited_request_mode
|
agent.limited_request_mode
|
||||||
? 'bg-purple-30'
|
? 'bg-primary'
|
||||||
: 'bg-gray-300 dark:bg-gray-600'
|
: 'bg-gray-300 dark:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1277,18 +1270,55 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
placeholder={t(
|
placeholder={t(
|
||||||
'agents.form.placeholders.enterRequestLimit',
|
'agents.form.placeholders.enterRequestLimit',
|
||||||
)}
|
)}
|
||||||
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] ${
|
className={`border-border text-foreground dark:text-foreground dark:placeholder:text-silver bg-card dark:border-border mt-2 w-full rounded-3xl border px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 ${
|
||||||
!agent.limited_request_mode
|
!agent.limited_request_mode
|
||||||
? 'cursor-not-allowed opacity-50'
|
? 'cursor-not-allowed opacity-50'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h2 className="text-sm font-medium">
|
||||||
|
{t('agents.form.advanced.systemPromptOverride')}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{t(
|
||||||
|
'agents.form.advanced.systemPromptOverrideDescription',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setAgent({
|
||||||
|
...agent,
|
||||||
|
allow_system_prompt_override:
|
||||||
|
!agent.allow_system_prompt_override,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||||
|
agent.allow_system_prompt_override
|
||||||
|
? 'bg-primary'
|
||||||
|
: '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.allow_system_prompt_override
|
||||||
|
? ''
|
||||||
|
: '-translate-x-5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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]">
|
<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">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
{t('agents.form.sections.preview')}
|
{t('agents.form.sections.preview')}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1331,7 +1361,7 @@ function AgentPreviewArea() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedAgent = useSelector(selectSelectedAgent);
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
return (
|
return (
|
||||||
<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]">
|
<div className="bg-card border-border w-full rounded-[30px] border max-[1179px]:h-[600px] min-[1180px]:h-full">
|
||||||
{selectedAgent?.status === 'published' ? (
|
{selectedAgent?.status === 'published' ? (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden rounded-[30px]">
|
<div className="flex h-full w-full flex-col overflow-hidden rounded-[30px]">
|
||||||
<AgentPreview />
|
<AgentPreview />
|
||||||
@@ -1339,7 +1369,7 @@ function AgentPreviewArea() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
||||||
<p className="dark:text-gray-4000 text-xs text-[#18181B]">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t('agents.form.preview.publishedPreview')}
|
{t('agents.form.preview.publishedPreview')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export default function SharedAgent() {
|
|||||||
alt="No agent found"
|
alt="No agent found"
|
||||||
className="mx-auto mb-6 h-32 w-32"
|
className="mx-auto mb-6 h-32 w-32"
|
||||||
/>
|
/>
|
||||||
<p className="dark:text-gray-4000 text-center text-lg text-[#71717A]">
|
<p className="text-muted-foreground text-center text-lg">
|
||||||
{t('agents.shared.notFound')}
|
{t('agents.shared.notFound')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +157,7 @@ export default function SharedAgent() {
|
|||||||
alt="agent-logo"
|
alt="agent-logo"
|
||||||
className="h-6 w-6 rounded-full object-contain"
|
className="h-6 w-6 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
<h2 className="text-eerie-black text-lg font-semibold dark:text-[#E0E0E0]">
|
<h2 className="text-foreground text-lg font-semibold">
|
||||||
{sharedAgent.name}
|
{sharedAgent.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +186,7 @@ export default function SharedAgent() {
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<p className="text-muted-foreground hidden w-screen self-center bg-transparent py-2 text-center text-xs md:inline md:w-full">
|
||||||
{t('tagline')}
|
{t('tagline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
|||||||
agent.shared_metadata !== null &&
|
agent.shared_metadata !== null &&
|
||||||
Object.keys(agent.shared_metadata).length > 0;
|
Object.keys(agent.shared_metadata).length > 0;
|
||||||
return (
|
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="border-border dark:border-border 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">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
||||||
<AgentImage
|
<AgentImage
|
||||||
@@ -19,10 +19,10 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
|
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
|
||||||
<h2 className="text-eerie-black text-base font-semibold sm:text-lg dark:text-[#E0E0E0]">
|
<h2 className="text-foreground text-base font-semibold sm:text-lg">
|
||||||
{agent.name}
|
{agent.name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="dark:text-gray-4000 overflow-y-auto text-xs text-wrap break-all text-[#71717A] sm:text-sm">
|
<p className="text-muted-foreground overflow-y-auto text-xs text-wrap break-all sm:text-sm">
|
||||||
{agent.description}
|
{agent.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,12 +30,12 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
|||||||
{hasSharedMetadata && (
|
{hasSharedMetadata && (
|
||||||
<div className="mt-4 flex items-center gap-8">
|
<div className="mt-4 flex items-center gap-8">
|
||||||
{agent.shared_metadata?.shared_by && (
|
{agent.shared_metadata?.shared_by && (
|
||||||
<p className="text-eerie-black text-xs font-light sm:text-sm dark:text-[#E0E0E0]">
|
<p className="text-foreground text-xs font-light sm:text-sm">
|
||||||
by {agent.shared_metadata.shared_by}
|
by {agent.shared_metadata.shared_by}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{agent.shared_metadata?.shared_at && (
|
{agent.shared_metadata?.shared_at && (
|
||||||
<p className="dark:text-gray-4000 text-xs font-light text-[#71717A] sm:text-sm">
|
<p className="text-muted-foreground text-xs font-light sm:text-sm">
|
||||||
Shared on{' '}
|
Shared on{' '}
|
||||||
{new Date(agent.shared_metadata.shared_at).toLocaleString(
|
{new Date(agent.shared_metadata.shared_at).toLocaleString(
|
||||||
'en-US',
|
'en-US',
|
||||||
@@ -54,14 +54,14 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
|||||||
)}
|
)}
|
||||||
{agent.tool_details && agent.tool_details.length > 0 && (
|
{agent.tool_details && agent.tool_details.length > 0 && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<p className="text-eerie-black text-sm font-semibold sm:text-base dark:text-[#E0E0E0]">
|
<p className="text-foreground text-sm font-semibold sm:text-base">
|
||||||
Connected Tools
|
Connected Tools
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{agent.tool_details.map((tool, index) => (
|
{agent.tool_details.map((tool, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-bright-gray text-eerie-black dark:bg-dark-charcoal flex items-center gap-1 rounded-full px-3 py-1 text-xs font-light dark:text-[#E0E0E0]"
|
className="bg-accent text-foreground dark:bg-card flex items-center gap-1 rounded-full px-3 py-1 text-xs font-light"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||||
|
|||||||
@@ -579,12 +579,12 @@ function WorkflowBuilderInner() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-lotion dark:bg-outer-space flex h-screen w-full flex-col">
|
<div className="bg-background flex h-screen w-full flex-col">
|
||||||
<div className="border-light-silver dark:bg-raisin-black flex items-center justify-between border-b bg-white px-6 py-4 dark:border-[#3A3A3A]">
|
<div className="border-border bg-card flex items-center justify-between border-b px-6 py-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/agents')}
|
onClick={() => navigate('/agents')}
|
||||||
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]"
|
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -613,7 +613,7 @@ function WorkflowBuilderInner() {
|
|||||||
{showWorkflowSettings && (
|
{showWorkflowSettings && (
|
||||||
<div
|
<div
|
||||||
ref={workflowSettingsRef}
|
ref={workflowSettingsRef}
|
||||||
className="dark:bg-raisin-black absolute top-full left-0 z-50 mt-2 w-80 rounded-xl border border-[#E5E5E5] bg-white p-4 shadow-lg dark:border-[#3A3A3A]"
|
className="border-border bg-card absolute top-full left-0 z-50 mt-2 w-80 rounded-xl border p-4 shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -623,7 +623,7 @@ function WorkflowBuilderInner() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={workflowName}
|
value={workflowName}
|
||||||
onChange={(e) => setWorkflowName(e.target.value)}
|
onChange={(e) => setWorkflowName(e.target.value)}
|
||||||
className="focus:ring-purple-30 w-full rounded-lg border border-[#E5E5E5] bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
placeholder="Enter workflow name"
|
placeholder="Enter workflow name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -634,14 +634,14 @@ function WorkflowBuilderInner() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={workflowDescription}
|
value={workflowDescription}
|
||||||
onChange={(e) => setWorkflowDescription(e.target.value)}
|
onChange={(e) => setWorkflowDescription(e.target.value)}
|
||||||
className="focus:ring-purple-30 w-full rounded-lg border border-[#E5E5E5] bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Describe what this workflow does"
|
placeholder="Describe what this workflow does"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowWorkflowSettings(false)}
|
onClick={() => setShowWorkflowSettings(false)}
|
||||||
className="bg-violets-are-blue hover:bg-purple-30 w-full rounded-lg px-3 py-2 text-sm font-medium text-white"
|
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-3 py-2 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
@@ -660,7 +660,7 @@ function WorkflowBuilderInner() {
|
|||||||
}
|
}
|
||||||
setShowPreview(true);
|
setShowPreview(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-gray-200 dark:hover:bg-[#383838]"
|
className="border-border bg-card text-foreground hover:bg-accent flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<Play size={16} />
|
<Play size={16} />
|
||||||
Preview
|
Preview
|
||||||
@@ -668,7 +668,7 @@ function WorkflowBuilderInner() {
|
|||||||
<button
|
<button
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className="bg-violets-are-blue hover:bg-purple-30 rounded-full px-6 py-2 text-sm font-medium text-white shadow-sm transition-colors disabled:opacity-50"
|
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-6 py-2 text-sm font-medium shadow-sm transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isPublishing ? 'Publishing...' : 'Publish'}
|
{isPublishing ? 'Publishing...' : 'Publish'}
|
||||||
</button>
|
</button>
|
||||||
@@ -705,20 +705,20 @@ function WorkflowBuilderInner() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="border-light-silver dark:bg-raisin-black flex w-64 flex-col gap-6 border-r bg-gray-50 p-4 dark:border-[#3A3A3A]">
|
<div className="border-border bg-muted flex w-64 flex-col gap-6 border-r p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||||
Core Nodes
|
Core Nodes
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card hover:bg-accent flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
e.dataTransfer.setData('application/reactflow', 'agent')
|
e.dataTransfer.setData('application/reactflow', 'agent')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-violets-are-blue group-hover:bg-violets-are-blue flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-purple-100 transition-colors group-hover:text-white">
|
<div className="bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors">
|
||||||
<Bot size={18} />
|
<Bot size={18} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||||
@@ -726,7 +726,7 @@ function WorkflowBuilderInner() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card hover:bg-accent flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
e.dataTransfer.setData('application/reactflow', 'end')
|
e.dataTransfer.setData('application/reactflow', 'end')
|
||||||
@@ -740,7 +740,7 @@ function WorkflowBuilderInner() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card hover:bg-accent flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
e.dataTransfer.setData('application/reactflow', 'note')
|
e.dataTransfer.setData('application/reactflow', 'note')
|
||||||
@@ -762,7 +762,7 @@ function WorkflowBuilderInner() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card hover:bg-accent flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
e.dataTransfer.setData('application/reactflow', 'state')
|
e.dataTransfer.setData('application/reactflow', 'state')
|
||||||
@@ -784,10 +784,7 @@ function WorkflowBuilderInner() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div ref={reactFlowWrapper} className="bg-muted relative flex-1">
|
||||||
ref={reactFlowWrapper}
|
|
||||||
className="dark:bg-raisin-black/10 relative flex-1 bg-gray-50"
|
|
||||||
>
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
@@ -807,9 +804,9 @@ function WorkflowBuilderInner() {
|
|||||||
{showNodeConfig && selectedNode && (
|
{showNodeConfig && selectedNode && (
|
||||||
<div
|
<div
|
||||||
ref={configPanelRef}
|
ref={configPanelRef}
|
||||||
className="border-light-silver dark:bg-raisin-black absolute top-4 right-4 w-96 rounded-2xl border bg-white shadow-[0px_4px_40px_-3px_#0000001A] dark:border-[#3A3A3A]"
|
className="border-border bg-card absolute top-4 right-4 w-96 rounded-2xl border shadow-[0px_4px_40px_-3px_#0000001A]"
|
||||||
>
|
>
|
||||||
<div className="border-light-silver flex items-center justify-between border-b p-4 dark:border-[#3A3A3A]">
|
<div className="border-border flex items-center justify-between border-b p-4">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
{selectedNode.type === 'start' && 'Start Node'}
|
{selectedNode.type === 'start' && 'Start Node'}
|
||||||
{selectedNode.type === 'end' && 'End Node'}
|
{selectedNode.type === 'end' && 'End Node'}
|
||||||
@@ -827,7 +824,7 @@ function WorkflowBuilderInner() {
|
|||||||
|
|
||||||
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
||||||
<div className="mb-4 flex flex-col gap-2">
|
<div className="mb-4 flex flex-col gap-2">
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:bg-[#2C2C2C]">
|
<div className="bg-muted rounded-lg p-3">
|
||||||
<div className="mb-1 text-xs text-gray-500 dark:text-gray-400">
|
<div className="mb-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Node ID
|
Node ID
|
||||||
</div>
|
</div>
|
||||||
@@ -856,7 +853,7 @@ function WorkflowBuilderInner() {
|
|||||||
label: e.target.value,
|
label: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
placeholder="Enter node title"
|
placeholder="Enter node title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -945,7 +942,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="System prompt for the agent"
|
placeholder="System prompt for the agent"
|
||||||
/>
|
/>
|
||||||
@@ -967,7 +964,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Use {{variable}} for dynamic content"
|
placeholder="Use {{variable}} for dynamic content"
|
||||||
/>
|
/>
|
||||||
@@ -990,7 +987,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
placeholder="Variable name for output"
|
placeholder="Variable name for output"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1057,7 +1054,7 @@ function WorkflowBuilderInner() {
|
|||||||
content: e.target.value,
|
content: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Enter note content"
|
placeholder="Enter note content"
|
||||||
/>
|
/>
|
||||||
@@ -1078,7 +1075,7 @@ function WorkflowBuilderInner() {
|
|||||||
variable: e.target.value,
|
variable: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
placeholder="e.g. analysis_type"
|
placeholder="e.g. analysis_type"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1094,7 +1091,7 @@ function WorkflowBuilderInner() {
|
|||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border bg-card text-foreground focus:ring-ring w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2"
|
||||||
placeholder="e.g. price_check"
|
placeholder="e.g. price_check"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1125,7 +1122,7 @@ function WorkflowBuilderInner() {
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="dark:bg-raisin-black w-full max-w-none p-0 sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px] dark:border-[#3A3A3A]"
|
className="border-border bg-card w-full max-w-none p-0 sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px]"
|
||||||
>
|
>
|
||||||
<WorkflowPreview
|
<WorkflowPreview
|
||||||
workflowData={{
|
workflowData={{
|
||||||
|
|||||||
@@ -41,4 +41,4 @@ export const agentSectionsConfig = [
|
|||||||
selectData: selectSharedAgents,
|
selectData: selectSharedAgents,
|
||||||
updateAction: setSharedAgents,
|
updateAction: setSharedAgents,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function AgentTypeModal({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-lg rounded-xl bg-white p-8 shadow-2xl dark:bg-[#1e1e1e]"
|
className="bg-card relative w-full max-w-lg rounded-xl p-8 shadow-2xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -43,7 +43,7 @@ export default function AgentTypeModal({
|
|||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 className="text-jet dark:text-bright-gray mb-3 text-2xl font-bold">
|
<h2 className="text-foreground dark:text-foreground mb-3 text-2xl font-bold">
|
||||||
Create New Agent
|
Create New Agent
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mb-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
@@ -53,13 +53,13 @@ export default function AgentTypeModal({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSelect('normal')}
|
onClick={() => handleSelect('normal')}
|
||||||
className="hover:border-purple-30 hover:bg-purple-30/5 dark:hover:border-purple-30 dark:hover:bg-purple-30/10 group flex items-start gap-5 rounded-xl border-2 border-gray-200 p-5 text-left transition-all dark:border-[#2E2F34]"
|
className="hover:border-primary hover:bg-primary/5 dark:hover:border-primary dark:hover:bg-primary/10 group dark:border-border flex items-start gap-5 rounded-xl border-2 border-gray-200 p-5 text-left transition-all"
|
||||||
>
|
>
|
||||||
<div className="dark:bg-purple-30/20 bg-purple-30/10 text-purple-30 group-hover:bg-purple-30 flex h-14 w-14 shrink-0 items-center justify-center rounded-xl transition-colors group-hover:text-white dark:text-purple-300">
|
<div className="dark:bg-primary/20 bg-primary/10 text-primary group-hover:bg-primary/90 flex h-14 w-14 shrink-0 items-center justify-center rounded-xl transition-colors group-hover:text-white dark:text-purple-300">
|
||||||
<Bot size={28} />
|
<Bot size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-jet dark:text-bright-gray mb-2 text-lg font-semibold">
|
<h3 className="text-foreground dark:text-foreground mb-2 text-lg font-semibold">
|
||||||
Classic Agent
|
Classic Agent
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||||
@@ -71,13 +71,13 @@ export default function AgentTypeModal({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSelect('workflow')}
|
onClick={() => handleSelect('workflow')}
|
||||||
className="hover:border-violets-are-blue hover:bg-violets-are-blue/5 dark:hover:border-violets-are-blue dark:hover:bg-violets-are-blue/10 group flex items-start gap-5 rounded-xl border-2 border-gray-200 p-5 text-left transition-all dark:border-[#2E2F34]"
|
className="hover:border-primary hover:bg-primary/5 dark:hover:border-primary dark:hover:bg-primary/10 group dark:border-border flex items-start gap-5 rounded-xl border-2 border-gray-200 p-5 text-left transition-all"
|
||||||
>
|
>
|
||||||
<div className="dark:bg-violets-are-blue/20 bg-violets-are-blue/10 text-violets-are-blue group-hover:bg-violets-are-blue flex h-14 w-14 shrink-0 items-center justify-center rounded-xl transition-colors group-hover:text-white dark:text-purple-300">
|
<div className="dark:bg-primary/20 bg-primary/10 text-primary group-hover:bg-primary flex h-14 w-14 shrink-0 items-center justify-center rounded-xl transition-colors group-hover:text-white dark:text-purple-300">
|
||||||
<Workflow size={28} />
|
<Workflow size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-jet dark:text-bright-gray mb-2 text-lg font-semibold">
|
<h3 className="text-foreground dark:text-foreground mb-2 text-lg font-semibold">
|
||||||
Workflow Agent
|
Workflow Agent
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ export default function Agents() {
|
|||||||
<Route path="/workflow/edit/:agentId" element={<WorkflowBuilder />} />
|
<Route path="/workflow/edit/:agentId" element={<WorkflowBuilder />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Agent = {
|
|||||||
default_model_id?: string;
|
default_model_id?: string;
|
||||||
folder_id?: string;
|
folder_id?: string;
|
||||||
workflow?: string;
|
workflow?: string;
|
||||||
|
allow_system_prompt_override?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentFolder = {
|
export type AgentFolder = {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
export type NodeType = 'start' | 'end' | 'agent' | 'note' | 'state' | 'condition';
|
export type NodeType =
|
||||||
|
| 'start'
|
||||||
|
| 'end'
|
||||||
|
| 'agent'
|
||||||
|
| 'note'
|
||||||
|
| 'state'
|
||||||
|
| 'condition';
|
||||||
|
|
||||||
export interface ConditionCase {
|
export interface ConditionCase {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import 'reactflow/dist/style.css';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
ChartColumn,
|
|
||||||
Bot,
|
Bot,
|
||||||
|
ChartColumn,
|
||||||
Database,
|
Database,
|
||||||
Flag,
|
Flag,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Loader2,
|
|
||||||
Link,
|
Link,
|
||||||
|
Loader2,
|
||||||
Pencil,
|
Pencil,
|
||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
@@ -301,6 +302,7 @@ function createWorkflowPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowBuilderInner() {
|
function WorkflowBuilderInner() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
const sourceDocs = useSelector(selectSourceDocs);
|
const sourceDocs = useSelector(selectSourceDocs);
|
||||||
@@ -1142,6 +1144,10 @@ function WorkflowBuilderInner() {
|
|||||||
workflowDescription || `Workflow agent: ${workflowName}`,
|
workflowDescription || `Workflow agent: ${workflowName}`,
|
||||||
);
|
);
|
||||||
agentFormData.append('status', 'published');
|
agentFormData.append('status', 'published');
|
||||||
|
agentFormData.append(
|
||||||
|
'allow_system_prompt_override',
|
||||||
|
currentAgent.allow_system_prompt_override ? 'True' : 'False',
|
||||||
|
);
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
agentFormData.append('image', imageFile);
|
agentFormData.append('image', imageFile);
|
||||||
}
|
}
|
||||||
@@ -1203,6 +1209,10 @@ function WorkflowBuilderInner() {
|
|||||||
agentFormData.append('agent_type', 'workflow');
|
agentFormData.append('agent_type', 'workflow');
|
||||||
agentFormData.append('status', 'published');
|
agentFormData.append('status', 'published');
|
||||||
agentFormData.append('workflow', savedWorkflowId || '');
|
agentFormData.append('workflow', savedWorkflowId || '');
|
||||||
|
agentFormData.append(
|
||||||
|
'allow_system_prompt_override',
|
||||||
|
currentAgent.allow_system_prompt_override ? 'True' : 'False',
|
||||||
|
);
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
agentFormData.append('image', imageFile);
|
agentFormData.append('image', imageFile);
|
||||||
}
|
}
|
||||||
@@ -1355,12 +1365,12 @@ function WorkflowBuilderInner() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MobileBlocker />
|
<MobileBlocker />
|
||||||
<div className="bg-lotion dark:bg-outer-space fixed inset-0 z-50 hidden h-screen w-full flex-col md:flex">
|
<div className="bg-background fixed inset-0 z-50 hidden h-screen w-full flex-col md:flex">
|
||||||
<div className="border-light-silver dark:bg-raisin-black flex items-center justify-between border-b bg-white px-6 py-4 dark:border-[#3A3A3A]">
|
<div className="border-border bg-card dark:bg-background flex items-center justify-between border-b px-6 py-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={navigateBackToAgents}
|
onClick={navigateBackToAgents}
|
||||||
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]"
|
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1390,7 +1400,7 @@ function WorkflowBuilderInner() {
|
|||||||
{showWorkflowSettings && (
|
{showWorkflowSettings && (
|
||||||
<div
|
<div
|
||||||
ref={workflowSettingsRef}
|
ref={workflowSettingsRef}
|
||||||
className="dark:bg-raisin-black absolute top-full left-0 z-50 mt-2 w-80 rounded-xl border border-[#E5E5E5] bg-white p-4 shadow-lg dark:border-[#3A3A3A]"
|
className="border-border bg-card absolute top-full left-0 z-50 mt-2 w-80 rounded-xl border p-4 shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -1400,7 +1410,7 @@ function WorkflowBuilderInner() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={workflowName}
|
value={workflowName}
|
||||||
onChange={(e) => setWorkflowName(e.target.value)}
|
onChange={(e) => setWorkflowName(e.target.value)}
|
||||||
className="focus:ring-purple-30 w-full rounded-lg border border-[#E5E5E5] bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="focus:ring-ring border-border bg-card w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Enter workflow name"
|
placeholder="Enter workflow name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1411,7 +1421,7 @@ function WorkflowBuilderInner() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={workflowDescription}
|
value={workflowDescription}
|
||||||
onChange={(e) => setWorkflowDescription(e.target.value)}
|
onChange={(e) => setWorkflowDescription(e.target.value)}
|
||||||
className="focus:ring-purple-30 w-full rounded-lg border border-[#E5E5E5] bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="focus:ring-ring border-border bg-card w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 dark:text-white"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Describe what this workflow does"
|
placeholder="Describe what this workflow does"
|
||||||
/>
|
/>
|
||||||
@@ -1441,23 +1451,57 @@ function WorkflowBuilderInner() {
|
|||||||
uploadText={[
|
uploadText={[
|
||||||
{
|
{
|
||||||
text: 'Click to upload',
|
text: 'Click to upload',
|
||||||
colorClass: 'text-violets-are-blue',
|
colorClass: 'text-primary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: ' or drag and drop',
|
text: ' or drag and drop',
|
||||||
colorClass: 'text-gray-500',
|
colorClass: 'text-muted-foreground',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
className="rounded-lg border-2 border-dashed border-[#E5E5E5] p-3 text-center transition-colors dark:border-[#3A3A3A] dark:bg-[#2C2C2C]"
|
className="border-border rounded-lg border-2 border-dashed p-3 text-center transition-colors"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
|
<p className="text-muted-foreground mt-1 text-[11px]">
|
||||||
Image updates are included the next time you save.
|
Image updates are included the next time you save.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{t('agents.form.advanced.systemPromptOverride')}
|
||||||
|
</label>
|
||||||
|
<p className="mt-0.5 text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
{t('agents.form.advanced.systemPromptOverrideDescription')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
allow_system_prompt_override:
|
||||||
|
!prev.allow_system_prompt_override,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||||
|
currentAgent.allow_system_prompt_override
|
||||||
|
? 'bg-primary'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||||
|
currentAgent.allow_system_prompt_override
|
||||||
|
? ''
|
||||||
|
: '-translate-x-5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleWorkflowSettingsDone}
|
onClick={handleWorkflowSettingsDone}
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className="bg-violets-are-blue hover:bg-purple-30 w-full rounded-lg px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
className="bg-primary hover:bg-primary/90 w-full rounded-lg px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
@@ -1468,7 +1512,7 @@ function WorkflowBuilderInner() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowWorkflowSettings((prev) => !prev)}
|
onClick={() => setShowWorkflowSettings((prev) => !prev)}
|
||||||
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-gray-200 dark:hover:bg-[#383838]"
|
className="border-border bg-card hover:bg-accent flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium text-gray-700 transition-colors dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<Settings2 size={16} />
|
<Settings2 size={16} />
|
||||||
Details
|
Details
|
||||||
@@ -1476,7 +1520,7 @@ function WorkflowBuilderInner() {
|
|||||||
{canManageAgent && (
|
{canManageAgent && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/agents/logs/${effectiveAgentId}`)}
|
onClick={() => navigate(`/agents/logs/${effectiveAgentId}`)}
|
||||||
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-gray-200 dark:hover:bg-[#383838]"
|
className="border-border bg-card hover:bg-accent flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium text-gray-700 transition-colors dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<ChartColumn size={16} />
|
<ChartColumn size={16} />
|
||||||
Logs
|
Logs
|
||||||
@@ -1485,7 +1529,7 @@ function WorkflowBuilderInner() {
|
|||||||
{canManageAgent && (
|
{canManageAgent && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setAgentDetails('ACTIVE')}
|
onClick={() => setAgentDetails('ACTIVE')}
|
||||||
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-gray-200 dark:hover:bg-[#383838]"
|
className="border-border bg-card hover:bg-accent flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium text-gray-700 transition-colors dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<Link size={16} />
|
<Link size={16} />
|
||||||
Access Details
|
Access Details
|
||||||
@@ -1495,7 +1539,7 @@ function WorkflowBuilderInner() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setDeleteConfirmation('ACTIVE')}
|
onClick={() => setDeleteConfirmation('ACTIVE')}
|
||||||
disabled={isDeletingAgent}
|
disabled={isDeletingAgent}
|
||||||
className="flex items-center gap-2 rounded-full border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-red-900/30 dark:bg-[#2C2C2C] dark:text-red-400 dark:hover:bg-red-900/10"
|
className="bg-card flex items-center gap-2 rounded-full border border-red-200 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-red-900/30 dark:text-red-400 dark:hover:bg-red-900/10"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
{isDeletingAgent ? 'Deleting...' : 'Delete'}
|
{isDeletingAgent ? 'Deleting...' : 'Delete'}
|
||||||
@@ -1511,7 +1555,7 @@ function WorkflowBuilderInner() {
|
|||||||
}
|
}
|
||||||
setShowPreview(true);
|
setShowPreview(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-gray-200 dark:hover:bg-[#383838]"
|
className="border-border bg-card hover:bg-accent flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium text-gray-700 transition-colors dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<Play size={16} />
|
<Play size={16} />
|
||||||
Preview
|
Preview
|
||||||
@@ -1521,8 +1565,8 @@ function WorkflowBuilderInner() {
|
|||||||
disabled={isPrimaryActionDisabled}
|
disabled={isPrimaryActionDisabled}
|
||||||
className={`relative inline-flex items-center justify-center rounded-full px-6 py-2 text-sm font-medium shadow-sm transition-colors disabled:cursor-not-allowed ${
|
className={`relative inline-flex items-center justify-center rounded-full px-6 py-2 text-sm font-medium shadow-sm transition-colors disabled:cursor-not-allowed ${
|
||||||
canManageAgent && !hasSavableChanges
|
canManageAgent && !hasSavableChanges
|
||||||
? 'bg-gray-200 text-gray-500 dark:bg-[#3A3A3A] dark:text-gray-400'
|
? 'dark:bg-accent bg-gray-200 text-gray-500 dark:text-gray-400'
|
||||||
: 'bg-violets-are-blue hover:bg-purple-30 text-white disabled:opacity-50'
|
: 'bg-primary hover:bg-primary/90 text-white disabled:opacity-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1571,18 +1615,18 @@ function WorkflowBuilderInner() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="border-light-silver dark:bg-raisin-black flex w-64 flex-col gap-6 border-r bg-gray-50 p-4 dark:border-[#3A3A3A]">
|
<div className="border-border bg-muted dark:bg-background flex w-64 flex-col gap-6 border-r p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||||
Core Nodes
|
Core Nodes
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleNodeDragStart(e, 'agent')}
|
onDragStart={(e) => handleNodeDragStart(e, 'agent')}
|
||||||
>
|
>
|
||||||
<div className="text-violets-are-blue group-hover:bg-violets-are-blue flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-purple-100 transition-colors group-hover:text-white">
|
<div className="text-primary group-hover:bg-primary flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-purple-100 transition-colors group-hover:text-white">
|
||||||
<Bot size={18} />
|
<Bot size={18} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||||
@@ -1590,7 +1634,7 @@ function WorkflowBuilderInner() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleNodeDragStart(e, 'end')}
|
onDragStart={(e) => handleNodeDragStart(e, 'end')}
|
||||||
>
|
>
|
||||||
@@ -1602,7 +1646,7 @@ function WorkflowBuilderInner() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleNodeDragStart(e, 'note')}
|
onDragStart={(e) => handleNodeDragStart(e, 'note')}
|
||||||
>
|
>
|
||||||
@@ -1622,7 +1666,7 @@ function WorkflowBuilderInner() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleNodeDragStart(e, 'state')}
|
onDragStart={(e) => handleNodeDragStart(e, 'state')}
|
||||||
>
|
>
|
||||||
@@ -1630,16 +1674,16 @@ function WorkflowBuilderInner() {
|
|||||||
<Database size={18} />
|
<Database size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<span className="text-foreground text-sm font-medium">
|
||||||
Set State
|
Set State
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-gray-400">
|
<span className="text-muted-foreground text-[10px]">
|
||||||
Modify workflow variables
|
Modify workflow variables
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-move items-center gap-3 rounded-full border bg-white px-4 py-3 shadow-sm transition-all hover:shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]"
|
className="group border-border bg-card flex cursor-move items-center gap-3 rounded-full border px-4 py-3 shadow-sm transition-all hover:shadow-md"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleNodeDragStart(e, 'condition')}
|
onDragStart={(e) => handleNodeDragStart(e, 'condition')}
|
||||||
>
|
>
|
||||||
@@ -1647,10 +1691,10 @@ function WorkflowBuilderInner() {
|
|||||||
<GitBranch size={18} />
|
<GitBranch size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<span className="text-foreground text-sm font-medium">
|
||||||
If / Else
|
If / Else
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-gray-400">
|
<span className="text-muted-foreground text-[10px]">
|
||||||
Conditional branching
|
Conditional branching
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1661,7 +1705,7 @@ function WorkflowBuilderInner() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={reactFlowWrapper}
|
ref={reactFlowWrapper}
|
||||||
className="dark:bg-raisin-black/10 relative flex-1 bg-gray-50"
|
className="bg-muted dark:bg-background/10 relative flex-1"
|
||||||
>
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -1687,8 +1731,8 @@ function WorkflowBuilderInner() {
|
|||||||
className="absolute inset-0 z-10"
|
className="absolute inset-0 z-10"
|
||||||
onClick={handlePanelBackdropClick}
|
onClick={handlePanelBackdropClick}
|
||||||
/>
|
/>
|
||||||
<div className="border-light-silver dark:bg-raisin-black absolute top-4 right-4 z-20 w-96 rounded-2xl border bg-white shadow-[0px_4px_40px_-3px_#0000001A] dark:border-[#3A3A3A]">
|
<div className="border-border bg-card absolute top-4 right-4 z-20 w-96 rounded-2xl border shadow-[0px_4px_40px_-3px_#0000001A]">
|
||||||
<div className="border-light-silver flex items-center justify-between border-b p-4 dark:border-[#3A3A3A]">
|
<div className="border-border flex items-center justify-between border-b p-4">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
{selectedNode.type === 'start' && 'Start Node'}
|
{selectedNode.type === 'start' && 'Start Node'}
|
||||||
{selectedNode.type === 'end' && 'End Node'}
|
{selectedNode.type === 'end' && 'End Node'}
|
||||||
@@ -1707,7 +1751,7 @@ function WorkflowBuilderInner() {
|
|||||||
|
|
||||||
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
||||||
<div className="mb-4 flex flex-col gap-2">
|
<div className="mb-4 flex flex-col gap-2">
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:bg-[#2C2C2C]">
|
<div className="bg-muted rounded-lg p-3">
|
||||||
<div className="mb-1 text-xs text-gray-500 dark:text-gray-400">
|
<div className="mb-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Node ID
|
Node ID
|
||||||
</div>
|
</div>
|
||||||
@@ -1736,7 +1780,7 @@ function WorkflowBuilderInner() {
|
|||||||
label: e.target.value,
|
label: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border focus:ring-ring bg-card w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Enter node title"
|
placeholder="Enter node title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1833,7 +1877,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border focus:ring-ring bg-card w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="System prompt for the agent"
|
placeholder="System prompt for the agent"
|
||||||
/>
|
/>
|
||||||
@@ -1876,7 +1920,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border focus:ring-ring bg-card w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Variable name for output"
|
placeholder="Variable name for output"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1969,7 +2013,7 @@ function WorkflowBuilderInner() {
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 font-mono text-xs transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border focus:ring-ring bg-card w-full rounded-xl border px-3 py-2 font-mono text-xs transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
rows={8}
|
rows={8}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -2009,7 +2053,7 @@ function WorkflowBuilderInner() {
|
|||||||
content: e.target.value,
|
content: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white"
|
className="border-border focus:ring-ring bg-card w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Enter note content"
|
placeholder="Enter note content"
|
||||||
/>
|
/>
|
||||||
@@ -2034,7 +2078,7 @@ function WorkflowBuilderInner() {
|
|||||||
) => (
|
) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="rounded-xl border border-gray-200 p-3 dark:border-[#3A3A3A]"
|
className="border-border rounded-xl border p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
@@ -2084,18 +2128,18 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 mb-1 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent mb-1 w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="input.foo + 1"
|
placeholder="input.foo + 1"
|
||||||
/>
|
/>
|
||||||
<p className="mb-3 text-[10px] text-gray-400">
|
<p className="text-muted-foreground mb-3 text-[10px]">
|
||||||
Use Common Expression Language to create
|
Use Common Expression Language to create
|
||||||
a custom expression.{' '}
|
a custom expression.{' '}
|
||||||
<a
|
<a
|
||||||
href="https://cel.dev/"
|
href="https://cel.dev/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-violets-are-blue underline"
|
className="text-primary underline"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
@@ -2124,7 +2168,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="variable_name"
|
placeholder="variable_name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -2145,7 +2189,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-[#383838]"
|
className="hover:bg-accent flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
Add
|
Add
|
||||||
@@ -2158,7 +2202,7 @@ function WorkflowBuilderInner() {
|
|||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Create conditions to branch your workflow
|
Create conditions to branch your workflow
|
||||||
</p>
|
</p>
|
||||||
<div className="flex overflow-hidden rounded-lg border border-gray-200 dark:border-[#3A3A3A]">
|
<div className="border-border flex overflow-hidden rounded-lg border">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleUpdateNodeData({
|
handleUpdateNodeData({
|
||||||
@@ -2171,8 +2215,8 @@ function WorkflowBuilderInner() {
|
|||||||
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
(selectedNode.data.config?.mode ||
|
(selectedNode.data.config?.mode ||
|
||||||
'simple') === 'simple'
|
'simple') === 'simple'
|
||||||
? 'bg-violets-are-blue text-white'
|
? 'bg-primary text-white'
|
||||||
: 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-[#383838]'
|
: 'hover:bg-accent text-gray-600 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Simple
|
Simple
|
||||||
@@ -2189,8 +2233,8 @@ function WorkflowBuilderInner() {
|
|||||||
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
selectedNode.data.config?.mode ===
|
selectedNode.data.config?.mode ===
|
||||||
'advanced'
|
'advanced'
|
||||||
? 'bg-violets-are-blue text-white'
|
? 'bg-primary text-white'
|
||||||
: 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-[#383838]'
|
: 'hover:bg-accent text-gray-600 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Advanced
|
Advanced
|
||||||
@@ -2201,7 +2245,7 @@ function WorkflowBuilderInner() {
|
|||||||
(c: ConditionCase, idx: number) => (
|
(c: ConditionCase, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={c.sourceHandle}
|
key={c.sourceHandle}
|
||||||
className="rounded-xl border border-gray-200 p-3 dark:border-[#3A3A3A]"
|
className="border-border rounded-xl border p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<span className="text-sm font-semibold text-orange-600 dark:text-orange-400">
|
<span className="text-sm font-semibold text-orange-600 dark:text-orange-400">
|
||||||
@@ -2266,7 +2310,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 mb-2 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent mb-2 w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Case name (optional)"
|
placeholder="Case name (optional)"
|
||||||
/>
|
/>
|
||||||
{(selectedNode.data.config?.mode ||
|
{(selectedNode.data.config?.mode ||
|
||||||
@@ -2302,7 +2346,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent w-full rounded-xl border px-3 py-2 text-sm outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Variable"
|
placeholder="Variable"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -2394,7 +2438,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent w-full rounded-xl border px-3 py-2 text-sm outline-none focus:ring-2 dark:text-white"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -2419,18 +2463,18 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="border-light-silver focus:ring-purple-30 w-full rounded-xl border bg-white px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:border-[#3A3A3A] dark:bg-[#383838] dark:text-white"
|
className="border-border focus:ring-ring bg-card dark:bg-accent w-full rounded-xl border px-3 py-2 text-sm transition-all outline-none focus:ring-2 dark:text-white"
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Enter condition, e.g. input == 5"
|
placeholder="Enter condition, e.g. input == 5"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-[10px] text-gray-400">
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
Use Common Expression Language to
|
Use Common Expression Language to
|
||||||
create a custom expression.{' '}
|
create a custom expression.{' '}
|
||||||
<a
|
<a
|
||||||
href="https://cel.dev/"
|
href="https://cel.dev/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-violets-are-blue underline"
|
className="text-primary underline"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
@@ -2461,7 +2505,7 @@ function WorkflowBuilderInner() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-[#383838]"
|
className="hover:bg-accent flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-gray-600 transition-colors dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
Add
|
Add
|
||||||
@@ -2493,7 +2537,7 @@ function WorkflowBuilderInner() {
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="dark:bg-raisin-black w-full max-w-none p-0 sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px] dark:border-[#3A3A3A]"
|
className="bg-card w-full max-w-none p-0 sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px]"
|
||||||
>
|
>
|
||||||
<WorkflowPreview
|
<WorkflowPreview
|
||||||
workflowData={{
|
workflowData={{
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function ExecutionDetails({
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (el && stepRefs) stepRefs.current.set(step.nodeId, el);
|
if (el && stepRefs) stepRefs.current.set(step.nodeId, el);
|
||||||
}}
|
}}
|
||||||
className="rounded-xl bg-[#F5F5F5] p-3 dark:bg-[#383838]"
|
className="bg-muted dark:bg-accent rounded-xl p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center text-xs font-medium text-gray-500 dark:text-gray-400">
|
<span className="flex h-5 w-5 shrink-0 items-center justify-center text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
@@ -181,7 +181,7 @@ function ExecutionDetails({
|
|||||||
{(hasOutput || step.error || stateVars.length > 0) && (
|
{(hasOutput || step.error || stateVars.length > 0) && (
|
||||||
<div className="mt-3 space-y-2 text-sm">
|
<div className="mt-3 space-y-2 text-sm">
|
||||||
{hasOutput && (
|
{hasOutput && (
|
||||||
<div className="rounded-lg bg-white p-2 dark:bg-[#2A2A2A]">
|
<div className="bg-muted rounded-lg p-2">
|
||||||
<span className="font-medium text-gray-600 dark:text-gray-400">
|
<span className="font-medium text-gray-600 dark:text-gray-400">
|
||||||
Output:{' '}
|
Output:{' '}
|
||||||
</span>
|
</span>
|
||||||
@@ -205,7 +205,7 @@ function ExecutionDetails({
|
|||||||
{stateVars.map(([key, value]) => (
|
{stateVars.map(([key, value]) => (
|
||||||
<span
|
<span
|
||||||
key={key}
|
key={key}
|
||||||
className="inline-flex items-center rounded-lg bg-white px-2 py-1 text-xs dark:bg-[#2A2A2A]"
|
className="bg-muted inline-flex items-center rounded-lg px-2 py-1 text-xs"
|
||||||
>
|
>
|
||||||
<span className="max-w-[100px] truncate font-medium text-gray-600 dark:text-gray-400">
|
<span className="max-w-[100px] truncate font-medium text-gray-600 dark:text-gray-400">
|
||||||
{key}:
|
{key}:
|
||||||
@@ -487,10 +487,10 @@ export default function WorkflowPreview({
|
|||||||
queries.length > 0 ? queries[queries.length - 1].executionSteps || [] : [];
|
queries.length > 0 ? queries[queries.length - 1].executionSteps || [] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-raisin-black flex h-full flex-col bg-white">
|
<div className="bg-card flex h-full flex-col">
|
||||||
<div className="border-light-silver dark:bg-raisin-black flex h-[77px] items-center justify-between border-b bg-white px-6 dark:border-[#3A3A3A]">
|
<div className="border-border flex h-[77px] items-center justify-between border-b px-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center rounded-full bg-gray-100 p-3 text-gray-600 dark:bg-[#2C2C2C] dark:text-gray-300">
|
<div className="bg-muted flex items-center justify-center rounded-full p-3 text-gray-600 dark:text-gray-300">
|
||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -504,7 +504,7 @@ export default function WorkflowPreview({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{status === 'loading' && (
|
{status === 'loading' && (
|
||||||
<span className="text-purple-30 dark:text-violets-are-blue flex items-center gap-1 text-xs">
|
<span className="text-primary dark:text-primary flex items-center gap-1 text-xs">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -512,7 +512,7 @@ export default function WorkflowPreview({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-1">
|
<div className="flex min-h-0 flex-1">
|
||||||
<div className="flex w-64 shrink-0 flex-col border-r border-gray-200 dark:border-[#3A3A3A]">
|
<div className="border-border flex w-64 shrink-0 flex-col border-r">
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
<h3 className="text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
<h3 className="text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||||
Workflow
|
Workflow
|
||||||
@@ -537,7 +537,7 @@ export default function WorkflowPreview({
|
|||||||
>
|
>
|
||||||
{queries.length === 0 ? (
|
{queries.length === 0 ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<div className="mb-2 flex size-14 shrink-0 items-center justify-center rounded-xl bg-gray-100 dark:bg-[#2C2C2C]">
|
<div className="bg-muted mb-2 flex size-14 shrink-0 items-center justify-center rounded-xl">
|
||||||
<MessageSquare className="size-6 text-gray-600 dark:text-gray-300" />
|
<MessageSquare className="size-6 text-gray-600 dark:text-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
<p className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||||
@@ -618,7 +618,7 @@ export default function WorkflowPreview({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-raisin-black absolute right-0 bottom-0 left-0 flex w-full flex-col gap-2 bg-white px-4 pt-2 pb-4">
|
<div className="bg-card absolute right-0 bottom-0 left-0 flex w-full flex-col gap-2 px-4 pt-2 pb-4">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSubmit={(text) => handleQuestionSubmission(text)}
|
onSubmit={(text) => handleQuestionSubmission(text)}
|
||||||
loading={status === 'loading'}
|
loading={status === 'loading'}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Monitor } from 'lucide-react';
|
|||||||
|
|
||||||
export default function MobileBlocker() {
|
export default function MobileBlocker() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-lotion dark:bg-raisin-black flex min-h-screen flex-col items-center justify-center px-6 text-center md:hidden">
|
<div className="bg-background flex min-h-screen flex-col items-center justify-center px-6 text-center md:hidden">
|
||||||
<div className="bg-violets-are-blue/10 dark:bg-violets-are-blue/20 mb-6 flex h-20 w-20 items-center justify-center rounded-2xl">
|
<div className="bg-primary/10 dark:bg-primary/20 mb-6 flex h-20 w-20 items-center justify-center rounded-2xl">
|
||||||
<Monitor className="text-violets-are-blue h-10 w-10" />
|
<Monitor className="text-primary h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mb-2 text-xl font-bold text-gray-900 dark:text-white">
|
<h2 className="text-foreground mb-2 text-xl font-bold">
|
||||||
Desktop Required
|
Desktop Required
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-sm text-sm leading-relaxed text-gray-500 dark:text-[#E0E0E0]">
|
<p className="text-muted-foreground max-w-sm text-sm leading-relaxed">
|
||||||
The Workflow Builder requires a larger screen for the best experience.
|
The Workflow Builder requires a larger screen for the best experience.
|
||||||
Please open this page on a desktop or laptop computer.
|
Please open this page on a desktop or laptop computer.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ function HighlightedOverlay({ text }: { text: string }) {
|
|||||||
<>
|
<>
|
||||||
{parts.map((part, i) =>
|
{parts.map((part, i) =>
|
||||||
/^\{\{[^}]*\}\}$/.test(part) ? (
|
/^\{\{[^}]*\}\}$/.test(part) ? (
|
||||||
<span key={i} className="text-violets-are-blue font-medium">
|
<span key={i} className="text-primary font-medium">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -222,7 +222,7 @@ function VariableListWithSearch({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col overflow-hidden">
|
<div className="flex w-full flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 border-b border-[#E5E5E5] px-3 py-2 dark:border-[#3A3A3A]">
|
<div className="border-border flex items-center gap-2 border-b px-3 py-2">
|
||||||
<Search className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
|
<Search className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -252,9 +252,9 @@ function VariableListWithSearch({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect(v.templatePath);
|
onSelect(v.templatePath);
|
||||||
}}
|
}}
|
||||||
className="flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-left text-sm transition-colors hover:bg-gray-50 dark:hover:bg-[#383838]"
|
className="hover:bg-accent flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-left text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<Braces className="text-violets-are-blue h-3.5 w-3.5 shrink-0" />
|
<Braces className="text-primary h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="truncate font-medium text-gray-800 dark:text-gray-200">
|
<span className="truncate font-medium text-gray-800 dark:text-gray-200">
|
||||||
{v.label}
|
{v.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -412,7 +412,7 @@ export default function PromptTextArea({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
className="border-light-silver focus-within:ring-purple-30 relative rounded-xl border bg-white transition-all focus-within:ring-2 dark:border-[#3A3A3A] dark:bg-[#2C2C2C]"
|
className="border-border focus-within:ring-ring bg-card relative rounded-xl border transition-all focus-within:ring-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
@@ -463,7 +463,7 @@ export default function PromptTextArea({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-violets-are-blue hover:bg-violets-are-blue/10 flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
|
className="text-primary hover:bg-primary/10 flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
Add context
|
Add context
|
||||||
@@ -472,7 +472,7 @@ export default function PromptTextArea({
|
|||||||
<PopoverContent
|
<PopoverContent
|
||||||
align="end"
|
align="end"
|
||||||
side="top"
|
side="top"
|
||||||
className="w-60 rounded-xl border border-[#E5E5E5] bg-white p-0 shadow-lg dark:border-[#3A3A3A] dark:bg-[#2C2C2C]"
|
className="border-border bg-card w-60 rounded-xl border p-0 shadow-lg"
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<VariableListWithSearch
|
<VariableListWithSearch
|
||||||
@@ -486,7 +486,7 @@ export default function PromptTextArea({
|
|||||||
{showDropdown && filtered.length > 0 && (
|
{showDropdown && filtered.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className="absolute z-50 w-64 rounded-xl border border-[#E5E5E5] bg-white shadow-lg dark:border-[#3A3A3A] dark:bg-[#2C2C2C]"
|
className="border-border bg-card absolute z-50 w-64 rounded-xl border shadow-lg"
|
||||||
style={{ top: dropdownPos.top, left: dropdownPos.left }}
|
style={{ top: dropdownPos.top, left: dropdownPos.left }}
|
||||||
>
|
>
|
||||||
<VariableListWithSearch
|
<VariableListWithSearch
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
|||||||
icon,
|
icon,
|
||||||
handles = { source: true, target: true },
|
handles = { source: true, target: true },
|
||||||
}) => {
|
}) => {
|
||||||
let bgColor = 'bg-white dark:bg-[#2C2C2C]';
|
let bgColor = 'bg-card';
|
||||||
let borderColor = 'border-gray-200 dark:border-[#3A3A3A]';
|
let borderColor = 'border-border';
|
||||||
let iconBg = 'bg-gray-100 dark:bg-gray-800';
|
let iconBg = 'bg-gray-100 dark:bg-gray-800';
|
||||||
let iconColor = 'text-gray-600 dark:text-gray-400';
|
let iconColor = 'text-gray-600 dark:text-gray-400';
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
borderColor =
|
borderColor = 'border-primary ring-2 ring-primary';
|
||||||
'border-violets-are-blue ring-2 ring-purple-300 dark:ring-violets-are-blue';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'start') {
|
if (type === 'start') {
|
||||||
@@ -56,7 +55,7 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
|||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
className="hover:bg-violets-are-blue! -left-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
className="hover:bg-primary/90! border-card! -left-1! h-3! w-3! rounded-full! border-2! bg-gray-400! transition-colors!"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -86,7 +85,7 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
|||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
className="hover:bg-violets-are-blue! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
className="hover:bg-primary/90! border-card! -right-1! h-3! w-3! rounded-full! border-2! bg-gray-400! transition-colors!"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative rounded-2xl border bg-white shadow-md transition-all dark:bg-[#2C2C2C] ${
|
className={`bg-card relative rounded-2xl border shadow-md transition-all ${
|
||||||
selected
|
selected
|
||||||
? 'border-violets-are-blue dark:ring-violets-are-blue scale-105 ring-2 ring-purple-300'
|
? 'border-primary dark:ring-primary scale-105 ring-2 ring-purple-300'
|
||||||
: 'border-gray-200 hover:shadow-lg dark:border-[#3A3A3A]'
|
: 'border-border hover:shadow-lg'
|
||||||
}`}
|
}`}
|
||||||
style={{ minWidth: 180, maxWidth: 220, height }}
|
style={{ minWidth: 180, maxWidth: 220, height }}
|
||||||
>
|
>
|
||||||
@@ -47,7 +47,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
|||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
isConnectable
|
isConnectable
|
||||||
className="hover:bg-violets-are-blue! top-1/2! -left-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
className="hover:bg-primary/90! border-card! top-1/2! -left-1! h-3! w-3! rounded-full! border-2! bg-gray-400! transition-colors!"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 px-3 py-2">
|
<div className="flex items-center gap-3 px-3 py-2">
|
||||||
@@ -100,7 +100,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
|||||||
id={c.sourceHandle}
|
id={c.sourceHandle}
|
||||||
isConnectable
|
isConnectable
|
||||||
style={{ top: getHandleTop(i, totalOutputs) }}
|
style={{ top: getHandleTop(i, totalOutputs) }}
|
||||||
className="hover:bg-violets-are-blue! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-orange-400! transition-colors dark:border-[#2C2C2C]!"
|
className="hover:bg-primary/90! dark:border-border! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-orange-400! transition-colors"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Handle
|
<Handle
|
||||||
@@ -109,7 +109,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
|||||||
id="else"
|
id="else"
|
||||||
isConnectable
|
isConnectable
|
||||||
style={{ top: getHandleTop(cases.length, totalOutputs) }}
|
style={{ top: getHandleTop(cases.length, totalOutputs) }}
|
||||||
className="hover:bg-violets-are-blue! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
className="hover:bg-primary/90! border-card! -right-1! h-3! w-3! rounded-full! border-2! bg-gray-400! transition-colors!"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const AgentNode = memo(function AgentNode({
|
|||||||
)}
|
)}
|
||||||
{config.model_id && (
|
{config.model_id && (
|
||||||
<div
|
<div
|
||||||
className="text-purple-30 dark:text-violets-are-blue truncate text-xs"
|
className="text-primary dark:text-primary truncate text-xs"
|
||||||
title={config.model_id}
|
title={config.model_id}
|
||||||
>
|
>
|
||||||
{config.model_id}
|
{config.model_id}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ const endpoints = {
|
|||||||
WORKFLOWS: '/api/workflows',
|
WORKFLOWS: '/api/workflows',
|
||||||
WORKFLOW: (id: string) => `/api/workflows/${id}`,
|
WORKFLOW: (id: string) => `/api/workflows/${id}`,
|
||||||
},
|
},
|
||||||
|
V1: {
|
||||||
|
CHAT_COMPLETIONS: '/v1/chat/completions',
|
||||||
|
MODELS: '/v1/models',
|
||||||
|
},
|
||||||
CONVERSATION: {
|
CONVERSATION: {
|
||||||
ANSWER: '/api/answer',
|
ANSWER: '/api/answer',
|
||||||
ANSWER_STREAMING: '/stream',
|
ANSWER_STREAMING: '/stream',
|
||||||
|
|||||||
@@ -54,6 +54,18 @@ const conversationService = {
|
|||||||
apiClient.get(endpoints.CONVERSATION.DELETE_ALL, token, {}),
|
apiClient.get(endpoints.CONVERSATION.DELETE_ALL, token, {}),
|
||||||
update: (data: any, token: string | null): Promise<any> =>
|
update: (data: any, token: string | null): Promise<any> =>
|
||||||
apiClient.post(endpoints.CONVERSATION.UPDATE, data, token, {}),
|
apiClient.post(endpoints.CONVERSATION.UPDATE, data, token, {}),
|
||||||
|
chatCompletions: (
|
||||||
|
data: any,
|
||||||
|
agentApiKey: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<any> =>
|
||||||
|
apiClient.post(
|
||||||
|
endpoints.V1.CHAT_COMPLETIONS,
|
||||||
|
data,
|
||||||
|
null,
|
||||||
|
{ Authorization: `Bearer ${agentApiKey}` },
|
||||||
|
signal,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default conversationService;
|
export default conversationService;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function Accordion({
|
|||||||
className={`flex w-full items-center justify-between focus:outline-hidden ${titleClassName}`}
|
className={`flex w-full items-center justify-between focus:outline-hidden ${titleClassName}`}
|
||||||
onClick={toggleAccordion}
|
onClick={toggleAccordion}
|
||||||
>
|
>
|
||||||
<p className="break-words">{title}</p>
|
<p className="wrap-break-word">{title}</p>
|
||||||
<img
|
<img
|
||||||
src={ChevronDown}
|
src={ChevronDown}
|
||||||
className={`h-5 w-5 transform transition-transform duration-200 dark:invert ${
|
className={`h-5 w-5 transform transition-transform duration-200 dark:invert ${
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function ActionButtons({
|
|||||||
<button
|
<button
|
||||||
title={t('actionButtons.openNewChat')}
|
title={t('actionButtons.openNewChat')}
|
||||||
onClick={newChat}
|
onClick={newChat}
|
||||||
className="hover:bg-bright-gray flex items-center gap-1 rounded-full p-2 lg:hidden dark:hover:bg-[#28292E]"
|
className="hover:bg-accent dark:hover:bg-accent flex items-center gap-1 rounded-full p-2 lg:hidden"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="filter dark:invert"
|
className="filter dark:invert"
|
||||||
@@ -70,7 +70,7 @@ export default function ActionButtons({
|
|||||||
<button
|
<button
|
||||||
title={t('actionButtons.share')}
|
title={t('actionButtons.share')}
|
||||||
onClick={() => setShareModalState(true)}
|
onClick={() => setShareModalState(true)}
|
||||||
className="hover:bg-bright-gray rounded-full p-2 dark:hover:bg-[#28292E]"
|
className="hover:bg-accent dark:hover:bg-accent rounded-full p-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="filter dark:invert"
|
className="filter dark:invert"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import { useSelector } from 'react-redux';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import {
|
import {
|
||||||
oneLight,
|
oneLight,
|
||||||
vscDarkPlus,
|
vscDarkPlus,
|
||||||
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
import Exit from '../assets/exit.svg';
|
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
import Spinner from './Spinner';
|
import Exit from '../assets/exit.svg';
|
||||||
import CopyButton from './CopyButton';
|
|
||||||
import { useDarkTheme } from '../hooks';
|
import { useDarkTheme } from '../hooks';
|
||||||
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
|
import CopyButton from './CopyButton';
|
||||||
|
import Spinner from './Spinner';
|
||||||
|
|
||||||
type TodoItem = {
|
type TodoItem = {
|
||||||
todo_id: number;
|
todo_id: number;
|
||||||
@@ -61,7 +61,8 @@ const ARTIFACT_TITLE_BY_TYPE: Record<ArtifactData['artifact_type'], string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getArtifactTitle(artifact: ArtifactData | null, toolName?: string) {
|
function getArtifactTitle(artifact: ArtifactData | null, toolName?: string) {
|
||||||
if (artifact) return ARTIFACT_TITLE_BY_TYPE[artifact.artifact_type] ?? 'Artifact';
|
if (artifact)
|
||||||
|
return ARTIFACT_TITLE_BY_TYPE[artifact.artifact_type] ?? 'Artifact';
|
||||||
|
|
||||||
const formattedToolName = (toolName ?? '')
|
const formattedToolName = (toolName ?? '')
|
||||||
.replace(/_/g, ' ')
|
.replace(/_/g, ' ')
|
||||||
@@ -161,7 +162,7 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
|||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
{data.content ? (
|
{data.content ? (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
className="flex flex-col gap-3 text-sm leading-normal break-words whitespace-pre-wrap text-gray-800 dark:text-gray-200"
|
className="flex flex-col gap-3 text-sm leading-normal wrap-break-word whitespace-pre-wrap text-gray-800 dark:text-gray-200"
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
code(props) {
|
code(props) {
|
||||||
@@ -178,9 +179,9 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
|||||||
const language = match ? match[1] : '';
|
const language = match ? match[1] : '';
|
||||||
|
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="group border-light-silver dark:border-raisin-black relative my-2 overflow-hidden rounded-[14px] border">
|
<div className="group border-border relative my-2 overflow-hidden rounded-[14px] border">
|
||||||
<div className="bg-platinum dark:bg-eerie-black-2 flex items-center justify-between px-2 py-1">
|
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||||
{language}
|
{language}
|
||||||
</span>
|
</span>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
@@ -203,7 +204,7 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<code
|
<code
|
||||||
className="dark:bg-independence dark:text-bright-gray rounded-[6px] bg-gray-200 px-[8px] py-[4px] text-xs font-normal"
|
className="dark:bg-accent dark:text-foreground rounded-[6px] bg-gray-200 px-2 py-1 text-xs font-normal"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -315,17 +316,17 @@ export default function ArtifactSidebar({
|
|||||||
// Generate a unique ID for this fetch
|
// Generate a unique ID for this fetch
|
||||||
const fetchId = `${effectiveArtifactId}-${Date.now()}`;
|
const fetchId = `${effectiveArtifactId}-${Date.now()}`;
|
||||||
currentFetchIdRef.current = fetchId;
|
currentFetchIdRef.current = fetchId;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Note: For todo artifacts, the endpoint always returns all todos for the tool; will be coversation scoped later
|
// Note: For todo artifacts, the endpoint always returns all todos for the tool; will be coversation scoped later
|
||||||
userService
|
userService
|
||||||
.getArtifact(effectiveArtifactId, token)
|
.getArtifact(effectiveArtifactId, token)
|
||||||
.then(async (res: any) => {
|
.then(async (res: any) => {
|
||||||
// Ignore if this is not the current fetch
|
// Ignore if this is not the current fetch
|
||||||
if (currentFetchIdRef.current !== fetchId) return;
|
if (currentFetchIdRef.current !== fetchId) return;
|
||||||
|
|
||||||
const isResponseLike = res && typeof res.json === 'function';
|
const isResponseLike = res && typeof res.json === 'function';
|
||||||
const status = isResponseLike ? res.status : undefined;
|
const status = isResponseLike ? res.status : undefined;
|
||||||
const ok = isResponseLike ? Boolean(res.ok) : true;
|
const ok = isResponseLike ? Boolean(res.ok) : true;
|
||||||
@@ -453,7 +454,7 @@ export default function ArtifactSidebar({
|
|||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="hover:bg-accent dark:hover:bg-accent rounded-full p-1"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -472,7 +473,7 @@ export default function ArtifactSidebar({
|
|||||||
return (
|
return (
|
||||||
<div ref={sidebarRef} className="h-vh relative">
|
<div ref={sidebarRef} className="h-vh relative">
|
||||||
<div
|
<div
|
||||||
className={`dark:bg-chinese-black fixed top-0 right-0 z-50 flex h-full w-80 transform flex-col bg-white shadow-xl transition-all duration-300 sm:w-96 ${
|
className={`dark:bg-card bg-card fixed top-0 right-0 z-50 flex h-full w-80 transform flex-col shadow-xl transition-all duration-300 sm:w-96 ${
|
||||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
} border-l border-[#9ca3af]/10`}
|
} border-l border-[#9ca3af]/10`}
|
||||||
>
|
>
|
||||||
@@ -481,7 +482,7 @@ export default function ArtifactSidebar({
|
|||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-gray-1000 dark:hover:bg-gun-metal rounded-full p-2"
|
className="hover:bg-accent rounded-full p-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
|
import FileIcon from '../assets/file.svg';
|
||||||
|
import FolderIcon from '../assets/folder.svg';
|
||||||
|
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||||
|
import NoFilesIcon from '../assets/no-files.svg';
|
||||||
|
import SearchIcon from '../assets/search.svg';
|
||||||
import {
|
import {
|
||||||
useDarkTheme,
|
useDarkTheme,
|
||||||
useLoaderState,
|
useLoaderState,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useOutsideAlerter,
|
useOutsideAlerter,
|
||||||
} from '../hooks';
|
} from '../hooks';
|
||||||
import userService from '../api/services/userService';
|
|
||||||
import ArrowLeft from '../assets/arrow-left.svg';
|
|
||||||
import NoFilesIcon from '../assets/no-files.svg';
|
|
||||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
|
||||||
import SkeletonLoader from './SkeletonLoader';
|
|
||||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import { ChunkType } from '../settings/types';
|
import { ChunkType } from '../settings/types';
|
||||||
import Pagination from './DocumentPagination';
|
import Pagination from './DocumentPagination';
|
||||||
import FileIcon from '../assets/file.svg';
|
import SkeletonLoader from './SkeletonLoader';
|
||||||
import FolderIcon from '../assets/folder.svg';
|
|
||||||
import SearchIcon from '../assets/search.svg';
|
|
||||||
interface LineNumberedTextareaProps {
|
interface LineNumberedTextareaProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
@@ -73,7 +75,7 @@ const LineNumberedTextarea: React.FC<LineNumberedTextareaProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className={`w-full resize-none overflow-hidden border-none bg-transparent pl-8 font-['Inter'] text-[13.68px] leading-[19.93px] text-[#18181B] outline-none lg:pl-12 dark:text-white ${isMobile ? 'min-h-[calc(100vh-200px)]' : 'min-h-[calc(100vh-300px)]'} ${!editable ? 'select-none' : ''}`}
|
className={`text-foreground w-full resize-none overflow-hidden border-none bg-transparent pl-8 font-['Inter'] text-[13.68px] leading-[19.93px] outline-none lg:pl-12 dark:text-white ${isMobile ? 'min-h-[calc(100vh-200px)]' : 'min-h-[calc(100vh-300px)]'} ${!editable ? 'select-none' : ''}`}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={editable ? handleChange : undefined}
|
onChange={editable ? handleChange : undefined}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
@@ -301,7 +303,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
<div className="mb-0 flex min-h-[38px] flex-col gap-2 text-base sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-0 flex min-h-[38px] flex-col gap-2 text-base sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex w-full items-center sm:w-auto">
|
<div className="flex w-full items-center sm:w-auto">
|
||||||
<button
|
<button
|
||||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 transition-all duration-200 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 transition-all duration-200 dark:border-0 dark:text-gray-500"
|
||||||
onClick={
|
onClick={
|
||||||
editingChunk
|
editingChunk
|
||||||
? () => setEditingChunk(null)
|
? () => setEditingChunk(null)
|
||||||
@@ -315,17 +317,17 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
{/* Removed the directory icon */}
|
{/* Removed the directory icon */}
|
||||||
<span className="font-semibold break-words text-[#7D54D1]">
|
<span className="font-semibold wrap-break-word text-[#7D54D1]">
|
||||||
{documentName}
|
{documentName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{pathParts.length > 0 && (
|
{pathParts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span className="mx-1 flex-shrink-0 text-gray-500">/</span>
|
<span className="mx-1 shrink-0 text-gray-500">/</span>
|
||||||
{pathParts.map((part, index) => (
|
{pathParts.map((part, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<span
|
<span
|
||||||
className={`break-words ${
|
className={`wrap-break-word ${
|
||||||
index < pathParts.length - 1
|
index < pathParts.length - 1
|
||||||
? 'font-medium text-[#7D54D1]'
|
? 'font-medium text-[#7D54D1]'
|
||||||
: 'text-gray-700 dark:text-gray-300'
|
: 'text-gray-700 dark:text-gray-300'
|
||||||
@@ -334,9 +336,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
{index < pathParts.length - 1 && (
|
{index < pathParts.length - 1 && (
|
||||||
<span className="mx-1 flex-shrink-0 text-gray-500">
|
<span className="mx-1 shrink-0 text-gray-500">/</span>
|
||||||
/
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -350,7 +350,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
!isEditing ? (
|
!isEditing ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap text-white"
|
className="bg-primary hover:bg-primary/90 flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap text-white"
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
>
|
>
|
||||||
{t('modals.chunk.edit')}
|
{t('modals.chunk.edit')}
|
||||||
@@ -370,7 +370,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}}
|
}}
|
||||||
className="dark:text-light-gray flex h-[38px] min-w-[108px] cursor-pointer items-center justify-center rounded-full px-4 py-1 text-sm font-medium text-nowrap hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
className="dark:text-foreground hover:bg-accent dark:hover:bg-accent flex h-[38px] min-w-[108px] cursor-pointer items-center justify-center rounded-full px-4 py-1 text-sm font-medium text-nowrap"
|
||||||
>
|
>
|
||||||
{t('modals.chunk.cancel')}
|
{t('modals.chunk.cancel')}
|
||||||
</button>
|
</button>
|
||||||
@@ -402,7 +402,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
editingText.trim() &&
|
editingText.trim() &&
|
||||||
(editingTitle !== (editingChunk?.metadata?.title || '') ||
|
(editingTitle !== (editingChunk?.metadata?.title || '') ||
|
||||||
editingText !== (editingChunk?.text || ''))
|
editingText !== (editingChunk?.text || ''))
|
||||||
? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer'
|
? 'bg-primary hover:bg-primary/90 cursor-pointer'
|
||||||
: 'cursor-not-allowed bg-gray-400'
|
: 'cursor-not-allowed bg-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -414,7 +414,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAddingChunk(false)}
|
onClick={() => setIsAddingChunk(false)}
|
||||||
className="dark:text-light-gray flex h-[38px] min-w-[108px] cursor-pointer items-center justify-center rounded-full px-4 py-1 text-sm font-medium text-nowrap hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
className="dark:text-foreground hover:bg-accent dark:hover:bg-accent flex h-[38px] min-w-[108px] cursor-pointer items-center justify-center rounded-full px-4 py-1 text-sm font-medium text-nowrap"
|
||||||
>
|
>
|
||||||
{t('modals.chunk.cancel')}
|
{t('modals.chunk.cancel')}
|
||||||
</button>
|
</button>
|
||||||
@@ -428,7 +428,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
disabled={!editingText.trim()}
|
disabled={!editingText.trim()}
|
||||||
className={`flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 py-1 text-[14px] font-medium text-nowrap text-white transition-all ${
|
className={`flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 py-1 text-[14px] font-medium text-nowrap text-white transition-all ${
|
||||||
editingText.trim()
|
editingText.trim()
|
||||||
? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer'
|
? 'bg-primary hover:bg-primary/90 cursor-pointer'
|
||||||
: 'cursor-not-allowed bg-gray-400'
|
: 'cursor-not-allowed bg-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -488,14 +488,14 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
value={fileSearchQuery}
|
value={fileSearchQuery}
|
||||||
onChange={(e) => handleFileSearchChange(e.target.value)}
|
onChange={(e) => handleFileSearchChange(e.target.value)}
|
||||||
placeholder={t('settings.sources.searchFiles')}
|
placeholder={t('settings.sources.searchFiles')}
|
||||||
className={`h-[38px] w-full border border-[#D1D9E0] py-2 pr-4 pl-10 dark:border-[#6A6A6A] ${
|
className={`border-border dark:border-border h-[38px] w-full border py-2 pr-4 pl-10 ${
|
||||||
fileSearchQuery ? 'rounded-t-[6px]' : 'rounded-[6px]'
|
fileSearchQuery ? 'rounded-t-[6px]' : 'rounded-[6px]'
|
||||||
} bg-transparent transition-all duration-200 focus:outline-none dark:text-[#E0E0E0]`}
|
} bg-transparent transition-all duration-200 focus:outline-none`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fileSearchQuery && (
|
{fileSearchQuery && (
|
||||||
<div className="absolute z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[6px] border border-t-0 border-[#D1D9E0] bg-white shadow-lg dark:border-[#6A6A6A] dark:bg-[#1F2023]">
|
<div className="border-border bg-card dark:border-border dark:bg-card absolute z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[6px] border border-t-0 shadow-lg">
|
||||||
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto">
|
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto">
|
||||||
{fileSearchResults.length === 0 ? (
|
{fileSearchResults.length === 0 ? (
|
||||||
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
@@ -507,18 +507,18 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
title={result.path}
|
title={result.path}
|
||||||
onClick={() => handleSearchResultClick(result)}
|
onClick={() => handleSearchResultClick(result)}
|
||||||
className={`flex cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${
|
className={`hover:bg-muted dark:hover:bg-muted flex cursor-pointer items-center px-3 py-2 ${
|
||||||
index !== fileSearchResults.length - 1
|
index !== fileSearchResults.length - 1
|
||||||
? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]'
|
? 'border-border dark:border-border border-b'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={result.isFile ? FileIcon : FolderIcon}
|
src={result.isFile ? FileIcon : FolderIcon}
|
||||||
alt={result.isFile ? 'File' : 'Folder'}
|
alt={result.isFile ? 'File' : 'Folder'}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
<span className="truncate text-sm">
|
||||||
{result.name ||
|
{result.name ||
|
||||||
result.path.split('/').pop() ||
|
result.path.split('/').pop() ||
|
||||||
result.path}
|
result.path}
|
||||||
@@ -546,8 +546,8 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
{!editingChunk && !isAddingChunk ? (
|
{!editingChunk && !isAddingChunk ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
<div className="mb-3 flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||||
<div className="flex h-[38px] w-full flex-1 items-center overflow-hidden rounded-md border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border flex h-[38px] w-full flex-1 items-center overflow-hidden rounded-md border">
|
||||||
<div className="flex h-full items-center px-4 font-medium whitespace-nowrap text-gray-700 dark:text-[#E0E0E0]">
|
<div className="dark:text-foreground flex h-full items-center px-4 font-medium whitespace-nowrap text-gray-700">
|
||||||
{totalChunks > 999999
|
{totalChunks > 999999
|
||||||
? `${(totalChunks / 1000000).toFixed(2)}M`
|
? `${(totalChunks / 1000000).toFixed(2)}M`
|
||||||
: totalChunks > 999
|
: totalChunks > 999
|
||||||
@@ -555,19 +555,19 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
: totalChunks}{' '}
|
: totalChunks}{' '}
|
||||||
{t('settings.sources.chunks')}
|
{t('settings.sources.chunks')}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-[1px] bg-[#D1D9E0] dark:bg-[#6A6A6A]"></div>
|
<div className="bg-border dark:bg-border h-full w-px"></div>
|
||||||
<div className="h-full flex-1">
|
<div className="h-full flex-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('settings.sources.searchPlaceholder')}
|
placeholder={t('settings.sources.searchPlaceholder')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="h-full w-full border-none bg-transparent px-3 py-2 text-[13.56px] leading-[100%] font-normal outline-none dark:text-[#E0E0E0]"
|
className="h-full w-full border-none bg-transparent px-3 py-2 text-[13.56px] leading-[100%] font-normal outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] w-full min-w-[108px] shrink-0 items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-normal text-white sm:w-auto"
|
className="bg-primary hover:bg-primary/90 flex h-[38px] w-full min-w-[108px] shrink-0 items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-normal text-white sm:w-auto"
|
||||||
title={t('settings.sources.addChunk')}
|
title={t('settings.sources.addChunk')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsAddingChunk(true);
|
setIsAddingChunk(true);
|
||||||
@@ -579,11 +579,11 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="grid w-full grid-cols-1 justify-items-start gap-4 sm:[grid-template-columns:repeat(auto-fit,minmax(400px,1fr))]">
|
<div className="grid w-full grid-cols-1 justify-items-start gap-4 sm:grid-cols-[repeat(auto-fit,minmax(400px,1fr))]">
|
||||||
<SkeletonLoader component="chunkCards" count={perPage} />
|
<SkeletonLoader component="chunkCards" count={perPage} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid w-full grid-cols-1 justify-items-start gap-4 sm:[grid-template-columns:repeat(auto-fit,minmax(400px,1fr))]">
|
<div className="grid w-full grid-cols-1 justify-items-start gap-4 sm:grid-cols-[repeat(auto-fit,minmax(400px,1fr))]">
|
||||||
{filteredChunks.length === 0 ? (
|
{filteredChunks.length === 0 ? (
|
||||||
<div className="col-span-full flex min-h-[50vh] w-full flex-col items-center justify-center text-center text-gray-500 dark:text-gray-400">
|
<div className="col-span-full flex min-h-[50vh] w-full flex-col items-center justify-center text-center text-gray-500 dark:text-gray-400">
|
||||||
<img
|
<img
|
||||||
@@ -597,7 +597,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
filteredChunks.map((chunk, index) => (
|
filteredChunks.map((chunk, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="relative flex h-[197px] w-full max-w-[487px] transform cursor-pointer flex-col justify-between overflow-hidden rounded-[5.86px] border border-[#D1D9E0] transition-transform duration-200 hover:scale-105 dark:border-[#6A6A6A]"
|
className="border-border dark:border-border relative flex h-[197px] w-full max-w-[487px] transform cursor-pointer flex-col justify-between overflow-hidden rounded-[5.86px] border transition-transform duration-200 hover:scale-105"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingChunk(chunk);
|
setEditingChunk(chunk);
|
||||||
setEditingTitle(chunk.metadata?.title || '');
|
setEditingTitle(chunk.metadata?.title || '');
|
||||||
@@ -605,8 +605,8 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] px-4 py-3 dark:border-[#6A6A6A] dark:bg-[#27282D]">
|
<div className="border-border bg-muted dark:border-border dark:bg-card flex w-full items-center justify-between border-b px-4 py-3">
|
||||||
<div className="text-sm text-[#59636E] dark:text-[#E0E0E0]">
|
<div className="dark:text-muted-foreground text-sm text-[#59636E]">
|
||||||
{chunk.metadata.token_count
|
{chunk.metadata.token_count
|
||||||
? chunk.metadata.token_count.toLocaleString()
|
? chunk.metadata.token_count.toLocaleString()
|
||||||
: '-'}{' '}
|
: '-'}{' '}
|
||||||
@@ -614,7 +614,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 pt-3 pb-6">
|
<div className="px-4 pt-3 pb-6">
|
||||||
<p className="line-clamp-6 font-['Inter'] text-[13.68px] leading-[19.93px] font-normal text-[#18181B] dark:text-[#E0E0E0]">
|
<p className="text-foreground line-clamp-6 font-['Inter'] text-[13.68px] leading-[19.93px] font-normal">
|
||||||
{chunk.text}
|
{chunk.text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -627,7 +627,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
</>
|
</>
|
||||||
) : isAddingChunk ? (
|
) : isAddingChunk ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative overflow-hidden rounded-lg border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border relative overflow-hidden rounded-lg border">
|
||||||
<LineNumberedTextarea
|
<LineNumberedTextarea
|
||||||
value={editingText}
|
value={editingText}
|
||||||
onChange={setEditingText}
|
onChange={setEditingText}
|
||||||
@@ -639,9 +639,9 @@ const Chunks: React.FC<ChunksProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
editingChunk && (
|
editingChunk && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative flex w-full flex-col overflow-hidden rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border relative flex w-full flex-col overflow-hidden rounded-[5.86px] border">
|
||||||
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] px-4 py-3 dark:border-[#6A6A6A] dark:bg-[#27282D]">
|
<div className="border-border bg-muted dark:border-border dark:bg-card flex w-full items-center justify-between border-b px-4 py-3">
|
||||||
<div className="text-sm text-[#59636E] dark:text-[#E0E0E0]">
|
<div className="dark:text-muted-foreground text-sm text-[#59636E]">
|
||||||
{editingChunk.metadata.token_count
|
{editingChunk.metadata.token_count
|
||||||
? editingChunk.metadata.token_count.toLocaleString()
|
? editingChunk.metadata.token_count.toLocaleString()
|
||||||
: '-'}{' '}
|
: '-'}{' '}
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ export default function ConfigFields({
|
|||||||
<div key={key} className="flex flex-col gap-1.5">
|
<div key={key} className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor={key}>
|
<Label htmlFor={key}>
|
||||||
{spec.label || key}
|
{spec.label || key}
|
||||||
{spec.required && (
|
{spec.required && <span className="text-red-500">*</span>}
|
||||||
<span className="text-red-500">*</span>
|
|
||||||
)}
|
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={value || spec.default || ''}
|
value={value || spec.default || ''}
|
||||||
@@ -82,7 +80,8 @@ export default function ConfigFields({
|
|||||||
size="lg"
|
size="lg"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-xl',
|
'w-full rounded-xl',
|
||||||
hasError && 'border-destructive aria-invalid:ring-destructive/20',
|
hasError &&
|
||||||
|
'border-destructive aria-invalid:ring-destructive/20',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectValue placeholder={spec.label || key} />
|
<SelectValue placeholder={spec.label || key} />
|
||||||
@@ -90,13 +89,14 @@ export default function ConfigFields({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
{spec.enum.map((v) => (
|
{spec.enum.map((v) => (
|
||||||
<SelectItem key={v} value={v}>
|
<SelectItem key={v} value={v}>
|
||||||
{v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, ' ')}
|
{v.charAt(0).toUpperCase() +
|
||||||
|
v.slice(1).replace(/_/g, ' ')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<p className="text-xs text-destructive">{errors[key]}</p>
|
<p className="text-destructive text-xs">{errors[key]}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -106,9 +106,7 @@ export default function ConfigFields({
|
|||||||
<div key={key} className="flex flex-col gap-1.5">
|
<div key={key} className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor={key}>
|
<Label htmlFor={key}>
|
||||||
{spec.label || key}
|
{spec.label || key}
|
||||||
{spec.required && (
|
{spec.required && <span className="text-red-500">*</span>}
|
||||||
<span className="text-red-500">*</span>
|
|
||||||
)}
|
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={key}
|
id={key}
|
||||||
@@ -134,12 +132,14 @@ export default function ConfigFields({
|
|||||||
}}
|
}}
|
||||||
placeholder={placeholder || spec.description || ''}
|
placeholder={placeholder || spec.description || ''}
|
||||||
min={spec.type === 'number' ? 1 : undefined}
|
min={spec.type === 'number' ? 1 : undefined}
|
||||||
max={spec.type === 'number' && key === 'timeout' ? 300 : undefined}
|
max={
|
||||||
|
spec.type === 'number' && key === 'timeout' ? 300 : undefined
|
||||||
|
}
|
||||||
aria-invalid={hasError || undefined}
|
aria-invalid={hasError || undefined}
|
||||||
className={cn('rounded-xl', hasError && 'border-destructive')}
|
className={cn('rounded-xl', hasError && 'border-destructive')}
|
||||||
/>
|
/>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<p className="text-xs text-destructive">{errors[key]}</p>
|
<p className="text-destructive text-xs">{errors[key]}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="text-sm text-[#E60000] dark:text-[#E37064]"
|
className="text-sm text-[#E60000] dark:text-red-400"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
lineHeight: '100%',
|
lineHeight: '100%',
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { formatBytes } from '../utils/stringUtils';
|
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import userService from '../api/services/userService';
|
||||||
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
|
import CheckmarkIcon from '../assets/checkMark2.svg';
|
||||||
|
import EyeView from '../assets/eye-view.svg';
|
||||||
|
import FileIcon from '../assets/file.svg';
|
||||||
|
import FolderIcon from '../assets/folder.svg';
|
||||||
|
import SyncIcon from '../assets/sync.svg';
|
||||||
|
import ThreeDots from '../assets/three-dots.svg';
|
||||||
|
import { useLoaderState, useOutsideAlerter } from '../hooks';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
|
import { formatBytes } from '../utils/stringUtils';
|
||||||
import Chunks from './Chunks';
|
import Chunks from './Chunks';
|
||||||
import ContextMenu, { MenuOption } from './ContextMenu';
|
import ContextMenu, { MenuOption } from './ContextMenu';
|
||||||
import SkeletonLoader from './SkeletonLoader';
|
import SkeletonLoader from './SkeletonLoader';
|
||||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
|
||||||
import userService from '../api/services/userService';
|
|
||||||
import FileIcon from '../assets/file.svg';
|
|
||||||
import FolderIcon from '../assets/folder.svg';
|
|
||||||
import ArrowLeft from '../assets/arrow-left.svg';
|
|
||||||
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, useLoaderState } from '../hooks';
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableBody,
|
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableRow,
|
||||||
} from './Table';
|
} from './Table';
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
@@ -325,26 +326,26 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
{/* Left side with path navigation */}
|
{/* Left side with path navigation */}
|
||||||
<div className="flex w-full items-center sm:w-auto">
|
<div className="flex w-full items-center sm:w-auto">
|
||||||
<button
|
<button
|
||||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
className="text-muted-foreground mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium dark:border-0"
|
||||||
onClick={handleBackNavigation}
|
onClick={handleBackNavigation}
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
<span className="font-semibold break-words text-[#7D54D1]">
|
<span className="font-semibold wrap-break-word text-[#7D54D1]">
|
||||||
{sourceName}
|
{sourceName}
|
||||||
</span>
|
</span>
|
||||||
{currentPath.length > 0 && (
|
{currentPath.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span className="mx-1 flex-shrink-0 text-gray-500">/</span>
|
<span className="text-muted-foreground mx-1 shrink-0">/</span>
|
||||||
{currentPath.map((dir, index) => (
|
{currentPath.map((dir, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<span className="break-words text-gray-700 dark:text-[#E0E0E0]">
|
<span className="dark:text-foreground wrap-break-word text-gray-700">
|
||||||
{dir}
|
{dir}
|
||||||
</span>
|
</span>
|
||||||
{index < currentPath.length - 1 && (
|
{index < currentPath.length - 1 && (
|
||||||
<span className="mx-1 flex-shrink-0 text-gray-500">
|
<span className="text-muted-foreground mx-1 shrink-0">
|
||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -364,8 +365,8 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
disabled={isSyncing}
|
disabled={isSyncing}
|
||||||
className={`flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap transition-colors ${
|
className={`flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap transition-colors ${
|
||||||
isSyncing
|
isSyncing
|
||||||
? 'cursor-not-allowed bg-gray-300 text-gray-600 dark:bg-gray-600 dark:text-gray-400'
|
? 'dark:bg-muted dark:text-muted-foreground cursor-not-allowed bg-gray-300 text-gray-600'
|
||||||
: 'bg-purple-30 hover:bg-violets-are-blue text-white'
|
: 'bg-primary hover:bg-primary/90 text-white'
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
isSyncing
|
isSyncing
|
||||||
@@ -402,7 +403,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={FolderIcon}
|
src={FolderIcon}
|
||||||
alt={t('settings.sources.parentFolderAlt')}
|
alt={t('settings.sources.parentFolderAlt')}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">..</span>
|
<span className="truncate">..</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,7 +450,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={FolderIcon}
|
src={FolderIcon}
|
||||||
alt={t('settings.sources.folderAlt')}
|
alt={t('settings.sources.folderAlt')}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{name}</span>
|
<span className="truncate">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -466,7 +467,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
<div ref={menuRef} className="relative">
|
<div ref={menuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleMenuClick(e, itemId)}
|
onClick={(e) => handleMenuClick(e, itemId)}
|
||||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
className="dark:hover:bg-muted inline-flex h-[35px] w-6 shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB]"
|
||||||
aria-label={t('settings.sources.menuAlt')}
|
aria-label={t('settings.sources.menuAlt')}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -512,7 +513,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={FileIcon}
|
src={FileIcon}
|
||||||
alt={t('settings.sources.fileAlt')}
|
alt={t('settings.sources.fileAlt')}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{displayName}</span>
|
<span className="truncate">{displayName}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -527,7 +528,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
<div ref={menuRef} className="relative">
|
<div ref={menuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleMenuClick(e, itemId)}
|
onClick={(e) => handleMenuClick(e, itemId)}
|
||||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
className="dark:hover:bg-muted inline-flex h-[35px] w-6 shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB]"
|
||||||
aria-label={t('settings.sources.menuAlt')}
|
aria-label={t('settings.sources.menuAlt')}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -625,14 +626,14 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={t('settings.sources.searchFiles')}
|
placeholder={t('settings.sources.searchFiles')}
|
||||||
className={`h-[38px] w-full border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none dark:text-[#E0E0E0]`}
|
className={`border-border dark:border-border h-[38px] w-full border px-4 py-2 ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<div className="absolute top-full right-0 left-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[12px] border border-t-0 border-[#D1D9E0] bg-white shadow-lg transition-all duration-200 dark:border-[#6A6A6A] dark:bg-[#1F2023]">
|
<div className="border-border bg-card dark:border-border dark:bg-card absolute top-full right-0 left-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-2xl border border-t-0 shadow-lg transition-all duration-200">
|
||||||
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto overscroll-contain">
|
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto overscroll-contain">
|
||||||
{searchResults.length === 0 ? (
|
{searchResults.length === 0 ? (
|
||||||
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-muted-foreground py-2 text-center text-sm">
|
||||||
{t('settings.sources.noResults')}
|
{t('settings.sources.noResults')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -641,9 +642,9 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
onClick={() => handleSearchSelect(result)}
|
onClick={() => handleSearchSelect(result)}
|
||||||
title={result.path}
|
title={result.path}
|
||||||
className={`flex min-w-0 cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${
|
className={`hover:bg-muted dark:hover:bg-muted flex min-w-0 cursor-pointer items-center px-3 py-2 ${
|
||||||
index !== searchResults.length - 1
|
index !== searchResults.length - 1
|
||||||
? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]'
|
? 'border-border dark:border-border border-b'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -654,9 +655,9 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
|||||||
? t('settings.sources.fileAlt')
|
? t('settings.sources.fileAlt')
|
||||||
: t('settings.sources.folderAlt')
|
: t('settings.sources.folderAlt')
|
||||||
}
|
}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="flex-1 truncate text-sm dark:text-[#E0E0E0]">
|
<span className="flex-1 truncate text-sm">
|
||||||
{result.name}
|
{result.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function ContextMenu({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bg-lotion dark:bg-charleston-green-2 flex flex-col rounded-xl text-sm shadow-xl"
|
className="bg-background dark:bg-card flex flex-col rounded-xl text-sm shadow-xl"
|
||||||
style={{ minWidth: '144px' }}
|
style={{ minWidth: '144px' }}
|
||||||
>
|
>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
@@ -141,8 +141,8 @@ export default function ContextMenu({
|
|||||||
}}
|
}}
|
||||||
className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${
|
className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${
|
||||||
option.variant === 'danger'
|
option.variant === 'danger'
|
||||||
? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey/20'
|
? 'text-destructive hover:bg-muted'
|
||||||
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey/20'
|
: 'text-foreground hover:bg-muted'
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
{option.icon && (
|
{option.icon && (
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ export default function CopyButton({
|
|||||||
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
|
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
|
||||||
padding,
|
padding,
|
||||||
{
|
{
|
||||||
[`bg-[#FFFFFF}] dark:bg-transparent hover:bg-[#EEEEEE] dark:hover:bg-purple-taupe`]:
|
[`bg-transparent hover:bg-muted`]: !isCopied,
|
||||||
!isCopied,
|
|
||||||
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
|
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
|
||||||
isCopied,
|
isCopied,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
className="dark:bg-dark-charcoal dark:text-light-gray rounded border px-3 py-1 hover:bg-gray-200 dark:hover:bg-neutral-700"
|
className="dark:bg-card dark:text-foreground hover:bg-accent dark:hover:bg-accent rounded border px-3 py-1"
|
||||||
>
|
>
|
||||||
{rowsPerPage}
|
{rowsPerPage}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
className={`ring-opacity-5 dark:bg-dark-charcoal absolute right-0 z-50 mt-1 w-28 transform bg-white shadow-lg ring-1 ring-black transition-all duration-200 ease-in-out ${
|
className={`ring-opacity-5 dark:bg-card bg-card absolute right-0 z-50 mt-1 w-28 transform shadow-lg ring-1 ring-black transition-all duration-200 ease-in-out ${
|
||||||
isDropdownOpen
|
isDropdownOpen
|
||||||
? 'block scale-100 opacity-100'
|
? 'block scale-100 opacity-100'
|
||||||
: 'hidden scale-95 opacity-0'
|
: 'hidden scale-95 opacity-0'
|
||||||
@@ -76,10 +76,10 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
<div
|
<div
|
||||||
key={option}
|
key={option}
|
||||||
onClick={() => handleSelectRowsPerPage(option)}
|
onClick={() => handleSelectRowsPerPage(option)}
|
||||||
className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${
|
className={`hover:bg-accent dark:hover:bg-accent cursor-pointer px-4 py-2 text-xs ${
|
||||||
rowsPerPage === option
|
rowsPerPage === option
|
||||||
? 'dark:text-light-gray bg-gray-100 dark:bg-neutral-700'
|
? 'dark:text-foreground bg-gray-100 dark:bg-neutral-700'
|
||||||
: 'dark:bg-dark-charcoal dark:text-light-gray bg-white'
|
: 'bg-card dark:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{option}
|
{option}
|
||||||
|
|||||||
@@ -1,163 +1,241 @@
|
|||||||
import React from 'react';
|
import { Check, ChevronDown, Pencil, Search, Trash2 } from 'lucide-react';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
type OptionBase = { id?: string; type?: string };
|
||||||
import Edit from '../assets/edit.svg';
|
type NameIdOption = { name: string; id: string } & OptionBase;
|
||||||
import Trash from '../assets/trash.svg';
|
type LabelValueOption = { label: string; value: string } & OptionBase;
|
||||||
import { DropdownOption, DropdownProps } from './types/Dropdown.types';
|
type ValueDescriptionOption = {
|
||||||
|
value: number;
|
||||||
|
description: string;
|
||||||
|
} & OptionBase;
|
||||||
|
|
||||||
|
export type DropdownOption =
|
||||||
|
| string
|
||||||
|
| NameIdOption
|
||||||
|
| LabelValueOption
|
||||||
|
| ValueDescriptionOption;
|
||||||
|
|
||||||
|
export type { NameIdOption, LabelValueOption, ValueDescriptionOption };
|
||||||
|
|
||||||
|
function getOptionText(option: DropdownOption): string {
|
||||||
|
if (typeof option === 'string') return option;
|
||||||
|
if ('name' in option) return option.name;
|
||||||
|
if ('label' in option) return option.label;
|
||||||
|
if ('description' in option)
|
||||||
|
return option.value < 1e9
|
||||||
|
? `${option.value} (${option.description})`
|
||||||
|
: option.description;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionMatches(
|
||||||
|
option: DropdownOption,
|
||||||
|
selected: DropdownOption | null,
|
||||||
|
): boolean {
|
||||||
|
if (!selected) return false;
|
||||||
|
if (typeof selected === 'string' && typeof option === 'string')
|
||||||
|
return selected === option;
|
||||||
|
if (typeof selected === 'string') return getOptionText(option) === selected;
|
||||||
|
if (typeof option === 'string') return getOptionText(selected) === option;
|
||||||
|
const a = option as Record<string, unknown>;
|
||||||
|
const b = selected as Record<string, unknown>;
|
||||||
|
if ('name' in a && 'name' in b) return a.name === b.name;
|
||||||
|
if ('label' in a && 'label' in b) return a.label === b.label;
|
||||||
|
if ('value' in a && 'value' in b) return a.value === b.value;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropdownProps<T extends DropdownOption = DropdownOption> {
|
||||||
|
options: T[];
|
||||||
|
selectedValue: DropdownOption | null;
|
||||||
|
onSelect: (value: T) => void;
|
||||||
|
size?: string;
|
||||||
|
rounded?: 'xl' | '3xl';
|
||||||
|
searchable?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
contentSize?: string;
|
||||||
|
showEdit?: boolean;
|
||||||
|
onEdit?: (value: NameIdOption) => void;
|
||||||
|
showDelete?: boolean | ((option: T) => boolean);
|
||||||
|
onDelete?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function Dropdown<T extends DropdownOption>({
|
function Dropdown<T extends DropdownOption>({
|
||||||
options,
|
options,
|
||||||
selectedValue,
|
selectedValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
size = 'w-32',
|
size = 'w-full',
|
||||||
rounded = 'xl',
|
rounded = '3xl',
|
||||||
buttonClassName = 'border-silver bg-white dark:bg-transparent dark:border-dim-gray',
|
searchable = false,
|
||||||
optionsClassName = 'border-silver bg-white dark:border-dim-gray dark:bg-dark-charcoal',
|
placeholder = 'Select...',
|
||||||
border = 'border-2',
|
contentSize = 'text-sm',
|
||||||
showEdit,
|
showEdit,
|
||||||
onEdit,
|
onEdit,
|
||||||
showDelete,
|
showDelete,
|
||||||
onDelete,
|
onDelete,
|
||||||
placeholder,
|
|
||||||
placeholderClassName = 'text-gray-500 dark:text-gray-400',
|
|
||||||
contentSize = 'text-base',
|
|
||||||
}: DropdownProps<T>) {
|
}: DropdownProps<T>) {
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
|
const [open, setOpen] = useState(false);
|
||||||
const borderTopRadius = rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl';
|
const [query, setQuery] = useState('');
|
||||||
|
const [dropUp, setDropUp] = useState(false);
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const radius = rounded === '3xl' ? 'rounded-3xl' : 'rounded-xl';
|
||||||
if (
|
const radiusTop = rounded === '3xl' ? 'rounded-t-3xl' : 'rounded-t-xl';
|
||||||
dropdownRef.current &&
|
const radiusBottom = rounded === '3xl' ? 'rounded-b-3xl' : 'rounded-b-xl';
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
const handler = (e: MouseEvent) => {
|
||||||
return () => {
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
setOpen(false);
|
||||||
|
setQuery('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
document.addEventListener('mousedown', handler, true);
|
||||||
|
return () => document.removeEventListener('mousedown', handler, true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && searchable && searchRef.current) searchRef.current.focus();
|
||||||
|
}, [open, searchable]);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (!open && ref.current) {
|
||||||
|
const rect = ref.current.getBoundingClientRect();
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
|
setDropUp(spaceBelow < 220);
|
||||||
|
}
|
||||||
|
setOpen((v) => !v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!searchable || !query.trim()) return options;
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return options.filter((o) => getOptionText(o).toLowerCase().includes(q));
|
||||||
|
}, [options, query, searchable]);
|
||||||
|
|
||||||
|
const displayValue = selectedValue ? getOptionText(selectedValue) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`relative ${size}`} ref={ref}>
|
||||||
className={[
|
|
||||||
typeof selectedValue === 'string'
|
|
||||||
? 'relative'
|
|
||||||
: 'relative align-middle',
|
|
||||||
size,
|
|
||||||
].join(' ')}
|
|
||||||
ref={dropdownRef}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
type="button"
|
||||||
className={`flex w-full cursor-pointer items-center justify-between ${border} ${buttonClassName} px-5 py-3 ${
|
onClick={handleToggle}
|
||||||
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
className={`border-border bg-card text-foreground flex w-full cursor-pointer items-center justify-between border px-5 py-3 ${open ? (dropUp ? radiusBottom : radiusTop) : radius}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{typeof selectedValue === 'string' ? (
|
<span
|
||||||
<span className={`dark:text-bright-gray truncate ${contentSize}`}>
|
className={`truncate ${contentSize} ${displayValue ? '' : 'text-muted-foreground'}`}
|
||||||
{selectedValue}
|
>
|
||||||
</span>
|
{displayValue ?? placeholder}
|
||||||
) : (
|
</span>
|
||||||
<span
|
<ChevronDown
|
||||||
className={`truncate ${selectedValue && `dark:text-bright-gray`} ${
|
className={`text-muted-foreground ml-2 h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
!selectedValue && ` ${placeholderClassName}`
|
|
||||||
} ${contentSize}`}
|
|
||||||
>
|
|
||||||
{selectedValue && 'label' in selectedValue
|
|
||||||
? selectedValue.label
|
|
||||||
: selectedValue && 'description' in selectedValue
|
|
||||||
? `${
|
|
||||||
selectedValue.value < 1e9
|
|
||||||
? selectedValue.value + ` (${selectedValue.description})`
|
|
||||||
: selectedValue.description
|
|
||||||
}`
|
|
||||||
: placeholder
|
|
||||||
? placeholder
|
|
||||||
: 'From URL'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<img
|
|
||||||
src={Arrow2}
|
|
||||||
alt="arrow"
|
|
||||||
className={`transform ${
|
|
||||||
isOpen ? 'rotate-180' : 'rotate-0'
|
|
||||||
} h-3 w-3 transition-transform`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
|
||||||
|
{open && (
|
||||||
<div
|
<div
|
||||||
className={`absolute right-0 left-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} ${optionsClassName} shadow-lg`}
|
className={`border-border bg-card absolute inset-x-0 z-20 overflow-hidden border shadow-lg ${
|
||||||
|
dropUp
|
||||||
|
? `bottom-full -mt-px border-b-0 ${radiusTop}`
|
||||||
|
: `-mt-px border-t-0 ${radiusBottom}`
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{options.map((option: any, index) => (
|
{searchable && (
|
||||||
<div
|
<div className="flex items-center px-3 py-2">
|
||||||
key={index}
|
<Search className="text-muted-foreground mr-2 h-4 w-4 shrink-0" />
|
||||||
className="hover:eerie-black flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:hover:bg-[#545561]"
|
<input
|
||||||
>
|
ref={searchRef}
|
||||||
<span
|
type="text"
|
||||||
onClick={() => {
|
value={query}
|
||||||
onSelect(option);
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
setIsOpen(false);
|
placeholder="Search..."
|
||||||
}}
|
className="text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm focus:outline-none"
|
||||||
className={`dark:text-light-gray ml-5 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap ${contentSize}`}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
/>
|
||||||
{typeof option === 'string'
|
|
||||||
? option
|
|
||||||
: option.name
|
|
||||||
? option.name
|
|
||||||
: option.label
|
|
||||||
? option.label
|
|
||||||
: `${
|
|
||||||
option.value < 1e9
|
|
||||||
? option.value + ` (${option.description})`
|
|
||||||
: option.description
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
{showEdit && onEdit && option.type !== 'public' && (
|
|
||||||
<img
|
|
||||||
src={Edit}
|
|
||||||
alt="Edit"
|
|
||||||
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
|
|
||||||
onClick={() => {
|
|
||||||
onEdit({
|
|
||||||
id: option.id,
|
|
||||||
name: option.name,
|
|
||||||
type: option.type,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showDelete && onDelete && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete?.(typeof option === 'string' ? option : option.id);
|
|
||||||
}}
|
|
||||||
className={`${
|
|
||||||
typeof showDelete === 'function' && !showDelete(option)
|
|
||||||
? 'hidden'
|
|
||||||
: ''
|
|
||||||
} mr-2 h-4 w-4 cursor-pointer hover:opacity-50`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Trash}
|
|
||||||
alt="Delete"
|
|
||||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
|
||||||
option.type === 'public'
|
|
||||||
? 'cursor-not-allowed opacity-50'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
<div className="scrollbar-thin border-border max-h-48 overflow-y-auto border-t">
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground px-4 py-3 text-center text-sm">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filtered.map((option, i) => {
|
||||||
|
const active = optionMatches(option, selectedValue);
|
||||||
|
const optObj =
|
||||||
|
typeof option !== 'string'
|
||||||
|
? (option as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`hover:bg-accent flex cursor-pointer items-center justify-between ${active ? 'bg-accent font-medium' : ''}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(option);
|
||||||
|
setOpen(false);
|
||||||
|
setQuery('');
|
||||||
|
}}
|
||||||
|
className={`text-foreground flex-1 truncate px-4 py-2.5 ${contentSize}`}
|
||||||
|
>
|
||||||
|
{getOptionText(option)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{active && !showEdit && !showDelete && (
|
||||||
|
<Check className="text-primary mr-3 h-4 w-4 shrink-0" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showEdit &&
|
||||||
|
onEdit &&
|
||||||
|
optObj &&
|
||||||
|
optObj.type !== 'public' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onEdit({
|
||||||
|
id: optObj.id as string,
|
||||||
|
name: optObj.name as string,
|
||||||
|
type: optObj.type as string,
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
setQuery('');
|
||||||
|
}}
|
||||||
|
className="hover:bg-accent mr-1 rounded p-1"
|
||||||
|
>
|
||||||
|
<Pencil className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDelete && onDelete && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(
|
||||||
|
typeof option === 'string'
|
||||||
|
? option
|
||||||
|
: ((optObj?.id as string) ?? ''),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={`hover:bg-accent mr-1 rounded p-1 ${
|
||||||
|
typeof showDelete === 'function' &&
|
||||||
|
!showDelete(option)
|
||||||
|
? 'hidden'
|
||||||
|
: ''
|
||||||
|
} ${optObj?.type === 'public' ? 'pointer-events-none opacity-30' : ''}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function DropdownMenu({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`ring-opacity-5 dark:bg-dark-charcoal w-28 transform rounded-md bg-white shadow-lg ring-1 ring-black transition-all duration-200 ease-in-out ${className}`}
|
className={`border-border bg-card w-28 transform rounded-md border shadow-lg transition-all duration-200 ease-in-out ${className}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
@@ -99,10 +99,8 @@ export default function DropdownMenu({
|
|||||||
{options.map((option, idx) => (
|
{options.map((option, idx) => (
|
||||||
<div
|
<div
|
||||||
id={`option-${idx}`}
|
id={`option-${idx}`}
|
||||||
className={`dark:text-light-gray dark:hover:bg-purple-taupe cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 ${
|
className={`dark:text-foreground hover:bg-muted cursor-pointer px-4 py-2 text-xs ${
|
||||||
selectedOption.value === option.value
|
selectedOption.value === option.value ? 'bg-muted' : 'bg-card'
|
||||||
? 'dark:bg-purple-taupe bg-gray-100'
|
|
||||||
: 'dark:bg-dark-charcoal bg-white'
|
|
||||||
}`}
|
}`}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
|||||||
@@ -80,15 +80,15 @@ export default function DropdownModel() {
|
|||||||
return (
|
return (
|
||||||
<div ref={dropdownRef}>
|
<div ref={dropdownRef}>
|
||||||
<div
|
<div
|
||||||
className={`bg-gray-1000 dark:bg-dark-charcoal mx-auto flex w-full cursor-pointer justify-between p-1 dark:text-white ${isOpen ? 'rounded-t-3xl' : 'rounded-3xl'}`}
|
className={`text-foreground bg-muted dark:bg-card mx-auto flex w-full cursor-pointer items-center justify-between px-3 py-4 ${isOpen ? 'rounded-t-4xl' : 'rounded-4xl'}`}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
{selectedModel?.display_name ? (
|
{selectedModel?.display_name ? (
|
||||||
<p className="mx-4 my-3 truncate overflow-hidden whitespace-nowrap">
|
<p className="ml-3 truncate overflow-hidden whitespace-nowrap">
|
||||||
{selectedModel.display_name}
|
{selectedModel.display_name}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mx-4 my-3 truncate overflow-hidden whitespace-nowrap">
|
<p className="text-muted-foreground truncate overflow-hidden whitespace-nowrap">
|
||||||
Select Model
|
Select Model
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -101,7 +101,7 @@ export default function DropdownModel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="no-scrollbar dark:bg-dark-charcoal absolute right-0 left-0 z-20 -mt-1 max-h-52 w-full overflow-y-auto rounded-b-3xl bg-white shadow-md">
|
<div className="no-scrollbar bg-muted dark:bg-card absolute right-0 left-0 z-20 -mt-1 max-h-52 w-full overflow-y-auto rounded-b-3xl shadow-md">
|
||||||
{availableModels && (availableModels?.length ?? 0) > 0 ? (
|
{availableModels && (availableModels?.length ?? 0) > 0 ? (
|
||||||
availableModels.map((model: Model) => (
|
availableModels.map((model: Model) => (
|
||||||
<div
|
<div
|
||||||
@@ -110,7 +110,7 @@ export default function DropdownModel() {
|
|||||||
dispatch(setSelectedModel(model));
|
dispatch(setSelectedModel(model));
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`border-gray-3000/75 dark:border-purple-taupe/50 hover:bg-gray-3000/75 dark:hover:bg-purple-taupe flex h-10 w-full cursor-pointer items-center justify-between border-t`}
|
className={`border-border/30 hover:bg-accent flex h-10 w-full cursor-pointer items-center justify-between border-t`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<p className="flex-1 truncate py-3 pr-2 pl-5">
|
<p className="flex-1 truncate py-3 pr-2 pl-5">
|
||||||
@@ -127,8 +127,10 @@ export default function DropdownModel() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="h-10 w-full border-x-2 border-b-2">
|
<div className="border-border/30 flex h-10 w-full items-center border-t">
|
||||||
<p className="ml-5 py-3 text-gray-500">No models available</p>
|
<p className="text-muted-foreground pl-5 text-sm">
|
||||||
|
No models available
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [userEmail, setUserEmail] = useState<string>('');
|
const [userEmail, setUserEmail] = useState<string>('');
|
||||||
const [allowsSharedContent, setAllowsSharedContent] = useState(false);
|
const [allowsSharedContent, setAllowsSharedContent] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<'my_files' | 'shared'>(
|
const [activeTab, setActiveTab] = useState<'my_files' | 'shared'>('my_files');
|
||||||
'my_files',
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -214,9 +212,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setAuthError('');
|
setAuthError('');
|
||||||
if (provider === 'share_point') {
|
if (provider === 'share_point') {
|
||||||
setAllowsSharedContent(
|
setAllowsSharedContent(validateData.allows_shared_content ?? false);
|
||||||
validateData.allows_shared_content ?? false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
@@ -369,9 +365,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
{
|
{
|
||||||
id: null,
|
id: null,
|
||||||
name:
|
name:
|
||||||
tab === 'shared'
|
tab === 'shared' ? 'Shared' : getProviderConfig(provider).rootName,
|
||||||
? 'Shared'
|
|
||||||
: getProviderConfig(provider).rootName,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const sessionToken = getSessionToken(provider);
|
const sessionToken = getSessionToken(provider);
|
||||||
@@ -380,8 +374,6 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleFileSelect = (fileId: string, isFolder: boolean) => {
|
const handleFileSelect = (fileId: string, isFolder: boolean) => {
|
||||||
if (isFolder) {
|
if (isFolder) {
|
||||||
const newSelectedFolders = selectedFolders.includes(fileId)
|
const newSelectedFolders = selectedFolders.includes(fileId)
|
||||||
@@ -459,10 +451,10 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{isConnected && (
|
{isConnected && (
|
||||||
<div className="mt-3 overflow-hidden rounded-lg border border-[#D7D7D7] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border mt-3 overflow-hidden rounded-lg border">
|
||||||
<div className="rounded-t-lg border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border rounded-t-lg">
|
||||||
{provider === 'share_point' && allowsSharedContent && (
|
{provider === 'share_point' && allowsSharedContent && (
|
||||||
<div className="flex border-b border-[#D7D7D7] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border flex border-b">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTabChange('my_files')}
|
onClick={() => handleTabChange('my_files')}
|
||||||
className={`px-4 py-2 text-sm font-medium ${
|
className={`px-4 py-2 text-sm font-medium ${
|
||||||
@@ -485,7 +477,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="rounded-t-lg bg-[#EEE6FF78] px-4 pt-4 dark:bg-[#2A262E]">
|
<div className="dark:bg-muted rounded-t-lg bg-[#EEE6FF78] px-4 pt-4">
|
||||||
<div className="mb-2 flex items-center gap-1">
|
<div className="mb-2 flex items-center gap-1">
|
||||||
{folderPath.map((path, index) => (
|
{folderPath.map((path, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -516,7 +508,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
colorVariant="silver"
|
colorVariant="silver"
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
labelBgClassName="bg-[#EEE6FF78] dark:bg-[#2A262E]"
|
labelBgClassName="bg-[#EEE6FF78] dark:bg-muted"
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<img src={SearchIcon} alt="Search" width={16} height={16} />
|
<img src={SearchIcon} alt="Search" width={16} height={16} />
|
||||||
}
|
}
|
||||||
@@ -531,7 +523,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-72 border-t border-[#D7D7D7] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border h-72 border-t">
|
||||||
<TableContainer
|
<TableContainer
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
height="288px"
|
height="288px"
|
||||||
@@ -586,7 +578,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
>
|
>
|
||||||
<TableCell width="40px" align="center">
|
<TableCell width="40px" align="center">
|
||||||
<div
|
<div
|
||||||
className="mx-auto flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center border border-[#EEE6FF78] p-[0.5px] text-sm dark:border-[#6A6A6A]"
|
className="border-border dark:border-border mx-auto flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center border p-[0.5px] text-sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleFileSelect(file.id, isFolder(file));
|
handleFileSelect(file.id, isFolder(file));
|
||||||
@@ -615,7 +607,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="truncate">{file.name}</span>
|
<span className="truncate">
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
@@ -626,7 +620,8 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{isLoading && files.length > 0 &&
|
{isLoading &&
|
||||||
|
files.length > 0 &&
|
||||||
Array.from({ length: 3 }).map((_, i) => (
|
Array.from({ length: 3 }).map((_, i) => (
|
||||||
<TableRow key={`load-more-skeleton-${i}`}>
|
<TableRow key={`load-more-skeleton-${i}`}>
|
||||||
<TableCell width="40px" align="center">
|
<TableCell width="40px" align="center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const FilesSectionSkeleton = () => (
|
const FilesSectionSkeleton = () => (
|
||||||
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border rounded-lg border">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
{/* Left side with path navigation */}
|
{/* Left side with path navigation */}
|
||||||
<div className="flex w-full items-center sm:w-auto">
|
<div className="flex w-full items-center sm:w-auto">
|
||||||
<button
|
<button
|
||||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 dark:border-0 dark:text-gray-500"
|
||||||
onClick={handleBackNavigation}
|
onClick={handleBackNavigation}
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||||
@@ -503,7 +503,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
{!processingRef.current && (
|
{!processingRef.current && (
|
||||||
<button
|
<button
|
||||||
onClick={handleAddFile}
|
onClick={handleAddFile}
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap text-white"
|
className="bg-primary hover:bg-primary/90 flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap text-white"
|
||||||
title={t('settings.sources.addFile')}
|
title={t('settings.sources.addFile')}
|
||||||
>
|
>
|
||||||
{t('settings.sources.addFile')}
|
{t('settings.sources.addFile')}
|
||||||
@@ -599,7 +599,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
<div ref={menuRef} className="relative">
|
<div ref={menuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleMenuClick(e, itemId)}
|
onClick={(e) => handleMenuClick(e, itemId)}
|
||||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
className="dark:hover:bg-muted inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB]"
|
||||||
aria-label={t('settings.sources.menuAlt')}
|
aria-label={t('settings.sources.menuAlt')}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -656,7 +656,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
<div ref={menuRef} className="relative">
|
<div ref={menuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleMenuClick(e, itemId)}
|
onClick={(e) => handleMenuClick(e, itemId)}
|
||||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
className="dark:hover:bg-muted inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB]"
|
||||||
aria-label={t('settings.sources.menuAlt')}
|
aria-label={t('settings.sources.menuAlt')}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -754,11 +754,11 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={t('settings.sources.searchFiles')}
|
placeholder={t('settings.sources.searchFiles')}
|
||||||
className={`h-[38px] w-full border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none dark:text-[#E0E0E0]`}
|
className={`border-border dark:border-border h-[38px] w-full border px-4 py-2 ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<div className="absolute top-full right-0 left-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[12px] border border-t-0 border-[#D1D9E0] bg-white shadow-lg transition-all duration-200 dark:border-[#6A6A6A] dark:bg-[#1F2023]">
|
<div className="border-border bg-card dark:border-border dark:bg-card absolute top-full right-0 left-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[12px] border border-t-0 shadow-lg transition-all duration-200">
|
||||||
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto overscroll-contain">
|
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto overscroll-contain">
|
||||||
{searchResults.length === 0 ? (
|
{searchResults.length === 0 ? (
|
||||||
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
@@ -770,9 +770,9 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
onClick={() => handleSearchSelect(result)}
|
onClick={() => handleSearchSelect(result)}
|
||||||
title={result.path}
|
title={result.path}
|
||||||
className={`flex min-w-0 cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${
|
className={`hover:bg-muted dark:hover:bg-muted flex min-w-0 cursor-pointer items-center px-3 py-2 ${
|
||||||
index !== searchResults.length - 1
|
index !== searchResults.length - 1
|
||||||
? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]'
|
? 'border-border dark:border-border border-b'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -785,7 +785,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
}
|
}
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="flex-1 truncate text-sm dark:text-[#E0E0E0]">
|
<span className="flex-1 truncate text-sm">
|
||||||
{result.name}
|
{result.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const FileUpload = ({
|
|||||||
showPreview = false,
|
showPreview = false,
|
||||||
previewSize = 80,
|
previewSize = 80,
|
||||||
children,
|
children,
|
||||||
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-border dark:border-border',
|
||||||
activeClassName = 'border-blue-500 bg-blue-50',
|
activeClassName = 'border-blue-500 bg-blue-50',
|
||||||
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
|
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
|
||||||
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
|
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
|
||||||
@@ -133,7 +133,7 @@ export const FileUpload = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentClassName = twMerge(
|
const currentClassName = twMerge(
|
||||||
'border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
'border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-colors border-border dark:border-border',
|
||||||
className,
|
className,
|
||||||
isDragActive && activeClassName,
|
isDragActive && activeClassName,
|
||||||
isDragAccept && acceptClassName,
|
isDragAccept && acceptClassName,
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{isConnected && (
|
{isConnected && (
|
||||||
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
<div className="border-border dark:border-border rounded-lg border">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-medium">
|
<h3 className="text-sm font-medium">
|
||||||
|
|||||||
@@ -36,20 +36,20 @@ const Help = () => {
|
|||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
className="mx-4 my-auto flex h-9 w-full items-center gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E]"
|
className="hover:bg-sidebar-accent mx-4 my-auto flex h-9 w-full items-center gap-4 rounded-3xl"
|
||||||
>
|
>
|
||||||
<img src={Info} alt="info" className="ml-2 w-5 filter dark:invert" />
|
<img src={Info} alt="info" className="ml-2 w-5 filter dark:invert" />
|
||||||
{t('help')}
|
{t('help')}
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className={`dark:bg-outer-space absolute z-10 w-48 translate-x-4 -translate-y-28 rounded-xl bg-white shadow-lg`}
|
className={`dark:bg-card bg-card absolute z-10 w-48 translate-x-4 -translate-y-28 rounded-xl shadow-lg`}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://docs.docsgpt.cloud/"
|
href="https://docs.docsgpt.cloud/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hover:bg-bright-gray flex items-start gap-4 rounded-t-xl px-4 py-2 text-black dark:text-white dark:hover:bg-[#545561]"
|
className="hover:bg-muted text-foreground flex items-start gap-4 rounded-t-xl px-4 py-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={PageIcon}
|
src={PageIcon}
|
||||||
@@ -61,7 +61,7 @@ const Help = () => {
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="mailto:support@docsgpt.cloud"
|
href="mailto:support@docsgpt.cloud"
|
||||||
className="hover:bg-bright-gray flex items-start gap-4 rounded-b-xl px-4 py-2 text-black dark:text-white dark:hover:bg-[#545561]"
|
className="hover:bg-muted text-foreground flex items-start gap-4 rounded-b-xl px-4 py-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={EmailIcon}
|
src={EmailIcon}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const Input = ({
|
|||||||
borderVariant = 'thick',
|
borderVariant = 'thick',
|
||||||
textSize = 'medium',
|
textSize = 'medium',
|
||||||
children,
|
children,
|
||||||
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
labelBgClassName = 'bg-card',
|
||||||
leftIcon,
|
leftIcon,
|
||||||
onChange,
|
onChange,
|
||||||
onPaste,
|
onPaste,
|
||||||
@@ -23,9 +23,9 @@ const Input = ({
|
|||||||
edgeRoundness = 'rounded-full',
|
edgeRoundness = 'rounded-full',
|
||||||
}: InputProps) => {
|
}: InputProps) => {
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
silver: 'border-silver dark:border-silver/40',
|
silver: 'border-border dark:border-border',
|
||||||
jet: 'border-jet',
|
jet: 'border-jet',
|
||||||
gray: 'border-gray-5000 dark:text-silver',
|
gray: 'border-gray-5000 dark:text-muted-foreground',
|
||||||
};
|
};
|
||||||
const borderStyles = {
|
const borderStyles = {
|
||||||
thin: 'border',
|
thin: 'border',
|
||||||
@@ -44,7 +44,7 @@ const Input = ({
|
|||||||
<div className={`relative ${className}`}>
|
<div className={`relative ${className}`}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={`peer text-jet dark:text-bright-gray h-[42px] w-full ${edgeRoundness} bg-transparent ${leftIcon ? 'pl-10' : 'px-3'} py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
className={`peer text-foreground dark:text-foreground h-[42px] w-full ${edgeRoundness} bg-transparent ${leftIcon ? 'pl-10' : 'px-3'} py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||||
type={type}
|
type={type}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
@@ -75,11 +75,11 @@ const Input = ({
|
|||||||
: 'peer-placeholder-shown:left-3'
|
: 'peer-placeholder-shown:left-3'
|
||||||
} peer-placeholder-shown:${
|
} peer-placeholder-shown:${
|
||||||
textSizeStyles[textSize]
|
textSizeStyles[textSize]
|
||||||
} text-gray-4000 pointer-events-none cursor-none peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:text-gray-400 ${labelBgClassName} max-w-[calc(100%-24px)] overflow-hidden text-ellipsis whitespace-nowrap`}
|
} text-muted-foreground pointer-events-none cursor-none peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs ${labelBgClassName} max-w-[calc(100%-24px)] overflow-hidden text-ellipsis whitespace-nowrap`}
|
||||||
>
|
>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{required && (
|
{required && (
|
||||||
<span className="ml-0.5 text-[#D30000] dark:text-[#D42626]">*</span>
|
<span className="ml-0.5 text-[#D30000] dark:text-red-500">*</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import mermaid from 'mermaid';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import mermaid from 'mermaid';
|
import { useSelector } from 'react-redux';
|
||||||
import CopyButton from './CopyButton';
|
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import {
|
import {
|
||||||
oneLight,
|
oneLight,
|
||||||
vscDarkPlus,
|
vscDarkPlus,
|
||||||
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||||
import { MermaidRendererProps } from './types';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { selectStatus } from '../conversation/conversationSlice';
|
import { selectStatus } from '../conversation/conversationSlice';
|
||||||
import { useDarkTheme } from '../hooks';
|
import { useDarkTheme } from '../hooks';
|
||||||
|
import CopyButton from './CopyButton';
|
||||||
|
import { MermaidRendererProps } from './types';
|
||||||
|
|
||||||
const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||||
code,
|
code,
|
||||||
@@ -262,9 +263,9 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
const errorRender = !isCurrentlyLoading && error;
|
const errorRender = !isCurrentlyLoading && error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-inherit group border-light-silver dark:border-raisin-black dark:bg-eerie-black relative rounded-lg border bg-white">
|
<div className="w-inherit group border-border bg-card relative rounded-lg border">
|
||||||
<div className="bg-platinum dark:bg-eerie-black-2 flex items-center justify-between px-2 py-1">
|
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||||
mermaid
|
mermaid
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -280,7 +281,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
Download <span className="ml-1">▼</span>
|
Download <span className="ml-1">▼</span>
|
||||||
</button>
|
</button>
|
||||||
{showDownloadMenu && (
|
{showDownloadMenu && (
|
||||||
<div className="absolute right-0 z-10 mt-1 w-40 rounded-sm border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
|
<div className="border-border bg-card absolute right-0 z-10 mt-1 w-40 rounded-sm border shadow-lg">
|
||||||
<ul>
|
<ul>
|
||||||
{downloadOptions.map((option, index) => (
|
{downloadOptions.map((option, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
@@ -289,7 +290,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
option.action();
|
option.action();
|
||||||
setShowDownloadMenu(false);
|
setShowDownloadMenu(false);
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-2 text-left text-xs hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="hover:bg-muted w-full px-4 py-2 text-left text-xs"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -318,14 +319,14 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCurrentlyLoading ? (
|
{isCurrentlyLoading ? (
|
||||||
<div className="dark:bg-eerie-black flex items-center justify-center bg-white p-4">
|
<div className="bg-card flex items-center justify-center p-4">
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Loading diagram...
|
Loading diagram...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : errorRender ? (
|
) : errorRender ? (
|
||||||
<div className="m-2 rounded-sm border-2 border-red-400 dark:border-red-700">
|
<div className="m-2 rounded-sm border-2 border-red-400 dark:border-red-700">
|
||||||
<div className="overflow-auto bg-red-100 px-4 py-2 text-sm break-words whitespace-normal text-red-800 dark:bg-red-900/30 dark:text-red-300">
|
<div className="overflow-auto bg-red-100 px-4 py-2 text-sm wrap-break-word whitespace-normal text-red-800 dark:bg-red-900/30 dark:text-red-300">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +334,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="no-scrollbar dark:bg-eerie-black relative block w-full bg-white p-4"
|
className="no-scrollbar bg-card relative block w-full p-4"
|
||||||
style={{
|
style={{
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
@@ -399,9 +400,9 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCode && (
|
{showCode && (
|
||||||
<div className="border-light-silver dark:border-raisin-black border-t">
|
<div className="border-border border-t">
|
||||||
<div className="bg-platinum dark:bg-eerie-black-2 p-2">
|
<div className="bg-platinum p-2">
|
||||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||||
Mermaid Code
|
Mermaid Code
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1438,7 +1438,7 @@ export default function MessageInput({
|
|||||||
onChange={handleVoiceFileAttachment}
|
onChange={handleVoiceFileAttachment}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="border-dark-gray bg-lotion dark:border-grey relative flex w-full flex-col rounded-[23px] border dark:bg-transparent">
|
<div className="border-border bg-card 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">
|
<div className="flex flex-wrap gap-1.5 px-2 py-2 sm:gap-2 sm:px-3">
|
||||||
{attachments.map((attachment) => {
|
{attachments.map((attachment) => {
|
||||||
return (
|
return (
|
||||||
@@ -1448,7 +1448,7 @@ export default function MessageInput({
|
|||||||
onDragStart={(e) => handleDragStart(e, attachment.id)}
|
onDragStart={(e) => handleDragStart(e, attachment.id)}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDropOn(e, attachment.id)}
|
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] ${
|
className={`group dark:text-foreground bg-muted text-muted-foreground dark:bg-accent relative flex items-center rounded-xl px-2 py-1 text-[12px] sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||||
attachment.status !== 'completed'
|
attachment.status !== 'completed'
|
||||||
? 'opacity-70'
|
? 'opacity-70'
|
||||||
: 'opacity-100'
|
: 'opacity-100'
|
||||||
@@ -1459,7 +1459,7 @@ export default function MessageInput({
|
|||||||
}`}
|
}`}
|
||||||
title={attachment.fileName}
|
title={attachment.fileName}
|
||||||
>
|
>
|
||||||
<div className="bg-purple-30 mr-2 flex h-8 w-8 items-center justify-center rounded-md p-1">
|
<div className="bg-primary mr-2 flex h-8 w-8 items-center justify-center rounded-md p-1">
|
||||||
{attachment.status === 'completed' && (
|
{attachment.status === 'completed' && (
|
||||||
<img
|
<img
|
||||||
src={DocumentationDark}
|
src={DocumentationDark}
|
||||||
@@ -1551,7 +1551,7 @@ export default function MessageInput({
|
|||||||
}
|
}
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
placeholder={t('inputPlaceholder')}
|
placeholder={t('inputPlaceholder')}
|
||||||
className="inputbox-style no-scrollbar bg-lotion dark:text-bright-gray dark:placeholder:text-bright-gray/50 w-full overflow-x-hidden overflow-y-auto rounded-t-[23px] px-2 text-base leading-tight whitespace-pre-wrap opacity-100 placeholder:text-gray-500 focus:outline-hidden sm:px-3 dark:bg-transparent"
|
className="inputbox-style no-scrollbar dark:text-foreground dark:placeholder:text-muted-foreground/50 w-full overflow-x-hidden overflow-y-auto rounded-t-[23px] bg-transparent px-2 text-base leading-tight whitespace-pre-wrap opacity-100 placeholder:text-gray-500 focus:outline-hidden sm:px-3"
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
@@ -1564,7 +1564,7 @@ export default function MessageInput({
|
|||||||
{showSourceButton && (
|
{showSourceButton && (
|
||||||
<button
|
<button
|
||||||
ref={sourceButtonRef}
|
ref={sourceButtonRef}
|
||||||
className="xs:px-3 xs:py-1.5 dark:border-purple-taupe flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 sm:max-w-[150px] dark:hover:bg-[#2C2E3C]"
|
className="xs:px-3 xs:py-1.5 dark:border-border border-border hover:bg-accent dark:hover:bg-muted flex max-w-[130px] items-center rounded-[32px] border px-2 py-1 transition-colors sm:max-w-[150px]"
|
||||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||||
title={
|
title={
|
||||||
selectedDocs && selectedDocs.length > 0
|
selectedDocs && selectedDocs.length > 0
|
||||||
@@ -1577,7 +1577,7 @@ export default function MessageInput({
|
|||||||
alt="Sources"
|
alt="Sources"
|
||||||
className="mr-1 h-3.5 w-3.5 shrink-0 sm:mr-1.5 sm:h-4"
|
className="mr-1 h-3.5 w-3.5 shrink-0 sm:mr-1.5 sm:h-4"
|
||||||
/>
|
/>
|
||||||
<span className="xs:text-[12px] dark:text-bright-gray truncate overflow-hidden text-[10px] font-medium text-[#5D5D5D] sm:text-[14px]">
|
<span className="xs:text-[12px] dark:text-foreground text-muted-foreground truncate overflow-hidden text-[10px] font-medium sm:text-[14px]">
|
||||||
{selectedDocs && selectedDocs.length > 0
|
{selectedDocs && selectedDocs.length > 0
|
||||||
? selectedDocs.length === 1
|
? selectedDocs.length === 1
|
||||||
? selectedDocs[0].name
|
? selectedDocs[0].name
|
||||||
@@ -1595,7 +1595,7 @@ export default function MessageInput({
|
|||||||
{showToolButton && (
|
{showToolButton && (
|
||||||
<button
|
<button
|
||||||
ref={toolButtonRef}
|
ref={toolButtonRef}
|
||||||
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] dark:border-purple-taupe flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:hover:bg-[#2C2E3C]"
|
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] dark:border-border border-border hover:bg-muted dark:hover:bg-muted flex max-w-[130px] items-center rounded-[32px] border px-2 py-1 transition-colors"
|
||||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -1603,7 +1603,7 @@ export default function MessageInput({
|
|||||||
alt="Tools"
|
alt="Tools"
|
||||||
className="mr-1 h-3.5 w-3.5 shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
className="mr-1 h-3.5 w-3.5 shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||||
/>
|
/>
|
||||||
<span className="xs:text-[12px] dark:text-bright-gray truncate overflow-hidden text-[10px] font-medium text-[#5D5D5D] sm:text-[14px]">
|
<span className="xs:text-[12px] dark:text-foreground text-muted-foreground truncate overflow-hidden text-[10px] font-medium sm:text-[14px]">
|
||||||
{t('settings.tools.label')}
|
{t('settings.tools.label')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1617,10 +1617,10 @@ export default function MessageInput({
|
|||||||
aria-label={voiceButtonLabel}
|
aria-label={voiceButtonLabel}
|
||||||
title={voiceButtonLabel}
|
title={voiceButtonLabel}
|
||||||
disabled={loading || recordingState === 'transcribing'}
|
disabled={loading || recordingState === 'transcribing'}
|
||||||
className={`xs:px-3 xs:py-1.5 dark:border-purple-taupe flex items-center rounded-[32px] border px-2 py-1 transition-colors ${
|
className={`xs:px-3 xs:py-1.5 dark:border-border flex items-center rounded-[32px] border px-2 py-1 transition-colors ${
|
||||||
recordingState === 'recording'
|
recordingState === 'recording'
|
||||||
? 'border-[#B42318] bg-[#FEE4E2] text-[#B42318] dark:bg-[#4A2323]'
|
? 'border-[#B42318] bg-[#FEE4E2] text-[#B42318] dark:bg-[#4A2323]'
|
||||||
: 'border-[#AAAAAA] hover:bg-gray-100 dark:hover:bg-[#2C2E3C]'
|
: 'border-border dark:hover:bg-accent hover:bg-gray-100'
|
||||||
} ${
|
} ${
|
||||||
loading || recordingState === 'transcribing'
|
loading || recordingState === 'transcribing'
|
||||||
? 'cursor-not-allowed opacity-60'
|
? 'cursor-not-allowed opacity-60'
|
||||||
@@ -1635,23 +1635,23 @@ export default function MessageInput({
|
|||||||
<Mic className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4" />
|
<Mic className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`xs:text-[12px] dark:text-bright-gray text-[10px] font-medium sm:text-[14px] ${
|
className={`xs:text-[12px] dark:text-foreground text-[10px] font-medium sm:text-[14px] ${
|
||||||
recordingState === 'recording'
|
recordingState === 'recording'
|
||||||
? 'text-[#B42318]'
|
? 'text-[#B42318]'
|
||||||
: 'text-[#5D5D5D]'
|
: 'text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{voiceButtonText}
|
{voiceButtonText}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<label className="xs:px-3 xs:py-1.5 dark:border-purple-taupe flex cursor-pointer items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:hover:bg-[#2C2E3C]">
|
<label className="xs:px-3 xs:py-1.5 dark:border-border border-border hover:bg-muted dark:hover:bg-muted flex cursor-pointer items-center rounded-[32px] border px-2 py-1 transition-colors">
|
||||||
<img
|
<img
|
||||||
src={ClipIcon}
|
src={ClipIcon}
|
||||||
alt="Attach"
|
alt="Attach"
|
||||||
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
|
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||||
/>
|
/>
|
||||||
<span className="xs:text-[12px] dark:text-bright-gray text-[10px] font-medium text-[#5D5D5D] sm:text-[14px]">
|
<span className="xs:text-[12px] dark:text-foreground text-muted-foreground text-[10px] font-medium sm:text-[14px]">
|
||||||
{t('conversation.attachments.attach')}
|
{t('conversation.attachments.attach')}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -1669,7 +1669,7 @@ export default function MessageInput({
|
|||||||
<button
|
<button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
aria-label={t('cancel')}
|
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`}
|
className={`bg-primary ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-white sm:h-9 sm:w-9`}
|
||||||
disabled={!loading}
|
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" />
|
<div className="flex h-3 w-3 items-center justify-center rounded-[3px] bg-white sm:h-3.5 sm:w-3.5" />
|
||||||
@@ -1683,8 +1683,8 @@ export default function MessageInput({
|
|||||||
!loading &&
|
!loading &&
|
||||||
recordingState !== 'recording' &&
|
recordingState !== 'recording' &&
|
||||||
recordingState !== 'transcribing'
|
recordingState !== 'transcribing'
|
||||||
? 'bg-purple-30 text-white'
|
? 'bg-primary text-white'
|
||||||
: 'bg-[#EDEDED] text-[#959595] dark:bg-[#37383D] dark:text-[#77787D]'
|
: 'bg-muted text-muted-foreground dark:bg-accent dark:text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
disabled={
|
disabled={
|
||||||
!value.trim() ||
|
!value.trim() ||
|
||||||
@@ -1729,12 +1729,12 @@ export default function MessageInput({
|
|||||||
|
|
||||||
{handleDragActive &&
|
{handleDragActive &&
|
||||||
createPortal(
|
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">
|
<div className="dark:bg-background/85 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} />
|
<img className="filter dark:invert" src={DragFileUpload} />
|
||||||
<span className="text-outer-space dark:text-silver px-2 text-2xl font-bold">
|
<span className="text-muted-foreground dark:text-muted-foreground px-2 text-2xl font-bold">
|
||||||
{t('modals.uploadDoc.drag.title')}
|
{t('modals.uploadDoc.drag.title')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-s text-outer-space dark:text-silver w-48 p-2 text-center">
|
<span className="text-s text-muted-foreground dark:text-muted-foreground w-48 p-2 text-center">
|
||||||
{t('modals.uploadDoc.drag.description')}
|
{t('modals.uploadDoc.drag.description')}
|
||||||
</span>
|
</span>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default function MultiSelectPopup({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
className="border-light-silver bg-lotion dark:border-dim-gray dark:bg-charleston-green-2 fixed z-9999 flex flex-col rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
className="border-border bg-background dark:border-border dark:bg-card fixed z-9999 flex flex-col rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
||||||
style={{
|
style={{
|
||||||
top: popupPosition.showAbove ? undefined : popupPosition.top,
|
top: popupPosition.showAbove ? undefined : popupPosition.top,
|
||||||
bottom: popupPosition.showAbove
|
bottom: popupPosition.showAbove
|
||||||
@@ -198,7 +198,7 @@ export default function MultiSelectPopup({
|
|||||||
searchPlaceholder ||
|
searchPlaceholder ||
|
||||||
t('settings.tools.searchPlaceholder', 'Search...')
|
t('settings.tools.searchPlaceholder', 'Search...')
|
||||||
}
|
}
|
||||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
labelBgClassName="bg-background dark:bg-card"
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
textSize="small"
|
textSize="small"
|
||||||
@@ -206,13 +206,13 @@ export default function MultiSelectPopup({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="dark:border-dim-gray mx-4 mb-4 grow overflow-auto rounded-md border border-[#D9D9D9]">
|
<div className="dark:border-border mx-4 mb-4 grow overflow-auto rounded-md border border-[#D9D9D9]">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex h-full items-center justify-center py-4">
|
<div className="flex h-full items-center justify-center py-4">
|
||||||
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full overflow-y-auto scrollbar-overlay">
|
<div className="scrollbar-overlay h-full overflow-y-auto">
|
||||||
{filteredOptions.length === 0 ? (
|
{filteredOptions.length === 0 ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
|
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
|
||||||
<img
|
<img
|
||||||
@@ -233,7 +233,7 @@ export default function MultiSelectPopup({
|
|||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option.id}
|
||||||
onClick={() => handleOptionClick(option.id)}
|
onClick={() => handleOptionClick(option.id)}
|
||||||
className="dark:border-dim-gray dark:hover:bg-charleston-green-3 flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0 hover:bg-gray-100"
|
className="dark:border-border dark:hover:bg-accent hover:bg-accent flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
>
|
>
|
||||||
@@ -248,7 +248,7 @@ export default function MultiSelectPopup({
|
|||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div
|
<div
|
||||||
className={`dark:bg-charleston-green-2 flex h-4 w-4 items-center justify-center rounded-xs border-2 border-[#C6C6C6] bg-white dark:border-[#757783]`}
|
className={`border-border bg-card flex h-4 w-4 items-center justify-center rounded-xs border-2`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
@@ -269,7 +269,7 @@ export default function MultiSelectPopup({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{footerContent && (
|
{footerContent && (
|
||||||
<div className="border-light-silver dark:border-dim-gray shrink-0 border-t p-4">
|
<div className="border-border dark:border-border shrink-0 border-t p-4">
|
||||||
{footerContent}
|
{footerContent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function Notification({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-white-3000 relative z-10 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
|
<p className="relative z-10 text-xs leading-6 font-semibold text-white xl:text-sm xl:leading-7">
|
||||||
{notificationText}
|
{notificationText}
|
||||||
</p>
|
</p>
|
||||||
<span className="relative z-10 flex items-center">
|
<span className="relative z-10 flex items-center">
|
||||||
@@ -132,4 +132,4 @@ export default function Notification({
|
|||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
|
||||||
import Edit from '../assets/edit.svg';
|
|
||||||
import Search from '../assets/search.svg';
|
|
||||||
import Trash from '../assets/trash.svg';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SearchableDropdown - A standalone dropdown component with built-in search functionality
|
|
||||||
*/
|
|
||||||
|
|
||||||
type SearchableDropdownOptionBase = {
|
|
||||||
id?: string;
|
|
||||||
type?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NameIdOption = { name: string; id: string } & SearchableDropdownOptionBase;
|
|
||||||
|
|
||||||
export type SearchableDropdownOption =
|
|
||||||
| string
|
|
||||||
| NameIdOption
|
|
||||||
| ({ label: string; value: string } & SearchableDropdownOptionBase)
|
|
||||||
| ({ value: number; description: string } & SearchableDropdownOptionBase);
|
|
||||||
|
|
||||||
export type SearchableDropdownSelectedValue = SearchableDropdownOption | null;
|
|
||||||
|
|
||||||
export interface SearchableDropdownProps<
|
|
||||||
T extends SearchableDropdownOption = SearchableDropdownOption,
|
|
||||||
> {
|
|
||||||
options: T[];
|
|
||||||
selectedValue: SearchableDropdownSelectedValue;
|
|
||||||
onSelect: (value: T) => void;
|
|
||||||
size?: string;
|
|
||||||
/** Controls border radius for both button and dropdown menu */
|
|
||||||
rounded?: 'xl' | '3xl';
|
|
||||||
border?: 'border' | 'border-2';
|
|
||||||
showEdit?: boolean;
|
|
||||||
onEdit?: (value: NameIdOption) => void;
|
|
||||||
showDelete?: boolean | ((option: T) => boolean);
|
|
||||||
onDelete?: (id: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchableDropdown<T extends SearchableDropdownOption>({
|
|
||||||
options,
|
|
||||||
selectedValue,
|
|
||||||
onSelect,
|
|
||||||
size = 'w-32',
|
|
||||||
rounded = 'xl',
|
|
||||||
border = 'border-2',
|
|
||||||
showEdit,
|
|
||||||
onEdit,
|
|
||||||
showDelete,
|
|
||||||
onDelete,
|
|
||||||
placeholder,
|
|
||||||
}: SearchableDropdownProps<T>) {
|
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
|
||||||
|
|
||||||
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
|
||||||
setSearchQuery('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isOpen && searchInputRef.current) {
|
|
||||||
searchInputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const getOptionText = (option: SearchableDropdownOption): string => {
|
|
||||||
if (typeof option === 'string') return option;
|
|
||||||
if ('name' in option) return option.name;
|
|
||||||
if ('label' in option) return option.label;
|
|
||||||
if ('description' in option) return option.description;
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredOptions = React.useMemo(() => {
|
|
||||||
if (!searchQuery.trim()) return options;
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
return options.filter((option) =>
|
|
||||||
getOptionText(option).toLowerCase().includes(query),
|
|
||||||
);
|
|
||||||
}, [options, searchQuery]);
|
|
||||||
|
|
||||||
const getDisplayValue = (): string => {
|
|
||||||
if (!selectedValue) return placeholder ?? 'From URL';
|
|
||||||
if (typeof selectedValue === 'string') return selectedValue;
|
|
||||||
if ('label' in selectedValue) return selectedValue.label;
|
|
||||||
if ('name' in selectedValue) return selectedValue.name;
|
|
||||||
if ('description' in selectedValue) {
|
|
||||||
return selectedValue.value < 1e9
|
|
||||||
? `${selectedValue.value} (${selectedValue.description})`
|
|
||||||
: selectedValue.description;
|
|
||||||
}
|
|
||||||
return placeholder ?? 'From URL';
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOptionSelected = (option: T): boolean => {
|
|
||||||
if (!selectedValue) return false;
|
|
||||||
if (typeof selectedValue === 'string')
|
|
||||||
return selectedValue === (option as unknown as string);
|
|
||||||
if (typeof option === 'string') return false;
|
|
||||||
|
|
||||||
const optionObj = option as Record<string, unknown>;
|
|
||||||
const selectedObj = selectedValue as Record<string, unknown>;
|
|
||||||
|
|
||||||
if ('name' in optionObj && 'name' in selectedObj)
|
|
||||||
return selectedObj.name === optionObj.name;
|
|
||||||
if ('label' in optionObj && 'label' in selectedObj)
|
|
||||||
return selectedObj.label === optionObj.label;
|
|
||||||
if ('value' in optionObj && 'value' in selectedObj)
|
|
||||||
return selectedObj.value === optionObj.value;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative ${typeof selectedValue === 'string' ? '' : 'align-middle'} ${size}`}
|
|
||||||
ref={dropdownRef}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className={`flex w-full cursor-pointer items-center justify-between ${border} border-silver dark:border-dim-gray bg-white px-5 py-3 dark:bg-transparent ${borderRadius}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`dark:text-bright-gray truncate ${!selectedValue ? 'text-gray-500 dark:text-gray-400' : ''}`}
|
|
||||||
>
|
|
||||||
{getDisplayValue()}
|
|
||||||
</span>
|
|
||||||
<img
|
|
||||||
src={Arrow2}
|
|
||||||
alt="arrow"
|
|
||||||
className={`h-3 w-3 transform transition-transform ${isOpen ? 'rotate-180' : 'rotate-0'}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div
|
|
||||||
className={`absolute right-0 left-0 z-20 mt-2 ${borderRadius} dark:bg-dark-charcoal bg-[#FBFBFB] shadow-[0px_24px_48px_0px_#00000029]`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`border-silver dark:border-dim-gray dark:bg-dark-charcoal sticky top-0 z-10 border-b bg-[#FBFBFB] px-3 py-2 ${rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl'}`}
|
|
||||||
>
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<img
|
|
||||||
src={Search}
|
|
||||||
alt="search"
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className="absolute left-3"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref={searchInputRef}
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder="Search..."
|
|
||||||
className="dark:text-bright-gray w-full rounded-lg border-0 bg-transparent py-2 pr-3 pl-10 font-['Inter'] text-[14px] leading-[16.5px] font-normal focus:ring-0 focus:outline-none"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-40 overflow-y-auto">
|
|
||||||
{filteredOptions.length === 0 ? (
|
|
||||||
<div className="px-5 py-3 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
No results found
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredOptions.map((option, index) => {
|
|
||||||
const selected = isOptionSelected(option);
|
|
||||||
const optionObj =
|
|
||||||
typeof option !== 'string'
|
|
||||||
? (option as Record<string, unknown>)
|
|
||||||
: null;
|
|
||||||
const optionType = optionObj?.type as string | undefined;
|
|
||||||
const optionId = optionObj?.id as string | undefined;
|
|
||||||
const optionName = optionObj?.name as string | undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`flex cursor-pointer items-center justify-between hover:bg-[#ECECEC] dark:hover:bg-[#545561] ${selected ? 'bg-[#ECECEC] dark:bg-[#545561]' : ''}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(option);
|
|
||||||
setIsOpen(false);
|
|
||||||
setSearchQuery('');
|
|
||||||
}}
|
|
||||||
className="dark:text-light-gray ml-5 flex-1 overflow-hidden py-3 font-['Inter'] text-[14px] leading-[16.5px] font-normal text-ellipsis whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{getOptionText(option)}
|
|
||||||
</span>
|
|
||||||
{showEdit &&
|
|
||||||
onEdit &&
|
|
||||||
optionObj &&
|
|
||||||
optionType !== 'public' && (
|
|
||||||
<img
|
|
||||||
src={Edit}
|
|
||||||
alt="Edit"
|
|
||||||
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
|
|
||||||
onClick={() => {
|
|
||||||
if (optionName && optionId) {
|
|
||||||
onEdit({
|
|
||||||
id: optionId,
|
|
||||||
name: optionName,
|
|
||||||
type: optionType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setIsOpen(false);
|
|
||||||
setSearchQuery('');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showDelete && onDelete && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const id =
|
|
||||||
typeof option === 'string'
|
|
||||||
? option
|
|
||||||
: (optionId ?? '');
|
|
||||||
onDelete(id);
|
|
||||||
}}
|
|
||||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
|
||||||
typeof showDelete === 'function' &&
|
|
||||||
!showDelete(option)
|
|
||||||
? 'hidden'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={Trash}
|
|
||||||
alt="Delete"
|
|
||||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
|
||||||
optionType === 'public'
|
|
||||||
? 'cursor-not-allowed opacity-50'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SearchableDropdown;
|
|
||||||
@@ -51,16 +51,16 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
|
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
|
||||||
<div
|
<div
|
||||||
className={`${hiddenGradient === 'left' ? 'hidden' : ''} dark:from-raisin-black pointer-events-none absolute inset-y-0 left-6 w-14 bg-linear-to-r from-white md:hidden`}
|
className={`${hiddenGradient === 'left' ? 'hidden' : ''} dark:from-background pointer-events-none absolute inset-y-0 left-6 w-14 bg-linear-to-r from-white md:hidden`}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`${hiddenGradient === 'right' ? 'hidden' : ''} dark:from-raisin-black pointer-events-none absolute inset-y-0 right-6 w-14 bg-linear-to-l from-white md:hidden`}
|
className={`${hiddenGradient === 'right' ? 'hidden' : ''} dark:from-background pointer-events-none absolute inset-y-0 right-6 w-14 bg-linear-to-l from-white md:hidden`}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div className="z-10 md:hidden">
|
<div className="z-10 md:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollTabs(-1)}
|
onClick={() => scrollTabs(-1)}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="hover:bg-muted dark:hover:bg-accent flex h-6 w-6 items-center justify-center rounded-full transition-all"
|
||||||
aria-label={t('settings.scrollTabsLeft')}
|
aria-label={t('settings.scrollTabsLeft')}
|
||||||
>
|
>
|
||||||
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
||||||
@@ -78,8 +78,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
|
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? 'dark:bg-dark-charcoal bg-[#F4F4F5] text-neutral-900 dark:text-white'
|
? 'bg-muted text-foreground dark:bg-accent dark:text-white'
|
||||||
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
|
: 'text-muted-foreground hover:text-foreground dark:text-neutral-400 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={activeTab === tab}
|
aria-selected={activeTab === tab}
|
||||||
@@ -93,7 +93,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
|||||||
<div className="z-10 md:hidden">
|
<div className="z-10 md:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollTabs(1)}
|
onClick={() => scrollTabs(1)}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="hover:bg-muted dark:hover:bg-accent flex h-6 w-6 items-center justify-center rounded-full"
|
||||||
aria-label={t('settings.scrollTabsRight')}
|
aria-label={t('settings.scrollTabsRight')}
|
||||||
>
|
>
|
||||||
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export default function Sidebar({
|
|||||||
return (
|
return (
|
||||||
<div ref={sidebarRef} className="h-vh relative">
|
<div ref={sidebarRef} className="h-vh relative">
|
||||||
<div
|
<div
|
||||||
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 ${
|
className={`bg-card fixed top-0 right-0 z-50 h-full w-64 transform shadow-xl transition-all duration-300 sm:w-80 ${
|
||||||
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
|
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
|
||||||
} border-l border-[#9ca3af]/10`}
|
} border-l border-[#9ca3af]/10`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
|
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
|
||||||
<button
|
<button
|
||||||
className="hover:bg-gray-1000 dark:hover:bg-gun-metal w-7 rounded-full p-2"
|
className="hover:bg-accent w-7 rounded-full p-2"
|
||||||
onClick={() => toggleState(!isOpen)}
|
onClick={() => toggleState(!isOpen)}
|
||||||
>
|
>
|
||||||
<img className="filter dark:invert" src={Exit} />
|
<img className="filter dark:invert" src={Exit} />
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ interface SkeletonLoaderProps {
|
|||||||
| 'chatbot'
|
| 'chatbot'
|
||||||
| 'dropdown'
|
| 'dropdown'
|
||||||
| 'chunkCards'
|
| 'chunkCards'
|
||||||
| 'sourceCards';
|
| 'sourceCards'
|
||||||
|
| 'toolCards'
|
||||||
|
| 'addToolCards';
|
||||||
}
|
}
|
||||||
|
|
||||||
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||||
@@ -97,7 +99,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
{[...Array(8)].map((_, idx) => (
|
{[...Array(8)].map((_, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="dark:hover:bg-dark-charcoal flex w-full items-start p-2 hover:bg-[#F9F9F9]"
|
className="dark:hover:bg-accent hover:bg-muted flex w-full items-start p-2"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<div className="h-3 w-3 rounded-lg bg-gray-300 dark:bg-gray-600"></div>
|
<div className="h-3 w-3 rounded-lg bg-gray-300 dark:bg-gray-600"></div>
|
||||||
@@ -119,7 +121,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
key={idx}
|
key={idx}
|
||||||
className={`p-6 ${
|
className={`p-6 ${
|
||||||
skeletonCount === 1 ? 'w-full' : 'w-60'
|
skeletonCount === 1 ? 'w-full' : 'w-60'
|
||||||
} dark:bg-raisin-black animate-pulse rounded-3xl`}
|
} animate-pulse rounded-3xl`}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -154,10 +156,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
const renderAnalysis = () => (
|
const renderAnalysis = () => (
|
||||||
<>
|
<>
|
||||||
{[...Array(skeletonCount)].map((_, idx) => (
|
{[...Array(skeletonCount)].map((_, idx) => (
|
||||||
<div
|
<div key={idx} className="bg-card w-full animate-pulse rounded-3xl p-6">
|
||||||
key={idx}
|
|
||||||
className="dark:bg-raisin-black w-full animate-pulse rounded-3xl p-6"
|
|
||||||
>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="mb-4 h-4 w-1/3 rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
<div className="mb-4 h-4 w-1/3 rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||||
@@ -189,10 +188,10 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
{Array.from({ length: count }).map((_, index) => (
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={`chunk-skel-${index}`}
|
key={`chunk-skel-${index}`}
|
||||||
className="relative flex h-[197px] w-full max-w-[487px] animate-pulse flex-col overflow-hidden rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A]"
|
className="border-border dark:border-border relative flex h-[197px] w-full max-w-[487px] animate-pulse flex-col overflow-hidden rounded-[5.86px] border"
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] px-4 py-3 dark:border-[#6A6A6A] dark:bg-[#27282D]">
|
<div className="border-border bg-muted dark:border-border dark:bg-card flex w-full items-center justify-between border-b px-4 py-3">
|
||||||
<div className="h-4 w-20 rounded bg-gray-300 dark:bg-gray-600"></div>
|
<div className="h-4 w-20 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 px-4 pt-4 pb-6">
|
<div className="space-y-3 px-4 pt-4 pb-6">
|
||||||
@@ -214,7 +213,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
{Array.from({ length: count }).map((_, idx) => (
|
{Array.from({ length: count }).map((_, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`source-skel-${idx}`}
|
key={`source-skel-${idx}`}
|
||||||
className="flex h-[130px] w-full animate-pulse flex-col rounded-2xl bg-[#F9F9F9] p-3 dark:bg-[#383838]"
|
className="bg-muted dark:bg-accent flex h-[130px] w-full animate-pulse flex-col rounded-2xl p-3"
|
||||||
>
|
>
|
||||||
<div className="w-full flex-1">
|
<div className="w-full flex-1">
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
@@ -240,6 +239,55 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderAddToolCards = () => (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: count }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={`add-tool-skel-${idx}`}
|
||||||
|
className="border-light-gainsboro dark:border-arsenic flex h-52 w-full animate-pulse flex-col justify-between rounded-2xl border p-6"
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full items-center justify-between px-1">
|
||||||
|
<div className="h-6 w-6 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-[9px] space-y-2 px-1">
|
||||||
|
<div className="h-4 w-2/3 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
<div className="h-3 w-full rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div className="h-3 w-5/6 rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div className="h-3 w-3/4 rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderToolCards = () => (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: count }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={`tool-skel-${idx}`}
|
||||||
|
className="bg-muted flex h-52 w-[300px] animate-pulse flex-col justify-between rounded-2xl p-6"
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center gap-2 px-1">
|
||||||
|
<div className="h-6 w-6 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-[9px] space-y-2 px-1">
|
||||||
|
<div className="h-4 w-2/3 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
<div className="h-3 w-full rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div className="h-3 w-5/6 rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div className="h-3 w-3/4 rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<div className="h-5 w-9 rounded-full bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
fileTable: renderTable,
|
fileTable: renderTable,
|
||||||
chatbot: renderChatbot,
|
chatbot: renderChatbot,
|
||||||
@@ -249,6 +297,8 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|||||||
analysis: renderAnalysis,
|
analysis: renderAnalysis,
|
||||||
chunkCards: renderChunkCards,
|
chunkCards: renderChunkCards,
|
||||||
sourceCards: renderSourceCards,
|
sourceCards: renderSourceCards,
|
||||||
|
toolCards: renderToolCards,
|
||||||
|
addToolCards: renderAddToolCards,
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = componentMap[component] || componentMap.default;
|
const render = componentMap[component] || componentMap.default;
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function SourcesPopup({
|
|||||||
const popupContent = (
|
const popupContent = (
|
||||||
<div
|
<div
|
||||||
ref={popupRef}
|
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]"
|
className="bg-background dark:bg-card fixed z-50 flex flex-col rounded-xl shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
||||||
style={{
|
style={{
|
||||||
top: popupPosition.showAbove ? popupPosition.top : undefined,
|
top: popupPosition.showAbove ? popupPosition.top : undefined,
|
||||||
bottom: popupPosition.showAbove
|
bottom: popupPosition.showAbove
|
||||||
@@ -122,7 +122,7 @@ export default function SourcesPopup({
|
|||||||
>
|
>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="shrink-0 px-4 py-4 md:px-6">
|
<div className="shrink-0 px-4 py-4 md:px-6">
|
||||||
<h2 className="dark:text-bright-gray mb-4 text-lg font-bold text-[#141414] dark:text-[20px]">
|
<h2 className="dark:text-foreground mb-4 text-lg font-bold text-[#141414] dark:text-[20px]">
|
||||||
{t('conversation.sources.text')}
|
{t('conversation.sources.text')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@ export default function SourcesPopup({
|
|||||||
placeholder={t('settings.sources.searchPlaceholder')}
|
placeholder={t('settings.sources.searchPlaceholder')}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
labelBgClassName="bg-background dark:bg-card"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dark:border-dim-gray mx-4 grow overflow-y-auto rounded-md border border-[#D9D9D9] scrollbar-overlay">
|
<div className="dark:border-border scrollbar-overlay mx-4 grow overflow-y-auto rounded-md border border-[#D9D9D9]">
|
||||||
{options ? (
|
{options ? (
|
||||||
<>
|
<>
|
||||||
{filteredOptions?.map((option: any, index: number) => {
|
{filteredOptions?.map((option: any, index: number) => {
|
||||||
@@ -154,7 +154,7 @@ export default function SourcesPopup({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border-opacity-80 dark:border-dim-gray flex cursor-pointer items-center border-b border-[#D9D9D9] p-3 transition-colors hover:bg-gray-100 dark:text-[14px] dark:hover:bg-[#2C2E3C]"
|
className="border-opacity-80 dark:border-border hover:bg-muted flex cursor-pointer items-center border-b border-[#D9D9D9] p-3 transition-colors dark:text-[14px]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const updatedDocs =
|
const updatedDocs =
|
||||||
@@ -186,11 +186,11 @@ export default function SourcesPopup({
|
|||||||
height={14}
|
height={14}
|
||||||
className="mr-3 shrink-0"
|
className="mr-3 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="dark:text-bright-gray mr-3 grow overflow-hidden font-medium text-ellipsis whitespace-nowrap text-[#5D5D5D]">
|
<span className="dark:text-foreground text-muted-foreground mr-3 grow overflow-hidden font-medium text-ellipsis whitespace-nowrap">
|
||||||
{option.name}
|
{option.name}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-xs border-2 border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
|
className={`dark:border-border flex h-4 w-4 shrink-0 items-center justify-center rounded-xs border-2 border-[#C6C6C6] p-[0.5px]`}
|
||||||
>
|
>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<img
|
<img
|
||||||
@@ -205,7 +205,7 @@ export default function SourcesPopup({
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="dark:text-bright-gray p-4 text-center text-gray-500 dark:text-[14px]">
|
<div className="dark:text-foreground p-4 text-center text-gray-500 dark:text-[14px]">
|
||||||
{t('conversation.sources.noSourcesAvailable')}
|
{t('conversation.sources.noSourcesAvailable')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -214,7 +214,7 @@ export default function SourcesPopup({
|
|||||||
<div className="shrink-0 px-4 py-4 opacity-75 transition-opacity duration-200 hover:opacity-100 md:px-6">
|
<div className="shrink-0 px-4 py-4 opacity-75 transition-opacity duration-200 hover:opacity-100 md:px-6">
|
||||||
<a
|
<a
|
||||||
href="/settings/sources"
|
href="/settings/sources"
|
||||||
className="text-violets-are-blue inline-flex items-center gap-2 text-base font-medium"
|
className="text-primary inline-flex items-center gap-2 text-base font-medium"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{t('settings.sources.goToSources')}
|
{t('settings.sources.goToSources')}
|
||||||
@@ -225,7 +225,7 @@ export default function SourcesPopup({
|
|||||||
<div className="flex shrink-0 justify-start px-4 py-3 md:px-6">
|
<div className="flex shrink-0 justify-start px-4 py-3 md:px-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleUploadClick}
|
onClick={handleUploadClick}
|
||||||
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue w-auto rounded-full border px-4 py-2 text-[14px] font-medium transition-colors duration-200 hover:text-white"
|
className="border-primary text-primary hover:bg-primary/90 w-auto rounded-full border px-4 py-2 text-[14px] font-medium transition-colors duration-200 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('settings.sources.uploadNew')}
|
{t('settings.sources.uploadNew')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const TableContainer = React.forwardRef<HTMLDivElement, TableContainerProps>(
|
|||||||
<div className={`relative rounded-[6px] ${className}`}>
|
<div className={`relative rounded-[6px] ${className}`}>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`w-full overflow-x-auto rounded-[6px] bg-transparent ${bordered ? 'border border-[#D7D7D7] dark:border-[#6A6A6A]' : ''}`}
|
className={`w-full overflow-x-auto rounded-[6px] bg-transparent ${bordered ? 'border-border dark:border-border border' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: height === 'auto' ? undefined : height,
|
maxHeight: height === 'auto' ? undefined : height,
|
||||||
overflowY: height === 'auto' ? 'hidden' : 'auto',
|
overflowY: height === 'auto' ? 'hidden' : 'auto',
|
||||||
@@ -75,7 +75,7 @@ const Table: React.FC<TableProps> = ({
|
|||||||
const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
className={`sticky top-0 z-10 bg-gray-100 dark:bg-[#27282D] ${className} `}
|
className={`dark:bg-card sticky top-0 z-10 bg-gray-100 ${className} `}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</thead>
|
</thead>
|
||||||
@@ -96,7 +96,7 @@ const TableRow: React.FC<TableRowProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
'border-b border-[#D7D7D7] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]';
|
'border-b border-border hover:bg-muted dark:border-border dark:hover:bg-muted';
|
||||||
const cursorClass = onClick ? 'cursor-pointer' : '';
|
const cursorClass = onClick ? 'cursor-pointer' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -127,7 +127,7 @@ const TableHeader: React.FC<TableCellProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseClasses = `px-2 py-3 text-sm font-medium text-gray-700 lg:px-3 dark:text-[#59636E] border-b border-[#D7D7D7] dark:border-[#6A6A6A] relative box-border ${getAlignmentClass()}`;
|
const baseClasses = `px-2 py-3 text-sm font-medium text-gray-700 lg:px-3 dark:text-muted-foreground border-b border-border dark:border-border relative box-border ${getAlignmentClass()}`;
|
||||||
const widthClasses = minWidth ? minWidth : '';
|
const widthClasses = minWidth ? minWidth : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +158,7 @@ const TableCell: React.FC<TableCellProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseClasses = `px-2 py-2 text-sm lg:px-3 dark:text-[#E0E0E0] box-border ${getAlignmentClass()}`;
|
const baseClasses = `px-2 py-2 text-sm lg:px-3 box-border ${getAlignmentClass()}`;
|
||||||
const widthClasses = minWidth ? minWidth : '';
|
const widthClasses = minWidth ? minWidth : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -176,9 +176,7 @@ export default function SpeakButton({ text }: { text: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex cursor-pointer items-center justify-center rounded-full p-2 ${
|
className={`flex cursor-pointer items-center justify-center rounded-full p-2 ${
|
||||||
isSpeaking || isLoading
|
isSpeaking || isLoading ? 'bg-accent' : 'hover:bg-accent bg-transparent'
|
||||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
|
||||||
: 'bg-white-3000 dark:hover:bg-purple-taupe hover:bg-[#EEEEEE] dark:bg-transparent'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={handleSpeakClick}
|
onClick={handleSpeakClick}
|
||||||
aria-label={
|
aria-label={
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user