mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-01-20 14:00:55 +00:00
feat: implement pinning functionality for agents with UI updates
This commit is contained in:
@@ -110,7 +110,9 @@ class DeleteConversation(Resource):
|
||||
{"_id": ObjectId(conversation_id), "user": decoded_token["sub"]}
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error deleting conversation: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error deleting conversation: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
@@ -128,7 +130,9 @@ class DeleteAllConversations(Resource):
|
||||
try:
|
||||
conversations_collection.delete_many({"user": user_id})
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error deleting all conversations: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error deleting all conversations: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
@@ -166,7 +170,9 @@ class GetConversations(Resource):
|
||||
for conversation in conversations
|
||||
]
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving conversations: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error retrieving conversations: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(list_conversations), 200)
|
||||
|
||||
@@ -194,7 +200,9 @@ class GetSingleConversation(Resource):
|
||||
if not conversation:
|
||||
return make_response(jsonify({"status": "not found"}), 404)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving conversation: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error retrieving conversation: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
data = {
|
||||
@@ -236,7 +244,9 @@ class UpdateConversationName(Resource):
|
||||
{"$set": {"name": data["name"]}},
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating conversation name: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating conversation name: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
@@ -377,7 +387,9 @@ class DeleteOldIndexes(Resource):
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error deleting old indexes: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error deleting old indexes: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
sources_collection.delete_one({"_id": ObjectId(source_id)})
|
||||
@@ -577,7 +589,9 @@ class UploadRemote(Resource):
|
||||
loader=data["source"],
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error uploading remote source: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error uploading remote source: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
|
||||
@@ -689,7 +703,9 @@ class PaginatedSources(Resource):
|
||||
return make_response(jsonify(response), 200)
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving paginated sources: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error retrieving paginated sources: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
|
||||
@@ -996,23 +1012,28 @@ class GetAgent(Resource):
|
||||
return make_response(jsonify({"status": "Not found"}), 404)
|
||||
data = {
|
||||
"id": str(agent["_id"]),
|
||||
"name": agent["name"],
|
||||
"description": agent["description"],
|
||||
"name": agent.get("name", ""),
|
||||
"description": agent.get("description", ""),
|
||||
"source": (
|
||||
str(db.dereference(agent["source"])["_id"])
|
||||
if "source" in agent and isinstance(agent["source"], DBRef)
|
||||
else ""
|
||||
),
|
||||
"chunks": agent["chunks"],
|
||||
"chunks": agent.get("chunks", ""),
|
||||
"retriever": agent.get("retriever", ""),
|
||||
"prompt_id": agent["prompt_id"],
|
||||
"prompt_id": agent.get("prompt_id", ""),
|
||||
"tools": agent.get("tools", []),
|
||||
"agent_type": agent["agent_type"],
|
||||
"status": agent["status"],
|
||||
"createdAt": agent["createdAt"],
|
||||
"updatedAt": agent["updatedAt"],
|
||||
"lastUsedAt": agent["lastUsedAt"],
|
||||
"key": f"{agent['key'][:4]}...{agent['key'][-4:]}",
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
"key": (
|
||||
f"{agent['key'][:4]}...{agent['key'][-4:]}"
|
||||
if "key" in agent
|
||||
else ""
|
||||
),
|
||||
"pinned": agent.get("pinned", False),
|
||||
}
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True)
|
||||
@@ -1034,23 +1055,28 @@ class GetAgents(Resource):
|
||||
list_agents = [
|
||||
{
|
||||
"id": str(agent["_id"]),
|
||||
"name": agent["name"],
|
||||
"description": agent["description"],
|
||||
"name": agent.get("name", ""),
|
||||
"description": agent.get("description", ""),
|
||||
"source": (
|
||||
str(db.dereference(agent["source"])["_id"])
|
||||
if "source" in agent and isinstance(agent["source"], DBRef)
|
||||
else ""
|
||||
),
|
||||
"chunks": agent["chunks"],
|
||||
"chunks": agent.get("chunks", ""),
|
||||
"retriever": agent.get("retriever", ""),
|
||||
"prompt_id": agent["prompt_id"],
|
||||
"prompt_id": agent.get("prompt_id", ""),
|
||||
"tools": agent.get("tools", []),
|
||||
"agent_type": agent["agent_type"],
|
||||
"status": agent["status"],
|
||||
"created_at": agent["createdAt"],
|
||||
"updated_at": agent["updatedAt"],
|
||||
"last_used_at": agent["lastUsedAt"],
|
||||
"key": f"{agent['key'][:4]}...{agent['key'][-4:]}",
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
"key": (
|
||||
f"{agent['key'][:4]}...{agent['key'][-4:]}"
|
||||
if "key" in agent
|
||||
else ""
|
||||
),
|
||||
"pinned": agent.get("pinned", False),
|
||||
}
|
||||
for agent in agents
|
||||
if "source" in agent or "retriever" in agent
|
||||
@@ -1196,7 +1222,9 @@ class UpdateAgent(Resource):
|
||||
existing_agent = agents_collection.find_one({"_id": oid, "user": user})
|
||||
except Exception as err:
|
||||
return make_response(
|
||||
current_app.logger.error(f"Error finding agent {agent_id}: {err}", exc_info=True),
|
||||
current_app.logger.error(
|
||||
f"Error finding agent {agent_id}: {err}", exc_info=True
|
||||
),
|
||||
jsonify({"success": False, "message": "Database error finding agent"}),
|
||||
500,
|
||||
)
|
||||
@@ -1319,7 +1347,9 @@ class UpdateAgent(Resource):
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating agent {agent_id}: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating agent {agent_id}: {err}", exc_info=True
|
||||
)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Database error during update"}),
|
||||
500,
|
||||
@@ -1368,6 +1398,86 @@ class DeleteAgent(Resource):
|
||||
return make_response(jsonify({"id": deleted_id}), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/pinned_agents")
|
||||
class PinnedAgents(Resource):
|
||||
@api.doc(description="Get pinned agents for the user")
|
||||
def get(self):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
user = decoded_token.get("sub")
|
||||
try:
|
||||
pinned_agents = agents_collection.find({"user": user, "pinned": True})
|
||||
list_pinned_agents = [
|
||||
{
|
||||
"id": str(agent["_id"]),
|
||||
"name": agent.get("name", ""),
|
||||
"description": agent.get("description", ""),
|
||||
"source": (
|
||||
str(db.dereference(agent["source"])["_id"])
|
||||
if "source" in agent and isinstance(agent["source"], DBRef)
|
||||
else ""
|
||||
),
|
||||
"chunks": agent.get("chunks", ""),
|
||||
"retriever": agent.get("retriever", ""),
|
||||
"prompt_id": agent.get("prompt_id", ""),
|
||||
"tools": agent.get("tools", []),
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
"key": (
|
||||
f"{agent['key'][:4]}...{agent['key'][-4:]}"
|
||||
if "key" in agent
|
||||
else ""
|
||||
),
|
||||
"pinned": agent.get("pinned", False),
|
||||
}
|
||||
for agent in pinned_agents
|
||||
if "source" in agent or "retriever" in agent
|
||||
]
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving pinned agents: {err}")
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(list_pinned_agents), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/pin_agent")
|
||||
class PinAgent(Resource):
|
||||
@api.doc(params={"id": "ID of the agent"}, description="Pin or unpin an agent")
|
||||
def post(self):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
user = decoded_token.get("sub")
|
||||
agent_id = request.args.get("id")
|
||||
if not agent_id:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "ID is required"}), 400
|
||||
)
|
||||
|
||||
try:
|
||||
agent = agents_collection.find_one(
|
||||
{"_id": ObjectId(agent_id), "user": user}
|
||||
)
|
||||
if not agent:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Agent not found"}), 404
|
||||
)
|
||||
|
||||
pinned_status = not agent.get("pinned", False)
|
||||
agents_collection.update_one(
|
||||
{"_id": ObjectId(agent_id), "user": user},
|
||||
{"$set": {"pinned": pinned_status}},
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error pinning/unpinning agent: {err}")
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/agent_webhook")
|
||||
class AgentWebhook(Resource):
|
||||
@api.doc(
|
||||
@@ -1405,7 +1515,9 @@ class AgentWebhook(Resource):
|
||||
full_webhook_url = f"{base_url}/api/webhooks/agents/{webhook_token}"
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error generating webhook URL: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error generating webhook URL: {err}", exc_info=True
|
||||
)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error generating webhook URL"}),
|
||||
400,
|
||||
@@ -1694,7 +1806,9 @@ class ShareConversation(Resource):
|
||||
201,
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error sharing conversation: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error sharing conversation: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
|
||||
@@ -1750,7 +1864,9 @@ class GetPubliclySharedConversations(Resource):
|
||||
res["api_key"] = shared["api_key"]
|
||||
return make_response(jsonify(res), 200)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting shared conversation: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error getting shared conversation: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
|
||||
@@ -1870,7 +1986,9 @@ class GetMessageAnalytics(Resource):
|
||||
daily_messages[entry["_id"]] = entry["count"]
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting message analytics: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error getting message analytics: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(
|
||||
@@ -2029,7 +2147,9 @@ class GetTokenAnalytics(Resource):
|
||||
daily_token_usage[entry["_id"]["day"]] = entry["total_tokens"]
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting token analytics: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error getting token analytics: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(
|
||||
@@ -2194,7 +2314,9 @@ class GetFeedbackAnalytics(Resource):
|
||||
}
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting feedback analytics: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error getting feedback analytics: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(
|
||||
@@ -2330,7 +2452,9 @@ class ManageSync(Resource):
|
||||
update_data,
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating sync frequency: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating sync frequency: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
@@ -2391,7 +2515,9 @@ class AvailableTools(Resource):
|
||||
}
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting available tools: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error getting available tools: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True, "data": tools_metadata}), 200)
|
||||
@@ -2595,7 +2721,9 @@ class UpdateToolConfig(Resource):
|
||||
{"$set": {"config": data["config"]}},
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating tool config: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating tool config: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
@@ -2634,7 +2762,9 @@ class UpdateToolActions(Resource):
|
||||
{"$set": {"actions": data["actions"]}},
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating tool actions: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating tool actions: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
@@ -2671,7 +2801,9 @@ class UpdateToolStatus(Resource):
|
||||
{"$set": {"status": data["status"]}},
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error updating tool status: {err}", exc_info=True)
|
||||
current_app.logger.error(
|
||||
f"Error updating tool status: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@@ -9415,13 +9415,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
|
||||
@@ -13,12 +13,14 @@ import Expand from './assets/expand.svg';
|
||||
import Github from './assets/github.svg';
|
||||
import Hamburger from './assets/hamburger.svg';
|
||||
import openNewChat from './assets/openNewChat.svg';
|
||||
import Pin from './assets/pin.svg';
|
||||
import Robot from './assets/robot.svg';
|
||||
import SettingGear from './assets/settingGear.svg';
|
||||
import Spark from './assets/spark.svg';
|
||||
import SpinnerDark from './assets/spinner-dark.svg';
|
||||
import Spinner from './assets/spinner.svg';
|
||||
import Twitter from './assets/TwitterX.svg';
|
||||
import UnPin from './assets/unpin.svg';
|
||||
import Help from './components/Help';
|
||||
import {
|
||||
handleAbort,
|
||||
@@ -35,16 +37,16 @@ import JWTModal from './modals/JWTModal';
|
||||
import { ActiveState } from './models/misc';
|
||||
import { getConversations } from './preferences/preferenceApi';
|
||||
import {
|
||||
selectAgents,
|
||||
selectConversationId,
|
||||
selectConversations,
|
||||
selectModalStateDeleteConv,
|
||||
selectSelectedAgent,
|
||||
selectToken,
|
||||
setAgents,
|
||||
setConversations,
|
||||
setModalStateDeleteConv,
|
||||
setSelectedAgent,
|
||||
setAgents,
|
||||
selectAgents,
|
||||
} from './preferences/preferenceSlice';
|
||||
import Upload from './upload/Upload';
|
||||
|
||||
@@ -80,24 +82,35 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
|
||||
async function fetchRecentAgents() {
|
||||
try {
|
||||
let recentAgents: Agent[] = [];
|
||||
const response = await userService.getPinnedAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch pinned agents');
|
||||
const pinnedAgents: Agent[] = await response.json();
|
||||
if (pinnedAgents.length >= 3) {
|
||||
setRecentAgents(pinnedAgents);
|
||||
return;
|
||||
}
|
||||
let tempAgents: Agent[] = [];
|
||||
if (!agents) {
|
||||
const response = await userService.getAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data: Agent[] = await response.json();
|
||||
dispatch(setAgents(data));
|
||||
recentAgents = data;
|
||||
} else recentAgents = agents;
|
||||
setRecentAgents(
|
||||
recentAgents
|
||||
.filter((agent: Agent) => agent.status === 'published')
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.last_used_at ?? 0).getTime() -
|
||||
new Date(a.last_used_at ?? 0).getTime(),
|
||||
)
|
||||
.slice(0, 3),
|
||||
);
|
||||
tempAgents = data;
|
||||
} else tempAgents = agents;
|
||||
const additionalAgents = tempAgents
|
||||
.filter(
|
||||
(agent: Agent) =>
|
||||
agent.status === 'published' &&
|
||||
!pinnedAgents.some((pinned) => pinned.id === agent.id),
|
||||
)
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.last_used_at ?? 0).getTime() -
|
||||
new Date(a.last_used_at ?? 0).getTime(),
|
||||
)
|
||||
.slice(0, 3 - pinnedAgents.length);
|
||||
setRecentAgents([...pinnedAgents, ...additionalAgents]);
|
||||
console.log(additionalAgents);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recent agents: ', error);
|
||||
}
|
||||
@@ -116,7 +129,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (token) fetchRecentAgents();
|
||||
fetchRecentAgents();
|
||||
}, [agents, token, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -152,6 +165,17 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleTogglePin = (agent: Agent) => {
|
||||
userService.togglePinAgent(agent.id ?? '', token).then((response) => {
|
||||
if (response.ok) {
|
||||
const updatedAgents = agents?.map((a) =>
|
||||
a.id === agent.id ? { ...a, pinned: !a.pinned } : a,
|
||||
);
|
||||
dispatch(setAgents(updatedAgents));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleConversationClick = (index: string) => {
|
||||
conversationService
|
||||
.getConversation(index, token)
|
||||
@@ -336,23 +360,39 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
{recentAgents.map((agent, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
|
||||
className={`mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
|
||||
agent.id === selectedAgent?.id && !conversationId
|
||||
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
>
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={agent.image ?? Robot}
|
||||
alt="agent-logo"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={agent.image ?? Robot}
|
||||
alt="agent-logo"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||
{agent.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center px-3">
|
||||
<button
|
||||
className="rounded-full hover:opacity-75"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTogglePin(agent);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={agent.pinned ? UnPin : Pin}
|
||||
className="h-4 w-4"
|
||||
></img>
|
||||
</button>
|
||||
</div>
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||
{agent.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import Spinner from '../components/Spinner';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import Analytics from '../settings/Analytics';
|
||||
import Logs from '../settings/Logs';
|
||||
import Spinner from '../components/Spinner';
|
||||
import { Agent } from './types';
|
||||
|
||||
export default function AgentLogs() {
|
||||
@@ -54,11 +54,16 @@ export default function AgentLogs() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-3 px-4">
|
||||
<h2 className="text-sm font-semibold text-black dark:text-[#E0E0E0]">
|
||||
Agent Name
|
||||
</h2>
|
||||
{agent && (
|
||||
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
|
||||
<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">
|
||||
{agent.last_used_at
|
||||
? 'Last used at ' +
|
||||
new Date(agent.last_used_at).toLocaleString()
|
||||
: 'No usage history'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loadingAgent ? (
|
||||
@@ -74,7 +79,7 @@ export default function AgentLogs() {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
agent && <Logs agentId={agentId} tableHeader="Agent endpoint logs" />
|
||||
agent && <Logs agentId={agent.id} tableHeader="Agent endpoint logs" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import React, { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Copy from '../assets/copy-linear.svg';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Monitoring from '../assets/monitoring.svg';
|
||||
import Pin from '../assets/pin.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import Robot from '../assets/robot.svg';
|
||||
import ThreeDots from '../assets/three-dots.svg';
|
||||
import UnPin from '../assets/unpin.svg';
|
||||
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||
import Spinner from '../components/Spinner';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
selectToken,
|
||||
setSelectedAgent,
|
||||
setAgents,
|
||||
selectAgents,
|
||||
selectSelectedAgent,
|
||||
selectToken,
|
||||
setAgents,
|
||||
setSelectedAgent,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import AgentLogs from './AgentLogs';
|
||||
import NewAgent from './NewAgent';
|
||||
import { Agent } from './types';
|
||||
import Spinner from '../components/Spinner';
|
||||
|
||||
export default function Agents() {
|
||||
return (
|
||||
@@ -42,7 +44,6 @@ function AgentsList() {
|
||||
const agents = useSelector(selectAgents);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const [userAgents, setUserAgents] = useState<Agent[]>(agents || []);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const getAgents = async () => {
|
||||
@@ -51,7 +52,6 @@ function AgentsList() {
|
||||
const response = await userService.getAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data = await response.json();
|
||||
setUserAgents(data);
|
||||
dispatch(setAgents(data));
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
@@ -133,14 +133,9 @@ function AgentsList() {
|
||||
<div className="flex h-72 w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : userAgents.length > 0 ? (
|
||||
userAgents.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
agents={userAgents}
|
||||
setUserAgents={setUserAgents}
|
||||
/>
|
||||
) : agents && agents.length > 0 ? (
|
||||
agents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} agents={agents} />
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
|
||||
@@ -159,15 +154,7 @@ function AgentsList() {
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCard({
|
||||
agent,
|
||||
agents,
|
||||
setUserAgents,
|
||||
}: {
|
||||
agent: Agent;
|
||||
agents: Agent[];
|
||||
setUserAgents: React.Dispatch<React.SetStateAction<Agent[]>>;
|
||||
}) {
|
||||
function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
@@ -178,6 +165,21 @@ function AgentCard({
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const togglePin = async () => {
|
||||
try {
|
||||
const response = await userService.togglePinAgent(agent.id ?? '', token);
|
||||
if (!response.ok) throw new Error('Failed to pin agent');
|
||||
const updatedAgents = agents.map((prevAgent) => {
|
||||
if (prevAgent.id === agent.id)
|
||||
return { ...prevAgent, pinned: !prevAgent.pinned };
|
||||
return prevAgent;
|
||||
});
|
||||
dispatch(setAgents(updatedAgents));
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
icon: Monitoring,
|
||||
@@ -201,6 +203,17 @@ function AgentCard({
|
||||
iconWidth: 14,
|
||||
iconHeight: 14,
|
||||
},
|
||||
{
|
||||
icon: agent.pinned ? UnPin : Pin,
|
||||
label: agent.pinned ? 'Unpin' : 'Pin agent',
|
||||
onClick: (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
togglePin();
|
||||
},
|
||||
variant: 'primary',
|
||||
iconWidth: 18,
|
||||
iconHeight: 18,
|
||||
},
|
||||
{
|
||||
icon: Trash,
|
||||
label: 'Delete',
|
||||
@@ -209,8 +222,8 @@ function AgentCard({
|
||||
setDeleteConfirmation('ACTIVE');
|
||||
},
|
||||
variant: 'danger',
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
iconWidth: 13,
|
||||
iconHeight: 13,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -225,9 +238,6 @@ function AgentCard({
|
||||
const response = await userService.deleteAgent(agentId, token);
|
||||
if (!response.ok) throw new Error('Failed to delete agent');
|
||||
const data = await response.json();
|
||||
setUserAgents((prevAgents) =>
|
||||
prevAgents.filter((prevAgent) => prevAgent.id !== data.id),
|
||||
);
|
||||
dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id)));
|
||||
};
|
||||
return (
|
||||
@@ -244,7 +254,7 @@ function AgentCard({
|
||||
e.stopPropagation();
|
||||
setIsMenuOpen(true);
|
||||
}}
|
||||
className="absolute right-4 top-4 z-50 cursor-pointer"
|
||||
className="absolute right-4 top-4 z-10 cursor-pointer"
|
||||
>
|
||||
<img src={ThreeDots} alt={'use-agent'} className="h-[19px] w-[19px]" />
|
||||
<ContextMenu
|
||||
|
||||
@@ -11,6 +11,8 @@ export type Agent = {
|
||||
agent_type: string;
|
||||
status: string;
|
||||
key?: string;
|
||||
incoming_webhook_token?: string;
|
||||
pinned?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
last_used_at?: string;
|
||||
|
||||
@@ -13,6 +13,8 @@ const endpoints = {
|
||||
CREATE_AGENT: '/api/create_agent',
|
||||
UPDATE_AGENT: (agent_id: string) => `/api/update_agent/${agent_id}`,
|
||||
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
|
||||
PINNED_AGENTS: '/api/pinned_agents',
|
||||
TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`,
|
||||
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
|
||||
PROMPTS: '/api/get_prompts',
|
||||
CREATE_PROMPT: '/api/create_prompt',
|
||||
|
||||
@@ -31,6 +31,10 @@ const userService = {
|
||||
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
|
||||
deleteAgent: (id: string, token: string | null): Promise<any> =>
|
||||
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
|
||||
getPinnedAgents: (token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.PINNED_AGENTS, token),
|
||||
togglePinAgent: (id: string, token: string | null): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token),
|
||||
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
|
||||
getPrompts: (token: string | null): Promise<any> =>
|
||||
|
||||
1
frontend/src/assets/pin.svg
Normal file
1
frontend/src/assets/pin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin-icon lucide-pin"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>
|
||||
|
After Width: | Height: | Size: 458 B |
1
frontend/src/assets/unpin.svg
Normal file
1
frontend/src/assets/unpin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin-off-icon lucide-pin-off"><path d="M12 17v5"/><path d="M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89"/><path d="m2 2 20 20"/><path d="M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"/></svg>
|
||||
|
After Width: | Height: | Size: 416 B |
@@ -104,8 +104,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'
|
||||
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey'
|
||||
? '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'
|
||||
} `}
|
||||
>
|
||||
{option.icon && (
|
||||
@@ -115,7 +115,7 @@ export default function ContextMenu({
|
||||
height={option.iconHeight || 16}
|
||||
src={option.icon}
|
||||
alt={option.label}
|
||||
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
|
||||
className={`cursor-pointer ${option.iconClassName || ''}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -50,11 +50,11 @@ body.dark {
|
||||
|
||||
@layer components {
|
||||
.table-default {
|
||||
@apply block w-full table-auto justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto;
|
||||
@apply block w-full table-auto justify-center overflow-auto rounded-xl border border-silver text-center dark:border-silver/40 dark:text-bright-gray;
|
||||
}
|
||||
|
||||
.table-default th {
|
||||
@apply p-4 font-normal text-gray-400 text-nowrap;
|
||||
@apply text-nowrap p-4 font-normal text-gray-400;
|
||||
}
|
||||
|
||||
.table-default th {
|
||||
@@ -66,7 +66,7 @@ body.dark {
|
||||
}
|
||||
|
||||
.table-default td {
|
||||
@apply border-t w-full border-silver dark:border-silver/40 px-4 py-2;
|
||||
@apply w-full border-t border-silver px-4 py-2 dark:border-silver/40;
|
||||
}
|
||||
|
||||
.table-default td:last-child {
|
||||
|
||||
Reference in New Issue
Block a user