From 07fa656e7ca89783372841c96a7cf7057303ac2b Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Tue, 6 May 2025 16:12:55 +0530 Subject: [PATCH] feat: implement pinning functionality for agents with UI updates --- application/api/user/routes.py | 214 ++++++++++++++++++----- frontend/package-lock.json | 7 - frontend/src/Navigation.tsx | 92 +++++++--- frontend/src/agents/AgentLogs.tsx | 17 +- frontend/src/agents/index.tsx | 72 ++++---- frontend/src/agents/types/index.ts | 2 + frontend/src/api/endpoints.ts | 2 + frontend/src/api/services/userService.ts | 4 + frontend/src/assets/pin.svg | 1 + frontend/src/assets/unpin.svg | 1 + frontend/src/components/ContextMenu.tsx | 6 +- frontend/src/index.css | 6 +- 12 files changed, 307 insertions(+), 117 deletions(-) create mode 100644 frontend/src/assets/pin.svg create mode 100644 frontend/src/assets/unpin.svg diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 3b3cb21f..93268102 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -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) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fa250e66..772753f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 53487dd6..c0710b9c 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -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) => (
handleAgentClick(agent)} > -
- agent-logo +
+
+ agent-logo +
+

+ {agent.name} +

+
+
+
-

- {agent.name} -

))}
diff --git a/frontend/src/agents/AgentLogs.tsx b/frontend/src/agents/AgentLogs.tsx index 864a85fe..12638b8e 100644 --- a/frontend/src/agents/AgentLogs.tsx +++ b/frontend/src/agents/AgentLogs.tsx @@ -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() {
-

- Agent Name -

{agent && ( -

{agent.name}

+
+

{agent.name}

+

+ {agent.last_used_at + ? 'Last used at ' + + new Date(agent.last_used_at).toLocaleString() + : 'No usage history'} +

+
)}
{loadingAgent ? ( @@ -74,7 +79,7 @@ export default function AgentLogs() { ) : ( - agent && + agent && )} ); diff --git a/frontend/src/agents/index.tsx b/frontend/src/agents/index.tsx index c2edb34a..9034e808 100644 --- a/frontend/src/agents/index.tsx +++ b/frontend/src/agents/index.tsx @@ -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(agents || []); const [loading, setLoading] = useState(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() {
- ) : userAgents.length > 0 ? ( - userAgents.map((agent) => ( - + ) : agents && agents.length > 0 ? ( + agents.map((agent) => ( + )) ) : (
@@ -159,15 +154,7 @@ function AgentsList() { ); } -function AgentCard({ - agent, - agents, - setUserAgents, -}: { - agent: Agent; - agents: Agent[]; - setUserAgents: React.Dispatch>; -}) { +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(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" > {'use-agent'} `/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', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 4a0f45d8..ae2bbe8e 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -31,6 +31,10 @@ const userService = { apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token), deleteAgent: (id: string, token: string | null): Promise => apiClient.delete(endpoints.USER.DELETE_AGENT(id), token), + getPinnedAgents: (token: string | null): Promise => + apiClient.get(endpoints.USER.PINNED_AGENTS, token), + togglePinAgent: (id: string, token: string | null): Promise => + apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token), getAgentWebhook: (id: string, token: string | null): Promise => apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token), getPrompts: (token: string | null): Promise => diff --git a/frontend/src/assets/pin.svg b/frontend/src/assets/pin.svg new file mode 100644 index 00000000..4e1d1071 --- /dev/null +++ b/frontend/src/assets/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/unpin.svg b/frontend/src/assets/unpin.svg new file mode 100644 index 00000000..edb46d5d --- /dev/null +++ b/frontend/src/assets/unpin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx index f8359371..762a7828 100644 --- a/frontend/src/components/ContextMenu.tsx +++ b/frontend/src/components/ContextMenu.tsx @@ -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 || ''}`} />
)} diff --git a/frontend/src/index.css b/frontend/src/index.css index 07760385..4c1bb30f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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 {