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) => {
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) => {

View file

@ -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: <Chat />
},
{
path: 'search/:query?',
element: <Search />
}
]
}

View file

@ -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.';

View file

@ -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() {
<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="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>
{messagesTree === null ? (
{_messagesTree === null ? (
<Spinner />
) : (
<>
<MultiMessage
key={conversationId} // avoid internal state mixture
conversation={conversation}
messagesTree={messagesTree}
messagesTree={_messagesTree}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
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 { Search } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import { useRecoilState } from 'recoil';
import store from '~/store';
export default function SearchBar({ fetch, clearSearch }) {
// const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
const setSearchQuery = useSetRecoilState(store.searchQuery);
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback(
debounce(q => {
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 === '') {

View file

@ -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

View file

@ -8,11 +8,9 @@ import TextChat from '../components/Input';
import store from '~/store';
import manualSWR from '~/utils/fetchers';
// import TextChat from './components/Main/TextChat';
// {/* <TextChat messages={messages} /> */}
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 (
<>

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 {
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 {

View file

@ -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
};