import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { NavLink, useNavigate } from 'react-router-dom'; import { Agent } from './agents/types'; import conversationService from './api/services/conversationService'; import userService from './api/services/userService'; import Add from './assets/add.svg'; import DocsGPT3 from './assets/cute_docsgpt3.svg'; import Discord from './assets/discord.svg'; import Expand from './assets/expand.svg'; import Github from './assets/git_nav.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, selectQueries, setConversation, updateConversationId, } from './conversation/conversationSlice'; import ConversationTile from './conversation/ConversationTile'; import { useDarkTheme, useMediaQuery } from './hooks'; import useDefaultDocument from './hooks/useDefaultDocument'; import useTokenAuth from './hooks/useTokenAuth'; import DeleteConvModal from './modals/DeleteConvModal'; import JWTModal from './modals/JWTModal'; import { ActiveState } from './models/misc'; import { getConversations } from './preferences/preferenceApi'; import { selectAgents, selectConversationId, selectConversations, selectModalStateDeleteConv, selectSelectedAgent, selectSharedAgents, selectToken, setAgents, setConversations, setModalStateDeleteConv, setSelectedAgent, setSharedAgents, } from './preferences/preferenceSlice'; import Upload from './upload/Upload'; interface NavigationProps { navOpen: boolean; setNavOpen: React.Dispatch>; } export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const dispatch = useDispatch(); const navigate = useNavigate(); const { t } = useTranslation(); const token = useSelector(selectToken); const queries = useSelector(selectQueries); const conversations = useSelector(selectConversations); const conversationId = useSelector(selectConversationId); const modalStateDeleteConv = useSelector(selectModalStateDeleteConv); const agents = useSelector(selectAgents); const sharedAgents = useSelector(selectSharedAgents); const selectedAgent = useSelector(selectSelectedAgent); const { isMobile, isTablet } = useMediaQuery(); const [isDarkTheme] = useDarkTheme(); const { showTokenModal, handleTokenSubmit } = useTokenAuth(); const [isDeletingConversation, setIsDeletingConversation] = useState(false); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); const [recentAgents, setRecentAgents] = useState([]); const navRef = useRef(null); useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( navRef.current && !navRef.current.contains(event.target as Node) && (isMobile || isTablet) && navOpen ) { setNavOpen(false); } } //event listener only for mobile/tablet when nav is open if ((isMobile || isTablet) && navOpen) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [navOpen, isMobile, isTablet, setNavOpen]); async function fetchRecentAgents() { try { 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)); 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]); } catch (error) { console.error('Failed to fetch recent agents: ', error); } } async function fetchConversations() { dispatch(setConversations({ ...conversations, loading: true })); return await getConversations(token) .then((fetchedConversations) => { dispatch(setConversations(fetchedConversations)); }) .catch((error) => { console.error('Failed to fetch conversations: ', error); dispatch(setConversations({ data: null, loading: false })); }); } useEffect(() => { fetchRecentAgents(); }, [agents, sharedAgents, token, dispatch]); useEffect(() => { if (!conversations?.data) fetchConversations(); if (queries.length === 0) resetConversation(); }, [conversations?.data, dispatch]); const handleDeleteAllConversations = () => { setIsDeletingConversation(true); conversationService .deleteAll(token) .then(() => { fetchConversations(); }) .catch((error) => console.error(error)); }; const handleDeleteConversation = (id: string) => { setIsDeletingConversation(true); conversationService .delete(id, {}, token) .then(() => { fetchConversations(); resetConversation(); }) .catch((error) => console.error(error)); }; const handleAgentClick = (agent: Agent) => { resetConversation(); dispatch(setSelectedAgent(agent)); if (isMobile || isTablet) setNavOpen(!navOpen); navigate('/'); }; const handleTogglePin = (agent: Agent) => { userService.togglePinAgent(agent.id ?? '', token).then((response) => { if (response.ok) { const updatePinnedStatus = (a: Agent) => a.id === agent.id ? { ...a, pinned: !a.pinned } : a; dispatch(setAgents(agents?.map(updatePinnedStatus))); dispatch(setSharedAgents(sharedAgents?.map(updatePinnedStatus))); } }); }; const handleConversationClick = async (index: string) => { try { dispatch(setSelectedAgent(null)); const response = await conversationService.getConversation(index, token); if (!response.ok) { navigate('/'); return; } const data = await response.json(); if (!data) return; dispatch(setConversation(data.queries)); dispatch(updateConversationId({ query: { conversationId: index } })); if (!data.agent_id) { navigate('/'); return; } let agent: Agent; if (data.is_shared_usage) { const sharedResponse = await userService.getSharedAgent( data.shared_token, token, ); if (!sharedResponse.ok) { navigate('/'); return; } agent = await sharedResponse.json(); navigate(`/agents/shared/${agent.shared_token}`); } else { const agentResponse = await userService.getAgent(data.agent_id, token); if (!agentResponse.ok) { navigate('/'); return; } agent = await agentResponse.json(); if (agent.shared_token) { navigate(`/agents/shared/${agent.shared_token}`); } else { await Promise.resolve(dispatch(setSelectedAgent(agent))); navigate('/'); } } } catch (error) { console.error('Error handling conversation click:', error); navigate('/'); } }; const resetConversation = () => { handleAbort(); dispatch(setConversation([])); dispatch( updateConversationId({ query: { conversationId: null }, }), ); dispatch(setSelectedAgent(null)); }; const newChat = () => { if (queries && queries?.length > 0) { resetConversation(); } }; async function updateConversationName(updatedConversation: { name: string; id: string; }) { await conversationService .update(updatedConversation, token) .then((response) => response.json()) .then((data) => { if (data) { navigate('/'); fetchConversations(); } }) .catch((err) => { console.error(err); }); } useEffect(() => { setNavOpen(!(isMobile || isTablet)); }, [isMobile, isTablet]); useDefaultDocument(); return ( <> {!navOpen && (
{queries?.length > 0 && ( )}
DocsGPT
)}
{ if (isMobile) { setNavOpen(!navOpen); } }} > DocsGPT Logo

