mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-30 00:53:14 +00:00
feat: implement pinning functionality for agents with UI updates
This commit is contained in:
@@ -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