mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
feat: shared and pinning agents + fix for streaming tools
This commit is contained in:
@@ -88,19 +88,28 @@ def run_async_chain(chain, question, chat_history):
|
|||||||
|
|
||||||
def get_agent_key(agent_id, user_id):
|
def get_agent_key(agent_id, user_id):
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
return None
|
return None, False, None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
|
agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
|
||||||
if agent is None:
|
if agent is None:
|
||||||
raise Exception("Agent not found", 404)
|
raise Exception("Agent not found", 404)
|
||||||
|
|
||||||
if agent.get("user") == user_id:
|
is_owner = agent.get("user") == user_id
|
||||||
|
|
||||||
|
if is_owner:
|
||||||
agents_collection.update_one(
|
agents_collection.update_one(
|
||||||
{"_id": ObjectId(agent_id)},
|
{"_id": ObjectId(agent_id)},
|
||||||
{"$set": {"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)}},
|
{"$set": {"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)}},
|
||||||
)
|
)
|
||||||
return str(agent["key"])
|
return str(agent["key"]), False, None
|
||||||
|
|
||||||
|
is_shared_with_user = agent.get(
|
||||||
|
"shared_publicly", False
|
||||||
|
) or user_id in agent.get("shared_with", [])
|
||||||
|
|
||||||
|
if is_shared_with_user:
|
||||||
|
return str(agent["key"]), True, agent.get("shared_token")
|
||||||
|
|
||||||
raise Exception("Unauthorized access to the agent", 403)
|
raise Exception("Unauthorized access to the agent", 403)
|
||||||
|
|
||||||
@@ -153,6 +162,8 @@ def save_conversation(
|
|||||||
index=None,
|
index=None,
|
||||||
api_key=None,
|
api_key=None,
|
||||||
agent_id=None,
|
agent_id=None,
|
||||||
|
is_shared_usage=False,
|
||||||
|
shared_token=None,
|
||||||
):
|
):
|
||||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
current_time = datetime.datetime.now(datetime.timezone.utc)
|
||||||
if conversation_id is not None and index is not None:
|
if conversation_id is not None and index is not None:
|
||||||
@@ -228,6 +239,9 @@ def save_conversation(
|
|||||||
if api_key:
|
if api_key:
|
||||||
if agent_id:
|
if agent_id:
|
||||||
conversation_data["agent_id"] = agent_id
|
conversation_data["agent_id"] = agent_id
|
||||||
|
if is_shared_usage:
|
||||||
|
conversation_data["is_shared_usage"] = is_shared_usage
|
||||||
|
conversation_data["shared_token"] = shared_token
|
||||||
api_key_doc = agents_collection.find_one({"key": api_key})
|
api_key_doc = agents_collection.find_one({"key": api_key})
|
||||||
if api_key_doc:
|
if api_key_doc:
|
||||||
conversation_data["api_key"] = api_key_doc["key"]
|
conversation_data["api_key"] = api_key_doc["key"]
|
||||||
@@ -261,6 +275,8 @@ def complete_stream(
|
|||||||
should_save_conversation=True,
|
should_save_conversation=True,
|
||||||
attachments=None,
|
attachments=None,
|
||||||
agent_id=None,
|
agent_id=None,
|
||||||
|
is_shared_usage=False,
|
||||||
|
shared_token=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
||||||
@@ -325,6 +341,8 @@ def complete_stream(
|
|||||||
index,
|
index,
|
||||||
api_key=user_api_key,
|
api_key=user_api_key,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
|
is_shared_usage=is_shared_usage,
|
||||||
|
shared_token=shared_token,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
conversation_id = None
|
conversation_id = None
|
||||||
@@ -433,7 +451,9 @@ class Stream(Resource):
|
|||||||
retriever_name = data.get("retriever", "classic")
|
retriever_name = data.get("retriever", "classic")
|
||||||
agent_id = data.get("agent_id", None)
|
agent_id = data.get("agent_id", None)
|
||||||
agent_type = settings.AGENT_NAME
|
agent_type = settings.AGENT_NAME
|
||||||
agent_key = get_agent_key(agent_id, request.decoded_token.get("sub"))
|
agent_key, is_shared_usage, shared_token = get_agent_key(
|
||||||
|
agent_id, request.decoded_token.get("sub")
|
||||||
|
)
|
||||||
|
|
||||||
if agent_key:
|
if agent_key:
|
||||||
data.update({"api_key": agent_key})
|
data.update({"api_key": agent_key})
|
||||||
@@ -448,7 +468,10 @@ class Stream(Resource):
|
|||||||
retriever_name = data_key.get("retriever", retriever_name)
|
retriever_name = data_key.get("retriever", retriever_name)
|
||||||
user_api_key = data["api_key"]
|
user_api_key = data["api_key"]
|
||||||
agent_type = data_key.get("agent_type", agent_type)
|
agent_type = data_key.get("agent_type", agent_type)
|
||||||
decoded_token = {"sub": data_key.get("user")}
|
if is_shared_usage:
|
||||||
|
decoded_token = request.decoded_token
|
||||||
|
else:
|
||||||
|
decoded_token = {"sub": data_key.get("user")}
|
||||||
|
|
||||||
elif "active_docs" in data:
|
elif "active_docs" in data:
|
||||||
source = {"active_docs": data["active_docs"]}
|
source = {"active_docs": data["active_docs"]}
|
||||||
@@ -514,6 +537,8 @@ class Stream(Resource):
|
|||||||
index=index,
|
index=index,
|
||||||
should_save_conversation=save_conv,
|
should_save_conversation=save_conv,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
|
is_shared_usage=is_shared_usage,
|
||||||
|
shared_token=shared_token,
|
||||||
),
|
),
|
||||||
mimetype="text/event-stream",
|
mimetype="text/event-stream",
|
||||||
)
|
)
|
||||||
@@ -881,6 +906,8 @@ def get_attachments_content(attachment_ids, user):
|
|||||||
if attachment_doc:
|
if attachment_doc:
|
||||||
attachments.append(attachment_doc)
|
attachments.append(attachment_doc)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error retrieving attachment {attachment_id}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Error retrieving attachment {attachment_id}: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
return attachments
|
return attachments
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ shared_conversations_collections = db["shared_conversations"]
|
|||||||
user_logs_collection = db["user_logs"]
|
user_logs_collection = db["user_logs"]
|
||||||
user_tools_collection = db["user_tools"]
|
user_tools_collection = db["user_tools"]
|
||||||
|
|
||||||
|
agents_collection.create_index(
|
||||||
|
[("shared", 1)],
|
||||||
|
name="shared_index",
|
||||||
|
background=True,
|
||||||
|
)
|
||||||
|
|
||||||
user = Blueprint("user", __name__)
|
user = Blueprint("user", __name__)
|
||||||
user_ns = Namespace("user", description="User related operations", path="/")
|
user_ns = Namespace("user", description="User related operations", path="/")
|
||||||
api.add_namespace(user_ns)
|
api.add_namespace(user_ns)
|
||||||
@@ -166,6 +172,8 @@ class GetConversations(Resource):
|
|||||||
"id": str(conversation["_id"]),
|
"id": str(conversation["_id"]),
|
||||||
"name": conversation["name"],
|
"name": conversation["name"],
|
||||||
"agent_id": conversation.get("agent_id", None),
|
"agent_id": conversation.get("agent_id", None),
|
||||||
|
"is_shared_usage": conversation.get("is_shared_usage", False),
|
||||||
|
"shared_token": conversation.get("shared_token", None),
|
||||||
}
|
}
|
||||||
for conversation in conversations
|
for conversation in conversations
|
||||||
]
|
]
|
||||||
@@ -208,6 +216,8 @@ class GetSingleConversation(Resource):
|
|||||||
data = {
|
data = {
|
||||||
"queries": conversation["queries"],
|
"queries": conversation["queries"],
|
||||||
"agent_id": conversation.get("agent_id"),
|
"agent_id": conversation.get("agent_id"),
|
||||||
|
"is_shared_usage": conversation.get("is_shared_usage", False),
|
||||||
|
"shared_token": conversation.get("shared_token", None),
|
||||||
}
|
}
|
||||||
return make_response(jsonify(data), 200)
|
return make_response(jsonify(data), 200)
|
||||||
|
|
||||||
@@ -1034,6 +1044,9 @@ class GetAgent(Resource):
|
|||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
"pinned": agent.get("pinned", False),
|
"pinned": agent.get("pinned", False),
|
||||||
|
"shared": agent.get("shared_publicly", False),
|
||||||
|
"shared_metadata": agent.get("shared_metadata", {}),
|
||||||
|
"shared_token": agent.get("shared_token", ""),
|
||||||
}
|
}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True)
|
current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True)
|
||||||
@@ -1077,6 +1090,9 @@ class GetAgents(Resource):
|
|||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
"pinned": agent.get("pinned", False),
|
"pinned": agent.get("pinned", False),
|
||||||
|
"shared": agent.get("shared_publicly", False),
|
||||||
|
"shared_metadata": agent.get("shared_metadata", {}),
|
||||||
|
"shared_token": agent.get("shared_token", ""),
|
||||||
}
|
}
|
||||||
for agent in agents
|
for agent in agents
|
||||||
if "source" in agent or "retriever" in agent
|
if "source" in agent or "retriever" in agent
|
||||||
@@ -1478,6 +1494,195 @@ class PinAgent(Resource):
|
|||||||
return make_response(jsonify({"success": True}), 200)
|
return make_response(jsonify({"success": True}), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@user_ns.route("/api/shared_agent")
|
||||||
|
class SharedAgent(Resource):
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"token": "Shared token of the agent",
|
||||||
|
},
|
||||||
|
description="Get a shared agent by token or ID",
|
||||||
|
)
|
||||||
|
def get(self):
|
||||||
|
shared_token = request.args.get("token")
|
||||||
|
|
||||||
|
if not shared_token:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Token or ID is required"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = {}
|
||||||
|
query["shared_publicly"] = True
|
||||||
|
query["shared_token"] = shared_token
|
||||||
|
|
||||||
|
shared_agent = agents_collection.find_one(query)
|
||||||
|
if not shared_agent:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Shared agent not found"}),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": str(shared_agent["_id"]),
|
||||||
|
"user": shared_agent.get("user", ""),
|
||||||
|
"name": shared_agent.get("name", ""),
|
||||||
|
"description": shared_agent.get("description", ""),
|
||||||
|
"tools": shared_agent.get("tools", []),
|
||||||
|
"agent_type": shared_agent.get("agent_type", ""),
|
||||||
|
"status": shared_agent.get("status", ""),
|
||||||
|
"created_at": shared_agent.get("createdAt", ""),
|
||||||
|
"updated_at": shared_agent.get("updatedAt", ""),
|
||||||
|
"shared": shared_agent.get("shared_publicly", False),
|
||||||
|
"shared_token": shared_agent.get("shared_token", ""),
|
||||||
|
"shared_metadata": shared_agent.get("shared_metadata", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["tools"]:
|
||||||
|
enriched_tools = []
|
||||||
|
for tool in data["tools"]:
|
||||||
|
tool_data = user_tools_collection.find_one({"_id": ObjectId(tool)})
|
||||||
|
if tool_data:
|
||||||
|
enriched_tools.append(tool_data.get("displayName", ""))
|
||||||
|
data["tools"] = enriched_tools
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error retrieving shared agent: {err}")
|
||||||
|
return make_response(jsonify({"success": False}), 400)
|
||||||
|
|
||||||
|
return make_response(jsonify(data), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@user_ns.route("/api/shared_agents")
|
||||||
|
class SharedAgents(Resource):
|
||||||
|
@api.doc(description="Get shared agents")
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
decoded_token = request.decoded_token
|
||||||
|
if not decoded_token:
|
||||||
|
return make_response(jsonify({"success": False}), 401)
|
||||||
|
user = decoded_token.get("sub")
|
||||||
|
shared_agents = agents_collection.find(
|
||||||
|
{"shared_publicly": True, "user": {"$ne": user}}
|
||||||
|
)
|
||||||
|
list_shared_agents = [
|
||||||
|
{
|
||||||
|
"id": str(shared_agent["_id"]),
|
||||||
|
"name": shared_agent.get("name", ""),
|
||||||
|
"description": shared_agent.get("description", ""),
|
||||||
|
"tools": shared_agent.get("tools", []),
|
||||||
|
"agent_type": shared_agent.get("agent_type", ""),
|
||||||
|
"status": shared_agent.get("status", ""),
|
||||||
|
"created_at": shared_agent.get("createdAt", ""),
|
||||||
|
"updated_at": shared_agent.get("updatedAt", ""),
|
||||||
|
"shared": shared_agent.get("shared_publicly", False),
|
||||||
|
"shared_token": shared_agent.get("shared_token", ""),
|
||||||
|
"shared_metadata": shared_agent.get("shared_metadata", {}),
|
||||||
|
}
|
||||||
|
for shared_agent in shared_agents
|
||||||
|
]
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error retrieving shared agents: {err}")
|
||||||
|
return make_response(jsonify({"success": False}), 400)
|
||||||
|
return make_response(jsonify(list_shared_agents), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@user_ns.route("/api/share_agent")
|
||||||
|
class ShareAgent(Resource):
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ShareAgentModel",
|
||||||
|
{
|
||||||
|
"id": fields.String(required=True, description="ID of the agent"),
|
||||||
|
"shared": fields.Boolean(
|
||||||
|
required=True, description="Share or unshare the agent"
|
||||||
|
),
|
||||||
|
"username": fields.String(
|
||||||
|
required=False, description="Name of the user"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.doc(description="Share or unshare an agent")
|
||||||
|
def put(self):
|
||||||
|
decoded_token = request.decoded_token
|
||||||
|
if not decoded_token:
|
||||||
|
return make_response(jsonify({"success": False}), 401)
|
||||||
|
|
||||||
|
user = decoded_token.get("sub")
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Missing JSON body"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_id = data.get("id")
|
||||||
|
shared = data.get("shared")
|
||||||
|
username = data.get("username", "")
|
||||||
|
|
||||||
|
if not agent_id:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "ID is required"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
if shared is None:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Shared parameter is required and must be true or false",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
agent_oid = ObjectId(agent_id)
|
||||||
|
except Exception:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid agent ID"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = agents_collection.find_one({"_id": agent_oid, "user": user})
|
||||||
|
if not agent:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Agent not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
if shared:
|
||||||
|
shared_metadata = {
|
||||||
|
"shared_by": username,
|
||||||
|
"shared_at": datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
}
|
||||||
|
shared_token = secrets.token_urlsafe(32)
|
||||||
|
agents_collection.update_one(
|
||||||
|
{"_id": agent_oid, "user": user},
|
||||||
|
{
|
||||||
|
"$set": {
|
||||||
|
"shared_publicly": shared,
|
||||||
|
"shared_metadata": shared_metadata,
|
||||||
|
"shared_token": shared_token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
agents_collection.update_one(
|
||||||
|
{"_id": agent_oid, "user": user},
|
||||||
|
{"$set": {"shared_publicly": shared, "shared_token": None}},
|
||||||
|
{"$unset": {"shared_metadata": ""}},
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
current_app.logger.error(f"Error sharing/unsharing agent: {err}")
|
||||||
|
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||||
|
|
||||||
|
shared_token = shared_token if shared else None
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": True, "shared_token": shared_token}), 200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/agent_webhook")
|
@user_ns.route("/api/agent_webhook")
|
||||||
class AgentWebhook(Resource):
|
class AgentWebhook(Resource):
|
||||||
@api.doc(
|
@api.doc(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# Please put appropriate value
|
# Please put appropriate value
|
||||||
VITE_API_HOST=http://0.0.0.0:7091
|
VITE_BASE_URL=http://localhost:5173
|
||||||
|
VITE_API_HOST=http://127.0.0.1:7091
|
||||||
VITE_API_STREAMING=true
|
VITE_API_STREAMING=true
|
||||||
@@ -110,7 +110,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
)
|
)
|
||||||
.slice(0, 3 - pinnedAgents.length);
|
.slice(0, 3 - pinnedAgents.length);
|
||||||
setRecentAgents([...pinnedAgents, ...additionalAgents]);
|
setRecentAgents([...pinnedAgents, ...additionalAgents]);
|
||||||
console.log(additionalAgents);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch recent agents: ', error);
|
console.error('Failed to fetch recent agents: ', error);
|
||||||
}
|
}
|
||||||
@@ -181,22 +180,37 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
.getConversation(index, token)
|
.getConversation(index, token)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
navigate('/');
|
if (data.agent_id) {
|
||||||
|
if (data.is_shared_usage) {
|
||||||
|
userService
|
||||||
|
.getSharedAgent(data.shared_token, token)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
response.json().then((agent: Agent) => {
|
||||||
|
navigate(`/agents/shared/${agent.shared_token}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userService.getAgent(data.agent_id, token).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
response.json().then((agent: Agent) => {
|
||||||
|
navigate('/');
|
||||||
|
dispatch(setSelectedAgent(agent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate('/');
|
||||||
|
dispatch(setSelectedAgent(null));
|
||||||
|
}
|
||||||
dispatch(setConversation(data.queries));
|
dispatch(setConversation(data.queries));
|
||||||
dispatch(
|
dispatch(
|
||||||
updateConversationId({
|
updateConversationId({
|
||||||
query: { conversationId: index },
|
query: { conversationId: index },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (data.agent_id) {
|
|
||||||
userService.getAgent(data.agent_id, token).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
response.json().then((agent: Agent) => {
|
|
||||||
dispatch(setSelectedAgent(agent));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else dispatch(setSelectedAgent(null));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
123
frontend/src/agents/AgentCard.tsx
Normal file
123
frontend/src/agents/AgentCard.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import Robot from '../assets/robot.svg';
|
||||||
|
import ThreeDots from '../assets/three-dots.svg';
|
||||||
|
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
|
import { ActiveState } from '../models/misc';
|
||||||
|
import {
|
||||||
|
selectToken,
|
||||||
|
setAgents,
|
||||||
|
setSelectedAgent,
|
||||||
|
} from '../preferences/preferenceSlice';
|
||||||
|
import { Agent } from './types';
|
||||||
|
|
||||||
|
type AgentCardProps = {
|
||||||
|
agent: Agent;
|
||||||
|
agents: Agent[];
|
||||||
|
menuOptions?: MenuOption[];
|
||||||
|
onDelete?: (agentId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AgentCard({
|
||||||
|
agent,
|
||||||
|
agents,
|
||||||
|
menuOptions,
|
||||||
|
onDelete,
|
||||||
|
}: AgentCardProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [deleteConfirmation, setDeleteConfirmation] =
|
||||||
|
useState<ActiveState>('INACTIVE');
|
||||||
|
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
if (agent.status === 'published') {
|
||||||
|
dispatch(setSelectedAgent(agent));
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDelete = async (agentId: string) => {
|
||||||
|
const response = await userService.deleteAgent(agentId, token);
|
||||||
|
if (!response.ok) throw new Error('Failed to delete agent');
|
||||||
|
const data = await response.json();
|
||||||
|
dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative flex h-44 w-48 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 hover:bg-[#ECECEC] dark:bg-[#383838] hover:dark:bg-[#383838]/80 ${
|
||||||
|
agent.status === 'published' ? 'cursor-pointer' : ''
|
||||||
|
}`}
|
||||||
|
onClick={handleCardClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsMenuOpen(true);
|
||||||
|
}}
|
||||||
|
className="absolute right-4 top-4 z-10 cursor-pointer"
|
||||||
|
>
|
||||||
|
<img src={ThreeDots} alt="options" className="h-[19px] w-[19px]" />
|
||||||
|
{menuOptions && (
|
||||||
|
<ContextMenu
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
setIsOpen={setIsMenuOpen}
|
||||||
|
options={menuOptions}
|
||||||
|
anchorRef={menuRef}
|
||||||
|
position="top-right"
|
||||||
|
offset={{ x: 0, y: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full items-center gap-1 px-1">
|
||||||
|
<img
|
||||||
|
src={agent.image ?? Robot}
|
||||||
|
alt={`${agent.name}`}
|
||||||
|
className="h-7 w-7 rounded-full"
|
||||||
|
/>
|
||||||
|
{agent.status === 'draft' && (
|
||||||
|
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">
|
||||||
|
(Draft)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p
|
||||||
|
title={agent.name}
|
||||||
|
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-[#020617] dark:text-[#E0E0E0]"
|
||||||
|
>
|
||||||
|
{agent.name}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-[#64748B] dark:text-sonic-silver-light">
|
||||||
|
{agent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
message="Are you sure you want to delete this agent?"
|
||||||
|
modalState={deleteConfirmation}
|
||||||
|
setModalState={setDeleteConfirmation}
|
||||||
|
submitLabel="Delete"
|
||||||
|
handleSubmit={() => {
|
||||||
|
onDelete ? onDelete(agent.id || '') : defaultDelete(agent.id || '');
|
||||||
|
setDeleteConfirmation('INACTIVE');
|
||||||
|
}}
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
253
frontend/src/agents/SharedAgent.tsx
Normal file
253
frontend/src/agents/SharedAgent.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import userService from '../api/services/userService';
|
||||||
|
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||||
|
import NoFilesIcon from '../assets/no-files.svg';
|
||||||
|
import Robot from '../assets/robot.svg';
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
|
import Spinner from '../components/Spinner';
|
||||||
|
import ConversationMessages from '../conversation/ConversationMessages';
|
||||||
|
import { Query } from '../conversation/conversationModels';
|
||||||
|
import {
|
||||||
|
addQuery,
|
||||||
|
fetchAnswer,
|
||||||
|
resendQuery,
|
||||||
|
selectQueries,
|
||||||
|
selectStatus,
|
||||||
|
updateConversationId,
|
||||||
|
} from '../conversation/conversationSlice';
|
||||||
|
import { useDarkTheme } from '../hooks';
|
||||||
|
import {
|
||||||
|
selectToken,
|
||||||
|
setConversations,
|
||||||
|
setSelectedAgent,
|
||||||
|
} from '../preferences/preferenceSlice';
|
||||||
|
import { AppDispatch } from '../store';
|
||||||
|
import { Agent } from './types';
|
||||||
|
|
||||||
|
export default function SharedAgent() {
|
||||||
|
const { agentId } = useParams();
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const [isDarkTheme] = useDarkTheme();
|
||||||
|
|
||||||
|
const token = useSelector(selectToken);
|
||||||
|
const queries = useSelector(selectQueries);
|
||||||
|
const status = useSelector(selectStatus);
|
||||||
|
|
||||||
|
const [sharedAgent, setSharedAgent] = useState<Agent>();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||||
|
|
||||||
|
const fetchStream = useRef<any>(null);
|
||||||
|
|
||||||
|
const getSharedAgent = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await userService.getSharedAgent(agentId ?? '', token);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch Shared Agent');
|
||||||
|
const agent: Agent = await response.json();
|
||||||
|
setSharedAgent(agent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error: ', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetchAnswer = useCallback(
|
||||||
|
({ question, index }: { question: string; index?: number }) => {
|
||||||
|
fetchStream.current = dispatch(
|
||||||
|
fetchAnswer({ question, indx: index, isPreview: false }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuestion = useCallback(
|
||||||
|
({
|
||||||
|
question,
|
||||||
|
isRetry = false,
|
||||||
|
index = undefined,
|
||||||
|
}: {
|
||||||
|
question: string;
|
||||||
|
isRetry?: boolean;
|
||||||
|
index?: number;
|
||||||
|
}) => {
|
||||||
|
const trimmedQuestion = question.trim();
|
||||||
|
if (trimmedQuestion === '') return;
|
||||||
|
|
||||||
|
if (index !== undefined) {
|
||||||
|
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||||
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
|
} else {
|
||||||
|
if (!isRetry) {
|
||||||
|
const newQuery: Query = { prompt: trimmedQuestion };
|
||||||
|
dispatch(addQuery(newQuery));
|
||||||
|
}
|
||||||
|
handleFetchAnswer({ question: trimmedQuestion, index: undefined });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, handleFetchAnswer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuestionSubmission = (
|
||||||
|
updatedQuestion?: string,
|
||||||
|
updated?: boolean,
|
||||||
|
indx?: number,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
updated === true &&
|
||||||
|
updatedQuestion !== undefined &&
|
||||||
|
indx !== undefined
|
||||||
|
) {
|
||||||
|
handleQuestion({
|
||||||
|
question: updatedQuestion,
|
||||||
|
index: indx,
|
||||||
|
isRetry: false,
|
||||||
|
});
|
||||||
|
} else if (input.trim() && status !== 'loading') {
|
||||||
|
const currentInput = input.trim();
|
||||||
|
if (lastQueryReturnedErr && queries.length > 0) {
|
||||||
|
const lastQueryIndex = queries.length - 1;
|
||||||
|
handleQuestion({
|
||||||
|
question: currentInput,
|
||||||
|
isRetry: true,
|
||||||
|
index: lastQueryIndex,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleQuestion({
|
||||||
|
question: currentInput,
|
||||||
|
isRetry: false,
|
||||||
|
index: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (agentId) getSharedAgent();
|
||||||
|
}, [agentId, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sharedAgent) dispatch(setSelectedAgent(sharedAgent));
|
||||||
|
}, [sharedAgent, dispatch]);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
if (!sharedAgent)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-4">
|
||||||
|
<img
|
||||||
|
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||||
|
alt="No agent found"
|
||||||
|
className="mx-auto mb-6 h-32 w-32"
|
||||||
|
/>
|
||||||
|
<p className="text-center text-lg text-[#71717A] dark:text-[#949494]">
|
||||||
|
No agent found. Please ensure the agent is shared.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full pt-10">
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-between">
|
||||||
|
<div className="flex w-full flex-col items-center overflow-y-auto">
|
||||||
|
<ConversationMessages
|
||||||
|
handleQuestion={handleQuestion}
|
||||||
|
handleQuestionSubmission={handleQuestionSubmission}
|
||||||
|
queries={queries}
|
||||||
|
status={status}
|
||||||
|
showHeroOnEmpty={false}
|
||||||
|
headerContent={
|
||||||
|
<div className="flex w-full items-center justify-center py-4">
|
||||||
|
<SharedAgentCard agent={sharedAgent} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||||
|
<MessageInput
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onSubmit={() => handleQuestionSubmission()}
|
||||||
|
loading={status === 'loading'}
|
||||||
|
showSourceButton={sharedAgent ? false : true}
|
||||||
|
showToolButton={sharedAgent ? false : true}
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
<p className="w-full self-center bg-transparent pt-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline">
|
||||||
|
This is a preview of the agent. You can publish it to start using it
|
||||||
|
in conversations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-md dark:border-grey">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
||||||
|
<img src={Robot} className="h-full w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
|
||||||
|
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
|
||||||
|
{agent.name}
|
||||||
|
</h2>
|
||||||
|
<p className="overflow-y-auto text-wrap break-all text-sm text-[#71717A] dark:text-[#949494]">
|
||||||
|
{agent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center gap-8">
|
||||||
|
{agent.shared_metadata?.shared_by && (
|
||||||
|
<p className="text-sm font-light text-[#212121] dark:text-[#E0E0E0]">
|
||||||
|
by {agent.shared_metadata.shared_by}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{agent.shared_metadata?.shared_at && (
|
||||||
|
<p className="text-sm font-light text-[#71717A] dark:text-[#949494]">
|
||||||
|
Shared on{' '}
|
||||||
|
{new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<p className="font-semibold text-[#212121] dark:text-[#E0E0E0]">
|
||||||
|
Connected Tools
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{agent.tools.map((tool, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="rounded-full bg-[#E0E0E0] px-3 py-1 text-xs font-light text-[#212121] dark:bg-[#4B5563] dark:text-[#E0E0E0]"
|
||||||
|
>
|
||||||
|
{tool}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <button className="mt-8 w-full rounded-xl bg-gradient-to-b from-violets-are-blue to-[#6e45c2] px-4 py-2 text-sm text-white shadow-lg transition duration-300 ease-in-out hover:shadow-xl">
|
||||||
|
Start using
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,10 +9,15 @@ import Monitoring from '../assets/monitoring.svg';
|
|||||||
import Pin from '../assets/pin.svg';
|
import Pin from '../assets/pin.svg';
|
||||||
import Trash from '../assets/red-trash.svg';
|
import Trash from '../assets/red-trash.svg';
|
||||||
import Robot from '../assets/robot.svg';
|
import Robot from '../assets/robot.svg';
|
||||||
|
import Link from '../assets/link-gray.svg';
|
||||||
import ThreeDots from '../assets/three-dots.svg';
|
import ThreeDots from '../assets/three-dots.svg';
|
||||||
import UnPin from '../assets/unpin.svg';
|
import UnPin from '../assets/unpin.svg';
|
||||||
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||||
import Spinner from '../components/Spinner';
|
import Spinner from '../components/Spinner';
|
||||||
|
import {
|
||||||
|
setConversation,
|
||||||
|
updateConversationId,
|
||||||
|
} from '../conversation/conversationSlice';
|
||||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +29,7 @@ import {
|
|||||||
} from '../preferences/preferenceSlice';
|
} from '../preferences/preferenceSlice';
|
||||||
import AgentLogs from './AgentLogs';
|
import AgentLogs from './AgentLogs';
|
||||||
import NewAgent from './NewAgent';
|
import NewAgent from './NewAgent';
|
||||||
|
import SharedAgent from './SharedAgent';
|
||||||
import { Agent } from './types';
|
import { Agent } from './types';
|
||||||
|
|
||||||
export default function Agents() {
|
export default function Agents() {
|
||||||
@@ -33,10 +39,26 @@ export default function Agents() {
|
|||||||
<Route path="/new" element={<NewAgent mode="new" />} />
|
<Route path="/new" element={<NewAgent mode="new" />} />
|
||||||
<Route path="/edit/:agentId" element={<NewAgent mode="edit" />} />
|
<Route path="/edit/:agentId" element={<NewAgent mode="edit" />} />
|
||||||
<Route path="/logs/:agentId" element={<AgentLogs />} />
|
<Route path="/logs/:agentId" element={<AgentLogs />} />
|
||||||
|
<Route path="/shared/:agentId" element={<SharedAgent />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sectionConfig = {
|
||||||
|
user: {
|
||||||
|
title: 'By me',
|
||||||
|
description: 'Agents created or published by you',
|
||||||
|
showNewAgentButton: true,
|
||||||
|
emptyStateDescription: 'You don’t have any created agents yet',
|
||||||
|
},
|
||||||
|
shared: {
|
||||||
|
title: 'Shared with me',
|
||||||
|
description: 'Agents imported by using a public link',
|
||||||
|
showNewAgentButton: false,
|
||||||
|
emptyStateDescription: 'No shared agents found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function AgentsList() {
|
function AgentsList() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -44,24 +66,47 @@ function AgentsList() {
|
|||||||
const agents = useSelector(selectAgents);
|
const agents = useSelector(selectAgents);
|
||||||
const selectedAgent = useSelector(selectSelectedAgent);
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [sharedAgents, setSharedAgents] = useState<Agent[]>([]);
|
||||||
|
const [loadingUserAgents, setLoadingUserAgents] = useState<boolean>(true);
|
||||||
|
const [loadingSharedAgents, setLoadingSharedAgents] = useState<boolean>(true);
|
||||||
|
|
||||||
const getAgents = async () => {
|
const getAgents = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoadingUserAgents(true);
|
||||||
const response = await userService.getAgents(token);
|
const response = await userService.getAgents(token);
|
||||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
dispatch(setAgents(data));
|
dispatch(setAgents(data));
|
||||||
setLoading(false);
|
setLoadingUserAgents(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
setLoading(false);
|
setLoadingUserAgents(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSharedAgents = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingSharedAgents(true);
|
||||||
|
const response = await userService.getSharedAgents(token);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch shared agents');
|
||||||
|
const data = await response.json();
|
||||||
|
setSharedAgents(data);
|
||||||
|
setLoadingSharedAgents(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
setLoadingSharedAgents(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAgents();
|
getAgents();
|
||||||
|
getSharedAgents();
|
||||||
|
dispatch(setConversation([]));
|
||||||
|
dispatch(
|
||||||
|
updateConversationId({
|
||||||
|
query: { conversationId: null },
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (selectedAgent) dispatch(setSelectedAgent(null));
|
if (selectedAgent) dispatch(setSelectedAgent(null));
|
||||||
}, [token]);
|
}, [token]);
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +116,7 @@ function AgentsList() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="mt-5 text-[15px] text-[#71717A] dark:text-[#949494]">
|
<p className="mt-5 text-[15px] text-[#71717A] dark:text-[#949494]">
|
||||||
Discover and create custom versions of DocsGPT that combine
|
Discover and create custom versions of DocsGPT that combine
|
||||||
instructions, extra knowledge, and any combination of skills.
|
instructions, extra knowledge, and any combination of skills
|
||||||
</p>
|
</p>
|
||||||
{/* Premade agents section */}
|
{/* Premade agents section */}
|
||||||
{/* <div className="mt-6">
|
{/* <div className="mt-6">
|
||||||
@@ -116,45 +161,91 @@ function AgentsList() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="mt-8 flex flex-col gap-4">
|
<AgentSection
|
||||||
<div className="flex w-full items-center justify-between">
|
agents={agents ?? []}
|
||||||
|
loading={loadingUserAgents}
|
||||||
|
section="user"
|
||||||
|
/>
|
||||||
|
<AgentSection
|
||||||
|
agents={sharedAgents ?? []}
|
||||||
|
loading={loadingSharedAgents}
|
||||||
|
section="shared"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentSection({
|
||||||
|
agents,
|
||||||
|
loading,
|
||||||
|
section,
|
||||||
|
}: {
|
||||||
|
agents: Agent[];
|
||||||
|
loading: boolean;
|
||||||
|
section: keyof typeof sectionConfig;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div className="mt-8 flex flex-col gap-4">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
||||||
Created by You
|
{sectionConfig[section].title}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="text-[13px] text-[#71717A]">
|
||||||
|
{sectionConfig[section].description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{sectionConfig[section].showNewAgentButton && (
|
||||||
<button
|
<button
|
||||||
className="rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
className="rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
||||||
onClick={() => navigate('/agents/new')}
|
onClick={() => navigate('/agents/new')}
|
||||||
>
|
>
|
||||||
New Agent
|
New Agent
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
<div className="flex w-full flex-wrap gap-4">
|
</div>
|
||||||
{loading ? (
|
<div className="flex w-full flex-wrap gap-4">
|
||||||
<div className="flex h-72 w-full items-center justify-center">
|
{loading ? (
|
||||||
<Spinner />
|
<div className="flex h-72 w-full items-center justify-center">
|
||||||
</div>
|
<Spinner />
|
||||||
) : agents && agents.length > 0 ? (
|
</div>
|
||||||
agents.map((agent) => (
|
) : agents && agents.length > 0 ? (
|
||||||
<AgentCard key={agent.id} agent={agent} agents={agents} />
|
agents.map((agent) => (
|
||||||
))
|
<AgentCard
|
||||||
) : (
|
key={agent.id}
|
||||||
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
|
agent={agent}
|
||||||
<p>You don’t have any created agents yet </p>
|
agents={agents}
|
||||||
|
section={section}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
|
||||||
|
<p>{sectionConfig[section].emptyStateDescription}</p>
|
||||||
|
{sectionConfig[section].showNewAgentButton && (
|
||||||
<button
|
<button
|
||||||
className="ml-2 rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
className="ml-2 rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
|
||||||
onClick={() => navigate('/agents/new')}
|
onClick={() => navigate('/agents/new')}
|
||||||
>
|
>
|
||||||
New Agent
|
New Agent
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) {
|
function AgentCard({
|
||||||
|
agent,
|
||||||
|
agents,
|
||||||
|
section,
|
||||||
|
}: {
|
||||||
|
agent: Agent;
|
||||||
|
agents: Agent[];
|
||||||
|
section: keyof typeof sectionConfig;
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
@@ -180,57 +271,79 @@ function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuOptions: MenuOption[] = [
|
const menuOptionsConfig: Record<string, MenuOption[]> = {
|
||||||
{
|
user: [
|
||||||
icon: Monitoring,
|
{
|
||||||
label: 'Logs',
|
icon: Monitoring,
|
||||||
onClick: (e: SyntheticEvent) => {
|
label: 'Logs',
|
||||||
e.stopPropagation();
|
onClick: (e: SyntheticEvent) => {
|
||||||
navigate(`/agents/logs/${agent.id}`);
|
e.stopPropagation();
|
||||||
|
navigate(`/agents/logs/${agent.id}`);
|
||||||
|
},
|
||||||
|
variant: 'primary',
|
||||||
|
iconWidth: 14,
|
||||||
|
iconHeight: 14,
|
||||||
},
|
},
|
||||||
variant: 'primary',
|
{
|
||||||
iconWidth: 14,
|
icon: Edit,
|
||||||
iconHeight: 14,
|
label: 'Edit',
|
||||||
},
|
onClick: (e: SyntheticEvent) => {
|
||||||
{
|
e.stopPropagation();
|
||||||
icon: Edit,
|
navigate(`/agents/edit/${agent.id}`);
|
||||||
label: 'Edit',
|
},
|
||||||
onClick: (e: SyntheticEvent) => {
|
variant: 'primary',
|
||||||
e.stopPropagation();
|
iconWidth: 14,
|
||||||
navigate(`/agents/edit/${agent.id}`);
|
iconHeight: 14,
|
||||||
},
|
},
|
||||||
variant: 'primary',
|
{
|
||||||
iconWidth: 14,
|
icon: agent.pinned ? UnPin : Pin,
|
||||||
iconHeight: 14,
|
label: agent.pinned ? 'Unpin' : 'Pin agent',
|
||||||
},
|
onClick: (e: SyntheticEvent) => {
|
||||||
{
|
e.stopPropagation();
|
||||||
icon: agent.pinned ? UnPin : Pin,
|
togglePin();
|
||||||
label: agent.pinned ? 'Unpin' : 'Pin agent',
|
},
|
||||||
onClick: (e: SyntheticEvent) => {
|
variant: 'primary',
|
||||||
e.stopPropagation();
|
iconWidth: 18,
|
||||||
togglePin();
|
iconHeight: 18,
|
||||||
},
|
},
|
||||||
variant: 'primary',
|
{
|
||||||
iconWidth: 18,
|
icon: Trash,
|
||||||
iconHeight: 18,
|
label: 'Delete',
|
||||||
},
|
onClick: (e: SyntheticEvent) => {
|
||||||
{
|
e.stopPropagation();
|
||||||
icon: Trash,
|
setDeleteConfirmation('ACTIVE');
|
||||||
label: 'Delete',
|
},
|
||||||
onClick: (e: SyntheticEvent) => {
|
variant: 'danger',
|
||||||
e.stopPropagation();
|
iconWidth: 13,
|
||||||
setDeleteConfirmation('ACTIVE');
|
iconHeight: 13,
|
||||||
},
|
},
|
||||||
variant: 'danger',
|
],
|
||||||
iconWidth: 13,
|
shared: [
|
||||||
iconHeight: 13,
|
{
|
||||||
},
|
icon: Link,
|
||||||
];
|
label: 'Open',
|
||||||
|
onClick: (e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/agents/shared/${agent.shared_token}`);
|
||||||
|
},
|
||||||
|
variant: 'primary',
|
||||||
|
iconWidth: 14,
|
||||||
|
iconHeight: 14,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuOptions = menuOptionsConfig[section] || [];
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (agent.status === 'published') {
|
if (section === 'user') {
|
||||||
dispatch(setSelectedAgent(agent));
|
if (agent.status === 'published') {
|
||||||
navigate(`/`);
|
dispatch(setSelectedAgent(agent));
|
||||||
|
navigate(`/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (section === 'shared') {
|
||||||
|
navigate(`/agents/shared/${agent.shared_token}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export type Agent = {
|
|||||||
key?: string;
|
key?: string;
|
||||||
incoming_webhook_token?: string;
|
incoming_webhook_token?: string;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
shared?: boolean;
|
||||||
|
shared_token?: string;
|
||||||
|
shared_metadata?: any;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const baseURL = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
export const baseURL =
|
||||||
|
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const endpoints = {
|
|||||||
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
|
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
|
||||||
PINNED_AGENTS: '/api/pinned_agents',
|
PINNED_AGENTS: '/api/pinned_agents',
|
||||||
TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`,
|
TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`,
|
||||||
|
SHARED_AGENT: (id: string) => `/api/shared_agent?token=${id}`,
|
||||||
|
SHARED_AGENTS: '/api/shared_agents',
|
||||||
|
SHARE_AGENT: `/api/share_agent`,
|
||||||
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
|
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
|
||||||
PROMPTS: '/api/get_prompts',
|
PROMPTS: '/api/get_prompts',
|
||||||
CREATE_PROMPT: '/api/create_prompt',
|
CREATE_PROMPT: '/api/create_prompt',
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ const userService = {
|
|||||||
apiClient.get(endpoints.USER.PINNED_AGENTS, token),
|
apiClient.get(endpoints.USER.PINNED_AGENTS, token),
|
||||||
togglePinAgent: (id: string, token: string | null): Promise<any> =>
|
togglePinAgent: (id: string, token: string | null): Promise<any> =>
|
||||||
apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token),
|
apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token),
|
||||||
|
getSharedAgent: (id: string, token: string | null): Promise<any> =>
|
||||||
|
apiClient.get(endpoints.USER.SHARED_AGENT(id), token),
|
||||||
|
getSharedAgents: (token: string | null): Promise<any> =>
|
||||||
|
apiClient.get(endpoints.USER.SHARED_AGENTS, token),
|
||||||
|
shareAgent: (data: any, token: string | null): Promise<any> =>
|
||||||
|
apiClient.put(endpoints.USER.SHARE_AGENT, data, token),
|
||||||
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
|
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
|
||||||
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
|
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
|
||||||
getPrompts: (token: string | null): Promise<any> =>
|
getPrompts: (token: string | null): Promise<any> =>
|
||||||
|
|||||||
3
frontend/src/assets/link-gray.svg
Normal file
3
frontend/src/assets/link-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 486 B |
@@ -1,4 +1,11 @@
|
|||||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
import {
|
||||||
|
Fragment,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ArrowDown from '../assets/arrow-down.svg';
|
import ArrowDown from '../assets/arrow-down.svg';
|
||||||
@@ -8,7 +15,12 @@ import { useDarkTheme } from '../hooks';
|
|||||||
import ConversationBubble from './ConversationBubble';
|
import ConversationBubble from './ConversationBubble';
|
||||||
import { FEEDBACK, Query, Status } from './conversationModels';
|
import { FEEDBACK, Query, Status } from './conversationModels';
|
||||||
|
|
||||||
interface ConversationMessagesProps {
|
const SCROLL_THRESHOLD = 10;
|
||||||
|
const LAST_BUBBLE_MARGIN = 'mb-32';
|
||||||
|
const DEFAULT_BUBBLE_MARGIN = 'mb-7';
|
||||||
|
const FIRST_QUESTION_BUBBLE_MARGIN_TOP = 'mt-5';
|
||||||
|
|
||||||
|
type ConversationMessagesProps = {
|
||||||
handleQuestion: (params: {
|
handleQuestion: (params: {
|
||||||
question: string;
|
question: string;
|
||||||
isRetry?: boolean;
|
isRetry?: boolean;
|
||||||
@@ -24,7 +36,8 @@ interface ConversationMessagesProps {
|
|||||||
queries: Query[];
|
queries: Query[];
|
||||||
status: Status;
|
status: Status;
|
||||||
showHeroOnEmpty?: boolean;
|
showHeroOnEmpty?: boolean;
|
||||||
}
|
headerContent?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ConversationMessages({
|
export default function ConversationMessages({
|
||||||
handleQuestion,
|
handleQuestion,
|
||||||
@@ -33,22 +46,23 @@ export default function ConversationMessages({
|
|||||||
status,
|
status,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
showHeroOnEmpty = true,
|
showHeroOnEmpty = true,
|
||||||
|
headerContent,
|
||||||
}: ConversationMessagesProps) {
|
}: ConversationMessagesProps) {
|
||||||
const [isDarkTheme] = useDarkTheme();
|
const [isDarkTheme] = useDarkTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const conversationRef = useRef<HTMLDivElement>(null);
|
const conversationRef = useRef<HTMLDivElement>(null);
|
||||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
||||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
const [userInterruptedScroll, setUserInterruptedScroll] = useState(false);
|
||||||
|
|
||||||
const handleUserInterruption = () => {
|
const handleUserScrollInterruption = useCallback(() => {
|
||||||
if (!eventInterrupt && status === 'loading') {
|
if (!userInterruptedScroll && status === 'loading') {
|
||||||
setEventInterrupt(true);
|
setUserInterruptedScroll(true);
|
||||||
}
|
}
|
||||||
};
|
}, [userInterruptedScroll, status]);
|
||||||
|
|
||||||
const scrollIntoView = () => {
|
const scrollConversationToBottom = useCallback(() => {
|
||||||
if (!conversationRef?.current || eventInterrupt) return;
|
if (!conversationRef.current || userInterruptedScroll) return;
|
||||||
|
|
||||||
if (status === 'idle' || !queries[queries.length - 1]?.response) {
|
if (status === 'idle' || !queries[queries.length - 1]?.response) {
|
||||||
conversationRef.current.scrollTo({
|
conversationRef.current.scrollTo({
|
||||||
@@ -58,39 +72,65 @@ export default function ConversationMessages({
|
|||||||
} else {
|
} else {
|
||||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
}, [userInterruptedScroll, status, queries]);
|
||||||
|
|
||||||
const checkScroll = () => {
|
const checkScrollPosition = useCallback(() => {
|
||||||
const el = conversationRef.current;
|
const el = conversationRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10;
|
const isAtBottom =
|
||||||
setHasScrolledToLast(isBottom);
|
el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_THRESHOLD;
|
||||||
};
|
setHasScrolledToLast(isAtBottom);
|
||||||
|
}, [setHasScrolledToLast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!eventInterrupt && scrollIntoView();
|
if (!userInterruptedScroll) {
|
||||||
}, [queries.length, queries[queries.length - 1]]);
|
scrollConversationToBottom();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
queries.length,
|
||||||
|
queries[queries.length - 1]?.response,
|
||||||
|
queries[queries.length - 1]?.error,
|
||||||
|
queries[queries.length - 1]?.thought,
|
||||||
|
userInterruptedScroll,
|
||||||
|
scrollConversationToBottom,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'idle') {
|
if (status === 'idle') {
|
||||||
setEventInterrupt(false);
|
setUserInterruptedScroll(false);
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
conversationRef.current?.addEventListener('scroll', checkScroll);
|
const currentConversationRef = conversationRef.current;
|
||||||
|
currentConversationRef?.addEventListener('scroll', checkScrollPosition);
|
||||||
return () => {
|
return () => {
|
||||||
conversationRef.current?.removeEventListener('scroll', checkScroll);
|
currentConversationRef?.removeEventListener(
|
||||||
|
'scroll',
|
||||||
|
checkScrollPosition,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [checkScrollPosition]);
|
||||||
|
|
||||||
|
const retryIconProps = {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
fill: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)',
|
||||||
|
stroke: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)',
|
||||||
|
strokeWidth: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResponseView = (query: Query, index: number) => {
|
||||||
|
const isLastMessage = index === queries.length - 1;
|
||||||
|
const bubbleMargin = isLastMessage
|
||||||
|
? LAST_BUBBLE_MARGIN
|
||||||
|
: DEFAULT_BUBBLE_MARGIN;
|
||||||
|
|
||||||
const prepResponseView = (query: Query, index: number) => {
|
|
||||||
let responseView;
|
|
||||||
if (query.thought || query.response) {
|
if (query.thought || query.response) {
|
||||||
responseView = (
|
return (
|
||||||
<ConversationBubble
|
<ConversationBubble
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
className={bubbleMargin}
|
||||||
key={`${index}ANSWER`}
|
key={`${index}-ANSWER`}
|
||||||
message={query.response}
|
message={query.response}
|
||||||
type={'ANSWER'}
|
type={'ANSWER'}
|
||||||
thought={query.thought}
|
thought={query.thought}
|
||||||
@@ -104,75 +144,79 @@ export default function ConversationMessages({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (query.error) {
|
}
|
||||||
const retryBtn = (
|
|
||||||
|
if (query.error) {
|
||||||
|
const retryButton = (
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-center gap-3 self-center rounded-full py-3 px-5 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed dark:text-bright-gray"
|
className="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 dark:text-bright-gray"
|
||||||
disabled={status === 'loading'}
|
disabled={status === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const questionToRetry = queries[index].prompt;
|
||||||
handleQuestion({
|
handleQuestion({
|
||||||
question: queries[queries.length - 1].prompt,
|
question: questionToRetry,
|
||||||
isRetry: true,
|
isRetry: true,
|
||||||
|
indx: index,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
aria-label={t('Retry') || 'Retry'}
|
||||||
>
|
>
|
||||||
<RetryIcon
|
<RetryIcon {...retryIconProps} />
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
fill={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
|
||||||
stroke={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
|
||||||
strokeWidth={10}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
responseView = (
|
return (
|
||||||
<ConversationBubble
|
<ConversationBubble
|
||||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
className={bubbleMargin}
|
||||||
key={`${index}ERROR`}
|
key={`${index}-ERROR`}
|
||||||
message={query.error}
|
message={query.error}
|
||||||
type="ERROR"
|
type="ERROR"
|
||||||
retryBtn={retryBtn}
|
retryBtn={retryButton}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return responseView;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={conversationRef}
|
ref={conversationRef}
|
||||||
onWheel={handleUserInterruption}
|
onWheel={handleUserScrollInterruption}
|
||||||
onTouchMove={handleUserInterruption}
|
onTouchMove={handleUserScrollInterruption}
|
||||||
className="flex justify-center w-full overflow-y-auto h-full sm:pt-12"
|
className="flex h-full w-full justify-center overflow-y-auto sm:pt-12"
|
||||||
>
|
>
|
||||||
{queries.length > 0 && !hasScrolledToLast && (
|
{queries.length > 0 && !hasScrolledToLast && (
|
||||||
<button
|
<button
|
||||||
onClick={scrollIntoView}
|
onClick={() => {
|
||||||
aria-label="scroll to bottom"
|
setUserInterruptedScroll(false);
|
||||||
|
scrollConversationToBottom();
|
||||||
|
}}
|
||||||
|
aria-label={t('Scroll to bottom') || 'Scroll to bottom'}
|
||||||
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-gunmetal md:h-9 md:w-9 md:bg-opacity-100"
|
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-gunmetal md:h-9 md:w-9 md:bg-opacity-100"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={ArrowDown}
|
src={ArrowDown}
|
||||||
alt="arrow down"
|
alt=""
|
||||||
className="h-4 w-4 opacity-50 md:h-5 md:w-5 filter dark:invert"
|
className="h-4 w-4 opacity-50 filter dark:invert md:h-5 md:w-5"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1300px] px-2">
|
<div className="w-full max-w-[1300px] px-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||||
|
{headerContent && headerContent}
|
||||||
|
|
||||||
{queries.length > 0 ? (
|
{queries.length > 0 ? (
|
||||||
queries.map((query, index) => (
|
queries.map((query, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={`${index}-query-fragment`}>
|
||||||
<ConversationBubble
|
<ConversationBubble
|
||||||
className={'first:mt-5'}
|
className={index === 0 ? FIRST_QUESTION_BUBBLE_MARGIN_TOP : ''}
|
||||||
key={`${index}QUESTION`}
|
key={`${index}-QUESTION`}
|
||||||
message={query.prompt}
|
message={query.prompt}
|
||||||
type="QUESTION"
|
type="QUESTION"
|
||||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||||
questionNumber={index}
|
questionNumber={index}
|
||||||
sources={query.sources}
|
sources={query.sources}
|
||||||
/>
|
/>
|
||||||
{prepResponseView(query, index)}
|
{renderResponseView(query, index)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))
|
))
|
||||||
) : showHeroOnEmpty ? (
|
) : showHeroOnEmpty ? (
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export function handleFetchAnswerSteaming(
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.body) throw Error('No response body');
|
if (!response.body) throw Error('No response body');
|
||||||
|
|
||||||
|
let buffer = '';
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder('utf-8');
|
const decoder = new TextDecoder('utf-8');
|
||||||
let counterrr = 0;
|
let counterrr = 0;
|
||||||
@@ -157,22 +158,24 @@ export function handleFetchAnswerSteaming(
|
|||||||
counterrr += 1;
|
counterrr += 1;
|
||||||
|
|
||||||
const chunk = decoder.decode(value);
|
const chunk = decoder.decode(value);
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
const lines = chunk.split('\n');
|
const events = buffer.split('\n\n');
|
||||||
|
buffer = events.pop() ?? '';
|
||||||
|
|
||||||
for (let line of lines) {
|
for (let event of events) {
|
||||||
if (line.trim() == '') {
|
if (event.trim().startsWith('data:')) {
|
||||||
continue;
|
const dataLine: string = event
|
||||||
|
.split('\n')
|
||||||
|
.map((line: string) => line.replace(/^data:\s?/, ''))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const messageEvent = new MessageEvent('message', {
|
||||||
|
data: dataLine.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(messageEvent);
|
||||||
}
|
}
|
||||||
if (line.startsWith('data:')) {
|
|
||||||
line = line.substring(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageEvent: MessageEvent = new MessageEvent('message', {
|
|
||||||
data: line,
|
|
||||||
});
|
|
||||||
|
|
||||||
onEvent(messageEvent); // handle each message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.read().then(processStream).catch(reject);
|
reader.read().then(processStream).catch(reject);
|
||||||
|
|||||||
@@ -285,9 +285,7 @@ export const conversationSlice = createSlice({
|
|||||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||||
) {
|
) {
|
||||||
const { index, query } = action.payload;
|
const { index, query } = action.payload;
|
||||||
if (!state.queries[index].tool_calls) {
|
state.queries[index].tool_calls = query?.tool_calls ?? [];
|
||||||
state.queries[index].tool_calls = query?.tool_calls;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
updateQuery(
|
updateQuery(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { Agent } from '../agents/types';
|
import { Agent } from '../agents/types';
|
||||||
@@ -9,6 +9,8 @@ import { ActiveState } from '../models/misc';
|
|||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import WrapperModal from './WrapperModal';
|
import WrapperModal from './WrapperModal';
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_BASE_URL;
|
||||||
|
|
||||||
type AgentDetailsModalProps = {
|
type AgentDetailsModalProps = {
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
mode: 'new' | 'edit' | 'draft';
|
mode: 'new' | 'edit' | 'draft';
|
||||||
@@ -24,7 +26,9 @@ export default function AgentDetailsModal({
|
|||||||
}: AgentDetailsModalProps) {
|
}: AgentDetailsModalProps) {
|
||||||
const token = useSelector(selectToken);
|
const token = useSelector(selectToken);
|
||||||
|
|
||||||
const [publicLink, setPublicLink] = useState<string | null>(null);
|
const [sharedToken, setSharedToken] = useState<string | null>(
|
||||||
|
agent.shared_token ?? null,
|
||||||
|
);
|
||||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string | null>(null);
|
const [webhookUrl, setWebhookUrl] = useState<string | null>(null);
|
||||||
const [loadingStates, setLoadingStates] = useState({
|
const [loadingStates, setLoadingStates] = useState({
|
||||||
@@ -40,6 +44,21 @@ export default function AgentDetailsModal({
|
|||||||
setLoadingStates((prev) => ({ ...prev, [key]: state }));
|
setLoadingStates((prev) => ({ ...prev, [key]: state }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGeneratePublicLink = async () => {
|
||||||
|
setLoading('publicLink', true);
|
||||||
|
const response = await userService.shareAgent(
|
||||||
|
{ id: agent.id ?? '', shared: true },
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
setLoading('publicLink', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setSharedToken(data.shared_token);
|
||||||
|
setLoading('publicLink', false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleGenerateWebhook = async () => {
|
const handleGenerateWebhook = async () => {
|
||||||
setLoading('webhook', true);
|
setLoading('webhook', true);
|
||||||
const response = await userService.getAgentWebhook(agent.id ?? '', token);
|
const response = await userService.getAgentWebhook(agent.id ?? '', token);
|
||||||
@@ -52,6 +71,11 @@ export default function AgentDetailsModal({
|
|||||||
setLoading('webhook', false);
|
setLoading('webhook', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSharedToken(agent.shared_token ?? null);
|
||||||
|
setApiKey(agent.key ?? null);
|
||||||
|
}, [agent]);
|
||||||
|
|
||||||
if (modalState !== 'ACTIVE') return null;
|
if (modalState !== 'ACTIVE') return null;
|
||||||
return (
|
return (
|
||||||
<WrapperModal
|
<WrapperModal
|
||||||
@@ -66,20 +90,45 @@ export default function AgentDetailsModal({
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="mt-8 flex flex-col gap-6">
|
<div className="mt-8 flex flex-col gap-6">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
<div className="flex items-center gap-2">
|
||||||
Public link
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
</h2>
|
Public Link
|
||||||
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
</h2>
|
||||||
Generate
|
{sharedToken && (
|
||||||
</button>
|
<div className="mb-1">
|
||||||
|
<CopyButton
|
||||||
|
textToCopy={`${baseURL}/agents/shared/${sharedToken}`}
|
||||||
|
padding="p-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{sharedToken ? (
|
||||||
|
<div className="flex flex-col flex-wrap items-start gap-2">
|
||||||
|
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
||||||
|
{`${baseURL}/agents/shared/${sharedToken}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="hover:bg-vi</button>olets-are-blue flex w-28 items-center justify-center rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
||||||
|
onClick={handleGeneratePublicLink}
|
||||||
|
>
|
||||||
|
{loadingStates.publicLink ? (
|
||||||
|
<Spinner size="small" color="#976af3" />
|
||||||
|
) : (
|
||||||
|
'Generate'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
API Key
|
API Key
|
||||||
</h2>
|
</h2>
|
||||||
{agent.key ? (
|
{apiKey ? (
|
||||||
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
||||||
{agent.key}
|
{apiKey}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
||||||
|
|||||||
Reference in New Issue
Block a user