diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 45b23d0bd6..0774b92ddf 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -13,7 +13,9 @@ router.get('/', async (req, res) => { router.get('/:conversationId', async (req, res) => { const { conversationId } = req.params; const convo = await getConvo(req?.session?.user?.username, conversationId); - res.status(200).send(convo.toObject()); + + if (convo) res.status(200).send(convo.toObject()); + else res.status(404).end(); }); router.post('/clear', async (req, res) => { diff --git a/client/src/App.jsx b/client/src/App.jsx index 404dce05a8..d8673b69ed 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'; import Root from './routes/Root'; import Chat from './routes/Chat'; +import Search from './routes/Search'; import store from './store'; import userAuth from './utils/userAuth'; import { useRecoilState, useSetRecoilState } from 'recoil'; @@ -23,8 +24,12 @@ const router = createBrowserRouter([ ) }, { - path: 'chat/:conversationId', + path: 'chat/:conversationId?', element: + }, + { + path: 'search/:query?', + element: } ] } diff --git a/client/src/components/Input/index.jsx b/client/src/components/Input/index.jsx index 359767aad0..9eac938c4c 100644 --- a/client/src/components/Input/index.jsx +++ b/client/src/components/Input/index.jsx @@ -13,7 +13,7 @@ import { useMessageHandler } from '../../utils/handleSubmit'; import store from '~/store'; -export default function TextChat() { +export default function TextChat({ isSearchView = false }) { const inputRef = useRef(null); const isComposing = useRef(false); @@ -36,7 +36,7 @@ export default function TextChat() { // auto focus to input, when enter a conversation. useEffect(() => { - inputRef.current?.focus(); + if (conversation?.conversationId !== 'search') inputRef.current?.focus(); setText(''); }, [conversation?.conversationId]); @@ -108,7 +108,6 @@ export default function TextChat() { setText(value); }; - const isSearchView = messages?.[0]?.searchResult === true; const getPlaceholderText = () => { if (isSearchView) { return 'Click a message title to open its conversation.'; diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index 262575116d..45c56f1616 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import Spinner from '../svg/Spinner'; import { throttle } from 'lodash'; import { CSSTransition } from 'react-transition-group'; @@ -8,18 +8,25 @@ import MultiMessage from './MultiMessage'; import store from '~/store'; -export default function Messages() { +export default function Messages({ isSearchView = false }) { const [currentEditId, setCurrentEditId] = useState(-1); - const messagesTree = useRecoilValue(store.messagesTree); - const conversation = useRecoilValue(store.conversation) || {}; - const { conversationId, model, chatGptLabel } = conversation; - const models = useRecoilValue(store.models) || []; const [showScrollButton, setShowScrollButton] = useState(false); const scrollableRef = useRef(null); const messagesEndRef = useRef(null); + const messagesTree = useRecoilValue(store.messagesTree); + const searchResultMessagesTree = useRecoilValue(store.searchResultMessagesTree); + + const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree; + + const conversation = useRecoilValue(store.conversation) || {}; + const { conversationId, model, chatGptLabel } = conversation; + + const models = useRecoilValue(store.models) || []; const modelName = models.find(element => element.model == model)?.name; + const searchQuery = useRecoilValue(store.searchQuery); + useEffect(() => { const timeoutId = setTimeout(() => { const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; @@ -36,7 +43,7 @@ export default function Messages() { clearTimeout(timeoutId); window.removeEventListener('scroll', handleScroll); }; - }, [messagesTree]); + }, [_messagesTree]); const scrollToBottom = useCallback( throttle( @@ -81,16 +88,18 @@ export default function Messages() {
- Model: {modelName} {chatGptLabel ? `(${chatGptLabel})` : null} + {isSearchView + ? `Search: ${searchQuery}` + : `Model: ${modelName} ${chatGptLabel ? `(${chatGptLabel})` : ''}`}
- {messagesTree === null ? ( + {_messagesTree === null ? ( ) : ( <> { setSearchQuery(q); - if (q.length > 0) { - fetch(q, 1); - } }, 750), [setSearchQuery] ); + useEffect(() => { + if (searchQuery.length > 0) { + fetch(searchQuery, 1); + setInputValue(searchQuery); + } + }, [searchQuery]); + const handleKeyUp = e => { const { value } = e.target; if (e.keyCode === 8 && value === '') { diff --git a/client/src/components/Nav/index.jsx b/client/src/components/Nav/index.jsx index 9d1c69d342..85523033ea 100644 --- a/client/src/components/Nav/index.jsx +++ b/client/src/components/Nav/index.jsx @@ -27,12 +27,12 @@ export default function Nav({ navVisible, setNavVisible }) { const searchQuery = useRecoilValue(store.searchQuery); const isSearchEnabled = useRecoilValue(store.isSearchEnabled); const isSearching = useRecoilValue(store.isSearching); - const { newConversation } = store.useConversation(); + const { newConversation, searchPlaceholderConversation } = store.useConversation(); // current conversation const conversation = useRecoilValue(store.conversation); const { conversationId } = conversation || {}; - const setMessages = useSetRecoilState(store.messages); + const setSearchResultMessages = useSetRecoilState(store.searchResultMessages); // refreshConversationsHint is used for other components to ask refresh of Nav const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint); @@ -66,10 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) { setPageNumber(res.pageNumber); setPages(res.pages); setIsFetching(false); - if (res.messages?.length > 0) { - setMessages(res.messages); - // dispatch(setDisabled(true)); - } + searchPlaceholderConversation(); + setSearchResultMessages(res.messages); }; // TODO: dont need this diff --git a/client/src/routes/Chat.jsx b/client/src/routes/Chat.jsx index 58aa0500d3..1468dfa654 100644 --- a/client/src/routes/Chat.jsx +++ b/client/src/routes/Chat.jsx @@ -8,11 +8,9 @@ import TextChat from '../components/Input'; import store from '~/store'; import manualSWR from '~/utils/fetchers'; -// import TextChat from './components/Main/TextChat'; - -// {/* */} export default function Chat() { + const searchQuery = useRecoilValue(store.searchQuery); const [conversation, setConversation] = useRecoilState(store.conversation); const setMessages = useSetRecoilState(store.messages); const messagesTree = useRecoilValue(store.messagesTree); @@ -28,18 +26,23 @@ export default function Chat() { useEffect(() => { if (conversation === null) { // no current conversation, we need to do something - if (conversationId == 'new') { + if (conversationId === 'new') { // create new newConversation(); - } else { + } else if (conversationId) { // fetch it from server conversationTrigger().then(setConversation); setMessages(null); - console.log('NEED TO FETCH DATA'); + } else { + navigate(`/chat/new`); } - } else if (conversation?.conversationId !== conversationId) + } else if (conversation?.conversationId === 'search') { + // jump to search page + navigate(`/search/${searchQuery}`); + } else if (conversation?.conversationId !== conversationId) { // conversationId (in url) should always follow conversation?.conversationId, unless conversation is null navigate(`/chat/${conversation?.conversationId}`); + } }, [conversation, conversationId]); // when messagesTree is null (<=> messages is null) @@ -50,7 +53,12 @@ export default function Chat() { } }, [conversation?.conversationId]); + // if not a conversation + if (conversation?.conversationId === 'search') return null; + // if conversationId not match if (conversation?.conversationId !== conversationId) return null; + // if conversationId is null + if (!conversationId) return null; return ( <> diff --git a/client/src/routes/Search.jsx b/client/src/routes/Search.jsx new file mode 100644 index 0000000000..f91e7e224b --- /dev/null +++ b/client/src/routes/Search.jsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import Messages from '../components/Messages'; +import TextChat from '../components/Input'; + +import store from '~/store'; + +export default function Search() { + const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery); + const conversation = useRecoilValue(store.conversation); + const { searchPlaceholderConversation } = store.useConversation(); + const { query } = useParams(); + const navigate = useNavigate(); + + // when conversation changed or conversationId (in url) changed + useEffect(() => { + if (conversation === null) { + // no current conversation, we need to do something + if (query) { + // create new + searchPlaceholderConversation(); + setSearchQuery(query); + } else { + navigate(`/chat/new`); + } + } else if (conversation?.conversationId === 'search') { + // jump to search page + if (searchQuery !== query) navigate(`/search/${searchQuery}`); + } else { + // conversationId (in url) should always follow conversation?.conversationId, unless conversation is null + navigate(`/chat/${conversation?.conversationId}`); + } + }, [conversation, query, searchQuery]); + + // if not a search + if (conversation?.conversationId !== 'search') return null; + // if query not match + if (searchQuery !== query) return null; + // if query is null + if (!query) return null; + + return ( + <> + + + + ); +} diff --git a/client/src/store/conversation.js b/client/src/store/conversation.js index 9e1c0f080e..7ae123c254 100644 --- a/client/src/store/conversation.js +++ b/client/src/store/conversation.js @@ -1,13 +1,5 @@ import models from './models'; -import { - atom, - selector, - useRecoilValue, - useSetRecoilState, - useResetRecoilState, - useRecoilCallback, - useRecoilState -} from 'recoil'; +import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; import buildTree from '~/utils/buildTree'; // current conversation, can be null (need to be fetched from server) @@ -42,9 +34,7 @@ const messages = atom({ const messagesTree = selector({ key: 'messagesTree', get: ({ get }) => { - const _messages = get(messages); - const groupAll = _messages?.[0]?.searchResult; - return buildTree(_messages, groupAll); + return buildTree(get(messages), false); } }); @@ -78,7 +68,6 @@ const useConversation = () => { try { // try to use current model const { _model = null, _chatGptLabel = null, _promptPrefix = null } = prev_conversation || {}; - console.log(_model, _chatGptLabel, _promptPrefix); if (prevModelsFilter[_model]) { model = _model; chatGptLabel = _chatGptLabel; @@ -142,7 +131,27 @@ const useConversation = () => { ); }; - return { newConversation, switchToConversation }; + const searchPlaceholderConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => { + switchToConversation( + { + conversationId: 'search', + title: 'Search', + jailbreakConversationId: null, + conversationSignature: null, + clientId: null, + invocationId: null, + model: model, + chatGptLabel: chatGptLabel, + promptPrefix: promptPrefix, + user: null, + suggestions: [], + toneStyle: null + }, + [] + ); + }; + + return { newConversation, switchToConversation, searchPlaceholderConversation }; }; export default { diff --git a/client/src/store/search.js b/client/src/store/search.js index 554a012b72..ebc956def5 100644 --- a/client/src/store/search.js +++ b/client/src/store/search.js @@ -1,4 +1,5 @@ import { atom, selector } from 'recoil'; +import buildTree from '~/utils/buildTree'; const isSearchEnabled = atom({ key: 'isSearchEnabled', @@ -10,6 +11,18 @@ const searchQuery = atom({ default: '' }); +const searchResultMessages = atom({ + key: 'searchResultMessages', + default: null +}); + +const searchResultMessagesTree = selector({ + key: 'searchResultMessagesTree', + get: ({ get }) => { + return buildTree(get(searchResultMessages), true); + } +}); + const isSearching = selector({ key: 'isSearching', get: ({ get }) => { @@ -21,5 +34,7 @@ const isSearching = selector({ export default { isSearchEnabled, isSearching, + searchResultMessages, + searchResultMessagesTree, searchQuery };