| | 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> |
| | </> |
| | ); |
| | } |
| |
|