DocsGPT

{ if (isMobile || isTablet) { setNavOpen(!navOpen); } resetConversation(); }} className={({ isActive }) => `${ isActive ? 'bg-transparent' : '' } group border-silver hover:border-rainy-gray dark:border-purple-taupe sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border p-3 hover:bg-transparent dark:text-white` } > Create new chat

{t('newChat')}

{conversations?.loading && !isDeletingConversation && (
Loading conversations
)} {recentAgents?.length > 0 ? (

Agents

{recentAgents.map((agent, idx) => (
handleAgentClick(agent)} >
agent-logo

{agent.name}

))}
{ dispatch(setSelectedAgent(null)); if (isMobile || isTablet) { setNavOpen(false); } navigate('/agents'); }} >
manage-agents

{t('manageAgents')}

) : (
{ if (isMobile || isTablet) { setNavOpen(false); } dispatch(setSelectedAgent(null)); navigate('/agents'); }} >
manage-agents

{t('manageAgents')}

)} {conversations?.data && conversations.data.length > 0 ? (

{t('chats')}

{conversations.data?.map((conversation) => ( handleConversationClick(id)} onConversationClick={() => { if (isMobile) { setNavOpen(false); } }} onDeleteConversation={(id) => handleDeleteConversation(id)} onSave={(conversation) => updateConversationName(conversation) } /> ))}
) : ( <> )}
{ if (isMobile || isTablet) { setNavOpen(false); } resetConversation(); }} to="/settings" className={({ isActive }) => `mx-4 my-auto flex h-9 cursor-pointer items-center gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ isActive ? 'bg-gray-3000 dark:bg-transparent' : '' }` } > Settings

{t('settings.label')}

Join Discord community Follow us on Twitter View on GitHub
DocsGPT
{uploadModalState === 'ACTIVE' && ( setUploadModalState('INACTIVE')} > )} ); }