feat: implement pinning functionality for agents with UI updates

This commit is contained in:
Siddhant Rai
2025-05-06 16:12:55 +05:30
parent dcfcbf54be
commit 07fa656e7c
12 changed files with 307 additions and 117 deletions

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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;

View File

@@ -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',

View File

@@ -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> =>

View 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

View 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

View File

@@ -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>
)}

View File

@@ -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 {