|
import { useEffect, useRef, useState } from 'react'; |
|
import { useDispatch, useSelector } from 'react-redux'; |
|
import { NavLink, useNavigate } from 'react-router-dom'; |
|
import DocsGPT3 from './assets/cute_docsgpt3.svg'; |
|
import Documentation from './assets/documentation.svg'; |
|
import DocumentationDark from './assets/documentation-dark.svg'; |
|
import Discord from './assets/discord.svg'; |
|
import DiscordDark from './assets/discord-dark.svg'; |
|
|
|
import Arrow2 from './assets/dropdown-arrow.svg'; |
|
import Expand from './assets/expand.svg'; |
|
import Trash from './assets/trash.svg'; |
|
import Github from './assets/github.svg'; |
|
import GithubDark from './assets/github-dark.svg'; |
|
import Hamburger from './assets/hamburger.svg'; |
|
import HamburgerDark from './assets/hamburger-dark.svg'; |
|
import Info from './assets/info.svg'; |
|
import InfoDark from './assets/info-dark.svg'; |
|
import SettingGear from './assets/settingGear.svg'; |
|
import SettingGearDark from './assets/settingGear-dark.svg'; |
|
import Add from './assets/add.svg'; |
|
import UploadIcon from './assets/upload.svg'; |
|
import { ActiveState } from './models/misc'; |
|
import APIKeyModal from './preferences/APIKeyModal'; |
|
import { |
|
selectApiKeyStatus, |
|
selectSelectedDocs, |
|
selectSelectedDocsStatus, |
|
selectSourceDocs, |
|
setSelectedDocs, |
|
selectConversations, |
|
setConversations, |
|
selectConversationId, |
|
} from './preferences/preferenceSlice'; |
|
import { |
|
setConversation, |
|
updateConversationId, |
|
} from './conversation/conversationSlice'; |
|
import { useMediaQuery, useOutsideAlerter } from './hooks'; |
|
import Upload from './upload/Upload'; |
|
import { Doc, getConversations } from './preferences/preferenceApi'; |
|
import SelectDocsModal from './preferences/SelectDocsModal'; |
|
import ConversationTile from './conversation/ConversationTile'; |
|
|
|
interface NavigationProps { |
|
navOpen: boolean; |
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|
} |
|
|
|
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { |
|
const dispatch = useDispatch(); |
|
const docs = useSelector(selectSourceDocs); |
|
const selectedDocs = useSelector(selectSelectedDocs); |
|
const conversations = useSelector(selectConversations); |
|
const conversationId = useSelector(selectConversationId); |
|
const { isMobile } = useMediaQuery(); |
|
const isDarkTheme = document.documentElement.classList.contains('dark'); |
|
const [isDocsListOpen, setIsDocsListOpen] = useState(false); |
|
|
|
const isApiKeySet = useSelector(selectApiKeyStatus); |
|
const [apiKeyModalState, setApiKeyModalState] = |
|
useState<ActiveState>('INACTIVE'); |
|
|
|
const isSelectedDocsSet = useSelector(selectSelectedDocsStatus); |
|
const [selectedDocsModalState, setSelectedDocsModalState] = |
|
useState<ActiveState>(isSelectedDocsSet ? 'INACTIVE' : 'ACTIVE'); |
|
|
|
const [uploadModalState, setUploadModalState] = |
|
useState<ActiveState>('INACTIVE'); |
|
|
|
const navRef = useRef(null); |
|
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; |
|
const embeddingsName = |
|
import.meta.env.VITE_EMBEDDINGS_NAME || |
|
'huggingface_sentence-transformers/all-mpnet-base-v2'; |
|
|
|
const navigate = useNavigate(); |
|
|
|
useEffect(() => { |
|
if (!conversations) { |
|
fetchConversations(); |
|
} |
|
}, [conversations, dispatch]); |
|
|
|
async function fetchConversations() { |
|
return await getConversations() |
|
.then((fetchedConversations) => { |
|
dispatch(setConversations(fetchedConversations)); |
|
}) |
|
.catch((error) => { |
|
console.error('Failed to fetch conversations: ', error); |
|
}); |
|
} |
|
|
|
const handleDeleteConversation = (id: string) => { |
|
fetch(`${apiHost}/api/delete_conversation?id=${id}`, { |
|
method: 'POST', |
|
}) |
|
.then(() => { |
|
fetchConversations(); |
|
}) |
|
.catch((error) => console.error(error)); |
|
}; |
|
|
|
const handleDeleteClick = (index: number, doc: Doc) => { |
|
const docPath = 'indexes/' + 'local' + '/' + doc.name; |
|
|
|
fetch(`${apiHost}/api/delete_old?path=${docPath}`, { |
|
method: 'GET', |
|
}) |
|
.then(() => { |
|
|
|
const imageElement = document.querySelector( |
|
`#img-${index}`, |
|
) as HTMLElement; |
|
const parentElement = imageElement.parentNode as HTMLElement; |
|
parentElement.parentNode?.removeChild(parentElement); |
|
}) |
|
.catch((error) => console.error(error)); |
|
}; |
|
|
|
const handleConversationClick = (index: string) => { |
|
|
|
fetch(`${apiHost}/api/get_single_conversation?id=${index}`, { |
|
method: 'GET', |
|
}) |
|
.then((response) => response.json()) |
|
.then((data) => { |
|
navigate('/'); |
|
dispatch(setConversation(data)); |
|
dispatch( |
|
updateConversationId({ |
|
query: { conversationId: index }, |
|
}), |
|
); |
|
}); |
|
}; |
|
|
|
async function updateConversationName(updatedConversation: { |
|
name: string; |
|
id: string; |
|
}) { |
|
await fetch(`${apiHost}/api/update_conversation_name`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify(updatedConversation), |
|
}) |
|
.then((response) => response.json()) |
|
.then((data) => { |
|
if (data) { |
|
navigate('/'); |
|
fetchConversations(); |
|
} |
|
}) |
|
.catch((err) => { |
|
console.error(err); |
|
}); |
|
} |
|
useOutsideAlerter( |
|
navRef, |
|
() => { |
|
if (isMobile && navOpen && apiKeyModalState === 'INACTIVE') { |
|
setNavOpen(false); |
|
setIsDocsListOpen(false); |
|
} |
|
}, |
|
[navOpen, isDocsListOpen, apiKeyModalState], |
|
); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
setNavOpen(!isMobile); |
|
}, [isMobile]); |
|
|
|
return ( |
|
<> |
|
{!navOpen && ( |
|
<button |
|
className="duration-25 absolute top-3 left-3 z-20 hidden transition-all md:block" |
|
onClick={() => { |
|
setNavOpen(!navOpen); |
|
}} |
|
> |
|
<img |
|
src={Expand} |
|
alt="menu toggle" |
|
className={`${!navOpen ? 'rotate-180' : 'rotate-0' |
|
} m-auto transition-all duration-200`} |
|
/> |
|
</button> |
|
)} |
|
<div |
|
ref={navRef} |
|
className={`${!navOpen && '-ml-96 md:-ml-[18rem]' |
|
} duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-r-[1px] border-b-0 dark:border-r-purple-taupe bg-white dark:bg-chinese-black transition-all dark:text-white`} |
|
> |
|
<div |
|
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'} |
|
> |
|
<div className="my-auto mx-4 flex cursor-pointer gap-1.5"> |
|
<img className="mb-2 h-10" src={DocsGPT3} alt="" /> |
|
<p className="my-auto text-2xl font-semibold">DocsGPT</p> |
|
</div> |
|
<button |
|
className="float-right mr-5" |
|
onClick={() => { |
|
setNavOpen(!navOpen); |
|
}} |
|
> |
|
<img |
|
src={Expand} |
|
alt="menu toggle" |
|
className={`${!navOpen ? 'rotate-180' : 'rotate-0' |
|
} m-auto transition-all duration-200`} |
|
/> |
|
</button> |
|
</div> |
|
<NavLink |
|
to={'/'} |
|
onClick={() => { |
|
dispatch(setConversation([])); |
|
dispatch( |
|
updateConversationId({ |
|
query: { conversationId: null }, |
|
}), |
|
); |
|
}} |
|
className={({ isActive }) => |
|
`${isActive ? 'bg-gray-3000 dark:bg-transparent' : '' |
|
} group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray dark:border-purple-taupe dark:text-white dark:hover:bg-transparent hover:bg-gray-3000` |
|
} |
|
> |
|
<img |
|
src={Add} |
|
alt="new" |
|
className="opacity-80 group-hover:opacity-100" |
|
/> |
|
<p className=" text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray"> |
|
New Chat |
|
</p> |
|
</NavLink> |
|
<div className="mb-auto h-[56vh] overflow-x-hidden dark:text-white overflow-y-scroll"> |
|
{conversations && ( |
|
<div> |
|
<p className="ml-6 mt-3 text-sm font-semibold">Chats</p> |
|
<div className="conversations-container"> |
|
{conversations?.map((conversation) => ( |
|
<ConversationTile |
|
key={conversation.id} |
|
conversation={conversation} |
|
selectConversation={(id) => handleConversationClick(id)} |
|
onDeleteConversation={(id) => handleDeleteConversation(id)} |
|
onSave={(conversation) => |
|
updateConversationName(conversation) |
|
} |
|
/> |
|
))} |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
|
|
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white"> |
|
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe"> |
|
<div className="relative my-4 flex gap-2 px-2"> |
|
<div |
|
className="flex h-12 w-5/6 cursor-pointer justify-between rounded-3xl border-2 dark:border-chinese-silver bg-white dark:bg-chinese-black" |
|
onClick={() => setIsDocsListOpen(!isDocsListOpen)} |
|
> |
|
{selectedDocs && ( |
|
<p className="my-3 mx-4 overflow-hidden text-ellipsis whitespace-nowrap"> |
|
{selectedDocs.name} {selectedDocs.version} |
|
</p> |
|
)} |
|
<img |
|
src={Arrow2} |
|
alt="arrow" |
|
className={`${!isDocsListOpen ? 'rotate-0' : 'rotate-180' |
|
} ml-auto mr-3 w-3 transition-all`} |
|
/> |
|
</div> |
|
<img |
|
className="mt-2 h-9 w-9 hover:cursor-pointer" |
|
src={UploadIcon} |
|
onClick={() => setUploadModalState('ACTIVE')} |
|
></img> |
|
{isDocsListOpen && ( |
|
<div className="absolute top-12 left-0 right-6 z-10 ml-2 mr-4 max-h-52 overflow-y-scroll bg-white dark:bg-chinese-black shadow-lg"> |
|
{docs ? ( |
|
docs.map((doc, index) => { |
|
if (doc.model === embeddingsName) { |
|
return ( |
|
<div |
|
key={index} |
|
onClick={() => { |
|
dispatch(setSelectedDocs(doc)); |
|
setIsDocsListOpen(false); |
|
}} |
|
className="flex h-10 w-full cursor-pointer items-center justify-between border-x-2 border-b-[1px] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-purple-taupe" |
|
> |
|
<p className="ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3"> |
|
{doc.name} {doc.version} |
|
</p> |
|
{doc.location === 'local' && ( |
|
<img |
|
src={Trash} |
|
alt="Delete" |
|
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50" |
|
id={`img-${index}`} |
|
onClick={(event) => { |
|
event.stopPropagation(); |
|
handleDeleteClick(index, doc); |
|
}} |
|
/> |
|
)} |
|
</div> |
|
); |
|
} |
|
}) |
|
) : ( |
|
<div className="h-10 w-full cursor-pointer border-b-[1px] dark:border-b-purple-taupe hover:bg-gray-100"> |
|
<p className="ml-5 py-3">No default documentation.</p> |
|
</div> |
|
)} |
|
</div> |
|
)} |
|
</div> |
|
<p className="ml-6 mt-3 text-sm font-semibold">Source Docs</p> |
|
</div> |
|
<div className="flex flex-col gap-2 border-b-[1px] dark:border-b-purple-taupe py-2"> |
|
<NavLink |
|
to="/settings" |
|
className={({ isActive }) => |
|
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${isActive ? 'bg-gray-3000 dark:bg-transparent' : '' |
|
}` |
|
} |
|
> |
|
<img |
|
src={isDarkTheme ? SettingGearDark : SettingGear} |
|
alt="settings" |
|
className="ml-2 w-5 opacity-60" |
|
/> |
|
<p className="my-auto text-sm text-eerie-black dark:text-white">Settings</p> |
|
</NavLink> |
|
</div> |
|
|
|
<div className="flex flex-col gap-2 border-b-[1.5px] dark:border-b-purple-taupe py-2"> |
|
<NavLink |
|
to="/about" |
|
className={({ isActive }) => |
|
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${isActive ? 'bg-gray-3000 dark:bg-purple-taupe' : '' |
|
}` |
|
} |
|
> |
|
<img src={isDarkTheme ? InfoDark : Info} alt="info" className="ml-2 w-5" /> |
|
<p className="my-auto text-sm">About</p> |
|
</NavLink> |
|
|
|
<a |
|
href="https://docs.docsgpt.co.uk/" |
|
target="_blank" |
|
rel="noreferrer" |
|
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe" |
|
> |
|
<img |
|
src={isDarkTheme ? DocumentationDark : Documentation} |
|
alt="documentation" |
|
className="ml-2 w-5" |
|
/> |
|
<p className="my-auto text-sm ">Documentation</p> |
|
</a> |
|
<a |
|
href="https://discord.gg/WHJdfbQDR4" |
|
target="_blank" |
|
rel="noreferrer" |
|
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe" |
|
> |
|
<img src={isDarkTheme ? DiscordDark : Discord} alt="discord-link" className="ml-2 w-5" /> |
|
<p className="my-auto text-sm"> |
|
Visit our Discord |
|
</p> |
|
</a> |
|
|
|
<a |
|
href="https://github.com/arc53/DocsGPT" |
|
target="_blank" |
|
rel="noreferrer" |
|
className="mt-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe" |
|
> |
|
<img src={isDarkTheme ? GithubDark : Github} alt="github-link" className="ml-2 w-5" /> |
|
<p className="my-auto text-sm"> |
|
Visit our Github |
|
</p> |
|
</a> |
|
</div> |
|
</div> |
|
</div> |
|
<div className="fixed z-10 h-16 w-full border-b-2 dark:border-b-purple-taupe bg-gray-50 dark:bg-chinese-black md:hidden"> |
|
<button |
|
className="mt-5 ml-6 h-6 w-6 md:hidden" |
|
onClick={() => setNavOpen(true)} |
|
> |
|
<img src={isDarkTheme ? HamburgerDark :Hamburger} alt="menu toggle" className="w-7" /> |
|
</button> |
|
</div> |
|
<SelectDocsModal |
|
modalState={selectedDocsModalState} |
|
setModalState={setSelectedDocsModalState} |
|
isCancellable={isSelectedDocsSet} |
|
/> |
|
<APIKeyModal |
|
modalState={apiKeyModalState} |
|
setModalState={setApiKeyModalState} |
|
isCancellable={isApiKeySet} |
|
/> |
|
<Upload |
|
modalState={uploadModalState} |
|
setModalState={setUploadModalState} |
|
></Upload> |
|
</> |
|
); |
|
} |
|
|