feat: support search-style-url

fix: url can be null in conversationId and query
fix: get conversation api should handle not found.
This commit is contained in:
Wentao Lyu 2023-03-29 00:08:02 +08:00
parent 8ea98cca5d
commit 370dc2dd8a
10 changed files with 147 additions and 48 deletions

View file

@ -13,7 +13,9 @@ router.get('/', async (req, res) => {
router.get('/:conversationId', async (req, res) => { router.get('/:conversationId', async (req, res) => {
const { conversationId } = req.params; const { conversationId } = req.params;
const convo = await getConvo(req?.session?.user?.username, conversationId); 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) => { router.post('/clear', async (req, res) => {

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'; import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import Root from './routes/Root'; import Root from './routes/Root';
import Chat from './routes/Chat'; import Chat from './routes/Chat';
import Search from './routes/Search';
import store from './store'; import store from './store';
import userAuth from './utils/userAuth'; import userAuth from './utils/userAuth';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
@ -23,8 +24,12 @@ const router = createBrowserRouter([
) )
}, },
{ {
path: 'chat/:conversationId', path: 'chat/:conversationId?',
element: <Chat /> element: <Chat />
},
{
path: 'search/:query?',
element: <Search />
} }
] ]
} }

View file

@ -13,7 +13,7 @@ import { useMessageHandler } from '../../utils/handleSubmit';
import store from '~/store'; import store from '~/store';
export default function TextChat() { export default function TextChat({ isSearchView = false }) {
const inputRef = useRef(null); const inputRef = useRef(null);
const isComposing = useRef(false); const isComposing = useRef(false);
@ -36,7 +36,7 @@ export default function TextChat() {
// auto focus to input, when enter a conversation. // auto focus to input, when enter a conversation.
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); if (conversation?.conversationId !== 'search') inputRef.current?.focus();
setText(''); setText('');
}, [conversation?.conversationId]); }, [conversation?.conversationId]);
@ -108,7 +108,6 @@ export default function TextChat() {
setText(value); setText(value);
}; };
const isSearchView = messages?.[0]?.searchResult === true;
const getPlaceholderText = () => { const getPlaceholderText = () => {
if (isSearchView) { if (isSearchView) {
return 'Click a message title to open its conversation.'; return 'Click a message title to open its conversation.';

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil';
import Spinner from '../svg/Spinner'; import Spinner from '../svg/Spinner';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
@ -8,18 +8,25 @@ import MultiMessage from './MultiMessage';
import store from '~/store'; import store from '~/store';
export default function Messages() { export default function Messages({ isSearchView = false }) {
const [currentEditId, setCurrentEditId] = useState(-1); 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 [showScrollButton, setShowScrollButton] = useState(false);
const scrollableRef = useRef(null); const scrollableRef = useRef(null);
const messagesEndRef = 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 modelName = models.find(element => element.model == model)?.name;
const searchQuery = useRecoilValue(store.searchQuery);
useEffect(() => { useEffect(() => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
@ -36,7 +43,7 @@ export default function Messages() {
clearTimeout(timeoutId); clearTimeout(timeoutId);
window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', handleScroll);
}; };
}, [messagesTree]); }, [_messagesTree]);
const scrollToBottom = useCallback( const scrollToBottom = useCallback(
throttle( throttle(
@ -81,16 +88,18 @@ export default function Messages() {
<div className="dark:gpt-dark-gray h-full"> <div className="dark:gpt-dark-gray h-full">
<div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm"> <div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
<div className="flex w-full items-center justify-center gap-1 border-b border-black/10 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-900/50 dark:bg-gray-700 dark:text-gray-300"> <div className="flex w-full items-center justify-center gap-1 border-b border-black/10 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-900/50 dark:bg-gray-700 dark:text-gray-300">
Model: {modelName} {chatGptLabel ? `(${chatGptLabel})` : null} {isSearchView
? `Search: ${searchQuery}`
: `Model: ${modelName} ${chatGptLabel ? `(${chatGptLabel})` : ''}`}
</div> </div>
{messagesTree === null ? ( {_messagesTree === null ? (
<Spinner /> <Spinner />
) : ( ) : (
<> <>
<MultiMessage <MultiMessage
key={conversationId} // avoid internal state mixture key={conversationId} // avoid internal state mixture
conversation={conversation} conversation={conversation}
messagesTree={messagesTree} messagesTree={_messagesTree}
scrollToBottom={scrollToBottom} scrollToBottom={scrollToBottom}
currentEditId={currentEditId} currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId} setCurrentEditId={setCurrentEditId}

View file

@ -1,27 +1,31 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { useSetRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import store from '~/store'; import store from '~/store';
export default function SearchBar({ fetch, clearSearch }) { export default function SearchBar({ fetch, clearSearch }) {
// const dispatch = useDispatch(); // const dispatch = useDispatch();
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const setSearchQuery = useSetRecoilState(store.searchQuery); const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState(''); // const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback( const debouncedChangeHandler = useCallback(
debounce(q => { debounce(q => {
setSearchQuery(q); setSearchQuery(q);
if (q.length > 0) {
fetch(q, 1);
}
}, 750), }, 750),
[setSearchQuery] [setSearchQuery]
); );
useEffect(() => {
if (searchQuery.length > 0) {
fetch(searchQuery, 1);
setInputValue(searchQuery);
}
}, [searchQuery]);
const handleKeyUp = e => { const handleKeyUp = e => {
const { value } = e.target; const { value } = e.target;
if (e.keyCode === 8 && value === '') { if (e.keyCode === 8 && value === '') {

View file

@ -27,12 +27,12 @@ export default function Nav({ navVisible, setNavVisible }) {
const searchQuery = useRecoilValue(store.searchQuery); const searchQuery = useRecoilValue(store.searchQuery);
const isSearchEnabled = useRecoilValue(store.isSearchEnabled); const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
const isSearching = useRecoilValue(store.isSearching); const isSearching = useRecoilValue(store.isSearching);
const { newConversation } = store.useConversation(); const { newConversation, searchPlaceholderConversation } = store.useConversation();
// current conversation // current conversation
const conversation = useRecoilValue(store.conversation); const conversation = useRecoilValue(store.conversation);
const { conversationId } = 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 // refreshConversationsHint is used for other components to ask refresh of Nav
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint); const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
@ -66,10 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) {
setPageNumber(res.pageNumber); setPageNumber(res.pageNumber);
setPages(res.pages); setPages(res.pages);
setIsFetching(false); setIsFetching(false);
if (res.messages?.length > 0) { searchPlaceholderConversation();
setMessages(res.messages); setSearchResultMessages(res.messages);
// dispatch(setDisabled(true));
}
}; };
// TODO: dont need this // TODO: dont need this

View file

@ -8,11 +8,9 @@ import TextChat from '../components/Input';
import store from '~/store'; import store from '~/store';
import manualSWR from '~/utils/fetchers'; import manualSWR from '~/utils/fetchers';
// import TextChat from './components/Main/TextChat';
// {/* <TextChat messages={messages} /> */}
export default function Chat() { export default function Chat() {
const searchQuery = useRecoilValue(store.searchQuery);
const [conversation, setConversation] = useRecoilState(store.conversation); const [conversation, setConversation] = useRecoilState(store.conversation);
const setMessages = useSetRecoilState(store.messages); const setMessages = useSetRecoilState(store.messages);
const messagesTree = useRecoilValue(store.messagesTree); const messagesTree = useRecoilValue(store.messagesTree);
@ -28,18 +26,23 @@ export default function Chat() {
useEffect(() => { useEffect(() => {
if (conversation === null) { if (conversation === null) {
// no current conversation, we need to do something // no current conversation, we need to do something
if (conversationId == 'new') { if (conversationId === 'new') {
// create new // create new
newConversation(); newConversation();
} else { } else if (conversationId) {
// fetch it from server // fetch it from server
conversationTrigger().then(setConversation); conversationTrigger().then(setConversation);
setMessages(null); 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 // conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
navigate(`/chat/${conversation?.conversationId}`); navigate(`/chat/${conversation?.conversationId}`);
}
}, [conversation, conversationId]); }, [conversation, conversationId]);
// when messagesTree is null (<=> messages is null) // when messagesTree is null (<=> messages is null)
@ -50,7 +53,12 @@ export default function Chat() {
} }
}, [conversation?.conversationId]); }, [conversation?.conversationId]);
// if not a conversation
if (conversation?.conversationId === 'search') return null;
// if conversationId not match
if (conversation?.conversationId !== conversationId) return null; if (conversation?.conversationId !== conversationId) return null;
// if conversationId is null
if (!conversationId) return null;
return ( return (
<> <>

View file

@ -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 (
<>
<Messages isSearchView={true} />
<TextChat isSearchView={true} />
</>
);
}

View file

@ -1,13 +1,5 @@
import models from './models'; import models from './models';
import { import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
atom,
selector,
useRecoilValue,
useSetRecoilState,
useResetRecoilState,
useRecoilCallback,
useRecoilState
} from 'recoil';
import buildTree from '~/utils/buildTree'; import buildTree from '~/utils/buildTree';
// current conversation, can be null (need to be fetched from server) // current conversation, can be null (need to be fetched from server)
@ -42,9 +34,7 @@ const messages = atom({
const messagesTree = selector({ const messagesTree = selector({
key: 'messagesTree', key: 'messagesTree',
get: ({ get }) => { get: ({ get }) => {
const _messages = get(messages); return buildTree(get(messages), false);
const groupAll = _messages?.[0]?.searchResult;
return buildTree(_messages, groupAll);
} }
}); });
@ -78,7 +68,6 @@ const useConversation = () => {
try { try {
// try to use current model // try to use current model
const { _model = null, _chatGptLabel = null, _promptPrefix = null } = prev_conversation || {}; const { _model = null, _chatGptLabel = null, _promptPrefix = null } = prev_conversation || {};
console.log(_model, _chatGptLabel, _promptPrefix);
if (prevModelsFilter[_model]) { if (prevModelsFilter[_model]) {
model = _model; model = _model;
chatGptLabel = _chatGptLabel; 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 { export default {

View file

@ -1,4 +1,5 @@
import { atom, selector } from 'recoil'; import { atom, selector } from 'recoil';
import buildTree from '~/utils/buildTree';
const isSearchEnabled = atom({ const isSearchEnabled = atom({
key: 'isSearchEnabled', key: 'isSearchEnabled',
@ -10,6 +11,18 @@ const searchQuery = atom({
default: '' default: ''
}); });
const searchResultMessages = atom({
key: 'searchResultMessages',
default: null
});
const searchResultMessagesTree = selector({
key: 'searchResultMessagesTree',
get: ({ get }) => {
return buildTree(get(searchResultMessages), true);
}
});
const isSearching = selector({ const isSearching = selector({
key: 'isSearching', key: 'isSearching',
get: ({ get }) => { get: ({ get }) => {
@ -21,5 +34,7 @@ const isSearching = selector({
export default { export default {
isSearchEnabled, isSearchEnabled,
isSearching, isSearching,
searchResultMessages,
searchResultMessagesTree,
searchQuery searchQuery
}; };