mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-05-08 06:53:40 +00:00
Compare commits
30 Commits
test-handl
...
messages-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6381f7dd4e | ||
|
|
e6ac4008fe | ||
|
|
1af09f114d | ||
|
|
be7da983e7 | ||
|
|
8b9e595d85 | ||
|
|
398f3acc8d | ||
|
|
e04baa7ed8 | ||
|
|
e5586b6f20 | ||
|
|
addf57cab7 | ||
|
|
73256389cf | ||
|
|
d609efca49 | ||
|
|
772860b667 | ||
|
|
ea2fd8b04a | ||
|
|
2c73deac20 | ||
|
|
47f3907e5e | ||
|
|
727495c553 | ||
|
|
a3b08a5b44 | ||
|
|
81532ada2a | ||
|
|
43f71374e5 | ||
|
|
d5c0322e2a | ||
|
|
3b66a3176c | ||
|
|
dc6db847ca | ||
|
|
9a6a55b6da | ||
|
|
12a8368216 | ||
|
|
193ca6fd63 | ||
|
|
174dee0fe6 | ||
|
|
844167ba06 | ||
|
|
6fa3acb1ca | ||
|
|
9fd063266b | ||
|
|
324a8cd4cf |
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
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.core.json_schema_utils import (
|
||||
@@ -9,6 +10,7 @@ from application.core.json_schema_utils import (
|
||||
normalize_json_schema_payload,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
from application.llm.handlers.base import ToolCall
|
||||
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.logging import build_stack_data, log_activity, LogContext
|
||||
@@ -113,6 +115,153 @@ class BaseAgent(ABC):
|
||||
) -> Generator[Dict, None, None]:
|
||||
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) ----
|
||||
|
||||
@property
|
||||
@@ -267,28 +416,35 @@ class BaseAgent(ABC):
|
||||
if "tool_calls" in i:
|
||||
for tool_call in i["tool_calls"]:
|
||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||
|
||||
function_call_dict = {
|
||||
"function_call": {
|
||||
"name": tool_call.get("action_name"),
|
||||
"args": tool_call.get("arguments"),
|
||||
"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]}
|
||||
args = tool_call.get("arguments")
|
||||
args_str = (
|
||||
json.dumps(args)
|
||||
if isinstance(args, dict)
|
||||
else (args or "{}")
|
||||
)
|
||||
messages.append(
|
||||
{"role": "tool", "content": [function_response_dict]}
|
||||
messages.append({
|
||||
"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})
|
||||
return messages
|
||||
|
||||
|
||||
@@ -593,16 +593,22 @@ class ResearchAgent(BaseAgent):
|
||||
)
|
||||
result = result_str
|
||||
|
||||
function_call_content = {
|
||||
"function_call": {
|
||||
"name": call.name,
|
||||
"args": call.arguments,
|
||||
"call_id": call_id,
|
||||
}
|
||||
}
|
||||
messages.append(
|
||||
{"role": "assistant", "content": [function_call_content]}
|
||||
import json as _json
|
||||
|
||||
args_str = (
|
||||
_json.dumps(call.arguments)
|
||||
if isinstance(call.arguments, dict)
|
||||
else call.arguments
|
||||
)
|
||||
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)
|
||||
messages.append(tool_message)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, List, Optional
|
||||
from collections import Counter
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
@@ -31,12 +32,23 @@ class ToolExecutor:
|
||||
self.tool_calls: List[Dict] = []
|
||||
self._loaded_tools: Dict[str, object] = {}
|
||||
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]:
|
||||
"""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:
|
||||
return self._get_tools_by_api_key(self.user_api_key)
|
||||
return self._get_user_tools(self.user or "local")
|
||||
tools = self._get_tools_by_api_key(self.user_api_key)
|
||||
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]:
|
||||
mongo = MongoDB.get_client()
|
||||
@@ -65,29 +77,123 @@ class ToolExecutor:
|
||||
user_tools = list(user_tools)
|
||||
return {str(i): tool for i, tool in enumerate(user_tools)}
|
||||
|
||||
def prepare_tools_for_llm(self, tools_dict: Dict) -> List[Dict]:
|
||||
"""Convert tool configs to LLM function schemas."""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": f"{action['name']}_{tool_id}",
|
||||
"description": action["description"],
|
||||
"parameters": self._build_tool_parameters(action),
|
||||
},
|
||||
def merge_client_tools(
|
||||
self, tools_dict: Dict, client_tools: List[Dict]
|
||||
) -> Dict:
|
||||
"""Merge client-provided tool definitions into tools_dict.
|
||||
|
||||
Client tools use the standard function-calling format::
|
||||
|
||||
[{"type": "function", "function": {"name": "get_weather",
|
||||
"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()
|
||||
if (
|
||||
(tool["name"] == "api_tool" and "actions" in tool.get("config", {}))
|
||||
or (tool["name"] != "api_tool" and "actions" in tool)
|
||||
)
|
||||
for action in (
|
||||
return tools_dict
|
||||
|
||||
def prepare_tools_for_llm(self, tools_dict: Dict) -> List[Dict]:
|
||||
"""Convert tool configs to LLM function schemas.
|
||||
|
||||
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()
|
||||
if tool["name"] == "api_tool"
|
||||
if is_api
|
||||
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:
|
||||
params = {"type": "object", "properties": {}, "required": []}
|
||||
@@ -104,23 +210,81 @@ class ToolExecutor:
|
||||
params["required"].append(k)
|
||||
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):
|
||||
"""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)
|
||||
llm_name = getattr(call, "name", "unknown")
|
||||
|
||||
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
||||
|
||||
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)
|
||||
|
||||
tool_call_data = {
|
||||
"tool_name": "unknown",
|
||||
"call_id": call_id,
|
||||
"action_name": getattr(call, "name", "unknown"),
|
||||
"action_name": llm_name,
|
||||
"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"}}
|
||||
self.tool_calls.append(tool_call_data)
|
||||
@@ -133,7 +297,7 @@ class ToolExecutor:
|
||||
tool_call_data = {
|
||||
"tool_name": "unknown",
|
||||
"call_id": call_id,
|
||||
"action_name": f"{action_name}_{tool_id}",
|
||||
"action_name": llm_name,
|
||||
"arguments": call_args,
|
||||
"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_name": tools_dict[tool_id]["name"],
|
||||
"call_id": call_id,
|
||||
"action_name": f"{action_name}_{tool_id}",
|
||||
"action_name": llm_name,
|
||||
"arguments": call_args,
|
||||
}
|
||||
yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}}
|
||||
|
||||
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Tool(ABC):
|
||||
internal: bool = False
|
||||
|
||||
@abstractmethod
|
||||
def execute_action(self, action_name: str, **kwargs):
|
||||
pass
|
||||
|
||||
@@ -20,6 +20,8 @@ class InternalSearchTool(Tool):
|
||||
- list_files action: browse the file/folder structure
|
||||
"""
|
||||
|
||||
internal = True
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
self.config = config
|
||||
self.retrieved_docs: List[Dict] = []
|
||||
|
||||
@@ -36,6 +36,8 @@ class ThinkTool(Tool):
|
||||
The reasoning content is captured in tool_call data for transparency.
|
||||
"""
|
||||
|
||||
internal = True
|
||||
|
||||
def __init__(self, config=None):
|
||||
pass
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolActionParser:
|
||||
def __init__(self, llm_type):
|
||||
def __init__(self, llm_type, name_mapping=None):
|
||||
self.llm_type = llm_type
|
||||
self.name_mapping = name_mapping
|
||||
self.parsers = {
|
||||
"OpenAILLM": self._parse_openai_llm,
|
||||
"GoogleLLM": self._parse_google_llm,
|
||||
@@ -16,22 +17,33 @@ class ToolActionParser:
|
||||
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
|
||||
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):
|
||||
try:
|
||||
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("_")
|
||||
|
||||
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
|
||||
if len(tool_parts) < 2:
|
||||
logger.warning(
|
||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
||||
f"Invalid tool name format: {call.name}. "
|
||||
"Could not resolve via mapping or legacy parsing."
|
||||
)
|
||||
return None, None, None
|
||||
|
||||
tool_id = tool_parts[-1]
|
||||
action_name = "_".join(tool_parts[:-1])
|
||||
|
||||
# Validate that tool_id looks like a numerical ID
|
||||
if not tool_id.isdigit():
|
||||
logger.warning(
|
||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||
@@ -45,19 +57,24 @@ class ToolActionParser:
|
||||
def _parse_google_llm(self, call):
|
||||
try:
|
||||
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("_")
|
||||
|
||||
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
|
||||
if len(tool_parts) < 2:
|
||||
logger.warning(
|
||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
||||
f"Invalid tool name format: {call.name}. "
|
||||
"Could not resolve via mapping or legacy parsing."
|
||||
)
|
||||
return None, None, None
|
||||
|
||||
tool_id = tool_parts[-1]
|
||||
action_name = "_".join(tool_parts[:-1])
|
||||
|
||||
# Validate that tool_id looks like a numerical ID
|
||||
if not tool_id.isdigit():
|
||||
logger.warning(
|
||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||
|
||||
@@ -19,7 +19,7 @@ class ToolManager:
|
||||
continue
|
||||
module = importlib.import_module(f"application.agents.tools.{name}")
|
||||
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, {})
|
||||
self.tools[name] = obj(tool_config)
|
||||
|
||||
|
||||
@@ -74,57 +74,72 @@ class AnswerResource(Resource, BaseAnswerResource):
|
||||
decoded_token = getattr(request, "decoded_token", None)
|
||||
processor = StreamProcessor(data, decoded_token)
|
||||
try:
|
||||
agent = processor.build_agent(data.get("question", ""))
|
||||
if not processor.decoded_token:
|
||||
return make_response({"error": "Unauthorized"}, 401)
|
||||
# ---- 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"]
|
||||
)
|
||||
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):
|
||||
return error
|
||||
if error := self.check_usage(processor.agent_config):
|
||||
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)
|
||||
|
||||
if len(stream_result) == 7:
|
||||
(
|
||||
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 stream_result["error"]:
|
||||
return make_response({"error": stream_result["error"]}, 400)
|
||||
|
||||
if error:
|
||||
return make_response({"error": error}, 400)
|
||||
result = {
|
||||
"conversation_id": conversation_id,
|
||||
"answer": response,
|
||||
"sources": sources,
|
||||
"tool_calls": tool_calls,
|
||||
"thought": thought,
|
||||
"conversation_id": stream_result["conversation_id"],
|
||||
"answer": stream_result["answer"],
|
||||
"sources": stream_result["sources"],
|
||||
"tool_calls": stream_result["tool_calls"],
|
||||
"thought": stream_result["thought"],
|
||||
}
|
||||
|
||||
if structured_info:
|
||||
result.update(structured_info)
|
||||
extra_info = stream_result.get("extra")
|
||||
if extra_info:
|
||||
result.update(extra_info)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
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_restx import Namespace
|
||||
|
||||
from application.api.answer.services.continuation_service import ContinuationService
|
||||
from application.api.answer.services.conversation_service import ConversationService
|
||||
from application.core.model_utils import (
|
||||
get_api_key_for_provider,
|
||||
@@ -39,7 +40,16 @@ class BaseAnswerResource:
|
||||
def validate_request(
|
||||
self, data: Dict[str, Any], require_conversation_id: bool = False
|
||||
) -> 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"]
|
||||
if require_conversation_id:
|
||||
required_fields.append("conversation_id")
|
||||
@@ -177,6 +187,7 @@ class BaseAnswerResource:
|
||||
is_shared_usage: bool = False,
|
||||
shared_token: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
_continuation: Optional[Dict] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generator function that streams the complete conversation response.
|
||||
@@ -207,8 +218,19 @@ class BaseAnswerResource:
|
||||
schema_info = None
|
||||
structured_chunks = []
|
||||
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:
|
||||
query_metadata.update(line["metadata"])
|
||||
elif "answer" in line:
|
||||
@@ -244,15 +266,21 @@ class BaseAnswerResource:
|
||||
data = json.dumps({"type": "thought", "thought": line["thought"]})
|
||||
yield f"data: {data}\n\n"
|
||||
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 = {
|
||||
"type": "error",
|
||||
"error": sanitize_api_error(line.get("error", "An error occurred"))
|
||||
}
|
||||
data = json.dumps(sanitized_error)
|
||||
yield f"data: {data}\n\n"
|
||||
else:
|
||||
data = json.dumps(line)
|
||||
yield f"data: {data}\n\n"
|
||||
yield f"data: {data}\n\n"
|
||||
if is_structured and structured_chunks:
|
||||
structured_data = {
|
||||
"type": "structured_answer",
|
||||
@@ -262,6 +290,93 @@ class BaseAnswerResource:
|
||||
}
|
||||
data = json.dumps(structured_data)
|
||||
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:
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
@@ -425,8 +540,13 @@ class BaseAnswerResource:
|
||||
yield f"data: {data}\n\n"
|
||||
return
|
||||
|
||||
def process_response_stream(self, stream):
|
||||
"""Process the stream response for non-streaming endpoint"""
|
||||
def process_response_stream(self, stream) -> Dict[str, Any]:
|
||||
"""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 = ""
|
||||
response_full = ""
|
||||
source_log_docs = []
|
||||
@@ -435,6 +555,7 @@ class BaseAnswerResource:
|
||||
stream_ended = False
|
||||
is_structured = False
|
||||
schema_info = None
|
||||
pending_tool_calls = None
|
||||
|
||||
for line in stream:
|
||||
try:
|
||||
@@ -453,11 +574,22 @@ class BaseAnswerResource:
|
||||
source_log_docs = event["source"]
|
||||
elif event["type"] == "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":
|
||||
thought = event["thought"]
|
||||
elif event["type"] == "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":
|
||||
stream_ended = True
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
@@ -465,18 +597,30 @@ class BaseAnswerResource:
|
||||
continue
|
||||
if not stream_ended:
|
||||
logger.error("Stream ended unexpectedly without an 'end' event.")
|
||||
return None, None, None, None, "Stream ended unexpectedly", None
|
||||
result = (
|
||||
conversation_id,
|
||||
response_full,
|
||||
source_log_docs,
|
||||
tool_calls,
|
||||
thought,
|
||||
None,
|
||||
)
|
||||
return {
|
||||
"conversation_id": None,
|
||||
"answer": None,
|
||||
"sources": None,
|
||||
"tool_calls": None,
|
||||
"thought": None,
|
||||
"error": "Stream ended unexpectedly",
|
||||
}
|
||||
|
||||
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:
|
||||
result = result + ({"structured": True, "schema": schema_info},)
|
||||
result["extra"] = {"structured": True, "schema": schema_info}
|
||||
|
||||
return result
|
||||
|
||||
def error_stream_generate(self, err_response):
|
||||
|
||||
@@ -79,7 +79,39 @@ class StreamResource(Resource, BaseAnswerResource):
|
||||
return error
|
||||
decoded_token = getattr(request, "decoded_token", None)
|
||||
processor = StreamProcessor(data, decoded_token)
|
||||
|
||||
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"]
|
||||
)
|
||||
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"])
|
||||
if not processor.decoded_token:
|
||||
return Response(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Message reconstruction utilities for compression."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, List, Optional
|
||||
@@ -49,28 +50,35 @@ class MessageBuilder:
|
||||
if include_tool_calls and "tool_calls" in query:
|
||||
for tool_call in query["tool_calls"]:
|
||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||
|
||||
function_call_dict = {
|
||||
"function_call": {
|
||||
"name": tool_call.get("action_name"),
|
||||
"args": tool_call.get("arguments"),
|
||||
"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]}
|
||||
args = tool_call.get("arguments")
|
||||
args_str = (
|
||||
json.dumps(args)
|
||||
if isinstance(args, dict)
|
||||
else (args or "{}")
|
||||
)
|
||||
messages.append(
|
||||
{"role": "tool", "content": [function_response_dict]}
|
||||
messages.append({
|
||||
"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 len(recent_queries) == 0 and compressed_summary:
|
||||
@@ -180,28 +188,35 @@ class MessageBuilder:
|
||||
if include_tool_calls and "tool_calls" in query:
|
||||
for tool_call in query["tool_calls"]:
|
||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||
|
||||
function_call_dict = {
|
||||
"function_call": {
|
||||
"name": tool_call.get("action_name"),
|
||||
"args": tool_call.get("arguments"),
|
||||
"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]}
|
||||
args = tool_call.get("arguments")
|
||||
args_str = (
|
||||
json.dumps(args)
|
||||
if isinstance(args, dict)
|
||||
else (args or "{}")
|
||||
)
|
||||
rebuilt_messages.append(
|
||||
{"role": "tool", "content": [function_response_dict]}
|
||||
rebuilt_messages.append({
|
||||
"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 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
|
||||
@@ -771,6 +771,121 @@ class StreamProcessor:
|
||||
logger.warning(f"Failed to fetch memory tool data: {str(e)}")
|
||||
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(
|
||||
self,
|
||||
docs_together: Optional[str] = None,
|
||||
@@ -841,6 +956,10 @@ class StreamProcessor:
|
||||
decoded_token=self.decoded_token,
|
||||
)
|
||||
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
|
||||
agent_kwargs = {
|
||||
|
||||
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"]
|
||||
314
application/api/v1/routes.py
Normal file
314
application/api/v1/routes.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""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
|
||||
|
||||
if is_stream:
|
||||
return Response(
|
||||
_stream_response(
|
||||
helper, question, agent, processor, model_name, continuation
|
||||
),
|
||||
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
|
||||
)
|
||||
|
||||
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],
|
||||
) -> 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,
|
||||
_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],
|
||||
) -> 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,
|
||||
_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())
|
||||
models.append({
|
||||
"id": str(ag.get("key", "")),
|
||||
"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,
|
||||
)
|
||||
415
application/api/v1/translator.py
Normal file
415
application/api/v1/translator.py
Normal file
@@ -0,0 +1,415 @@
|
||||
"""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 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)
|
||||
|
||||
result = {
|
||||
"question": question,
|
||||
"api_key": api_key,
|
||||
"history": json.dumps(history),
|
||||
"save_conversation": True,
|
||||
}
|
||||
|
||||
# Client tools
|
||||
if data.get("tools"):
|
||||
result["client_tools"] = data["tools"]
|
||||
|
||||
# DocsGPT extensions
|
||||
docsgpt = data.get("docsgpt", {})
|
||||
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.user.routes import user # 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.core.settings import settings # 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(internal)
|
||||
app.register_blueprint(connector)
|
||||
app.register_blueprint(v1_bp)
|
||||
app.config.update(
|
||||
UPLOAD_FOLDER="inputs",
|
||||
CELERY_BROKER_URL=settings.CELERY_BROKER_URL,
|
||||
|
||||
@@ -167,6 +167,8 @@ class GoogleLLM(BaseLLM):
|
||||
return "\n".join(parts)
|
||||
return ""
|
||||
|
||||
import json as _json
|
||||
|
||||
for message in messages:
|
||||
role = message.get("role")
|
||||
content = message.get("content")
|
||||
@@ -180,9 +182,66 @@ class GoogleLLM(BaseLLM):
|
||||
|
||||
if role == "assistant":
|
||||
role = "model"
|
||||
elif role == "tool":
|
||||
role = "model"
|
||||
|
||||
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 isinstance(content, str):
|
||||
parts = [types.Part.from_text(text=content)]
|
||||
@@ -191,15 +250,11 @@ class GoogleLLM(BaseLLM):
|
||||
if "text" in item:
|
||||
parts.append(types.Part.from_text(text=item["text"]))
|
||||
elif "function_call" in item:
|
||||
# Remove null values from args to avoid API errors
|
||||
|
||||
# Legacy format support
|
||||
cleaned_args = self._remove_null_values(
|
||||
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:
|
||||
# Use Part constructor with functionCall and thoughtSignature
|
||||
parts.append(
|
||||
types.Part(
|
||||
functionCall=types.FunctionCall(
|
||||
@@ -210,7 +265,6 @@ class GoogleLLM(BaseLLM):
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Use helper method when no thought_signature
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
name=item["function_call"]["name"],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -315,10 +316,34 @@ class LLMHandler(ABC):
|
||||
current_prompt = self._extract_text_from_content(content)
|
||||
|
||||
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):
|
||||
has_fc = False
|
||||
for item in content:
|
||||
if "function_call" in item:
|
||||
has_fc = True
|
||||
fc = item["function_call"]
|
||||
call_id = fc.get("call_id") or str(uuid.uuid4())
|
||||
current_tool_calls[call_id] = {
|
||||
@@ -329,37 +354,30 @@ class LLMHandler(ABC):
|
||||
"status": "called",
|
||||
"call_id": call_id,
|
||||
}
|
||||
elif "function_response" in item:
|
||||
fr = item["function_response"]
|
||||
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
|
||||
if has_fc:
|
||||
continue
|
||||
|
||||
response_text = self._extract_text_from_content(content)
|
||||
_commit_query(response_text)
|
||||
|
||||
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)
|
||||
# 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:
|
||||
current_tool_calls[call_id]["result"] = tool_text
|
||||
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(
|
||||
{
|
||||
"tool_name": "unknown_tool",
|
||||
@@ -648,6 +666,13 @@ class LLMHandler(ABC):
|
||||
"""
|
||||
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:
|
||||
agent: The agent instance
|
||||
tool_calls: List of tool calls to execute
|
||||
@@ -655,9 +680,11 @@ class LLMHandler(ABC):
|
||||
messages: Current conversation history
|
||||
|
||||
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()
|
||||
pending_actions: List[Dict] = []
|
||||
|
||||
for i, call in enumerate(tool_calls):
|
||||
# Check context limit before executing tool call
|
||||
@@ -763,6 +790,29 @@ class LLMHandler(ABC):
|
||||
# Set flag on agent
|
||||
agent.context_limit_reached = True
|
||||
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:
|
||||
self.tool_calls.append(call)
|
||||
tool_executor_gen = agent._execute_tool_action(tools_dict, call)
|
||||
@@ -772,25 +822,30 @@ class LLMHandler(ABC):
|
||||
except StopIteration as e:
|
||||
tool_response, call_id = e.value
|
||||
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))
|
||||
except Exception as e:
|
||||
@@ -802,16 +857,15 @@ class LLMHandler(ABC):
|
||||
error_message = self.create_tool_message(error_call, error_response)
|
||||
updated_messages.append(error_message)
|
||||
|
||||
call_parts = call.name.split("_")
|
||||
if len(call_parts) >= 2:
|
||||
tool_id = call_parts[-1] # Last part is tool ID (e.g., "1")
|
||||
action_name = "_".join(call_parts[:-1])
|
||||
tool_name = tools_dict.get(tool_id, {}).get("name", "unknown_tool")
|
||||
full_action_name = f"{action_name}_{tool_id}"
|
||||
mapping = agent.tool_executor._name_to_tool
|
||||
if call.name in mapping:
|
||||
resolved_tool_id, _ = mapping[call.name]
|
||||
tool_name = tools_dict.get(resolved_tool_id, {}).get(
|
||||
"name", "unknown_tool"
|
||||
)
|
||||
else:
|
||||
tool_name = "unknown_tool"
|
||||
action_name = call.name
|
||||
full_action_name = call.name
|
||||
full_action_name = call.name
|
||||
yield {
|
||||
"type": "tool_call",
|
||||
"data": {
|
||||
@@ -823,7 +877,7 @@ class LLMHandler(ABC):
|
||||
"status": "error",
|
||||
},
|
||||
}
|
||||
return updated_messages
|
||||
return updated_messages, pending_actions if pending_actions else None
|
||||
|
||||
def handle_non_streaming(
|
||||
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
|
||||
@@ -851,8 +905,22 @@ class LLMHandler(ABC):
|
||||
try:
|
||||
yield next(tool_handler_gen)
|
||||
except StopIteration as e:
|
||||
messages = e.value
|
||||
messages, pending_actions = e.value
|
||||
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(
|
||||
model=agent.model_id, messages=messages, tools=agent.tools
|
||||
)
|
||||
@@ -913,10 +981,23 @@ class LLMHandler(ABC):
|
||||
try:
|
||||
yield next(tool_handler_gen)
|
||||
except StopIteration as e:
|
||||
messages = e.value
|
||||
messages, pending_actions = e.value
|
||||
break
|
||||
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
|
||||
if hasattr(agent, 'context_limit_reached') and agent.context_limit_reached:
|
||||
# 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:
|
||||
"""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 {
|
||||
"role": "model",
|
||||
"content": [
|
||||
{
|
||||
"function_response": {
|
||||
"name": tool_call.name,
|
||||
"response": {"result": result},
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
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:
|
||||
"""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 {
|
||||
"role": "tool",
|
||||
"content": [
|
||||
{
|
||||
"function_response": {
|
||||
"name": tool_call.name,
|
||||
"response": {"result": result},
|
||||
"call_id": tool_call.id,
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
def _iterate_stream(self, response: Any) -> Generator:
|
||||
|
||||
@@ -91,16 +91,52 @@ class OpenAILLM(BaseLLM):
|
||||
|
||||
if role == "model":
|
||||
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 isinstance(content, str):
|
||||
cleaned_messages.append({"role": role, "content": content})
|
||||
elif isinstance(content, list):
|
||||
# Collect all content parts into a single message
|
||||
content_parts = []
|
||||
|
||||
for item in content:
|
||||
# Legacy format support: function_call / function_response
|
||||
if "function_call" in item:
|
||||
# Function calls need their own message
|
||||
args = item["function_call"]["args"]
|
||||
if isinstance(args, str):
|
||||
try:
|
||||
@@ -116,28 +152,20 @@ class OpenAILLM(BaseLLM):
|
||||
"arguments": json.dumps(cleaned_args),
|
||||
},
|
||||
}
|
||||
cleaned_messages.append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [tool_call],
|
||||
}
|
||||
)
|
||||
cleaned_messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [tool_call],
|
||||
})
|
||||
elif "function_response" in item:
|
||||
# Function responses need their own message
|
||||
cleaned_messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": item["function_response"][
|
||||
"call_id"
|
||||
],
|
||||
"content": json.dumps(
|
||||
item["function_response"]["response"]["result"]
|
||||
),
|
||||
}
|
||||
)
|
||||
cleaned_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": item["function_response"]["call_id"],
|
||||
"content": json.dumps(
|
||||
item["function_response"]["response"]["result"]
|
||||
),
|
||||
})
|
||||
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:
|
||||
content_parts.append(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:
|
||||
content_parts.append(item)
|
||||
elif "text" in item and "type" not in item:
|
||||
# Legacy format: {"text": "..."} without type
|
||||
content_parts.append({"type": "text", "text": item["text"]})
|
||||
|
||||
# Add the collected content parts as a single message
|
||||
if content_parts:
|
||||
cleaned_messages.append({"role": role, "content": content_parts})
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
anthropic==0.75.0
|
||||
boto3==1.42.17
|
||||
anthropic==0.86.0
|
||||
boto3==1.42.24
|
||||
beautifulsoup4==4.14.3
|
||||
cel-python==0.5.0
|
||||
celery==5.6.0
|
||||
cryptography==46.0.3
|
||||
celery==5.6.3
|
||||
cryptography==46.0.6
|
||||
dataclasses-json==0.6.7
|
||||
defusedxml==0.7.1
|
||||
docling>=2.16.0
|
||||
@@ -12,88 +12,83 @@ onnxruntime>=1.19.0
|
||||
docx2txt==0.9
|
||||
ddgs>=8.0.0
|
||||
ebooklib==0.20
|
||||
escodegen==1.0.11
|
||||
esprima==4.0.1
|
||||
esutils==1.0.1
|
||||
elevenlabs==2.27.0
|
||||
Flask==3.1.2
|
||||
elevenlabs==2.40.0
|
||||
Flask==3.1.3
|
||||
faiss-cpu==1.13.2
|
||||
fastmcp==2.14.1
|
||||
fastmcp==2.14.6
|
||||
flask-restx==1.3.2
|
||||
google-genai==1.54.0
|
||||
google-api-python-client==2.187.0
|
||||
google-auth-httplib2==0.3.0
|
||||
google-auth-oauthlib==1.2.3
|
||||
google-genai==1.69.0
|
||||
google-api-python-client==2.193.0
|
||||
google-auth-httplib2==0.3.1
|
||||
google-auth-oauthlib==1.3.1
|
||||
gTTS==2.5.4
|
||||
gunicorn==23.0.0
|
||||
gunicorn==25.3.0
|
||||
html2text==2025.4.15
|
||||
javalang==0.13.0
|
||||
jinja2==3.1.6
|
||||
jiter==0.12.0
|
||||
jiter==0.13.0
|
||||
jmespath==1.0.1
|
||||
joblib==1.5.3
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
kombu==5.6.1
|
||||
langchain==1.2.0
|
||||
kombu==5.6.2
|
||||
langchain==1.2.3
|
||||
langchain-community==0.4.1
|
||||
langchain-core==1.2.5
|
||||
langchain-openai==1.1.6
|
||||
langchain-text-splitters==1.1.0
|
||||
langsmith==0.5.1
|
||||
langchain-core==1.2.23
|
||||
langchain-openai==1.1.7
|
||||
langchain-text-splitters==1.1.1
|
||||
langsmith==0.7.23
|
||||
lazy-object-proxy==1.12.0
|
||||
lxml==6.0.2
|
||||
markupsafe==3.0.3
|
||||
marshmallow>=3.18.0,<5.0.0
|
||||
mpmath==1.3.0
|
||||
multidict==6.7.0
|
||||
msal==1.34.0
|
||||
multidict==6.7.1
|
||||
msal==1.35.1
|
||||
mypy-extensions==1.1.0
|
||||
networkx==3.6.1
|
||||
numpy==2.4.0
|
||||
openai==2.14.0
|
||||
numpy==2.4.4
|
||||
openai==2.30.0
|
||||
openapi3-parser==1.1.22
|
||||
orjson==3.11.5
|
||||
packaging==24.2
|
||||
pandas==2.3.3
|
||||
orjson==3.11.7
|
||||
packaging==26.0
|
||||
pandas==3.0.2
|
||||
openpyxl==3.1.5
|
||||
pathable==0.4.4
|
||||
pdf2image>=1.17.0
|
||||
pillow
|
||||
portalocker>=2.7.0,<3.0.0
|
||||
prance==25.4.8.0
|
||||
prompt-toolkit==3.0.52
|
||||
protobuf==6.33.2
|
||||
protobuf==7.34.1
|
||||
psycopg2-binary==2.9.11
|
||||
py==1.11.0
|
||||
pydantic
|
||||
pydantic-core
|
||||
pydantic-settings
|
||||
pymongo==4.15.5
|
||||
pypdf==6.5.0
|
||||
pymongo==4.16.0
|
||||
pypdf==6.6.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv
|
||||
python-jose==3.5.0
|
||||
python-pptx==1.0.2
|
||||
redis==7.1.0
|
||||
redis==7.4.0
|
||||
referencing>=0.28.0,<0.38.0
|
||||
regex==2025.11.3
|
||||
requests==2.32.5
|
||||
regex==2026.3.32
|
||||
requests==2.33.1
|
||||
retry==0.9.2
|
||||
sentence-transformers==5.2.0
|
||||
sentence-transformers==5.3.0
|
||||
tiktoken==0.12.0
|
||||
tokenizers==0.22.1
|
||||
torch==2.9.1
|
||||
tqdm==4.67.1
|
||||
transformers==4.57.3
|
||||
tokenizers==0.22.2
|
||||
torch==2.11.0
|
||||
tqdm==4.67.3
|
||||
transformers==5.4.0
|
||||
typing-extensions==4.15.0
|
||||
typing-inspect==0.9.0
|
||||
tzdata==2025.3
|
||||
urllib3==2.6.3
|
||||
vine==5.1.0
|
||||
wcwidth==0.2.14
|
||||
wcwidth==0.6.0
|
||||
werkzeug>=3.1.0
|
||||
yarl==1.22.0
|
||||
yarl==1.23.0
|
||||
markdownify==1.2.2
|
||||
tldextract==5.3.0
|
||||
websockets==15.0.1
|
||||
tldextract==5.3.1
|
||||
websockets==16.0
|
||||
@@ -37,7 +37,7 @@ function MainLayout() {
|
||||
const [navOpen, setNavOpen] = useState(!(isMobile || isTablet));
|
||||
|
||||
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} />
|
||||
<ActionButtons showNewChat={true} showShare={true} />
|
||||
<div
|
||||
|
||||
@@ -21,10 +21,10 @@ export default function Hero({
|
||||
}>;
|
||||
|
||||
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 */}
|
||||
<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>
|
||||
<img className="mb-1 inline w-14" src={DocsGPT3} alt="docsgpt" />
|
||||
</div>
|
||||
@@ -44,9 +44,9 @@ export default function Hero({
|
||||
<button
|
||||
key={key}
|
||||
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}
|
||||
</p>
|
||||
<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>
|
||||
)}
|
||||
<div className="text-gray-4000 text-[20px] font-medium">
|
||||
<div className="text-muted-foreground text-[20px] font-medium">
|
||||
DocsGPT
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,7 +338,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
ref={navRef}
|
||||
className={`${
|
||||
!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
|
||||
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 }) =>
|
||||
`${
|
||||
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
|
||||
@@ -388,13 +388,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
alt="Create new chat"
|
||||
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')}
|
||||
</p>
|
||||
</NavLink>
|
||||
<div
|
||||
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 && (
|
||||
<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) => (
|
||||
<div
|
||||
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
|
||||
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||
? 'bg-sidebar-accent'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
@@ -432,7 +432,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
className="h-6 w-6 rounded-full object-contain"
|
||||
/>
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -456,7 +456,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
))}
|
||||
</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={() => {
|
||||
dispatch(setSelectedAgent(null));
|
||||
if (isMobile || isTablet) {
|
||||
@@ -472,7 +472,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
className="h-[18px] w-[18px]"
|
||||
/>
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -480,7 +480,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
</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={() => {
|
||||
if (isMobile || isTablet) {
|
||||
setNavOpen(false);
|
||||
@@ -496,7 +496,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
className="h-[18px] w-[18px]"
|
||||
/>
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -529,8 +529,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-eerie-black 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="text-foreground flex h-auto flex-col justify-end dark:text-white">
|
||||
<div className="dark:border-b-sidebar-border flex flex-col gap-2 border-b py-2">
|
||||
<NavLink
|
||||
onClick={() => {
|
||||
if (isMobile || isTablet) {
|
||||
@@ -540,8 +540,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
}}
|
||||
to="/settings"
|
||||
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] ${
|
||||
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
|
||||
`hover:bg-sidebar-accent mx-4 my-auto flex h-9 cursor-pointer items-center gap-4 rounded-3xl ${
|
||||
isActive ? 'bg-sidebar-accent' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -552,12 +552,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
height={21}
|
||||
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')}
|
||||
</p>
|
||||
</NavLink>
|
||||
</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">
|
||||
<Help />
|
||||
|
||||
@@ -565,9 +565,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://discord.gg/vN7YFfdMpj'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
className={'hover:bg-sidebar-accent rounded-full'}
|
||||
>
|
||||
<img
|
||||
src={Discord}
|
||||
@@ -580,9 +578,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://x.com/docsgptai'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
className={'hover:bg-sidebar-accent rounded-full'}
|
||||
>
|
||||
<img
|
||||
src={Twitter}
|
||||
@@ -595,9 +591,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://github.com/arc53/docsgpt'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
className={'hover:bg-sidebar-accent rounded-full'}
|
||||
>
|
||||
<img
|
||||
src={Github}
|
||||
@@ -612,7 +606,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
</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">
|
||||
<button
|
||||
className="h-6 w-6 lg:hidden"
|
||||
@@ -624,7 +618,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
className="w-7 filter dark:invert"
|
||||
/>
|
||||
</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>
|
||||
<DeleteConvModal
|
||||
|
||||
@@ -5,8 +5,8 @@ export default function PageNotFound() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="dark:bg-raisin-black 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">
|
||||
<div className="bg-background grid min-h-screen">
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@@ -251,7 +251,7 @@ export default function AgentCard({
|
||||
};
|
||||
return (
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
handleClick();
|
||||
@@ -283,17 +283,17 @@ export default function AgentCard({
|
||||
className="h-7 w-7 rounded-full object-contain"
|
||||
/>
|
||||
{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 className="mt-2">
|
||||
<p
|
||||
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}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -320,4 +320,4 @@ export default function AgentCard({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,25 +41,25 @@ export default function AgentLogs() {
|
||||
<div className="p-4 md:p-12">
|
||||
<div className="flex items-center gap-3 px-4">
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||
onClick={() => navigate('/agents')}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
<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')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-3 px-4">
|
||||
{agent && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
|
||||
<p className="text-xs text-[#28292E] dark:text-[#E0E0E0]/40">
|
||||
<p className="text-foreground">{agent.name}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{agent.last_used_at
|
||||
? t('agents.logs.lastUsedAt') +
|
||||
' ' +
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function AgentPreview() {
|
||||
autoFocus={false}
|
||||
/>
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,10 @@ export default function AgentsList() {
|
||||
|
||||
return (
|
||||
<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')}
|
||||
</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')}
|
||||
</p>
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function AgentsList() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -189,8 +189,8 @@ export default function AgentsList() {
|
||||
onClick={() => setActiveFilter(tab.id)}
|
||||
className={`rounded-full px-4 py-2 text-sm transition-colors ${
|
||||
activeFilter === tab.id
|
||||
? 'bg-[#E0E0E0] text-[#18181B] dark:bg-[#4A4A4A] dark:text-white'
|
||||
: 'dark:text-gray bg-transparent text-[#71717A] hover:bg-[#F5F5F5] dark:hover:bg-[#383838]/50'
|
||||
? 'bg-border text-foreground dark:bg-accent dark:text-white'
|
||||
: 'dark:text-gray text-muted-foreground hover:bg-accent/50 bg-transparent'
|
||||
}`}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
@@ -224,7 +224,7 @@ export default function AgentsList() {
|
||||
))}
|
||||
|
||||
{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-sm">{t('agents.tryDifferentSearch')}</p>
|
||||
</div>
|
||||
@@ -399,7 +399,7 @@ function AgentSection({
|
||||
|
||||
if (isFilteredView && isSearchingWithNoResults) {
|
||||
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-sm">{t('agents.tryDifferentSearch')}</p>
|
||||
</div>
|
||||
@@ -408,11 +408,11 @@ function AgentSection({
|
||||
|
||||
if (isFilteredView && hasNoAgentsAtAll) {
|
||||
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>
|
||||
{config.showNewAgentButton && (
|
||||
<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={() => {
|
||||
setModalFolderId(null);
|
||||
setShowAgentTypeModal(true);
|
||||
@@ -456,12 +456,12 @@ function AgentSection({
|
||||
<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 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 ? (
|
||||
<>
|
||||
<button
|
||||
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`)}
|
||||
</button>
|
||||
@@ -473,7 +473,7 @@ function AgentSection({
|
||||
) : (
|
||||
<button
|
||||
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}
|
||||
</button>
|
||||
@@ -485,7 +485,7 @@ function AgentSection({
|
||||
t(`agents.sections.${config.id}.title`)
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-[13px] text-[#71717A]">
|
||||
<p className="text-muted-foreground text-[13px]">
|
||||
{t(`agents.sections.${config.id}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -513,12 +513,12 @@ function AgentSection({
|
||||
}
|
||||
}}
|
||||
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
|
||||
/>
|
||||
) : (
|
||||
<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={() => {
|
||||
setIsCreatingFolder(true);
|
||||
setTimeout(() => newFolderInputRef.current?.focus(), 0);
|
||||
@@ -529,7 +529,7 @@ function AgentSection({
|
||||
))}
|
||||
{config.showNewAgentButton && (
|
||||
<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={() => {
|
||||
setModalFolderId(currentFolderId);
|
||||
setShowAgentTypeModal(true);
|
||||
@@ -579,7 +579,7 @@ function AgentSection({
|
||||
))}
|
||||
</div>
|
||||
) : 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>
|
||||
{currentFolderId
|
||||
? t('agents.folders.empty')
|
||||
@@ -587,7 +587,7 @@ function AgentSection({
|
||||
</p>
|
||||
{config.showNewAgentButton && !currentFolderId && (
|
||||
<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={() => {
|
||||
setModalFolderId(currentFolderId);
|
||||
setShowAgentTypeModal(true);
|
||||
@@ -603,4 +603,4 @@ function AgentSection({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,17 +70,15 @@ export default function FolderCard({
|
||||
<>
|
||||
<div
|
||||
className={`relative flex cursor-pointer items-center justify-between rounded-[1.2rem] px-4 py-3 sm:w-48 ${
|
||||
isExpanded
|
||||
? 'bg-[#E5E5E5] dark:bg-[#454545]'
|
||||
: 'bg-[#F6F6F6] hover:bg-[#ECECEC] dark:bg-[#383838] dark:hover:bg-[#383838]/80'
|
||||
isExpanded ? 'bg-accent' : 'bg-muted hover:bg-accent'
|
||||
}`}
|
||||
onClick={() => onToggleExpand(folder.id)}
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-[#71717A]">
|
||||
<span className="text-muted-foreground shrink-0 text-xs">
|
||||
({agentCount})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -677,17 +677,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 items-center gap-3 px-4">
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
className="border-border text-muted-foreground hover:bg-accent rounded-full border p-3 text-sm"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
<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}
|
||||
</h1>
|
||||
{agent.agent_type === 'workflow' && (
|
||||
@@ -697,14 +697,14 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<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}
|
||||
>
|
||||
{t('agents.form.buttons.cancel')}
|
||||
</button>
|
||||
{modeConfig[effectiveMode].showDelete && agent.id && (
|
||||
<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')}
|
||||
>
|
||||
<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 +714,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
{modeConfig[effectiveMode].showSaveDraft && (
|
||||
<button
|
||||
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' : ''
|
||||
}`}
|
||||
onClick={handleSaveDraft}
|
||||
@@ -730,7 +730,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
)}
|
||||
{modeConfig[effectiveMode].showAccessDetails && (
|
||||
<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}`)}
|
||||
>
|
||||
<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 +739,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
)}
|
||||
{modeConfig[effectiveMode].showAccessDetails && (
|
||||
<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')}
|
||||
>
|
||||
{t('agents.form.buttons.accessDetails')}
|
||||
@@ -747,7 +747,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
)}
|
||||
<button
|
||||
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}
|
||||
>
|
||||
<span className="flex items-center justify-center transition-all duration-200">
|
||||
@@ -760,21 +760,21 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
</button>
|
||||
</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="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">
|
||||
{t('agents.form.sections.meta')}
|
||||
</h2>
|
||||
<input
|
||||
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
|
||||
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"
|
||||
value={agent.name}
|
||||
placeholder={t('agents.form.placeholders.agentName')}
|
||||
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
|
||||
/>
|
||||
<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')}
|
||||
value={agent.description}
|
||||
onChange={(e) =>
|
||||
@@ -784,7 +784,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<div className="mt-3">
|
||||
<FileUpload
|
||||
showPreview
|
||||
className="dark:bg-raisin-black"
|
||||
className="bg-card"
|
||||
onUpload={handleUpload}
|
||||
onRemove={() => setImageFile(null)}
|
||||
uploadText={[
|
||||
@@ -800,7 +800,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
/>
|
||||
</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">
|
||||
{t('agents.form.sections.source')}
|
||||
</h2>
|
||||
@@ -809,10 +809,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<button
|
||||
ref={sourceAnchorButtonRef}
|
||||
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
|
||||
? 'text-jet dark:text-bright-gray'
|
||||
: 'dark:text-silver text-gray-400'
|
||||
? 'text-foreground dark:text-foreground'
|
||||
: 'dark:text-muted-foreground text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selectedSourceIds.size > 0
|
||||
@@ -892,17 +892,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
size="w-full"
|
||||
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')}
|
||||
placeholderClassName="text-gray-400 dark:text-silver"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</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="min-w-20 grow basis-full sm:basis-0">
|
||||
<Prompts
|
||||
@@ -920,30 +916,24 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
setPrompts={(newPrompts) => dispatch(setPrompts(newPrompts))}
|
||||
title={t('agents.form.sections.prompt')}
|
||||
titleClassName="text-lg font-semibold dark:text-[#E0E0E0]"
|
||||
titleClassName="text-lg font-semibold"
|
||||
showAddButton={false}
|
||||
dropdownProps={{
|
||||
size: 'w-full',
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<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')}
|
||||
>
|
||||
{t('agents.form.buttons.add')}
|
||||
</button>
|
||||
</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">
|
||||
{t('agents.form.sections.tools')}
|
||||
</h2>
|
||||
@@ -951,10 +941,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<button
|
||||
ref={toolAnchorButtonRef}
|
||||
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
|
||||
? 'text-jet dark:text-bright-gray'
|
||||
: 'dark:text-silver text-gray-400'
|
||||
? 'text-foreground dark:text-foreground'
|
||||
: 'dark:text-muted-foreground text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selectedTools.length > 0
|
||||
@@ -992,7 +982,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
/>
|
||||
</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">
|
||||
{t('agents.form.sections.agentType')}
|
||||
</h2>
|
||||
@@ -1010,16 +1000,12 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
size="w-full"
|
||||
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')}
|
||||
placeholderClassName="text-gray-400 dark:text-silver"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</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">
|
||||
{t('agents.form.sections.models')}
|
||||
</h2>
|
||||
@@ -1027,10 +1013,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<button
|
||||
ref={modelAnchorButtonRef}
|
||||
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
|
||||
? 'text-jet dark:text-bright-gray'
|
||||
: 'dark:text-silver text-gray-400'
|
||||
? 'text-foreground dark:text-foreground'
|
||||
: 'dark:text-muted-foreground text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selectedModelIds.size > 0
|
||||
@@ -1082,20 +1068,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
size="w-full"
|
||||
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.selectDefaultModel',
|
||||
)}
|
||||
placeholderClassName="text-gray-400 dark:text-silver"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
</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
|
||||
onClick={() =>
|
||||
setIsAdvancedSectionExpanded(!isAdvancedSectionExpanded)
|
||||
@@ -1148,7 +1130,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
"additionalProperties": false
|
||||
}`}
|
||||
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() !== '' && (
|
||||
<div
|
||||
@@ -1194,7 +1176,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}}
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
agent.limited_token_mode
|
||||
? 'bg-purple-30'
|
||||
? 'bg-primary'
|
||||
: 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
@@ -1219,7 +1201,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
disabled={!agent.limited_token_mode}
|
||||
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
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
@@ -1250,7 +1232,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}}
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
agent.limited_request_mode
|
||||
? 'bg-purple-30'
|
||||
? 'bg-primary'
|
||||
: 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
@@ -1277,7 +1259,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
placeholder={t(
|
||||
'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
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
@@ -1288,7 +1270,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
)}
|
||||
</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">
|
||||
{t('agents.form.sections.preview')}
|
||||
</h2>
|
||||
@@ -1331,7 +1313,7 @@ function AgentPreviewArea() {
|
||||
const { t } = useTranslation();
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
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' ? (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden rounded-[30px]">
|
||||
<AgentPreview />
|
||||
@@ -1339,7 +1321,7 @@ function AgentPreviewArea() {
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
||||
<p className="dark:text-gray-4000 text-xs text-[#18181B]">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('agents.form.preview.publishedPreview')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function SharedAgent() {
|
||||
alt="No agent found"
|
||||
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')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@ export default function SharedAgent() {
|
||||
alt="agent-logo"
|
||||
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}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@ export default function SharedAgent() {
|
||||
autoFocus={false}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-4000 dark:text-sonic-silver hidden w-screen self-center bg-transparent py-2 text-center text-xs md:inline md:w-full">
|
||||
<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')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
agent.shared_metadata !== null &&
|
||||
Object.keys(agent.shared_metadata).length > 0;
|
||||
return (
|
||||
<div className="border-dark-gray dark:border-grey flex w-full max-w-[720px] flex-col rounded-3xl border p-6 shadow-xs sm:w-fit sm:min-w-[480px]">
|
||||
<div className="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 h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
||||
<AgentImage
|
||||
@@ -19,10 +19,10 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
/>
|
||||
</div>
|
||||
<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}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -30,12 +30,12 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
{hasSharedMetadata && (
|
||||
<div className="mt-4 flex items-center gap-8">
|
||||
{agent.shared_metadata?.shared_by && (
|
||||
<p className="text-eerie-black text-xs font-light sm:text-sm dark:text-[#E0E0E0]">
|
||||
<p className="text-foreground text-xs font-light sm:text-sm">
|
||||
by {agent.shared_metadata.shared_by}
|
||||
</p>
|
||||
)}
|
||||
{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{' '}
|
||||
{new Date(agent.shared_metadata.shared_at).toLocaleString(
|
||||
'en-US',
|
||||
@@ -54,14 +54,14 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
)}
|
||||
{agent.tool_details && agent.tool_details.length > 0 && (
|
||||
<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
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{agent.tool_details.map((tool, index) => (
|
||||
<span
|
||||
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
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
|
||||
@@ -579,12 +579,12 @@ function WorkflowBuilderInner() {
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="bg-lotion dark:bg-outer-space 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="bg-background flex h-screen w-full flex-col">
|
||||
<div className="border-border bg-card flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -613,7 +613,7 @@ function WorkflowBuilderInner() {
|
||||
{showWorkflowSettings && (
|
||||
<div
|
||||
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">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
@@ -623,7 +623,7 @@ function WorkflowBuilderInner() {
|
||||
type="text"
|
||||
value={workflowName}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -634,14 +634,14 @@ function WorkflowBuilderInner() {
|
||||
<textarea
|
||||
value={workflowDescription}
|
||||
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}
|
||||
placeholder="Describe what this workflow does"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -660,7 +660,7 @@ function WorkflowBuilderInner() {
|
||||
}
|
||||
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} />
|
||||
Preview
|
||||
@@ -668,7 +668,7 @@ function WorkflowBuilderInner() {
|
||||
<button
|
||||
onClick={handlePublish}
|
||||
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'}
|
||||
</button>
|
||||
@@ -705,20 +705,20 @@ function WorkflowBuilderInner() {
|
||||
)}
|
||||
|
||||
<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>
|
||||
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||
Core Nodes
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<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
|
||||
onDragStart={(e) =>
|
||||
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} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
@@ -726,7 +726,7 @@ function WorkflowBuilderInner() {
|
||||
</span>
|
||||
</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
|
||||
onDragStart={(e) =>
|
||||
e.dataTransfer.setData('application/reactflow', 'end')
|
||||
@@ -740,7 +740,7 @@ function WorkflowBuilderInner() {
|
||||
</span>
|
||||
</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
|
||||
onDragStart={(e) =>
|
||||
e.dataTransfer.setData('application/reactflow', 'note')
|
||||
@@ -762,7 +762,7 @@ function WorkflowBuilderInner() {
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<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
|
||||
onDragStart={(e) =>
|
||||
e.dataTransfer.setData('application/reactflow', 'state')
|
||||
@@ -784,10 +784,7 @@ function WorkflowBuilderInner() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={reactFlowWrapper}
|
||||
className="dark:bg-raisin-black/10 relative flex-1 bg-gray-50"
|
||||
>
|
||||
<div ref={reactFlowWrapper} className="bg-muted relative flex-1">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
@@ -807,9 +804,9 @@ function WorkflowBuilderInner() {
|
||||
{showNodeConfig && selectedNode && (
|
||||
<div
|
||||
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">
|
||||
{selectedNode.type === 'start' && 'Start 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="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">
|
||||
Node ID
|
||||
</div>
|
||||
@@ -856,7 +853,7 @@ function WorkflowBuilderInner() {
|
||||
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"
|
||||
/>
|
||||
</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}
|
||||
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}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1057,7 +1054,7 @@ function WorkflowBuilderInner() {
|
||||
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}
|
||||
placeholder="Enter note content"
|
||||
/>
|
||||
@@ -1078,7 +1075,7 @@ function WorkflowBuilderInner() {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1094,7 +1091,7 @@ function WorkflowBuilderInner() {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1125,7 +1122,7 @@ function WorkflowBuilderInner() {
|
||||
<SheetContent
|
||||
side="right"
|
||||
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
|
||||
workflowData={{
|
||||
|
||||
@@ -41,4 +41,4 @@ export const agentSectionsConfig = [
|
||||
selectData: selectSharedAgents,
|
||||
updateAction: setSharedAgents,
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function AgentTypeModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<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()}
|
||||
>
|
||||
<button
|
||||
@@ -43,7 +43,7 @@ export default function AgentTypeModal({
|
||||
<X size={20} />
|
||||
</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
|
||||
</h2>
|
||||
<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">
|
||||
<button
|
||||
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} />
|
||||
</div>
|
||||
<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
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||
@@ -71,13 +71,13 @@ export default function AgentTypeModal({
|
||||
|
||||
<button
|
||||
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} />
|
||||
</div>
|
||||
<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
|
||||
</h3>
|
||||
<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 />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
name?: string;
|
||||
|
||||
@@ -2,13 +2,13 @@ import 'reactflow/dist/style.css';
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
ChartColumn,
|
||||
Bot,
|
||||
ChartColumn,
|
||||
Database,
|
||||
Flag,
|
||||
GitBranch,
|
||||
Loader2,
|
||||
Link,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
@@ -1355,12 +1355,12 @@ function WorkflowBuilderInner() {
|
||||
return (
|
||||
<>
|
||||
<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="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="bg-background fixed inset-0 z-50 hidden h-screen w-full flex-col md:flex">
|
||||
<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">
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -1390,7 +1390,7 @@ function WorkflowBuilderInner() {
|
||||
{showWorkflowSettings && (
|
||||
<div
|
||||
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">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
@@ -1400,7 +1400,7 @@ function WorkflowBuilderInner() {
|
||||
type="text"
|
||||
value={workflowName}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1411,7 +1411,7 @@ function WorkflowBuilderInner() {
|
||||
<textarea
|
||||
value={workflowDescription}
|
||||
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}
|
||||
placeholder="Describe what this workflow does"
|
||||
/>
|
||||
@@ -1441,23 +1441,23 @@ function WorkflowBuilderInner() {
|
||||
uploadText={[
|
||||
{
|
||||
text: 'Click to upload',
|
||||
colorClass: 'text-violets-are-blue',
|
||||
colorClass: 'text-primary',
|
||||
},
|
||||
{
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleWorkflowSettingsDone}
|
||||
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
|
||||
</button>
|
||||
@@ -1468,7 +1468,7 @@ function WorkflowBuilderInner() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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} />
|
||||
Details
|
||||
@@ -1476,7 +1476,7 @@ function WorkflowBuilderInner() {
|
||||
{canManageAgent && (
|
||||
<button
|
||||
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} />
|
||||
Logs
|
||||
@@ -1485,7 +1485,7 @@ function WorkflowBuilderInner() {
|
||||
{canManageAgent && (
|
||||
<button
|
||||
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} />
|
||||
Access Details
|
||||
@@ -1495,7 +1495,7 @@ function WorkflowBuilderInner() {
|
||||
<button
|
||||
onClick={() => setDeleteConfirmation('ACTIVE')}
|
||||
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} />
|
||||
{isDeletingAgent ? 'Deleting...' : 'Delete'}
|
||||
@@ -1511,7 +1511,7 @@ function WorkflowBuilderInner() {
|
||||
}
|
||||
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} />
|
||||
Preview
|
||||
@@ -1521,8 +1521,8 @@ function WorkflowBuilderInner() {
|
||||
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 ${
|
||||
canManageAgent && !hasSavableChanges
|
||||
? 'bg-gray-200 text-gray-500 dark:bg-[#3A3A3A] dark:text-gray-400'
|
||||
: 'bg-violets-are-blue hover:bg-purple-30 text-white disabled:opacity-50'
|
||||
? 'dark:bg-accent bg-gray-200 text-gray-500 dark:text-gray-400'
|
||||
: 'bg-primary hover:bg-primary/90 text-white disabled:opacity-50'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -1571,18 +1571,18 @@ function WorkflowBuilderInner() {
|
||||
)}
|
||||
|
||||
<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>
|
||||
<h3 className="mb-3 text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||
Core Nodes
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<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
|
||||
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} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
@@ -1590,7 +1590,7 @@ function WorkflowBuilderInner() {
|
||||
</span>
|
||||
</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
|
||||
onDragStart={(e) => handleNodeDragStart(e, 'end')}
|
||||
>
|
||||
@@ -1602,7 +1602,7 @@ function WorkflowBuilderInner() {
|
||||
</span>
|
||||
</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
|
||||
onDragStart={(e) => handleNodeDragStart(e, 'note')}
|
||||
>
|
||||
@@ -1622,7 +1622,7 @@ function WorkflowBuilderInner() {
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<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
|
||||
onDragStart={(e) => handleNodeDragStart(e, 'state')}
|
||||
>
|
||||
@@ -1630,16 +1630,16 @@ function WorkflowBuilderInner() {
|
||||
<Database size={18} />
|
||||
</div>
|
||||
<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
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
Modify workflow variables
|
||||
</span>
|
||||
</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
|
||||
onDragStart={(e) => handleNodeDragStart(e, 'condition')}
|
||||
>
|
||||
@@ -1647,10 +1647,10 @@ function WorkflowBuilderInner() {
|
||||
<GitBranch size={18} />
|
||||
</div>
|
||||
<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
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
Conditional branching
|
||||
</span>
|
||||
</div>
|
||||
@@ -1661,7 +1661,7 @@ function WorkflowBuilderInner() {
|
||||
|
||||
<div
|
||||
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
|
||||
nodes={nodes}
|
||||
@@ -1687,8 +1687,8 @@ function WorkflowBuilderInner() {
|
||||
className="absolute inset-0 z-10"
|
||||
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-light-silver flex items-center justify-between border-b p-4 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-border flex items-center justify-between border-b p-4">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{selectedNode.type === 'start' && 'Start Node'}
|
||||
{selectedNode.type === 'end' && 'End Node'}
|
||||
@@ -1707,7 +1707,7 @@ function WorkflowBuilderInner() {
|
||||
|
||||
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
||||
<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">
|
||||
Node ID
|
||||
</div>
|
||||
@@ -1736,7 +1736,7 @@ function WorkflowBuilderInner() {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1833,7 +1833,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}
|
||||
placeholder="System prompt for the agent"
|
||||
/>
|
||||
@@ -1876,7 +1876,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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1969,7 +1969,7 @@ function WorkflowBuilderInner() {
|
||||
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}
|
||||
placeholder={`{
|
||||
"type": "object",
|
||||
@@ -2009,7 +2009,7 @@ function WorkflowBuilderInner() {
|
||||
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}
|
||||
placeholder="Enter note content"
|
||||
/>
|
||||
@@ -2034,7 +2034,7 @@ function WorkflowBuilderInner() {
|
||||
) => (
|
||||
<div
|
||||
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">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
@@ -2084,18 +2084,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}
|
||||
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
|
||||
a custom expression.{' '}
|
||||
<a
|
||||
href="https://cel.dev/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-violets-are-blue underline"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
@@ -2124,7 +2124,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"
|
||||
/>
|
||||
</div>
|
||||
@@ -2145,7 +2145,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} />
|
||||
Add
|
||||
@@ -2158,7 +2158,7 @@ function WorkflowBuilderInner() {
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Create conditions to branch your workflow
|
||||
</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
|
||||
onClick={() =>
|
||||
handleUpdateNodeData({
|
||||
@@ -2171,8 +2171,8 @@ function WorkflowBuilderInner() {
|
||||
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
(selectedNode.data.config?.mode ||
|
||||
'simple') === 'simple'
|
||||
? 'bg-violets-are-blue text-white'
|
||||
: 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-[#383838]'
|
||||
? 'bg-primary text-white'
|
||||
: 'hover:bg-accent text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Simple
|
||||
@@ -2189,8 +2189,8 @@ function WorkflowBuilderInner() {
|
||||
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
selectedNode.data.config?.mode ===
|
||||
'advanced'
|
||||
? 'bg-violets-are-blue text-white'
|
||||
: 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-[#383838]'
|
||||
? 'bg-primary text-white'
|
||||
: 'hover:bg-accent text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Advanced
|
||||
@@ -2201,7 +2201,7 @@ function WorkflowBuilderInner() {
|
||||
(c: ConditionCase, idx: number) => (
|
||||
<div
|
||||
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">
|
||||
<span className="text-sm font-semibold text-orange-600 dark:text-orange-400">
|
||||
@@ -2266,7 +2266,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)"
|
||||
/>
|
||||
{(selectedNode.data.config?.mode ||
|
||||
@@ -2302,7 +2302,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"
|
||||
/>
|
||||
<Select
|
||||
@@ -2394,7 +2394,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"
|
||||
/>
|
||||
</div>
|
||||
@@ -2419,18 +2419,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}
|
||||
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
|
||||
create a custom expression.{' '}
|
||||
<a
|
||||
href="https://cel.dev/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-violets-are-blue underline"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
@@ -2461,7 +2461,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} />
|
||||
Add
|
||||
@@ -2493,7 +2493,7 @@ function WorkflowBuilderInner() {
|
||||
<SheetContent
|
||||
side="right"
|
||||
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
|
||||
workflowData={{
|
||||
|
||||
@@ -150,7 +150,7 @@ function ExecutionDetails({
|
||||
ref={(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">
|
||||
<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) && (
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
{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">
|
||||
Output:{' '}
|
||||
</span>
|
||||
@@ -205,7 +205,7 @@ function ExecutionDetails({
|
||||
{stateVars.map(([key, value]) => (
|
||||
<span
|
||||
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">
|
||||
{key}:
|
||||
@@ -487,10 +487,10 @@ export default function WorkflowPreview({
|
||||
queries.length > 0 ? queries[queries.length - 1].executionSteps || [] : [];
|
||||
|
||||
return (
|
||||
<div className="dark:bg-raisin-black flex h-full flex-col bg-white">
|
||||
<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="bg-card flex h-full flex-col">
|
||||
<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 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" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -504,7 +504,7 @@ export default function WorkflowPreview({
|
||||
</div>
|
||||
</div>
|
||||
{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" />
|
||||
Running
|
||||
</span>
|
||||
@@ -512,7 +512,7 @@ export default function WorkflowPreview({
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">
|
||||
Workflow
|
||||
@@ -537,7 +537,7 @@ export default function WorkflowPreview({
|
||||
>
|
||||
{queries.length === 0 ? (
|
||||
<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" />
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
@@ -618,7 +618,7 @@ export default function WorkflowPreview({
|
||||
</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
|
||||
onSubmit={(text) => handleQuestionSubmission(text)}
|
||||
loading={status === 'loading'}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Monitor } from 'lucide-react';
|
||||
|
||||
export default function MobileBlocker() {
|
||||
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-violets-are-blue/10 dark:bg-violets-are-blue/20 mb-6 flex h-20 w-20 items-center justify-center rounded-2xl">
|
||||
<Monitor className="text-violets-are-blue h-10 w-10" />
|
||||
<div className="bg-background flex min-h-screen flex-col items-center justify-center px-6 text-center md:hidden">
|
||||
<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-primary h-10 w-10" />
|
||||
</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
|
||||
</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.
|
||||
Please open this page on a desktop or laptop computer.
|
||||
</p>
|
||||
|
||||
@@ -186,7 +186,7 @@ function HighlightedOverlay({ text }: { text: string }) {
|
||||
<>
|
||||
{parts.map((part, i) =>
|
||||
/^\{\{[^}]*\}\}$/.test(part) ? (
|
||||
<span key={i} className="text-violets-are-blue font-medium">
|
||||
<span key={i} className="text-primary font-medium">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
@@ -222,7 +222,7 @@ function VariableListWithSearch({
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<input
|
||||
type="text"
|
||||
@@ -252,9 +252,9 @@ function VariableListWithSearch({
|
||||
e.stopPropagation();
|
||||
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">
|
||||
{v.label}
|
||||
</span>
|
||||
@@ -412,7 +412,7 @@ export default function PromptTextArea({
|
||||
)}
|
||||
<div
|
||||
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
|
||||
ref={overlayRef}
|
||||
@@ -463,7 +463,7 @@ export default function PromptTextArea({
|
||||
<PopoverTrigger asChild>
|
||||
<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" />
|
||||
Add context
|
||||
@@ -472,7 +472,7 @@ export default function PromptTextArea({
|
||||
<PopoverContent
|
||||
align="end"
|
||||
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()}
|
||||
>
|
||||
<VariableListWithSearch
|
||||
@@ -486,7 +486,7 @@ export default function PromptTextArea({
|
||||
{showDropdown && filtered.length > 0 && (
|
||||
<div
|
||||
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 }}
|
||||
>
|
||||
<VariableListWithSearch
|
||||
|
||||
@@ -21,14 +21,13 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
||||
icon,
|
||||
handles = { source: true, target: true },
|
||||
}) => {
|
||||
let bgColor = 'bg-white dark:bg-[#2C2C2C]';
|
||||
let borderColor = 'border-gray-200 dark:border-[#3A3A3A]';
|
||||
let bgColor = 'bg-card';
|
||||
let borderColor = 'border-border';
|
||||
let iconBg = 'bg-gray-100 dark:bg-gray-800';
|
||||
let iconColor = 'text-gray-600 dark:text-gray-400';
|
||||
|
||||
if (selected) {
|
||||
borderColor =
|
||||
'border-violets-are-blue ring-2 ring-purple-300 dark:ring-violets-are-blue';
|
||||
borderColor = 'border-primary ring-2 ring-primary';
|
||||
}
|
||||
|
||||
if (type === 'start') {
|
||||
@@ -56,7 +55,7 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
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"
|
||||
position={Position.Right}
|
||||
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>
|
||||
|
||||
@@ -36,10 +36,10 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
|
||||
return (
|
||||
<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
|
||||
? 'border-violets-are-blue dark:ring-violets-are-blue scale-105 ring-2 ring-purple-300'
|
||||
: 'border-gray-200 hover:shadow-lg dark:border-[#3A3A3A]'
|
||||
? 'border-primary dark:ring-primary scale-105 ring-2 ring-purple-300'
|
||||
: 'border-border hover:shadow-lg'
|
||||
}`}
|
||||
style={{ minWidth: 180, maxWidth: 220, height }}
|
||||
>
|
||||
@@ -47,7 +47,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
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">
|
||||
@@ -100,7 +100,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
id={c.sourceHandle}
|
||||
isConnectable
|
||||
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
|
||||
@@ -109,7 +109,7 @@ const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
id="else"
|
||||
isConnectable
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ export const AgentNode = memo(function AgentNode({
|
||||
)}
|
||||
{config.model_id && (
|
||||
<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}
|
||||
>
|
||||
{config.model_id}
|
||||
|
||||
@@ -77,6 +77,10 @@ const endpoints = {
|
||||
WORKFLOWS: '/api/workflows',
|
||||
WORKFLOW: (id: string) => `/api/workflows/${id}`,
|
||||
},
|
||||
V1: {
|
||||
CHAT_COMPLETIONS: '/v1/chat/completions',
|
||||
MODELS: '/v1/models',
|
||||
},
|
||||
CONVERSATION: {
|
||||
ANSWER: '/api/answer',
|
||||
ANSWER_STREAMING: '/stream',
|
||||
|
||||
@@ -54,6 +54,18 @@ const conversationService = {
|
||||
apiClient.get(endpoints.CONVERSATION.DELETE_ALL, token, {}),
|
||||
update: (data: any, token: string | null): Promise<any> =>
|
||||
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;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function Accordion({
|
||||
className={`flex w-full items-center justify-between focus:outline-hidden ${titleClassName}`}
|
||||
onClick={toggleAccordion}
|
||||
>
|
||||
<p className="break-words">{title}</p>
|
||||
<p className="wrap-break-word">{title}</p>
|
||||
<img
|
||||
src={ChevronDown}
|
||||
className={`h-5 w-5 transform transition-transform duration-200 dark:invert ${
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function ActionButtons({
|
||||
<button
|
||||
title={t('actionButtons.openNewChat')}
|
||||
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
|
||||
className="filter dark:invert"
|
||||
@@ -70,7 +70,7 @@ export default function ActionButtons({
|
||||
<button
|
||||
title={t('actionButtons.share')}
|
||||
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
|
||||
className="filter dark:invert"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import {
|
||||
oneLight,
|
||||
vscDarkPlus,
|
||||
} 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 Spinner from './Spinner';
|
||||
import CopyButton from './CopyButton';
|
||||
import Exit from '../assets/exit.svg';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import CopyButton from './CopyButton';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
type TodoItem = {
|
||||
todo_id: number;
|
||||
@@ -61,7 +61,8 @@ const ARTIFACT_TITLE_BY_TYPE: Record<ArtifactData['artifact_type'], 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 ?? '')
|
||||
.replace(/_/g, ' ')
|
||||
@@ -161,7 +162,7 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{data.content ? (
|
||||
<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]}
|
||||
components={{
|
||||
code(props) {
|
||||
@@ -178,9 +179,9 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
||||
const language = match ? match[1] : '';
|
||||
|
||||
return match ? (
|
||||
<div className="group border-light-silver dark:border-raisin-black 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">
|
||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
||||
<div className="group border-border relative my-2 overflow-hidden rounded-[14px] border">
|
||||
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||
{language}
|
||||
</span>
|
||||
<CopyButton
|
||||
@@ -203,7 +204,7 @@ function NoteView({ data }: { data: NoteArtifactData }) {
|
||||
</div>
|
||||
) : (
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
@@ -315,17 +316,17 @@ export default function ArtifactSidebar({
|
||||
// Generate a unique ID for this fetch
|
||||
const fetchId = `${effectiveArtifactId}-${Date.now()}`;
|
||||
currentFetchIdRef.current = fetchId;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
// Note: For todo artifacts, the endpoint always returns all todos for the tool; will be coversation scoped later
|
||||
userService
|
||||
.getArtifact(effectiveArtifactId, token)
|
||||
.then(async (res: any) => {
|
||||
// Ignore if this is not the current fetch
|
||||
if (currentFetchIdRef.current !== fetchId) return;
|
||||
|
||||
|
||||
const isResponseLike = res && typeof res.json === 'function';
|
||||
const status = isResponseLike ? res.status : undefined;
|
||||
const ok = isResponseLike ? Boolean(res.ok) : true;
|
||||
@@ -453,7 +454,7 @@ export default function ArtifactSidebar({
|
||||
{title}
|
||||
</span>
|
||||
<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}
|
||||
>
|
||||
<img
|
||||
@@ -472,7 +473,7 @@ export default function ArtifactSidebar({
|
||||
return (
|
||||
<div ref={sidebarRef} className="h-vh relative">
|
||||
<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'
|
||||
} border-l border-[#9ca3af]/10`}
|
||||
>
|
||||
@@ -481,7 +482,7 @@ export default function ArtifactSidebar({
|
||||
{title}
|
||||
</span>
|
||||
<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}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
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 {
|
||||
useDarkTheme,
|
||||
useLoaderState,
|
||||
useMediaQuery,
|
||||
useOutsideAlerter,
|
||||
} 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 { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { ChunkType } from '../settings/types';
|
||||
import Pagination from './DocumentPagination';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
import FolderIcon from '../assets/folder.svg';
|
||||
import SearchIcon from '../assets/search.svg';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
|
||||
interface LineNumberedTextareaProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@@ -73,7 +75,7 @@ const LineNumberedTextarea: React.FC<LineNumberedTextareaProps> = ({
|
||||
))}
|
||||
</div>
|
||||
<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}
|
||||
onChange={editable ? handleChange : undefined}
|
||||
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="flex w-full items-center sm:w-auto">
|
||||
<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={
|
||||
editingChunk
|
||||
? () => setEditingChunk(null)
|
||||
@@ -315,17 +317,17 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
|
||||
<div className="flex flex-wrap items-center">
|
||||
{/* Removed the directory icon */}
|
||||
<span className="font-semibold break-words text-[#7D54D1]">
|
||||
<span className="font-semibold wrap-break-word text-[#7D54D1]">
|
||||
{documentName}
|
||||
</span>
|
||||
|
||||
{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) => (
|
||||
<React.Fragment key={index}>
|
||||
<span
|
||||
className={`break-words ${
|
||||
className={`wrap-break-word ${
|
||||
index < pathParts.length - 1
|
||||
? 'font-medium text-[#7D54D1]'
|
||||
: 'text-gray-700 dark:text-gray-300'
|
||||
@@ -334,9 +336,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
{part}
|
||||
</span>
|
||||
{index < pathParts.length - 1 && (
|
||||
<span className="mx-1 flex-shrink-0 text-gray-500">
|
||||
/
|
||||
</span>
|
||||
<span className="mx-1 shrink-0 text-gray-500">/</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
@@ -350,7 +350,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
!isEditing ? (
|
||||
<>
|
||||
<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)}
|
||||
>
|
||||
{t('modals.chunk.edit')}
|
||||
@@ -370,7 +370,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
onClick={() => {
|
||||
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')}
|
||||
</button>
|
||||
@@ -402,7 +402,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
editingText.trim() &&
|
||||
(editingTitle !== (editingChunk?.metadata?.title || '') ||
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -414,7 +414,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
<>
|
||||
<button
|
||||
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')}
|
||||
</button>
|
||||
@@ -428,7 +428,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
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 ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -488,14 +488,14 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
value={fileSearchQuery}
|
||||
onChange={(e) => handleFileSearchChange(e.target.value)}
|
||||
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]'
|
||||
} bg-transparent transition-all duration-200 focus:outline-none dark:text-[#E0E0E0]`}
|
||||
} bg-transparent transition-all duration-200 focus:outline-none`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
{fileSearchResults.length === 0 ? (
|
||||
<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}
|
||||
title={result.path}
|
||||
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
|
||||
? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]'
|
||||
? 'border-border dark:border-border border-b'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={result.isFile ? FileIcon : FolderIcon}
|
||||
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.path.split('/').pop() ||
|
||||
result.path}
|
||||
@@ -546,8 +546,8 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
{!editingChunk && !isAddingChunk ? (
|
||||
<>
|
||||
<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="flex h-full items-center px-4 font-medium whitespace-nowrap text-gray-700 dark:text-[#E0E0E0]">
|
||||
<div className="border-border dark:border-border flex h-[38px] w-full flex-1 items-center overflow-hidden rounded-md border">
|
||||
<div className="dark:text-foreground flex h-full items-center px-4 font-medium whitespace-nowrap text-gray-700">
|
||||
{totalChunks > 999999
|
||||
? `${(totalChunks / 1000000).toFixed(2)}M`
|
||||
: totalChunks > 999
|
||||
@@ -555,19 +555,19 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
: totalChunks}{' '}
|
||||
{t('settings.sources.chunks')}
|
||||
</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">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('settings.sources.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
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>
|
||||
<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')}
|
||||
onClick={() => {
|
||||
setIsAddingChunk(true);
|
||||
@@ -579,11 +579,11 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
{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} />
|
||||
</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 ? (
|
||||
<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
|
||||
@@ -597,7 +597,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
filteredChunks.map((chunk, index) => (
|
||||
<div
|
||||
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={() => {
|
||||
setEditingChunk(chunk);
|
||||
setEditingTitle(chunk.metadata?.title || '');
|
||||
@@ -605,8 +605,8 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
}}
|
||||
>
|
||||
<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="text-sm text-[#59636E] dark:text-[#E0E0E0]">
|
||||
<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="dark:text-muted-foreground text-sm text-[#59636E]">
|
||||
{chunk.metadata.token_count
|
||||
? chunk.metadata.token_count.toLocaleString()
|
||||
: '-'}{' '}
|
||||
@@ -614,7 +614,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
</>
|
||||
) : isAddingChunk ? (
|
||||
<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
|
||||
value={editingText}
|
||||
onChange={setEditingText}
|
||||
@@ -639,9 +639,9 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
) : (
|
||||
editingChunk && (
|
||||
<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="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="text-sm text-[#59636E] dark:text-[#E0E0E0]">
|
||||
<div className="border-border dark:border-border relative flex w-full flex-col overflow-hidden rounded-[5.86px] border">
|
||||
<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="dark:text-muted-foreground text-sm text-[#59636E]">
|
||||
{editingChunk.metadata.token_count
|
||||
? editingChunk.metadata.token_count.toLocaleString()
|
||||
: '-'}{' '}
|
||||
|
||||
@@ -68,9 +68,7 @@ export default function ConfigFields({
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<Label htmlFor={key}>
|
||||
{spec.label || key}
|
||||
{spec.required && (
|
||||
<span className="text-red-500">*</span>
|
||||
)}
|
||||
{spec.required && <span className="text-red-500">*</span>}
|
||||
</Label>
|
||||
<Select
|
||||
value={value || spec.default || ''}
|
||||
@@ -82,7 +80,8 @@ export default function ConfigFields({
|
||||
size="lg"
|
||||
className={cn(
|
||||
'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} />
|
||||
@@ -90,13 +89,14 @@ export default function ConfigFields({
|
||||
<SelectContent>
|
||||
{spec.enum.map((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>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{hasError && (
|
||||
<p className="text-xs text-destructive">{errors[key]}</p>
|
||||
<p className="text-destructive text-xs">{errors[key]}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -106,9 +106,7 @@ export default function ConfigFields({
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<Label htmlFor={key}>
|
||||
{spec.label || key}
|
||||
{spec.required && (
|
||||
<span className="text-red-500">*</span>
|
||||
)}
|
||||
{spec.required && <span className="text-red-500">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={key}
|
||||
@@ -134,12 +132,14 @@ export default function ConfigFields({
|
||||
}}
|
||||
placeholder={placeholder || spec.description || ''}
|
||||
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}
|
||||
className={cn('rounded-xl', hasError && 'border-destructive')}
|
||||
/>
|
||||
{hasError && (
|
||||
<p className="text-xs text-destructive">{errors[key]}</p>
|
||||
<p className="text-destructive text-xs">{errors[key]}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -136,7 +136,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
</svg>
|
||||
|
||||
<span
|
||||
className="text-sm text-[#E60000] dark:text-[#E37064]"
|
||||
className="text-sm text-[#E60000] dark:text-red-400"
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
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 { 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 { selectToken } from '../preferences/preferenceSlice';
|
||||
import { formatBytes } from '../utils/stringUtils';
|
||||
import Chunks from './Chunks';
|
||||
import ContextMenu, { MenuOption } from './ContextMenu';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import userService from '../api/services/userService';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
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 {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from './Table';
|
||||
|
||||
interface FileNode {
|
||||
@@ -325,26 +326,26 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
{/* Left side with path navigation */}
|
||||
<div className="flex w-full items-center sm:w-auto">
|
||||
<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}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<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}
|
||||
</span>
|
||||
{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) => (
|
||||
<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}
|
||||
</span>
|
||||
{index < currentPath.length - 1 && (
|
||||
<span className="mx-1 flex-shrink-0 text-gray-500">
|
||||
<span className="text-muted-foreground mx-1 shrink-0">
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
@@ -364,8 +365,8 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
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 ${
|
||||
isSyncing
|
||||
? 'cursor-not-allowed bg-gray-300 text-gray-600 dark:bg-gray-600 dark:text-gray-400'
|
||||
: 'bg-purple-30 hover:bg-violets-are-blue text-white'
|
||||
? 'dark:bg-muted dark:text-muted-foreground cursor-not-allowed bg-gray-300 text-gray-600'
|
||||
: 'bg-primary hover:bg-primary/90 text-white'
|
||||
}`}
|
||||
title={
|
||||
isSyncing
|
||||
@@ -402,7 +403,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
<img
|
||||
src={FolderIcon}
|
||||
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>
|
||||
</div>
|
||||
@@ -449,7 +450,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
<img
|
||||
src={FolderIcon}
|
||||
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>
|
||||
</div>
|
||||
@@ -466,7 +467,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img
|
||||
@@ -512,7 +513,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
<img
|
||||
src={FileIcon}
|
||||
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>
|
||||
</div>
|
||||
@@ -527,7 +528,7 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img
|
||||
@@ -625,14 +626,14 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
}
|
||||
}}
|
||||
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 && (
|
||||
<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">
|
||||
{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')}
|
||||
</div>
|
||||
) : (
|
||||
@@ -641,9 +642,9 @@ const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
key={index}
|
||||
onClick={() => handleSearchSelect(result)}
|
||||
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
|
||||
? '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.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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function ContextMenu({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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' }}
|
||||
>
|
||||
{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' : ''} ${
|
||||
option.variant === 'danger'
|
||||
? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey/20'
|
||||
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey/20'
|
||||
? 'text-destructive hover:bg-muted'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
} `}
|
||||
>
|
||||
{option.icon && (
|
||||
|
||||
@@ -39,8 +39,7 @@ export default function CopyButton({
|
||||
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
|
||||
padding,
|
||||
{
|
||||
[`bg-[#FFFFFF}] dark:bg-transparent hover:bg-[#EEEEEE] dark:hover:bg-purple-taupe`]:
|
||||
!isCopied,
|
||||
[`bg-transparent hover:bg-muted`]: !isCopied,
|
||||
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
|
||||
isCopied,
|
||||
},
|
||||
|
||||
@@ -61,12 +61,12 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
<div className="relative">
|
||||
<button
|
||||
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}
|
||||
</button>
|
||||
<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
|
||||
? 'block scale-100 opacity-100'
|
||||
: 'hidden scale-95 opacity-0'
|
||||
@@ -76,10 +76,10 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
<div
|
||||
key={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
|
||||
? 'dark:text-light-gray bg-gray-100 dark:bg-neutral-700'
|
||||
: 'dark:bg-dark-charcoal dark:text-light-gray bg-white'
|
||||
? 'dark:text-foreground bg-gray-100 dark:bg-neutral-700'
|
||||
: 'bg-card dark:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
|
||||
@@ -1,163 +1,227 @@
|
||||
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';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import { DropdownOption, DropdownProps } from './types/Dropdown.types';
|
||||
type OptionBase = { id?: string; type?: string };
|
||||
type NameIdOption = { name: string; id: string } & OptionBase;
|
||||
type LabelValueOption = { label: string; value: string } & OptionBase;
|
||||
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>({
|
||||
options,
|
||||
selectedValue,
|
||||
onSelect,
|
||||
size = 'w-32',
|
||||
rounded = 'xl',
|
||||
buttonClassName = 'border-silver bg-white dark:bg-transparent dark:border-dim-gray',
|
||||
optionsClassName = 'border-silver bg-white dark:border-dim-gray dark:bg-dark-charcoal',
|
||||
border = 'border-2',
|
||||
size = 'w-full',
|
||||
rounded = '3xl',
|
||||
searchable = false,
|
||||
placeholder = 'Select...',
|
||||
contentSize = 'text-sm',
|
||||
showEdit,
|
||||
onEdit,
|
||||
showDelete,
|
||||
onDelete,
|
||||
placeholder,
|
||||
placeholderClassName = 'text-gray-500 dark:text-gray-400',
|
||||
contentSize = 'text-base',
|
||||
}: DropdownProps<T>) {
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
|
||||
const borderTopRadius = rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl';
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
const radius = rounded === '3xl' ? 'rounded-3xl' : 'rounded-xl';
|
||||
const radiusTop = rounded === '3xl' ? 'rounded-t-3xl' : 'rounded-t-xl';
|
||||
const radiusBottom = rounded === '3xl' ? 'rounded-b-3xl' : 'rounded-b-xl';
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
setQuery('');
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && searchable && searchRef.current) searchRef.current.focus();
|
||||
}, [open, searchable]);
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={[
|
||||
typeof selectedValue === 'string'
|
||||
? 'relative'
|
||||
: 'relative align-middle',
|
||||
size,
|
||||
].join(' ')}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className={`relative ${size}`} ref={ref}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`flex w-full cursor-pointer items-center justify-between ${border} ${buttonClassName} px-5 py-3 ${
|
||||
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={`border-border bg-card text-foreground flex w-full cursor-pointer items-center justify-between border px-5 py-3 ${open ? radiusTop : radius}`}
|
||||
>
|
||||
{typeof selectedValue === 'string' ? (
|
||||
<span className={`dark:text-bright-gray truncate ${contentSize}`}>
|
||||
{selectedValue}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`truncate ${selectedValue && `dark:text-bright-gray`} ${
|
||||
!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`}
|
||||
<span
|
||||
className={`truncate ${contentSize} ${displayValue ? '' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{displayValue ?? placeholder}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`text-muted-foreground ml-2 h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
|
||||
{open && (
|
||||
<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 -mt-px overflow-hidden border border-t-0 shadow-lg ${radiusBottom}`}
|
||||
>
|
||||
{options.map((option: any, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="hover:eerie-black flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:hover:bg-[#545561]"
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`dark:text-light-gray ml-5 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap ${contentSize}`}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
{searchable && (
|
||||
<div className="flex items-center px-3 py-2">
|
||||
<Search className="text-muted-foreground mr-2 h-4 w-4 shrink-0" />
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
className="text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm focus:outline-none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function DropdownMenu({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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
|
||||
role="menu"
|
||||
@@ -99,10 +99,8 @@ export default function DropdownMenu({
|
||||
{options.map((option, idx) => (
|
||||
<div
|
||||
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 ${
|
||||
selectedOption.value === option.value
|
||||
? 'dark:bg-purple-taupe bg-gray-100'
|
||||
: 'dark:bg-dark-charcoal bg-white'
|
||||
className={`dark:text-foreground hover:bg-muted cursor-pointer px-4 py-2 text-xs ${
|
||||
selectedOption.value === option.value ? 'bg-muted' : 'bg-card'
|
||||
}`}
|
||||
role="menuitem"
|
||||
key={option.value}
|
||||
|
||||
@@ -80,15 +80,15 @@ export default function DropdownModel() {
|
||||
return (
|
||||
<div ref={dropdownRef}>
|
||||
<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)}
|
||||
>
|
||||
{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}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mx-4 my-3 truncate overflow-hidden whitespace-nowrap">
|
||||
<p className="text-muted-foreground truncate overflow-hidden whitespace-nowrap">
|
||||
Select Model
|
||||
</p>
|
||||
)}
|
||||
@@ -101,7 +101,7 @@ export default function DropdownModel() {
|
||||
/>
|
||||
</div>
|
||||
{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.map((model: Model) => (
|
||||
<div
|
||||
@@ -110,7 +110,7 @@ export default function DropdownModel() {
|
||||
dispatch(setSelectedModel(model));
|
||||
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">
|
||||
<p className="flex-1 truncate py-3 pr-2 pl-5">
|
||||
@@ -127,8 +127,10 @@ export default function DropdownModel() {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-10 w-full border-x-2 border-b-2">
|
||||
<p className="ml-5 py-3 text-gray-500">No models available</p>
|
||||
<div className="border-border/30 flex h-10 w-full items-center border-t">
|
||||
<p className="text-muted-foreground pl-5 text-sm">
|
||||
No models available
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -93,9 +93,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [allowsSharedContent, setAllowsSharedContent] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'my_files' | 'shared'>(
|
||||
'my_files',
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<'my_files' | 'shared'>('my_files');
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -214,9 +212,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
if (provider === 'share_point') {
|
||||
setAllowsSharedContent(
|
||||
validateData.allows_shared_content ?? false,
|
||||
);
|
||||
setAllowsSharedContent(validateData.allows_shared_content ?? false);
|
||||
}
|
||||
|
||||
setFiles([]);
|
||||
@@ -369,9 +365,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
{
|
||||
id: null,
|
||||
name:
|
||||
tab === 'shared'
|
||||
? 'Shared'
|
||||
: getProviderConfig(provider).rootName,
|
||||
tab === 'shared' ? 'Shared' : getProviderConfig(provider).rootName,
|
||||
},
|
||||
]);
|
||||
const sessionToken = getSessionToken(provider);
|
||||
@@ -380,8 +374,6 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleFileSelect = (fileId: string, isFolder: boolean) => {
|
||||
if (isFolder) {
|
||||
const newSelectedFolders = selectedFolders.includes(fileId)
|
||||
@@ -459,10 +451,10 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
/>
|
||||
|
||||
{isConnected && (
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-[#D7D7D7] dark:border-[#6A6A6A]">
|
||||
<div className="rounded-t-lg border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="border-border dark:border-border mt-3 overflow-hidden rounded-lg border">
|
||||
<div className="border-border dark:border-border rounded-t-lg">
|
||||
{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
|
||||
onClick={() => handleTabChange('my_files')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
@@ -485,7 +477,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
</button>
|
||||
</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">
|
||||
{folderPath.map((path, index) => (
|
||||
<div
|
||||
@@ -516,7 +508,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
colorVariant="silver"
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-[#EEE6FF78] dark:bg-[#2A262E]"
|
||||
labelBgClassName="bg-[#EEE6FF78] dark:bg-muted"
|
||||
leftIcon={
|
||||
<img src={SearchIcon} alt="Search" width={16} height={16} />
|
||||
}
|
||||
@@ -531,7 +523,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
</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
|
||||
ref={scrollContainerRef}
|
||||
height="288px"
|
||||
@@ -586,7 +578,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
>
|
||||
<TableCell width="40px" align="center">
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
handleFileSelect(file.id, isFolder(file));
|
||||
@@ -615,7 +607,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate">{file.name}</span>
|
||||
<span className="truncate">
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
@@ -626,7 +620,8 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{isLoading && files.length > 0 &&
|
||||
{isLoading &&
|
||||
files.length > 0 &&
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<TableRow key={`load-more-skeleton-${i}`}>
|
||||
<TableCell width="40px" align="center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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="mb-4 flex items-center justify-between">
|
||||
<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 */}
|
||||
<div className="flex w-full items-center sm:w-auto">
|
||||
<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}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
@@ -503,7 +503,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
{!processingRef.current && (
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
{t('settings.sources.addFile')}
|
||||
@@ -599,7 +599,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img
|
||||
@@ -656,7 +656,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img
|
||||
@@ -754,11 +754,11 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
}
|
||||
}}
|
||||
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 && (
|
||||
<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">
|
||||
{searchResults.length === 0 ? (
|
||||
<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}
|
||||
onClick={() => handleSearchSelect(result)}
|
||||
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
|
||||
? '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"
|
||||
/>
|
||||
<span className="flex-1 truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="flex-1 truncate text-sm">
|
||||
{result.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const FileUpload = ({
|
||||
showPreview = false,
|
||||
previewSize = 80,
|
||||
children,
|
||||
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
||||
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',
|
||||
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',
|
||||
@@ -133,7 +133,7 @@ export const FileUpload = ({
|
||||
});
|
||||
|
||||
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,
|
||||
isDragActive && activeClassName,
|
||||
isDragAccept && acceptClassName,
|
||||
|
||||
@@ -262,7 +262,7 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
/>
|
||||
|
||||
{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="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">
|
||||
|
||||
@@ -36,20 +36,20 @@ const Help = () => {
|
||||
<button
|
||||
ref={buttonRef}
|
||||
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" />
|
||||
{t('help')}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<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
|
||||
href="https://docs.docsgpt.cloud/"
|
||||
target="_blank"
|
||||
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
|
||||
src={PageIcon}
|
||||
@@ -61,7 +61,7 @@ const Help = () => {
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
src={EmailIcon}
|
||||
|
||||
@@ -15,7 +15,7 @@ const Input = ({
|
||||
borderVariant = 'thick',
|
||||
textSize = 'medium',
|
||||
children,
|
||||
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
||||
labelBgClassName = 'bg-card',
|
||||
leftIcon,
|
||||
onChange,
|
||||
onPaste,
|
||||
@@ -23,9 +23,9 @@ const Input = ({
|
||||
edgeRoundness = 'rounded-full',
|
||||
}: InputProps) => {
|
||||
const colorStyles = {
|
||||
silver: 'border-silver dark:border-silver/40',
|
||||
silver: 'border-border dark:border-border',
|
||||
jet: 'border-jet',
|
||||
gray: 'border-gray-5000 dark:text-silver',
|
||||
gray: 'border-gray-5000 dark:text-muted-foreground',
|
||||
};
|
||||
const borderStyles = {
|
||||
thin: 'border',
|
||||
@@ -44,7 +44,7 @@ const Input = ({
|
||||
<div className={`relative ${className}`}>
|
||||
<input
|
||||
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}
|
||||
id={id}
|
||||
name={name}
|
||||
@@ -75,11 +75,11 @@ const Input = ({
|
||||
: 'peer-placeholder-shown:left-3'
|
||||
} peer-placeholder-shown:${
|
||||
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}
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import mermaid from 'mermaid';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import mermaid from 'mermaid';
|
||||
import CopyButton from './CopyButton';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import {
|
||||
oneLight,
|
||||
vscDarkPlus,
|
||||
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import { MermaidRendererProps } from './types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectStatus } from '../conversation/conversationSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import CopyButton from './CopyButton';
|
||||
import { MermaidRendererProps } from './types';
|
||||
|
||||
const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
code,
|
||||
@@ -262,9 +263,9 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
const errorRender = !isCurrentlyLoading && error;
|
||||
|
||||
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="bg-platinum dark:bg-eerie-black-2 flex items-center justify-between px-2 py-1">
|
||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
||||
<div className="w-inherit group border-border bg-card relative rounded-lg border">
|
||||
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||
mermaid
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -280,7 +281,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
Download <span className="ml-1">▼</span>
|
||||
</button>
|
||||
{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>
|
||||
{downloadOptions.map((option, index) => (
|
||||
<li key={index}>
|
||||
@@ -289,7 +290,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
option.action();
|
||||
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}
|
||||
</button>
|
||||
@@ -318,14 +319,14 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
</div>
|
||||
|
||||
{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">
|
||||
Loading diagram...
|
||||
</div>
|
||||
</div>
|
||||
) : errorRender ? (
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,7 +334,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
<>
|
||||
<div
|
||||
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={{
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
@@ -399,9 +400,9 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
</div>
|
||||
|
||||
{showCode && (
|
||||
<div className="border-light-silver dark:border-raisin-black border-t">
|
||||
<div className="bg-platinum dark:bg-eerie-black-2 p-2">
|
||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
||||
<div className="border-border border-t">
|
||||
<div className="bg-platinum p-2">
|
||||
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||
Mermaid Code
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1438,7 +1438,7 @@ export default function MessageInput({
|
||||
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">
|
||||
{attachments.map((attachment) => {
|
||||
return (
|
||||
@@ -1448,7 +1448,7 @@ export default function MessageInput({
|
||||
onDragStart={(e) => handleDragStart(e, attachment.id)}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDropOn(e, attachment.id)}
|
||||
className={`group dark:text-bright-gray relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] sm:px-3 sm:py-1.5 sm:text-[14px] dark:bg-[#393B3D] ${
|
||||
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'
|
||||
? 'opacity-70'
|
||||
: 'opacity-100'
|
||||
@@ -1459,7 +1459,7 @@ export default function MessageInput({
|
||||
}`}
|
||||
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' && (
|
||||
<img
|
||||
src={DocumentationDark}
|
||||
@@ -1551,7 +1551,7 @@ export default function MessageInput({
|
||||
}
|
||||
tabIndex={1}
|
||||
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}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
@@ -1564,7 +1564,7 @@ export default function MessageInput({
|
||||
{showSourceButton && (
|
||||
<button
|
||||
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)}
|
||||
title={
|
||||
selectedDocs && selectedDocs.length > 0
|
||||
@@ -1577,7 +1577,7 @@ export default function MessageInput({
|
||||
alt="Sources"
|
||||
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.length === 1
|
||||
? selectedDocs[0].name
|
||||
@@ -1595,7 +1595,7 @@ export default function MessageInput({
|
||||
{showToolButton && (
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
<img
|
||||
@@ -1603,7 +1603,7 @@ export default function MessageInput({
|
||||
alt="Tools"
|
||||
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')}
|
||||
</span>
|
||||
</button>
|
||||
@@ -1617,10 +1617,10 @@ export default function MessageInput({
|
||||
aria-label={voiceButtonLabel}
|
||||
title={voiceButtonLabel}
|
||||
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'
|
||||
? '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'
|
||||
? '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" />
|
||||
)}
|
||||
<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'
|
||||
? 'text-[#B42318]'
|
||||
: 'text-[#5D5D5D]'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{voiceButtonText}
|
||||
</span>
|
||||
</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
|
||||
src={ClipIcon}
|
||||
alt="Attach"
|
||||
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] 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')}
|
||||
</span>
|
||||
<input
|
||||
@@ -1669,7 +1669,7 @@ export default function MessageInput({
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
aria-label={t('cancel')}
|
||||
className={`ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#7F54D6] text-white sm:h-9 sm:w-9`}
|
||||
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}
|
||||
>
|
||||
<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 &&
|
||||
recordingState !== 'recording' &&
|
||||
recordingState !== 'transcribing'
|
||||
? 'bg-purple-30 text-white'
|
||||
: 'bg-[#EDEDED] text-[#959595] dark:bg-[#37383D] dark:text-[#77787D]'
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-muted text-muted-foreground dark:bg-accent dark:text-muted-foreground'
|
||||
}`}
|
||||
disabled={
|
||||
!value.trim() ||
|
||||
@@ -1729,12 +1729,12 @@ export default function MessageInput({
|
||||
|
||||
{handleDragActive &&
|
||||
createPortal(
|
||||
<div className="dark:bg-gray-alpha/50 pointer-events-none fixed top-0 left-0 z-50 flex size-full flex-col items-center justify-center bg-white/85">
|
||||
<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} />
|
||||
<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')}
|
||||
</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')}
|
||||
</span>
|
||||
</div>,
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function MultiSelectPopup({
|
||||
return (
|
||||
<div
|
||||
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={{
|
||||
top: popupPosition.showAbove ? undefined : popupPosition.top,
|
||||
bottom: popupPosition.showAbove
|
||||
@@ -198,7 +198,7 @@ export default function MultiSelectPopup({
|
||||
searchPlaceholder ||
|
||||
t('settings.tools.searchPlaceholder', 'Search...')
|
||||
}
|
||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
||||
labelBgClassName="bg-background dark:bg-card"
|
||||
borderVariant="thin"
|
||||
className="mb-4"
|
||||
textSize="small"
|
||||
@@ -206,13 +206,13 @@ export default function MultiSelectPopup({
|
||||
)}
|
||||
</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 ? (
|
||||
<div className="flex h-full items-center justify-center py-4">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto scrollbar-overlay">
|
||||
<div className="scrollbar-overlay h-full overflow-y-auto">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
|
||||
<img
|
||||
@@ -233,7 +233,7 @@ export default function MultiSelectPopup({
|
||||
<div
|
||||
key={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"
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
@@ -248,7 +248,7 @@ export default function MultiSelectPopup({
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<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"
|
||||
>
|
||||
{isSelected && (
|
||||
@@ -269,7 +269,7 @@ export default function MultiSelectPopup({
|
||||
)}
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function Notification({
|
||||
))}
|
||||
</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}
|
||||
</p>
|
||||
<span className="relative z-10 flex items-center">
|
||||
@@ -132,4 +132,4 @@ export default function Notification({
|
||||
</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 (
|
||||
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
|
||||
<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
|
||||
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 className="z-10 md:hidden">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
||||
@@ -78,8 +78,8 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
|
||||
activeTab === tab
|
||||
? 'dark:bg-dark-charcoal bg-[#F4F4F5] text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
|
||||
? 'bg-muted text-foreground dark:bg-accent dark:text-white'
|
||||
: 'text-muted-foreground hover:text-foreground dark:text-neutral-400 dark:hover:text-white'
|
||||
}`}
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab}
|
||||
@@ -93,7 +93,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
<div className="z-10 md:hidden">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
||||
|
||||
@@ -33,13 +33,13 @@ export default function Sidebar({
|
||||
return (
|
||||
<div ref={sidebarRef} className="h-vh relative">
|
||||
<div
|
||||
className={`dark:bg-chinese-black fixed top-0 right-0 z-50 h-full w-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'
|
||||
} border-l border-[#9ca3af]/10`}
|
||||
>
|
||||
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
|
||||
<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)}
|
||||
>
|
||||
<img className="filter dark:invert" src={Exit} />
|
||||
|
||||
@@ -97,7 +97,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
{[...Array(8)].map((_, idx) => (
|
||||
<div
|
||||
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="h-3 w-3 rounded-lg bg-gray-300 dark:bg-gray-600"></div>
|
||||
@@ -119,7 +119,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
key={idx}
|
||||
className={`p-6 ${
|
||||
skeletonCount === 1 ? 'w-full' : 'w-60'
|
||||
} dark:bg-raisin-black animate-pulse rounded-3xl`}
|
||||
} animate-pulse rounded-3xl`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -154,10 +154,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
const renderAnalysis = () => (
|
||||
<>
|
||||
{[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="dark:bg-raisin-black w-full animate-pulse rounded-3xl p-6"
|
||||
>
|
||||
<div key={idx} className="bg-card w-full animate-pulse rounded-3xl p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4 h-4 w-1/3 rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
@@ -189,10 +186,10 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
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="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>
|
||||
<div className="space-y-3 px-4 pt-4 pb-6">
|
||||
@@ -214,7 +211,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
{Array.from({ length: count }).map((_, idx) => (
|
||||
<div
|
||||
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="flex w-full items-center justify-between gap-2">
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function SourcesPopup({
|
||||
const popupContent = (
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="bg-lotion dark:bg-charleston-green-2 fixed z-50 flex flex-col rounded-xl shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
||||
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={{
|
||||
top: popupPosition.showAbove ? popupPosition.top : undefined,
|
||||
bottom: popupPosition.showAbove
|
||||
@@ -122,7 +122,7 @@ export default function SourcesPopup({
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<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')}
|
||||
</h2>
|
||||
|
||||
@@ -135,11 +135,11 @@ export default function SourcesPopup({
|
||||
placeholder={t('settings.sources.searchPlaceholder')}
|
||||
borderVariant="thin"
|
||||
className="mb-4"
|
||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
||||
labelBgClassName="bg-background dark:bg-card"
|
||||
/>
|
||||
</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 ? (
|
||||
<>
|
||||
{filteredOptions?.map((option: any, index: number) => {
|
||||
@@ -154,7 +154,7 @@ export default function SourcesPopup({
|
||||
return (
|
||||
<div
|
||||
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={() => {
|
||||
if (isSelected) {
|
||||
const updatedDocs =
|
||||
@@ -186,11 +186,11 @@ export default function SourcesPopup({
|
||||
height={14}
|
||||
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}
|
||||
</span>
|
||||
<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 && (
|
||||
<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')}
|
||||
</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">
|
||||
<a
|
||||
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}
|
||||
>
|
||||
{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">
|
||||
<button
|
||||
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')}
|
||||
</button>
|
||||
|
||||
@@ -46,7 +46,7 @@ const TableContainer = React.forwardRef<HTMLDivElement, TableContainerProps>(
|
||||
<div className={`relative rounded-[6px] ${className}`}>
|
||||
<div
|
||||
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={{
|
||||
maxHeight: height === 'auto' ? undefined : height,
|
||||
overflowY: height === 'auto' ? 'hidden' : 'auto',
|
||||
@@ -75,7 +75,7 @@ const Table: React.FC<TableProps> = ({
|
||||
const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<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}
|
||||
</thead>
|
||||
@@ -96,7 +96,7 @@ const TableRow: React.FC<TableRowProps> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
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' : '';
|
||||
|
||||
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 : '';
|
||||
|
||||
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 : '';
|
||||
|
||||
return (
|
||||
|
||||
@@ -176,9 +176,7 @@ export default function SpeakButton({ text }: { text: string }) {
|
||||
<button
|
||||
type="button"
|
||||
className={`flex cursor-pointer items-center justify-center rounded-full p-2 ${
|
||||
isSpeaking || isLoading
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:hover:bg-purple-taupe hover:bg-[#EEEEEE] dark:bg-transparent'
|
||||
isSpeaking || isLoading ? 'bg-accent' : 'hover:bg-accent bg-transparent'
|
||||
}`}
|
||||
onClick={handleSpeakClick}
|
||||
aria-label={
|
||||
|
||||
@@ -52,7 +52,7 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||
>
|
||||
{label && (
|
||||
<span
|
||||
className={`text-eerie-black dark:text-white ${
|
||||
className={`text-foreground dark:text-white ${
|
||||
labelPosition === 'left' ? 'mr-3' : 'ml-3'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -137,7 +137,7 @@ export default function ToolsPopup({
|
||||
const popupContent = (
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="border-light-silver bg-lotion dark:border-dim-gray dark:bg-charleston-green-2 fixed z-50 rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
||||
className="border-border bg-background dark:border-border dark:bg-card fixed z-50 rounded-lg border shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
|
||||
style={{
|
||||
top: popupPosition.showAbove ? popupPosition.top : undefined,
|
||||
bottom: popupPosition.showAbove
|
||||
@@ -163,7 +163,7 @@ export default function ToolsPopup({
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('settings.tools.searchPlaceholder')}
|
||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
||||
labelBgClassName="bg-background dark:bg-card"
|
||||
borderVariant="thin"
|
||||
className="mb-4"
|
||||
/>
|
||||
@@ -174,8 +174,8 @@ export default function ToolsPopup({
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dark:border-dim-gray mx-4 grow overflow-hidden rounded-md border border-[#D9D9D9]">
|
||||
<div className="h-full overflow-y-auto scrollbar-overlay">
|
||||
<div className="dark:border-border mx-4 grow overflow-hidden rounded-md border border-[#D9D9D9]">
|
||||
<div className="scrollbar-overlay h-full overflow-y-auto">
|
||||
{filteredTools.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center py-8">
|
||||
<img
|
||||
@@ -192,7 +192,7 @@ export default function ToolsPopup({
|
||||
<div
|
||||
key={tool.id}
|
||||
onClick={() => updateToolStatus(tool.id, !tool.status)}
|
||||
className="dark:border-dim-gray dark:hover:bg-charleston-green-3 flex items-center justify-between border-b border-[#D9D9D9] p-3 hover:bg-gray-100"
|
||||
className="dark:border-border dark:hover:bg-accent hover:bg-accent flex items-center justify-between border-b border-[#D9D9D9] p-3"
|
||||
>
|
||||
<div className="mr-3 flex grow items-center">
|
||||
<img
|
||||
@@ -208,7 +208,7 @@ export default function ToolsPopup({
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div
|
||||
className={`flex h-4 w-4 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 items-center justify-center rounded-xs border-2 border-[#C6C6C6] p-[0.5px]`}
|
||||
>
|
||||
{tool.status && (
|
||||
<img
|
||||
@@ -230,7 +230,7 @@ export default function ToolsPopup({
|
||||
<div className="shrink-0 p-4 opacity-75 transition-opacity duration-200 hover:opacity-100">
|
||||
<a
|
||||
href="/settings/tools"
|
||||
className="text-purple-30 inline-flex items-center text-base font-medium"
|
||||
className="text-primary inline-flex items-center text-base font-medium"
|
||||
>
|
||||
{t('settings.tools.manageTools')}
|
||||
<img
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { selectUploadTasks, dismissUploadTask } from '../upload/uploadSlice';
|
||||
import ChevronDown from '../assets/chevron-down.svg';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import CheckCircleFilled from '../assets/check-circle-filled.svg';
|
||||
import ChevronDown from '../assets/chevron-down.svg';
|
||||
import WarnIcon from '../assets/warn.svg';
|
||||
import { dismissUploadTask, selectUploadTasks } from '../upload/uploadSlice';
|
||||
|
||||
const PROGRESS_RADIUS = 10;
|
||||
const PROGRESS_CIRCUMFERENCE = 2 * Math.PI * PROGRESS_RADIUS;
|
||||
@@ -65,23 +65,17 @@ export default function UploadToast() {
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className={`w-[271px] overflow-hidden rounded-2xl border border-[#00000021] shadow-[0px_24px_48px_0px_#00000029] transition-all duration-300 ${
|
||||
task.status === 'completed'
|
||||
? 'bg-[#FBFBFB] dark:bg-[#26272E]'
|
||||
: task.status === 'failed'
|
||||
? 'bg-[#FBFBFB] dark:bg-[#26272E]'
|
||||
: 'bg-[#FBFBFB] dark:bg-[#26272E]'
|
||||
}`}
|
||||
className={`border-border bg-card w-[271px] overflow-hidden rounded-2xl border shadow-[0px_24px_48px_0px_#00000029] transition-all duration-300`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
task.status !== 'failed'
|
||||
? 'bg-[#FBF2FE] dark:bg-transparent'
|
||||
: ''
|
||||
? 'bg-accent/50 dark:bg-muted'
|
||||
: 'bg-destructive/10 dark:bg-destructive/10'
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-inter text-[14px] leading-[16.5px] font-medium text-black dark:text-[#DCDCDC]">
|
||||
<h3 className="font-inter dark:text-foreground text-[14px] leading-[16.5px] font-medium text-black">
|
||||
{getStatusHeading(task.status)}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -147,7 +141,7 @@ export default function UploadToast() {
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<p
|
||||
className="font-inter max-w-[200px] truncate text-[13px] leading-[16.5px] font-normal text-black dark:text-[#B7BAB8]"
|
||||
className="font-inter dark:text-muted-foreground max-w-[200px] truncate text-[13px] leading-[16.5px] font-normal text-black"
|
||||
title={task.fileName}
|
||||
>
|
||||
{task.fileName}
|
||||
@@ -159,7 +153,7 @@ export default function UploadToast() {
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-6 w-6 flex-shrink-0 text-[#7D54D1]"
|
||||
className="h-6 w-6 shrink-0 text-[#7D54D1]"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
@@ -172,7 +166,7 @@ export default function UploadToast() {
|
||||
)}
|
||||
>
|
||||
<circle
|
||||
className="text-gray-300 dark:text-gray-700"
|
||||
className="text-muted dark:text-muted-foreground/30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
cx="12"
|
||||
@@ -200,7 +194,7 @@ export default function UploadToast() {
|
||||
<img
|
||||
src={CheckCircleFilled}
|
||||
alt=""
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
className="h-6 w-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
@@ -209,7 +203,7 @@ export default function UploadToast() {
|
||||
<img
|
||||
src={WarnIcon}
|
||||
alt=""
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
className="h-6 w-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
export type DropdownOptionBase = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type StringOption = string;
|
||||
export type NameIdOption = { name: string; id: string } & DropdownOptionBase;
|
||||
export type LabelValueOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
} & DropdownOptionBase;
|
||||
export type ValueDescriptionOption = {
|
||||
value: number;
|
||||
description: string;
|
||||
} & DropdownOptionBase;
|
||||
|
||||
export type DropdownOption =
|
||||
| StringOption
|
||||
| NameIdOption
|
||||
| LabelValueOption
|
||||
| ValueDescriptionOption;
|
||||
|
||||
export type DropdownSelectedValue = DropdownOption | null;
|
||||
|
||||
export type OnSelectHandler<T extends DropdownOption = DropdownOption> = (
|
||||
value: T,
|
||||
) => void;
|
||||
|
||||
export interface DropdownProps<T extends DropdownOption = DropdownOption> {
|
||||
options: T[];
|
||||
selectedValue: DropdownSelectedValue;
|
||||
onSelect: OnSelectHandler<T>;
|
||||
size?: string;
|
||||
rounded?: 'xl' | '3xl';
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
border?: 'border' | 'border-2';
|
||||
showEdit?: boolean;
|
||||
onEdit?: (value: NameIdOption) => void;
|
||||
showDelete?: boolean | ((option: T) => boolean);
|
||||
onDelete?: (id: string) => void;
|
||||
placeholder?: string;
|
||||
placeholderClassName?: string;
|
||||
contentSize?: string;
|
||||
}
|
||||
@@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'text-foreground file:text-foreground placeholder:text-muted-foreground border-silver h-[42px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'dark:border-silver/40 dark:bg-transparent dark:text-white dark:placeholder:text-gray-400',
|
||||
'text-foreground file:text-foreground placeholder:text-muted-foreground border-border h-[42px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'dark:border-border dark:text-white dark:placeholder:text-gray-400',
|
||||
'selection:bg-primary selection:text-primary-foreground',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
|
||||
@@ -70,7 +70,7 @@ export function MultiSelect({
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'h-auto min-h-[2.5rem] w-full justify-between border-[#E5E5E5] bg-white py-1.5 hover:bg-gray-50 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838]',
|
||||
'border-border bg-card hover:bg-accent h-auto min-h-10 w-full justify-between py-1.5',
|
||||
!selected.length && 'text-gray-500 dark:text-gray-400',
|
||||
className,
|
||||
)}
|
||||
@@ -85,7 +85,7 @@ export function MultiSelect({
|
||||
return (
|
||||
<span
|
||||
key={option?.value || label}
|
||||
className="dark:bg-purple-30/30 bg-violets-are-blue/20 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300"
|
||||
className="bg-primary/20 dark:bg-primary/30 inline-flex max-w-[calc(100%-1rem)] min-w-0 items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300"
|
||||
>
|
||||
<span className="truncate">{label}</span>
|
||||
<span
|
||||
@@ -124,7 +124,7 @@ export function MultiSelect({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-(--radix-popover-trigger-width) border-[#E5E5E5] bg-white p-0 dark:border-[#3A3A3A] dark:bg-[#2C2C2C]"
|
||||
className="border-border bg-card w-(--radix-popover-trigger-width) p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command className="bg-transparent">
|
||||
@@ -141,13 +141,13 @@ export function MultiSelect({
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
className="cursor-pointer dark:hover:bg-[#383838]"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border-2',
|
||||
isSelected
|
||||
? 'border-purple-30 bg-purple-30 text-white'
|
||||
? 'border-primary bg-primary text-white'
|
||||
: 'border-gray-400 dark:border-gray-500',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -37,11 +37,11 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=lg]:h-[42px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-gray-600 dark:[&_svg:not([class*='text-'])]:text-gray-400",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=lg]:h-[42px] data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-gray-600 dark:[&_svg:not([class*='text-'])]:text-gray-400",
|
||||
variant === 'default' &&
|
||||
'border-light-silver bg-white focus-visible:ring-purple-30/50 hover:bg-gray-50 data-placeholder:text-gray-500 dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:hover:bg-[#383838] dark:data-placeholder:text-gray-400',
|
||||
'border-border bg-card focus-visible:ring-ring/50 hover:bg-accent data-placeholder:text-muted-foreground',
|
||||
variant === 'ghost' &&
|
||||
'border-silver bg-transparent focus-visible:ring-purple-30/50 hover:bg-gray-50 data-[state=open]:bg-gray-50 data-placeholder:text-gray-500 dark:border-silver/40 dark:bg-transparent dark:hover:bg-white/5 dark:data-[state=open]:bg-white/10 dark:data-placeholder:text-gray-400',
|
||||
'border-border focus-visible:ring-ring/50 hover:bg-accent data-[state=open]:bg-muted data-placeholder:text-muted-foreground dark:border-border dark:hover:bg-accent dark:data-[state=open]:bg-muted bg-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -66,7 +66,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'border-light-silver bg-lotion data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-200 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border text-gray-900 shadow-md dark:border-[#3A3A3A] dark:bg-[#2C2C2C] dark:text-white',
|
||||
'border-border bg-card data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-200 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border text-gray-900 shadow-md dark:text-white',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
@@ -113,7 +113,7 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"[&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none hover:bg-gray-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:hover:bg-[#383838] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"[&_svg:not([class*='text-'])]:text-muted-foreground hover:bg-muted relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from 'react';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
@@ -34,23 +34,23 @@ function SheetOverlay({
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@@ -58,16 +58,16 @@ function SheetContent({
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
side === 'top' &&
|
||||
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||
side === 'bottom' &&
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -80,27 +80,27 @@ function SheetContent({
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
@@ -110,10 +110,10 @@ function SheetTitle({
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
@@ -123,10 +123,10 @@ function SheetDescription({
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -138,4 +138,4 @@ export {
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
resendQuery,
|
||||
selectQueries,
|
||||
selectStatus,
|
||||
submitToolActions,
|
||||
updateQuery,
|
||||
} from './conversationSlice';
|
||||
import { selectCompletedAttachments } from '../upload/uploadSlice';
|
||||
@@ -41,6 +42,17 @@ export default function Conversation() {
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const handleToolAction = useCallback(
|
||||
(callId: string, decision: 'approved' | 'denied', comment?: string) => {
|
||||
dispatch(
|
||||
submitToolActions({
|
||||
toolActions: [{ call_id: callId, decision, comment }],
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const lastAutoOpenedArtifactId = useRef<string | null>(null);
|
||||
const didInitArtifactAutoOpen = useRef(false);
|
||||
const prevConversationId = useRef<string | null>(conversationId);
|
||||
@@ -233,6 +245,7 @@ export default function Conversation() {
|
||||
status={status}
|
||||
showHeroOnEmpty={selectedAgent ? false : true}
|
||||
onOpenArtifact={handleOpenArtifact}
|
||||
onToolAction={handleToolAction}
|
||||
isSplitView={isSplitArtifactOpen}
|
||||
headerContent={
|
||||
selectedAgent ? (
|
||||
@@ -263,7 +276,7 @@ export default function Conversation() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-4000 dark:text-sonic-silver hidden w-full self-center bg-transparent py-2 text-center text-xs md:inline">
|
||||
<p className="text-muted-foreground hidden w-full self-center bg-transparent py-2 text-center text-xs md:inline">
|
||||
{t('tagline')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +65,11 @@ const ConversationBubble = forwardRef<
|
||||
) => void;
|
||||
filesAttached?: { id: string; fileName: string }[];
|
||||
onOpenArtifact?: (artifact: { id: string; toolName: string }) => void;
|
||||
onToolAction?: (
|
||||
callId: string,
|
||||
decision: 'approved' | 'denied',
|
||||
comment?: string,
|
||||
) => void;
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{
|
||||
@@ -83,6 +88,7 @@ const ConversationBubble = forwardRef<
|
||||
handleUpdatedQuestionSubmission,
|
||||
filesAttached,
|
||||
onOpenArtifact,
|
||||
onToolAction,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -140,9 +146,9 @@ const ConversationBubble = forwardRef<
|
||||
<div
|
||||
key={index}
|
||||
title={file.fileName}
|
||||
className="dark:text-bright-gray flex items-center rounded-xl bg-[#EFF3F4] p-2 text-[14px] text-[#5D5D5D] dark:bg-[#393B3D]"
|
||||
className="dark:text-foreground dark:bg-accent text-muted-foreground bg-muted flex items-center rounded-xl p-2 text-[14px]"
|
||||
>
|
||||
<div className="bg-purple-30 mr-2 items-center justify-center rounded-lg p-[5.5px]">
|
||||
<div className="bg-primary mr-2 items-center justify-center rounded-lg p-[5.5px]">
|
||||
<img
|
||||
src={DocumentationDark}
|
||||
alt="Attachment"
|
||||
@@ -201,7 +207,7 @@ const ConversationBubble = forwardRef<
|
||||
setIsEditClicked(true);
|
||||
setEditInputBox(message ?? '');
|
||||
}}
|
||||
className={`hover:bg-light-silver mt-3 flex h-fit shrink-0 cursor-pointer items-center rounded-full p-2 pt-1.5 pl-1.5 dark:hover:bg-[#35363B] ${isEditClicked ? 'visible' : 'invisible group-hover:visible'}`}
|
||||
className={`hover:bg-accent dark:hover:bg-accent mt-3 flex h-fit shrink-0 cursor-pointer items-center rounded-full p-2 pt-1.5 pl-1.5 ${isEditClicked ? 'visible' : 'invisible group-hover:visible'}`}
|
||||
>
|
||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||
</button>
|
||||
@@ -226,17 +232,17 @@ const ConversationBubble = forwardRef<
|
||||
}}
|
||||
rows={5}
|
||||
value={editInputBox}
|
||||
className="border-silver text-carbon dark:border-philippine-grey dark:bg-raisin-black dark:text-chinese-white w-full resize-none rounded-3xl border px-4 py-3 text-base leading-relaxed focus:outline-hidden"
|
||||
className="border-border text-carbon dark:border-philippine-grey dark:text-foreground w-full resize-none rounded-3xl border px-4 py-3 text-base leading-relaxed focus:outline-hidden"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
className="text-purple-30 hover:bg-gainsboro hover:text-chinese-black-2 dark:hover:bg-onyx-2 rounded-full px-4 py-2 text-sm font-semibold transition-colors dark:hover:text-[#B9BCBE]"
|
||||
className="text-primary hover:bg-muted hover:text-foreground dark:hover:bg-accent dark:hover:text-foreground rounded-full px-4 py-2 text-sm font-semibold transition-colors"
|
||||
onClick={() => setIsEditClicked(false)}
|
||||
>
|
||||
{t('conversation.edit.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="bg-purple-30 hover:bg-violets-are-blue dark:hover:bg-royal-purple rounded-full px-4 py-2 text-sm font-medium text-white transition-colors"
|
||||
className="bg-primary hover:bg-primary/90 dark:hover:bg-primary/90 rounded-full px-4 py-2 text-sm font-medium text-white transition-colors"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
{t('conversation.edit.update')}
|
||||
@@ -305,7 +311,7 @@ const ConversationBubble = forwardRef<
|
||||
bubble = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-wrap self-start ${className} group dark:text-bright-gray flex-col`}
|
||||
className={`flex flex-wrap self-start ${className} group dark:text-foreground flex-col`}
|
||||
>
|
||||
{DisableSourceFE ||
|
||||
type === 'ERROR' ||
|
||||
@@ -332,9 +338,13 @@ const ConversationBubble = forwardRef<
|
||||
<div className="fade-in mr-5 ml-3 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
{sources?.slice(0, 3)?.map((source, index) => (
|
||||
<div key={index} id={`source-${index}`} className="relative transition-all duration-300">
|
||||
<div
|
||||
key={index}
|
||||
id={`source-${index}`}
|
||||
className="relative transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
className="bg-gray-1000 dark:bg-gun-metal h-28 cursor-pointer rounded-4xl p-4 hover:bg-[#F1F1F1] dark:hover:bg-[#2C2E3C]"
|
||||
className="bg-muted hover:bg-accent dark:bg-answer-bubble dark:hover:bg-muted h-28 cursor-pointer rounded-4xl p-4"
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
@@ -344,7 +354,7 @@ const ConversationBubble = forwardRef<
|
||||
<div
|
||||
className={`mt-3.5 flex flex-row items-center gap-1.5 underline-offset-2 ${
|
||||
source.link && source.link !== 'local'
|
||||
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
|
||||
? 'hover:text-[#007DFF] hover:underline dark:hover:text-blue-400'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
@@ -378,7 +388,7 @@ const ConversationBubble = forwardRef<
|
||||
</div>
|
||||
{activeTooltip === index && (
|
||||
<div
|
||||
className={`dark:bg-chinese-black dark:text-chinese-silver absolute left-1/2 z-50 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl sm:w-56`}
|
||||
className={`dark:bg-card dark:text-foreground absolute left-1/2 z-50 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl sm:w-56`}
|
||||
onMouseOver={() => setActiveTooltip(index)}
|
||||
onMouseOut={() => setActiveTooltip(null)}
|
||||
>
|
||||
@@ -391,7 +401,7 @@ const ConversationBubble = forwardRef<
|
||||
))}
|
||||
{(sources?.length ?? 0) > 3 && (
|
||||
<div
|
||||
className="bg-gray-1000 text-purple-30 dark:bg-gun-metal flex h-28 cursor-pointer flex-col-reverse rounded-4xl p-4 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
|
||||
className="bg-muted text-primary hover:bg-accent hover:text-primary dark:bg-answer-bubble dark:hover:bg-muted dark:hover:text-primary flex h-28 cursor-pointer flex-col-reverse rounded-4xl p-4"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<p className="ellipsis-text h-22 text-xs">
|
||||
@@ -407,7 +417,7 @@ const ConversationBubble = forwardRef<
|
||||
)}
|
||||
{research && <ResearchProgress research={research} />}
|
||||
{toolCalls && toolCalls.length > 0 && (
|
||||
<ToolCalls toolCalls={toolCalls} />
|
||||
<ToolCalls toolCalls={toolCalls} onToolAction={onToolAction} />
|
||||
)}
|
||||
{!message && primaryArtifactCall?.artifact_id && onOpenArtifact && (
|
||||
<div className="my-2 ml-2 flex justify-start">
|
||||
@@ -469,9 +479,9 @@ const ConversationBubble = forwardRef<
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`fade-in-bubble bg-gray-1000 dark:bg-gun-metal mr-5 flex max-w-full rounded-[18px] px-6 py-4.5 ${
|
||||
className={`fade-in-bubble bg-answer-bubble mr-5 flex max-w-full rounded-[18px] px-6 py-4.5 ${
|
||||
type === 'ERROR'
|
||||
? 'text-red-3000 dark:border-red-2000 relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal dark:text-white'
|
||||
? 'text-destructive/80 dark:border-destructive dark:bg-destructive/15 relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal dark:text-white'
|
||||
: 'flex-col rounded-3xl'
|
||||
}`}
|
||||
>
|
||||
@@ -495,11 +505,26 @@ const ConversationBubble = forwardRef<
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`source-${sourceIdx}`);
|
||||
const el = document.getElementById(
|
||||
`source-${sourceIdx}`,
|
||||
);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('ring-2', 'ring-purple-500');
|
||||
setTimeout(() => el.classList.remove('ring-2', 'ring-purple-500'), 2000);
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
el.classList.add(
|
||||
'ring-2',
|
||||
'ring-purple-500',
|
||||
);
|
||||
setTimeout(
|
||||
() =>
|
||||
el.classList.remove(
|
||||
'ring-2',
|
||||
'ring-purple-500',
|
||||
),
|
||||
2000,
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mx-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-purple-100 px-1.5 text-xs font-semibold text-purple-700 transition-colors hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60"
|
||||
@@ -509,7 +534,15 @@ const ConversationBubble = forwardRef<
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>;
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code(props) {
|
||||
const {
|
||||
@@ -525,9 +558,9 @@ const ConversationBubble = forwardRef<
|
||||
const language = match ? match[1] : '';
|
||||
|
||||
return match ? (
|
||||
<div className="group border-light-silver dark:border-raisin-black relative overflow-hidden rounded-[14px] border">
|
||||
<div className="bg-platinum dark:bg-eerie-black-2 flex items-center justify-between px-2 py-1">
|
||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
||||
<div className="group border-border relative overflow-hidden rounded-[14px] border">
|
||||
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||
{language}
|
||||
</span>
|
||||
<CopyButton
|
||||
@@ -554,7 +587,7 @@ const ConversationBubble = forwardRef<
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<code className="dark:bg-independence dark:text-bright-gray rounded-[6px] bg-gray-200 px-2 py-1 text-xs font-normal whitespace-pre-line">
|
||||
<code className="dark:bg-accent dark:text-foreground rounded-[6px] bg-gray-200 px-2 py-1 text-xs font-normal whitespace-pre-line">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -579,8 +612,8 @@ const ConversationBubble = forwardRef<
|
||||
},
|
||||
table({ children }) {
|
||||
return (
|
||||
<div className="border-silver/40 dark:border-silver/40 relative overflow-x-auto rounded-lg border">
|
||||
<table className="dark:text-bright-gray w-full text-left text-gray-700">
|
||||
<div className="border-border relative overflow-x-auto rounded-lg border">
|
||||
<table className="dark:text-foreground w-full text-left text-gray-700">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@@ -588,14 +621,14 @@ const ConversationBubble = forwardRef<
|
||||
},
|
||||
thead({ children }) {
|
||||
return (
|
||||
<thead className="dark:text-bright-gray bg-gray-50 text-xs text-gray-900 uppercase dark:bg-[#26272E]/50">
|
||||
<thead className="bg-muted text-foreground text-xs uppercase">
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
},
|
||||
tr({ children }) {
|
||||
return (
|
||||
<tr className="dark:border-silver/40 border-b border-gray-200 odd:bg-white even:bg-gray-50 dark:odd:bg-[#26272E] dark:even:bg-[#26272E]/50">
|
||||
<tr className="border-border odd:bg-card even:bg-muted border-b">
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
@@ -691,7 +724,9 @@ const ConversationBubble = forwardRef<
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const blob = new Blob([message], { type: 'text/markdown' });
|
||||
const blob = new Blob([message], {
|
||||
type: 'text/markdown',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -699,12 +734,21 @@ const ConversationBubble = forwardRef<
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
|
||||
className="bg-card dark:hover:bg-accent hover:bg-muted flex cursor-pointer items-center justify-center rounded-full p-2 dark:bg-transparent"
|
||||
aria-label="Export as Markdown"
|
||||
title="Export as Markdown"
|
||||
>
|
||||
<svg className="h-5 w-5 stroke-gray-4000" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
<svg
|
||||
className="stroke-muted-foreground h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -717,7 +761,7 @@ const ConversationBubble = forwardRef<
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
|
||||
className="hover:bg-accent flex cursor-pointer items-center justify-center rounded-full bg-transparent p-2"
|
||||
onClick={() => {
|
||||
if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
@@ -730,7 +774,7 @@ const ConversationBubble = forwardRef<
|
||||
}
|
||||
>
|
||||
<Like
|
||||
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
|
||||
className={`${feedback === 'LIKE' ? 'stroke-primary fill-white dark:fill-transparent' : 'stroke-muted-foreground fill-none'}`}
|
||||
></Like>
|
||||
</button>
|
||||
</div>
|
||||
@@ -738,7 +782,7 @@ const ConversationBubble = forwardRef<
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
|
||||
className="hover:bg-accent flex cursor-pointer items-center justify-center rounded-full bg-transparent p-2"
|
||||
onClick={() => {
|
||||
if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
@@ -753,7 +797,7 @@ const ConversationBubble = forwardRef<
|
||||
}
|
||||
>
|
||||
<Dislike
|
||||
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
|
||||
className={`${feedback === 'DISLIKE' ? 'stroke-destructive fill-white dark:fill-transparent' : 'stroke-muted-foreground fill-none'}`}
|
||||
></Dislike>
|
||||
</button>
|
||||
</div>
|
||||
@@ -806,7 +850,7 @@ function AllSources(sources: AllSourcesProps) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`group/card bg-gray-1000 relative w-full rounded-4xl p-4 transition-colors hover:bg-[#F1F1F1] dark:bg-[#28292E] dark:hover:bg-[#2C2E3C] ${
|
||||
className={`group/card bg-muted hover:bg-accent dark:bg-card dark:hover:bg-muted relative w-full rounded-4xl p-4 transition-colors ${
|
||||
isExternalSource ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
@@ -817,7 +861,7 @@ function AllSources(sources: AllSourcesProps) {
|
||||
title={source.title}
|
||||
className={`ellipsis-text text-left text-sm font-semibold wrap-break-word ${
|
||||
isExternalSource
|
||||
? 'group-hover/card:text-purple-30 dark:group-hover/card:text-[#8C67D7]'
|
||||
? 'group-hover/card:text-primary dark:group-hover/card:text-[#8C67D7]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
@@ -834,7 +878,7 @@ function AllSources(sources: AllSourcesProps) {
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p className="dark:text-chinese-silver mt-3 line-clamp-4 rounded-md text-left text-xs wrap-break-word text-black">
|
||||
<p className="dark:text-foreground mt-3 line-clamp-4 rounded-md text-left text-xs wrap-break-word text-black">
|
||||
{source.text}
|
||||
</p>
|
||||
</div>
|
||||
@@ -846,109 +890,264 @@ function AllSources(sources: AllSourcesProps) {
|
||||
}
|
||||
export default ConversationBubble;
|
||||
|
||||
function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
|
||||
const [isToolCallsOpen, setIsToolCallsOpen] = useState(false);
|
||||
function ToolCallApprovalBar({
|
||||
toolCall,
|
||||
onToolAction,
|
||||
}: {
|
||||
toolCall: ToolCallsType;
|
||||
onToolAction?: (
|
||||
callId: string,
|
||||
decision: 'approved' | 'denied',
|
||||
comment?: string,
|
||||
) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [comment, setComment] = useState('');
|
||||
const actionLabel = toolCall.action_name.substring(
|
||||
0,
|
||||
toolCall.action_name.lastIndexOf('_'),
|
||||
);
|
||||
const argPreview = JSON.stringify(toolCall.arguments);
|
||||
const truncated =
|
||||
argPreview.length > 60 ? argPreview.slice(0, 57) + '...' : argPreview;
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[26px] w-[30px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt={'ToolCalls'}
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="flex flex-row items-center gap-2"
|
||||
onClick={() => setIsToolCallsOpen(!isToolCallsOpen)}
|
||||
<div className="border-border bg-muted dark:bg-card mb-2 w-full overflow-hidden rounded-2xl border">
|
||||
<div className="flex items-center gap-3 px-4 py-2.5">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="text-sm font-semibold whitespace-nowrap">
|
||||
{toolCall.tool_name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">{actionLabel}</span>
|
||||
<span
|
||||
className="text-muted-foreground hidden min-w-0 truncate font-mono text-xs md:block"
|
||||
title={argPreview}
|
||||
>
|
||||
{truncated}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className={`rounded-full px-4 py-1 text-xs font-medium transition-colors ${
|
||||
comment
|
||||
? 'bg-muted text-muted-foreground cursor-default opacity-50'
|
||||
: 'bg-primary hover:bg-primary/90 text-white'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!comment) onToolAction?.(toolCall.call_id, 'approved');
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
className={`rounded-full border px-4 py-1 text-xs font-medium transition-colors ${
|
||||
comment
|
||||
? 'border-destructive bg-destructive/10 text-destructive font-semibold'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (expanded && comment) {
|
||||
onToolAction?.(toolCall.call_id, 'denied', comment);
|
||||
} else if (expanded) {
|
||||
onToolAction?.(toolCall.call_id, 'denied');
|
||||
} else {
|
||||
setExpanded(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Deny
|
||||
</button>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground flex h-6 w-6 items-center justify-center rounded-full transition-colors"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title="Details"
|
||||
>
|
||||
<p className="text-base font-semibold">Tool Calls</p>
|
||||
<img
|
||||
src={ChevronDown}
|
||||
alt="ChevronDown"
|
||||
className={`h-4 w-4 transform transition-transform duration-200 dark:invert ${isToolCallsOpen ? 'rotate-180' : ''}`}
|
||||
alt="expand"
|
||||
className={`h-3.5 w-3.5 transition-transform duration-200 dark:invert ${expanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isToolCallsOpen && (
|
||||
<div className="fade-in mr-5 ml-3 w-[90vw] md:w-[70vw] lg:w-full">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{toolCalls.map((toolCall, index) => (
|
||||
<Accordion
|
||||
key={`tool-call-${index}`}
|
||||
title={`${toolCall.tool_name} - ${toolCall.action_name.substring(0, toolCall.action_name.lastIndexOf('_'))}`}
|
||||
className="bg-gray-1000 dark:bg-gun-metal w-full rounded-4xl hover:bg-[#F1F1F1] dark:hover:bg-[#2C2E3C]"
|
||||
titleClassName="px-6 py-2 text-sm font-semibold"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="border-silver dark:border-silver/20 flex flex-col rounded-2xl border">
|
||||
<p className="dark:bg-eerie-black-2 flex flex-row items-center justify-between rounded-t-2xl bg-black/10 px-2 py-1 text-sm font-semibold wrap-break-word">
|
||||
<span style={{ fontFamily: 'IBMPlexMono-Medium' }}>
|
||||
Arguments
|
||||
</span>{' '}
|
||||
<CopyButton
|
||||
textToCopy={JSON.stringify(toolCall.arguments, null, 2)}
|
||||
/>
|
||||
</p>
|
||||
<p className="dark:tex dark:bg-raisin-black rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="leading-[23px] text-black dark:text-gray-400"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-silver dark:border-silver/20 flex flex-col rounded-2xl border">
|
||||
<p className="dark:bg-eerie-black-2 flex flex-row items-center justify-between rounded-t-2xl bg-black/10 px-2 py-1 text-sm font-semibold wrap-break-word">
|
||||
<span style={{ fontFamily: 'IBMPlexMono-Medium' }}>
|
||||
Response
|
||||
</span>{' '}
|
||||
<CopyButton
|
||||
textToCopy={
|
||||
toolCall.status === 'error'
|
||||
? toolCall.error || 'Unknown error'
|
||||
: JSON.stringify(toolCall.result, null, 2)
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
{toolCall.status === 'pending' && (
|
||||
<span className="dark:bg-raisin-black flex w-full items-center justify-center rounded-b-2xl p-2">
|
||||
<Spinner size="small" />
|
||||
</span>
|
||||
)}
|
||||
{toolCall.status === 'completed' && (
|
||||
<p className="dark:bg-raisin-black rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="leading-[23px] text-black dark:text-gray-400"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{JSON.stringify(toolCall.result, null, 2)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{toolCall.status === 'error' && (
|
||||
<p className="dark:bg-raisin-black rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="leading-[23px] text-red-500 dark:text-red-400"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{toolCall.error}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="border-border border-t px-4 py-3">
|
||||
<p className="text-muted-foreground mb-1 text-xs font-medium">
|
||||
Arguments
|
||||
</p>
|
||||
<pre className="bg-background dark:bg-background/50 mb-2 max-h-40 overflow-auto rounded-lg p-2 font-mono text-xs">
|
||||
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||
</pre>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional reason for denying..."
|
||||
className="border-border bg-background w-full rounded-lg border px-3 py-1.5 text-sm"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && comment) {
|
||||
onToolAction?.(toolCall.call_id, 'denied', comment);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCalls({
|
||||
toolCalls,
|
||||
onToolAction,
|
||||
}: {
|
||||
toolCalls: ToolCallsType[];
|
||||
onToolAction?: (
|
||||
callId: string,
|
||||
decision: 'approved' | 'denied',
|
||||
comment?: string,
|
||||
) => void;
|
||||
}) {
|
||||
const [isToolCallsOpen, setIsToolCallsOpen] = useState(false);
|
||||
|
||||
const awaitingCalls = toolCalls.filter(
|
||||
(tc) => tc.status === 'awaiting_approval',
|
||||
);
|
||||
const resolvedCalls = toolCalls.filter(
|
||||
(tc) => tc.status !== 'awaiting_approval',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
{/* Approval bars — always visible, compact inline */}
|
||||
{awaitingCalls.length > 0 && (
|
||||
<div className="fade-in mt-4 ml-3 w-[90vw] md:w-[70vw] lg:w-full">
|
||||
{awaitingCalls.map((tc) => (
|
||||
<ToolCallApprovalBar
|
||||
key={`approval-${tc.call_id}`}
|
||||
toolCall={tc}
|
||||
onToolAction={onToolAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Regular tool calls accordion */}
|
||||
{resolvedCalls.length > 0 && (
|
||||
<>
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
<Avatar
|
||||
className="h-[26px] w-[30px] text-xl"
|
||||
avatar={
|
||||
<img
|
||||
src={Sources}
|
||||
alt={'ToolCalls'}
|
||||
className="h-full w-full object-fill"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="flex flex-row items-center gap-2"
|
||||
onClick={() => setIsToolCallsOpen(!isToolCallsOpen)}
|
||||
>
|
||||
<p className="text-base font-semibold">Tool Calls</p>
|
||||
<img
|
||||
src={ChevronDown}
|
||||
alt="ChevronDown"
|
||||
className={`h-4 w-4 transform transition-transform duration-200 dark:invert ${isToolCallsOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isToolCallsOpen && (
|
||||
<div className="fade-in mr-5 ml-3 w-[90vw] md:w-[70vw] lg:w-full">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{resolvedCalls.map((toolCall, index) => (
|
||||
<Accordion
|
||||
key={`tool-call-${index}`}
|
||||
title={`${toolCall.tool_name} - ${toolCall.action_name.substring(0, toolCall.action_name.lastIndexOf('_'))}`}
|
||||
className="bg-muted dark:bg-answer-bubble w-full rounded-4xl"
|
||||
titleClassName="px-6 py-2 text-sm font-semibold"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="border-border flex flex-col rounded-2xl border">
|
||||
<p className="dark:bg-background flex flex-row items-center justify-between rounded-t-2xl bg-black/10 px-2 py-1 text-sm font-semibold wrap-break-word">
|
||||
<span style={{ fontFamily: 'IBMPlexMono-Medium' }}>
|
||||
Arguments
|
||||
</span>{' '}
|
||||
<CopyButton
|
||||
textToCopy={JSON.stringify(
|
||||
toolCall.arguments,
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
/>
|
||||
</p>
|
||||
<p className="dark:bg-card rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="dark:text-muted-foreground leading-[23px] text-black"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-border flex flex-col rounded-2xl border">
|
||||
<p className="dark:bg-background flex flex-row items-center justify-between rounded-t-2xl bg-black/10 px-2 py-1 text-sm font-semibold wrap-break-word">
|
||||
<span style={{ fontFamily: 'IBMPlexMono-Medium' }}>
|
||||
Response
|
||||
</span>{' '}
|
||||
<CopyButton
|
||||
textToCopy={
|
||||
toolCall.status === 'error'
|
||||
? toolCall.error || 'Unknown error'
|
||||
: JSON.stringify(toolCall.result, null, 2)
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
{toolCall.status === 'pending' && (
|
||||
<span className="dark:bg-card flex w-full items-center justify-center rounded-b-2xl p-2">
|
||||
<Spinner size="small" />
|
||||
</span>
|
||||
)}
|
||||
{toolCall.status === 'completed' && (
|
||||
<p className="dark:bg-card rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="dark:text-muted-foreground leading-[23px] text-black"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{JSON.stringify(toolCall.result, null, 2)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{toolCall.status === 'error' && (
|
||||
<p className="dark:bg-card rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="text-destructive leading-[23px]"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
{toolCall.error}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{toolCall.status === 'denied' && (
|
||||
<p className="dark:bg-card rounded-b-2xl p-2 font-mono text-sm wrap-break-word">
|
||||
<span
|
||||
className="text-muted-foreground leading-[23px]"
|
||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||
>
|
||||
Denied by user
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -992,7 +1191,7 @@ function Thought({
|
||||
</div>
|
||||
{isThoughtOpen && (
|
||||
<div className="fade-in mr-5 ml-2 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
|
||||
<div className="bg-gray-1000 dark:bg-gun-metal rounded-[28px] px-7 py-[18px]">
|
||||
<div className="bg-muted dark:bg-answer-bubble rounded-[28px] px-7 py-[18px]">
|
||||
<ReactMarkdown
|
||||
className="fade-in leading-normal wrap-break-word whitespace-pre-wrap"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
@@ -1004,9 +1203,9 @@ function Thought({
|
||||
const language = match ? match[1] : '';
|
||||
|
||||
return match ? (
|
||||
<div className="group border-light-silver dark:border-raisin-black relative overflow-hidden rounded-[14px] border">
|
||||
<div className="bg-platinum dark:bg-eerie-black-2 flex items-center justify-between px-2 py-1">
|
||||
<span className="text-just-black dark:text-chinese-white text-xs font-medium">
|
||||
<div className="group border-border relative overflow-hidden rounded-[14px] border">
|
||||
<div className="bg-platinum flex items-center justify-between px-2 py-1">
|
||||
<span className="text-foreground dark:text-foreground text-xs font-medium">
|
||||
{language}
|
||||
</span>
|
||||
<CopyButton
|
||||
@@ -1028,7 +1227,7 @@ function Thought({
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<code className="dark:bg-independence dark:text-bright-gray rounded-[6px] bg-gray-200 px-2 py-1 text-xs font-normal whitespace-pre-line">
|
||||
<code className="dark:bg-accent dark:text-foreground rounded-[6px] bg-gray-200 px-2 py-1 text-xs font-normal whitespace-pre-line">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -1049,8 +1248,8 @@ function Thought({
|
||||
},
|
||||
table({ children }) {
|
||||
return (
|
||||
<div className="border-silver/40 dark:border-silver/40 relative overflow-x-auto rounded-lg border">
|
||||
<table className="dark:text-bright-gray w-full text-left text-gray-700">
|
||||
<div className="border-border relative overflow-x-auto rounded-lg border">
|
||||
<table className="dark:text-foreground w-full text-left text-gray-700">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@@ -1058,14 +1257,14 @@ function Thought({
|
||||
},
|
||||
thead({ children }) {
|
||||
return (
|
||||
<thead className="dark:text-bright-gray bg-gray-50 text-xs text-gray-900 uppercase dark:bg-[#26272E]/50">
|
||||
<thead className="bg-muted text-foreground text-xs uppercase">
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
},
|
||||
tr({ children }) {
|
||||
return (
|
||||
<tr className="dark:border-silver/40 border-b border-gray-200 odd:bg-white even:bg-gray-50 dark:odd:bg-[#26272E] dark:even:bg-[#26272E]/50">
|
||||
<tr className="border-border odd:bg-card even:bg-muted border-b">
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,11 @@ type ConversationMessagesProps = {
|
||||
showHeroOnEmpty?: boolean;
|
||||
headerContent?: ReactNode;
|
||||
onOpenArtifact?: (artifact: { id: string; toolName: string }) => void;
|
||||
onToolAction?: (
|
||||
callId: string,
|
||||
decision: 'approved' | 'denied',
|
||||
comment?: string,
|
||||
) => void;
|
||||
isSplitView?: boolean;
|
||||
};
|
||||
|
||||
@@ -50,6 +55,7 @@ export default function ConversationMessages({
|
||||
showHeroOnEmpty = true,
|
||||
headerContent,
|
||||
onOpenArtifact,
|
||||
onToolAction,
|
||||
isSplitView = false,
|
||||
}: ConversationMessagesProps) {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
@@ -154,6 +160,7 @@ export default function ConversationMessages({
|
||||
toolCalls={query.tool_calls}
|
||||
research={query.research}
|
||||
onOpenArtifact={onOpenArtifact}
|
||||
onToolAction={onToolAction}
|
||||
feedback={query.feedback}
|
||||
isStreaming={isCurrentlyStreaming}
|
||||
handleFeedback={
|
||||
@@ -168,7 +175,7 @@ export default function ConversationMessages({
|
||||
if (query.error) {
|
||||
const retryButton = (
|
||||
<button
|
||||
className="dark:text-bright-gray flex items-center justify-center gap-3 self-center rounded-full px-5 py-3 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed"
|
||||
className="dark:text-foreground flex items-center justify-center gap-3 self-center rounded-full px-5 py-3 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed"
|
||||
disabled={status === 'loading'}
|
||||
onClick={() => {
|
||||
const questionToRetry = queries[index].prompt;
|
||||
@@ -197,7 +204,7 @@ export default function ConversationMessages({
|
||||
if (status === 'loading' && isLastMessage) {
|
||||
return (
|
||||
<div
|
||||
className={`fade-in-bubble flex flex-wrap self-start ${bubbleMargin} group dark:text-bright-gray flex-col`}
|
||||
className={`fade-in-bubble flex flex-wrap self-start ${bubbleMargin} group dark:text-foreground flex-col`}
|
||||
>
|
||||
<div className="flex max-w-full flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
<div className="my-2 flex flex-row items-center justify-center gap-3">
|
||||
@@ -241,7 +248,7 @@ export default function ConversationMessages({
|
||||
scrollConversationToBottom();
|
||||
}}
|
||||
aria-label={t('Scroll to bottom') || 'Scroll to bottom'}
|
||||
className="border-gray-alpha bg-opacity-50 dark:bg-gunmetal md:bg-opacity-100 fixed right-14 bottom-40 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] bg-gray-100 md:h-9 md:w-9"
|
||||
className="border-border bg-card fixed right-14 bottom-40 z-10 flex h-7 w-7 items-center justify-center rounded-full border md:h-9 md:w-9"
|
||||
>
|
||||
<img
|
||||
src={ArrowDown}
|
||||
|
||||
@@ -192,9 +192,9 @@ export default function ConversationTile({
|
||||
conversationId !== conversation.id &&
|
||||
selectConversation(conversation.id);
|
||||
}}
|
||||
className={`hover:bg-bright-gray dark:hover:bg-dark-charcoal mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl pl-4 ${
|
||||
className={`hover:bg-sidebar-accent mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl pl-4 ${
|
||||
conversationId === conversation.id || isOpen || isHovered || isEdit
|
||||
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||
? 'bg-sidebar-accent'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
@@ -203,19 +203,22 @@ export default function ConversationTile({
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
className="h-6 w-full bg-transparent px-1 text-sm leading-6 rounded-2xl font-normal outline-none"
|
||||
className="h-6 w-full rounded-2xl bg-transparent px-1 text-sm leading-6 font-normal outline-none"
|
||||
value={conversationName}
|
||||
onChange={(e) => setConversationsName(e.target.value)}
|
||||
onKeyDown={handleRenameKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-eerie-black dark:text-bright-gray my-auto overflow-hidden text-sm leading-6 font-normal text-ellipsis whitespace-nowrap">
|
||||
<p className="text-foreground dark:text-foreground my-auto overflow-hidden text-sm leading-6 font-normal text-ellipsis whitespace-nowrap">
|
||||
{conversationName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{(conversationId === conversation.id || isHovered || isOpen) && (
|
||||
<div className="dark:text-sonic-silver flex text-white" ref={menuRef}>
|
||||
<div
|
||||
className="dark:text-muted-foreground flex text-white"
|
||||
ref={menuRef}
|
||||
>
|
||||
{isEdit ? (
|
||||
<div className="flex gap-1">
|
||||
<img
|
||||
@@ -248,7 +251,7 @@ export default function ConversationTile({
|
||||
event.stopPropagation();
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
className="mr-2 flex h-6 w-6 items-center justify-center rounded-full transition-colors duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
className="hover:bg-accent dark:hover:bg-accent mr-2 flex h-6 w-6 items-center justify-center rounded-full transition-colors duration-200"
|
||||
>
|
||||
<img src={threeDots} width={8} alt="menu" />
|
||||
</button>
|
||||
|
||||
@@ -4,15 +4,36 @@ import Avatar from '../components/Avatar';
|
||||
import { ResearchState } from './conversationModels';
|
||||
|
||||
const SmallCheck = () => (
|
||||
<svg className="h-3 w-3 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<svg
|
||||
className="h-3 w-3 text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SmallSpinner = () => (
|
||||
<svg className="h-3 w-3 animate-spin text-purple-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
<svg
|
||||
className="h-3 w-3 animate-spin text-purple-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -32,7 +53,9 @@ function StatusText({ status, elapsed }: { status: string; elapsed?: number }) {
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{status === 'complete' ? (
|
||||
<>
|
||||
<span className="text-green-600 dark:text-green-400">{labels.complete}</span>
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
{labels.complete}
|
||||
</span>
|
||||
{elapsed_str}
|
||||
</>
|
||||
) : (
|
||||
@@ -62,13 +85,15 @@ export default function ResearchProgress({
|
||||
|
||||
if (!plan && !status) return null;
|
||||
|
||||
const completedSteps = plan?.filter((s) => s.status === 'complete').length ?? 0;
|
||||
const completedSteps =
|
||||
plan?.filter((s) => s.status === 'complete').length ?? 0;
|
||||
const totalSteps = plan?.length ?? 0;
|
||||
|
||||
// Collapsed: single-line summary
|
||||
const summaryText = totalSteps > 0
|
||||
? `Researched ${completedSteps} topic${completedSteps !== 1 ? 's' : ''}`
|
||||
: 'Research';
|
||||
const summaryText =
|
||||
totalSteps > 0
|
||||
? `Researched ${completedSteps} topic${completedSteps !== 1 ? 's' : ''}`
|
||||
: 'Research';
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
@@ -93,9 +118,16 @@ export default function ResearchProgress({
|
||||
</p>
|
||||
<svg
|
||||
className={`h-4 w-4 text-gray-500 transition-transform duration-200 dark:text-gray-400 ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{status && <StatusText status={status} elapsed={elapsed_seconds} />}
|
||||
@@ -126,13 +158,15 @@ export default function ResearchProgress({
|
||||
</div>
|
||||
{/* Step content */}
|
||||
<div className={`pb-3 ${isLast ? '' : ''}`}>
|
||||
<p className={`text-sm ${
|
||||
step.status === 'complete'
|
||||
? 'text-gray-700 dark:text-gray-300'
|
||||
: step.status === 'researching'
|
||||
? 'font-medium text-purple-700 dark:text-purple-300'
|
||||
: 'text-gray-500 dark:text-gray-500'
|
||||
}`}>
|
||||
<p
|
||||
className={`text-sm ${
|
||||
step.status === 'complete'
|
||||
? 'text-gray-700 dark:text-gray-300'
|
||||
: step.status === 'researching'
|
||||
? 'font-medium text-purple-700 dark:text-purple-300'
|
||||
: 'text-gray-500 dark:text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{step.query}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -138,18 +138,18 @@ export const SharedConversation = () => {
|
||||
twitterTitle={title}
|
||||
twitterDescription="Shared conversations with DocsGPT"
|
||||
/>
|
||||
<div className="dark:bg-raisin-black flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden">
|
||||
<div className="bg-background flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden">
|
||||
<div className="dark:border-b-silver w-full max-w-[1200px] border-b p-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
<h1 className="font-semi-bold text-chinese-black dark:text-chinese-silver text-4xl">
|
||||
<h1 className="font-semi-bold text-foreground dark:text-foreground text-4xl">
|
||||
{title}
|
||||
</h1>
|
||||
<h2 className="font-semi-bold text-chinese-black dark:text-chinese-silver text-base">
|
||||
<h2 className="font-semi-bold text-foreground dark:text-foreground text-base">
|
||||
{t('sharedConv.subtitle')}{' '}
|
||||
<a href="/" className="text-[#007DFF]">
|
||||
DocsGPT
|
||||
</a>
|
||||
</h2>
|
||||
<h2 className="font-semi-bold text-chinese-black dark:text-chinese-silver text-base">
|
||||
<h2 className="font-semi-bold text-foreground dark:text-foreground text-base">
|
||||
{date}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -174,13 +174,13 @@ export const SharedConversation = () => {
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue mb-14 w-fit rounded-full px-5 py-3 text-white shadow-xl transition-colors duration-200 sm:mb-0"
|
||||
className="bg-primary hover:bg-primary/90 mb-14 w-fit rounded-full px-5 py-3 text-white shadow-xl transition-colors duration-200 sm:mb-0"
|
||||
>
|
||||
{t('sharedConv.button')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<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('sharedConv.meta')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -188,6 +188,264 @@ export function handleFetchAnswerSteaming(
|
||||
});
|
||||
}
|
||||
|
||||
export function handleSubmitToolActions(
|
||||
conversationId: string,
|
||||
toolActions: {
|
||||
call_id: string;
|
||||
decision?: 'approved' | 'denied';
|
||||
comment?: string;
|
||||
result?: Record<string, any>;
|
||||
}[],
|
||||
token: string | null,
|
||||
signal: AbortSignal,
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
): Promise<Answer> {
|
||||
const payload = {
|
||||
conversation_id: conversationId,
|
||||
tool_actions: toolActions,
|
||||
};
|
||||
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
conversationService
|
||||
.answerStream(payload, token, signal)
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
let buffer = '';
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) return;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
buffer += chunk;
|
||||
|
||||
const events = buffer.split('\n\n');
|
||||
buffer = events.pop() ?? '';
|
||||
|
||||
for (const event of events) {
|
||||
if (event.trim().startsWith('data:')) {
|
||||
const dataLine: string = event
|
||||
.split('\n')
|
||||
.map((line: string) => line.replace(/^data:\s?/, ''))
|
||||
.join('');
|
||||
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: dataLine.trim(),
|
||||
});
|
||||
|
||||
onEvent(messageEvent);
|
||||
}
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
};
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Tool actions submission failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a chat completion via the /v1/chat/completions endpoint.
|
||||
*
|
||||
* Translates the standard streaming format (choices[0].delta) back into
|
||||
* the internal DocsGPT event shape so the existing Redux reducers can
|
||||
* consume the events without any changes.
|
||||
*/
|
||||
export function handleV1ChatCompletionStreaming(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
agentApiKey: string,
|
||||
history: { prompt: string; response: string }[],
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
tools?: any[],
|
||||
attachments?: string[],
|
||||
): Promise<Answer> {
|
||||
// Build messages array from history + current question
|
||||
const messages: any[] = [];
|
||||
for (const h of history) {
|
||||
messages.push({ role: 'user', content: h.prompt });
|
||||
messages.push({ role: 'assistant', content: h.response });
|
||||
}
|
||||
messages.push({ role: 'user', content: question });
|
||||
|
||||
const payload: any = {
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
if (tools && tools.length > 0) {
|
||||
payload.tools = tools;
|
||||
}
|
||||
if (attachments && attachments.length > 0) {
|
||||
payload.docsgpt = { attachments };
|
||||
}
|
||||
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
conversationService
|
||||
.chatCompletions(payload, agentApiKey, signal)
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
let buffer = '';
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) return;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
buffer += chunk;
|
||||
|
||||
const events = buffer.split('\n\n');
|
||||
buffer = events.pop() ?? '';
|
||||
|
||||
for (const event of events) {
|
||||
if (!event.trim().startsWith('data:')) continue;
|
||||
|
||||
const dataLine = event
|
||||
.split('\n')
|
||||
.map((line: string) => line.replace(/^data:\s?/, ''))
|
||||
.join('');
|
||||
|
||||
const trimmed = dataLine.trim();
|
||||
|
||||
// Handle [DONE] sentinel
|
||||
if (trimmed === '[DONE]') {
|
||||
onEvent(
|
||||
new MessageEvent('message', {
|
||||
data: JSON.stringify({ type: 'end' }),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
// Translate standard format to DocsGPT internal events
|
||||
const translated = translateV1ChunkToInternalEvents(parsed);
|
||||
for (const evt of translated) {
|
||||
onEvent(
|
||||
new MessageEvent('message', {
|
||||
data: JSON.stringify(evt),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Skip unparseable chunks
|
||||
}
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
};
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('V1 chat completion stream failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a single v1 streaming chunk to internal DocsGPT event(s).
|
||||
*
|
||||
* Standard format: {"choices": [{"delta": {"content": "chunk"}, ...}]}
|
||||
* Extension format: {"docsgpt": {"type": "source", ...}}
|
||||
*/
|
||||
function translateV1ChunkToInternalEvents(
|
||||
chunk: any,
|
||||
): { type: string; [key: string]: any }[] {
|
||||
const events: { type: string; [key: string]: any }[] = [];
|
||||
|
||||
// DocsGPT extension chunks
|
||||
if (chunk.docsgpt) {
|
||||
const ext = chunk.docsgpt;
|
||||
if (ext.type === 'source') {
|
||||
events.push({ type: 'source', source: ext.sources });
|
||||
} else if (ext.type === 'tool_call') {
|
||||
events.push({ type: 'tool_call', data: ext.data });
|
||||
} else if (ext.type === 'tool_calls_pending') {
|
||||
events.push({
|
||||
type: 'tool_calls_pending',
|
||||
data: { pending_tool_calls: ext.pending_tool_calls },
|
||||
});
|
||||
} else if (ext.type === 'id') {
|
||||
events.push({ type: 'id', id: ext.conversation_id });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
// Error chunks
|
||||
if (chunk.error) {
|
||||
events.push({ type: 'error', error: chunk.error.message || 'Error' });
|
||||
return events;
|
||||
}
|
||||
|
||||
// Standard choices chunks
|
||||
const choice = chunk.choices?.[0];
|
||||
if (!choice) return events;
|
||||
|
||||
const delta = choice.delta || {};
|
||||
const finishReason = choice.finish_reason;
|
||||
|
||||
if (delta.content) {
|
||||
events.push({ type: 'answer', answer: delta.content });
|
||||
}
|
||||
|
||||
if (delta.reasoning_content) {
|
||||
events.push({ type: 'thought', thought: delta.reasoning_content });
|
||||
}
|
||||
|
||||
if (delta.tool_calls) {
|
||||
for (const tc of delta.tool_calls) {
|
||||
let parsedArgs: Record<string, any> = {};
|
||||
if (tc.function?.arguments) {
|
||||
try {
|
||||
parsedArgs = JSON.parse(tc.function.arguments);
|
||||
} catch {
|
||||
// Arguments may arrive as fragments during streaming;
|
||||
// keep the raw string so downstream can accumulate it.
|
||||
parsedArgs = { _raw: tc.function.arguments };
|
||||
}
|
||||
}
|
||||
events.push({
|
||||
type: 'tool_call',
|
||||
data: {
|
||||
call_id: tc.id,
|
||||
action_name: tc.function?.name || '',
|
||||
tool_name: tc.function?.name || '',
|
||||
arguments: parsedArgs,
|
||||
status: 'requires_client_execution',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (finishReason === 'stop') {
|
||||
events.push({ type: 'end' });
|
||||
} else if (finishReason === 'tool_calls') {
|
||||
events.push({
|
||||
type: 'tool_calls_pending',
|
||||
data: { pending_tool_calls: [] },
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
export function handleSearch(
|
||||
question: string,
|
||||
token: string | null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToolCallsType } from './types';
|
||||
|
||||
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
|
||||
export type Status = 'idle' | 'loading' | 'failed';
|
||||
export type Status = 'idle' | 'loading' | 'failed' | 'awaiting_tool_actions';
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE' | null;
|
||||
|
||||
export interface Message {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
handleSubmitToolActions,
|
||||
handleV1ChatCompletionStreaming,
|
||||
} from './conversationHandlers';
|
||||
import {
|
||||
Answer,
|
||||
@@ -27,6 +29,7 @@ const initialState: ConversationState = {
|
||||
};
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
const USE_V1_API = import.meta.env.VITE_USE_V1_API === 'true';
|
||||
|
||||
let abortController: AbortController | null = null;
|
||||
export function handleAbort() {
|
||||
@@ -60,7 +63,102 @@ export const fetchAnswer = createAsyncThunk<
|
||||
state.preference.selectedModel?.id;
|
||||
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
const agentKey = state.preference.selectedAgent?.key;
|
||||
if (USE_V1_API && agentKey) {
|
||||
// Build history from prior queries for v1 format
|
||||
const v1History = state.conversation.queries
|
||||
.filter((q) => q.response)
|
||||
.map((q) => ({ prompt: q.prompt, response: q.response || '' }));
|
||||
|
||||
await handleV1ChatCompletionStreaming(
|
||||
question,
|
||||
signal,
|
||||
agentKey,
|
||||
v1History,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||
|
||||
if (currentConversationId === state.conversation.conversationId) {
|
||||
if (data.type === 'end') {
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
if (!isSourceUpdated) {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { sources: [] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (data.type === 'id') {
|
||||
const currentState = getState() as RootState;
|
||||
if (currentState.conversation.conversationId === null) {
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: data.id },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (data.type === 'thought') {
|
||||
dispatch(
|
||||
updateThought({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { thought: data.thought },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'source') {
|
||||
isSourceUpdated = true;
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_call') {
|
||||
dispatch(
|
||||
updateToolCall({
|
||||
index: targetIndex,
|
||||
tool_call: data.data as ToolCallsType,
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_calls_pending') {
|
||||
dispatch(
|
||||
conversationSlice.actions.setStatus('awaiting_tool_actions'),
|
||||
);
|
||||
} else if (data.type === 'error') {
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { response: data.answer },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
attachmentIds.length > 0 ? attachmentIds : undefined,
|
||||
);
|
||||
} else if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
@@ -138,6 +236,10 @@ export const fetchAnswer = createAsyncThunk<
|
||||
tool_call: data.data as ToolCallsType,
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_calls_pending') {
|
||||
dispatch(
|
||||
conversationSlice.actions.setStatus('awaiting_tool_actions'),
|
||||
);
|
||||
} else if (data.type === 'research_plan') {
|
||||
dispatch(
|
||||
updateResearchPlan({
|
||||
@@ -260,6 +362,94 @@ export const fetchAnswer = createAsyncThunk<
|
||||
};
|
||||
});
|
||||
|
||||
export const submitToolActions = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
toolActions: {
|
||||
call_id: string;
|
||||
decision?: 'approved' | 'denied';
|
||||
comment?: string;
|
||||
result?: Record<string, any>;
|
||||
}[];
|
||||
}
|
||||
>('submitToolActions', async ({ toolActions }, { dispatch, getState }) => {
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
const state = getState() as RootState;
|
||||
const conversationId = state.conversation.conversationId;
|
||||
if (!conversationId) return;
|
||||
|
||||
dispatch(conversationSlice.actions.setStatus('loading'));
|
||||
|
||||
await handleSubmitToolActions(
|
||||
conversationId,
|
||||
toolActions,
|
||||
state.preference.token,
|
||||
signal,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const targetIndex = state.conversation.queries.length - 1;
|
||||
|
||||
if (data.type === 'end') {
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
} else if (data.type === 'id') {
|
||||
// conversation ID already set
|
||||
} else if (data.type === 'thought') {
|
||||
dispatch(
|
||||
updateThought({
|
||||
conversationId,
|
||||
index: targetIndex,
|
||||
query: { thought: data.thought },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'source') {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
conversationId,
|
||||
index: targetIndex,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_call') {
|
||||
dispatch(
|
||||
updateToolCall({
|
||||
index: targetIndex,
|
||||
tool_call: data.data as ToolCallsType,
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_calls_pending') {
|
||||
dispatch(conversationSlice.actions.setStatus('awaiting_tool_actions'));
|
||||
} else if (data.type === 'error') {
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
conversationId,
|
||||
index: targetIndex,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'answer') {
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
conversationId,
|
||||
index: targetIndex,
|
||||
query: { response: data.answer },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
export const conversationSlice = createSlice({
|
||||
name: 'conversation',
|
||||
initialState,
|
||||
@@ -427,7 +617,10 @@ export const conversationSlice = createSlice({
|
||||
status: 'pending',
|
||||
});
|
||||
}
|
||||
if (progress.status === 'researching' || progress.status === 'complete') {
|
||||
if (
|
||||
progress.status === 'researching' ||
|
||||
progress.status === 'complete'
|
||||
) {
|
||||
research.plan[stepIndex].status = progress.status;
|
||||
}
|
||||
if (progress.query) {
|
||||
|
||||
@@ -5,6 +5,12 @@ export type ToolCallsType = {
|
||||
arguments: Record<string, any>;
|
||||
result?: Record<string, any>;
|
||||
error?: string;
|
||||
status?: 'pending' | 'completed' | 'error';
|
||||
status?:
|
||||
| 'pending'
|
||||
| 'completed'
|
||||
| 'error'
|
||||
| 'awaiting_approval'
|
||||
| 'denied'
|
||||
| 'requires_client_execution';
|
||||
artifact_id?: string;
|
||||
};
|
||||
|
||||
@@ -17,12 +17,9 @@ layer(base);
|
||||
--color-gray-1000: #f6f6f6;
|
||||
--color-gray-2000: rgba(0, 0, 0, 0.5);
|
||||
--color-gray-3000: rgba(243, 243, 243, 1);
|
||||
--color-gray-4000: #949494;
|
||||
--color-gray-5000: #bbbbbb;
|
||||
--color-gray-6000: #757575;
|
||||
--color-red-1000: rgb(254, 202, 202);
|
||||
--color-red-2000: #f44336;
|
||||
--color-red-3000: #621b16;
|
||||
--color-blue-1000: #7d54d1;
|
||||
--color-blue-2000: #002b49;
|
||||
--color-blue-3000: #4b02e2;
|
||||
@@ -32,7 +29,6 @@ layer(base);
|
||||
--color-blue-5000: rgba(0, 125, 255);
|
||||
--color-green-2000: #0fff50;
|
||||
--color-light-gray: #edeef0;
|
||||
--color-white-3000: #ffffff;
|
||||
--color-just-black: #00000;
|
||||
--color-purple-taupe: #464152;
|
||||
--color-dove-gray: #6c6c6c;
|
||||
@@ -57,7 +53,6 @@ layer(base);
|
||||
--color-charleston-green-2: #26272e;
|
||||
--color-charleston-green-3: #26272a;
|
||||
--color-grey: #7e7e7e;
|
||||
--color-lotion: #fbfbfb;
|
||||
--color-platinum: #e6e6e6;
|
||||
--color-eerie-black-2: #191919;
|
||||
--color-light-silver: #d9d9d9;
|
||||
@@ -69,15 +64,11 @@ layer(base);
|
||||
--color-onyx-2: #35383c;
|
||||
--color-philippine-grey: #929292;
|
||||
--color-charcoal-grey: #53545d;
|
||||
--color-rosso-corsa: #d30000;
|
||||
--color-north-texas-green: #0c9d35;
|
||||
--color-medium-purple: #8d66dd;
|
||||
--color-slate-blue: #6f5fca;
|
||||
--color-old-silver: #848484;
|
||||
--color-arsenic: #4d4e58;
|
||||
--color-light-gainsboro: #d7d7d7;
|
||||
--color-raisin-black-light: #18181b;
|
||||
--color-gunmetal: #32333b;
|
||||
--color-sonic-silver-light: #7f7f82;
|
||||
--color-violets-are-blue: #976af3;
|
||||
}
|
||||
@@ -201,7 +192,7 @@ layer(base);
|
||||
}
|
||||
|
||||
@utility table-default {
|
||||
@apply border-silver dark:border-silver/40 dark:text-bright-gray block w-full table-auto justify-center overflow-auto rounded-xl border text-center;
|
||||
@apply border-border text-foreground block w-full table-auto justify-center overflow-auto rounded-xl border text-center;
|
||||
|
||||
& th {
|
||||
@apply p-4 font-normal text-nowrap text-gray-400;
|
||||
@@ -216,7 +207,7 @@ layer(base);
|
||||
}
|
||||
|
||||
& td {
|
||||
@apply border-silver dark:border-silver/40 w-full border-t px-4 py-2;
|
||||
@apply border-border w-full border-t px-4 py-2;
|
||||
}
|
||||
|
||||
& td:last-child {
|
||||
@@ -262,7 +253,7 @@ layer(base);
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: #202124; /* raisin-black */
|
||||
background-color: var(--background);
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -868,6 +859,7 @@ Avoid over-scrolling in mobile browsers
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-answer-bubble: var(--answer-bubble);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -876,37 +868,38 @@ Avoid over-scrolling in mobile browsers
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.554 0.185 294.8); /* purple-30 #7d54d1 */
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.914 0.035 300.2); /* purple-3000 - light purple */
|
||||
--secondary-foreground: oklch(0.554 0.185 294.8);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.914 0.035 300.2); /* purple-3000 - light purple hover */
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.870 0 0); /* neutral gray border */
|
||||
--ring: oklch(0.554 0.185 294.8); /* purple-30 focus ring */
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #171717;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #171717;
|
||||
--primary: #7d54d1;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f4f4f4;
|
||||
--secondary-foreground: #7d54d1;
|
||||
--muted: #f6f6f6;
|
||||
--muted-foreground: #737373;
|
||||
--accent: #ececec;
|
||||
--accent-foreground: #171717;
|
||||
--destructive: #ef4444;
|
||||
--border: #d9d9d9;
|
||||
--input: #d9d9d9;
|
||||
--ring: #7d54d1;
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.554 0.185 294.8); /* purple-30 */
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.914 0.035 300.2); /* purple-3000 */
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.554 0.185 294.8); /* purple-30 */
|
||||
--sidebar: #fbfbfb;
|
||||
--sidebar-foreground: #171717;
|
||||
--sidebar-primary: #7d54d1;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #ececec;
|
||||
--sidebar-accent-foreground: #171717;
|
||||
--sidebar-border: #d9d9d9;
|
||||
--sidebar-ring: #7d54d1;
|
||||
--answer-bubble: #f6f6f6;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -914,37 +907,38 @@ Avoid over-scrolling in mobile browsers
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.636 0.197 295.4); /* violets-are-blue #976af3 */
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.269 0.03 295.0); /* dark muted purple */
|
||||
--secondary-foreground: oklch(0.867 0.052 300.1);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0.04 295.0); /* dark purple hover */
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.636 0.197 295.4); /* violets-are-blue focus ring */
|
||||
--background: #222327;
|
||||
--foreground: #fafafa;
|
||||
--card: #2b2c31;
|
||||
--card-foreground: #fafafa;
|
||||
--popover: #2b2c31;
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #976af3;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #2b2c31;
|
||||
--secondary-foreground: #e0e0e0;
|
||||
--muted: #35363b;
|
||||
--muted-foreground: #a1a1a1;
|
||||
--accent: #3e3f45;
|
||||
--accent-foreground: #fafafa;
|
||||
--destructive: #dc2626;
|
||||
--border: #44454c;
|
||||
--input: #44454c;
|
||||
--ring: #976af3;
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.636 0.197 295.4); /* violets-are-blue */
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0.04 295.0); /* dark purple */
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.636 0.197 295.4); /* violets-are-blue */
|
||||
--sidebar: #161616;
|
||||
--sidebar-foreground: #fafafa;
|
||||
--sidebar-primary: #976af3;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #222327;
|
||||
--sidebar-accent-foreground: #fafafa;
|
||||
--sidebar-border: #2b2c31;
|
||||
--sidebar-ring: #976af3;
|
||||
--answer-bubble: #2e303e;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user