feat: main styling/ui/ux final changes

This commit is contained in:
Danny Avila 2023-03-22 16:06:11 -04:00
parent 67161c983f
commit 8f58c95452
17 changed files with 166 additions and 70 deletions

View file

@ -16,7 +16,7 @@ async function indexSync(req, res, next) {
}); });
const { status } = await client.health(); const { status } = await client.health();
console.log(`Meilisearch: ${status}`); // console.log(`Meilisearch: ${status}`);
const result = status === 'available' && !!process.env.SEARCH; const result = status === 'available' && !!process.env.SEARCH;
if (!result) { if (!result) {

View file

@ -81,6 +81,7 @@ module.exports = {
} }
const cache = {}; const cache = {};
const convoMap = {};
const promises = []; const promises = [];
// will handle a syncing solution soon // will handle a syncing solution soon
const deletedConvoIds = []; const deletedConvoIds = [];
@ -104,6 +105,7 @@ module.exports = {
cache[page] = []; cache[page] = [];
} }
cache[page].push(convo); cache[page].push(convo);
convoMap[convo.conversationId] = convo;
return true; return true;
} }
}); });
@ -120,7 +122,8 @@ module.exports = {
pageNumber, pageNumber,
pageSize, pageSize,
// will handle a syncing solution soon // will handle a syncing solution soon
filter: new Set(deletedConvoIds) filter: new Set(deletedConvoIds),
convoMap
}; };
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View file

@ -4,6 +4,7 @@ const { MeiliSearch } = require('meilisearch');
const { Message } = require('../../models/Message'); const { Message } = require('../../models/Message');
const { Conversation, getConvosQueried } = require('../../models/Conversation'); const { Conversation, getConvosQueried } = require('../../models/Conversation');
const { reduceHits } = require('../../lib/utils/reduceHits'); const { reduceHits } = require('../../lib/utils/reduceHits');
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
const cache = new Map(); const cache = new Map();
router.get('/sync', async function (req, res) { router.get('/sync', async function (req, res) {
@ -52,9 +53,25 @@ router.get('/', async function (req, res) {
console.log('messages', messages.length, 'titles', titles.length); console.log('messages', messages.length, 'titles', titles.length);
const sortedHits = reduceHits(messages, titles); const sortedHits = reduceHits(messages, titles);
const result = await getConvosQueried(user, sortedHits, pageNumber); const result = await getConvosQueried(user, sortedHits, pageNumber);
cache.set(q, result.cache); cache.set(key, result.cache);
delete result.cache; delete result.cache;
result.messages = messages.filter(message => !result.filter.has(message.conversationId)); // result.messages = messages.filter(message => { });
// !result.filter.has(message.conversationId)
const activeMessages = [];
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.conversationId.includes('--')) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
}
if (result.convoMap[message.conversationId] && !message.error) {
message = { ...message, title: result.convoMap[message.conversationId].title };
activeMessages.push(message);
}
}
result.messages = activeMessages;
delete result.cache;
delete result.convoMap;
// for debugging // for debugging
// console.log(result, messages.length); // console.log(result, messages.length);
res.status(200).send(result); res.status(200).send(result);
@ -89,7 +106,7 @@ router.get('/enable', async function (req, res) {
}); });
const { status } = await client.health(); const { status } = await client.health();
console.log(`Meilisearch: ${status}`); // console.log(`Meilisearch: ${status}`);
result = status === 'available' && !!process.env.SEARCH; result = status === 'available' && !!process.env.SEARCH;
return res.send(result); return res.send(result);
} catch (error) { } catch (error) {

View file

@ -350,7 +350,7 @@ export default function TextChat({ messages }) {
const isSearchView = messages?.[0]?.searchResult === true; const isSearchView = messages?.[0]?.searchResult === true;
const getPlaceholderText = () => { const getPlaceholderText = () => {
if (isSearchView) { if (isSearchView) {
return 'Click a message to open its conversation.' return 'Click a message title to open its conversation.'
} }
if (disabled) { if (disabled) {

View file

@ -53,7 +53,7 @@ const code = React.memo((props) => {
}); });
const p = React.memo((props) => { const p = React.memo((props) => {
return <p className="whitespace-pre-wrap ">{props?.children}</p>; return <span className="whitespace-pre-wrap mb-2">{props?.children}</span>;
}); });
const blinker = ({ node }) => { const blinker = ({ node }) => {

View file

@ -0,0 +1,9 @@
import React from 'react';
export default function SubRow({ children, classes = '', subclasses = '', onClick }) {
return (
<div className={`flex justify-between ${classes}`} onClick={onClick}>
<div className={`flex items-center justify-center gap-1 self-center pt-2 text-xs ${subclasses}`}>{children}</div>
</div>
);
}

View file

@ -3,14 +3,14 @@ import TextWrapper from './TextWrapper';
import Content from './Content'; import Content from './Content';
const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => { const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => {
if (!isCreatedByUser && searchResult) { if (searchResult) {
return ( return (
<Content <Content
content={text} content={text}
generateCursor={generateCursor} generateCursor={generateCursor}
/> />
); );
} else if (!isCreatedByUser && !searchResult) { } else if (!isCreatedByUser) {
return ( return (
<TextWrapper <TextWrapper
text={text} text={text}

View file

@ -1,11 +1,12 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import SubRow from './Content/SubRow';
import Wrapper from './Content/Wrapper'; import Wrapper from './Content/Wrapper';
import MultiMessage from './MultiMessage'; import MultiMessage from './MultiMessage';
import { useSelector, useDispatch } from 'react-redux';
import HoverButtons from './HoverButtons'; import HoverButtons from './HoverButtons';
import SiblingSwitch from './SiblingSwitch'; import SiblingSwitch from './SiblingSwitch';
import { setConversation, setLatestMessage } from '~/store/convoSlice'; import { setConversation, setLatestMessage } from '~/store/convoSlice';
import { setModel, setCustomModel, setCustomGpt, toggleCursor, setDisabled } from '~/store/submitSlice'; import { setModel, setCustomModel, setCustomGpt, setDisabled } from '~/store/submitSlice';
import { setMessages } from '~/store/messageSlice'; import { setMessages } from '~/store/messageSlice';
import { fetchById } from '~/utils/fetchers'; import { fetchById } from '~/utils/fetchers';
import { getIconOfModel } from '~/utils'; import { getIconOfModel } from '~/utils';
@ -21,17 +22,15 @@ export default function Message({
siblingCount, siblingCount,
setSiblingIdx setSiblingIdx
}) { }) {
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector( const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(state => state.submit);
(state) => state.submit
);
const [abortScroll, setAbort] = useState(false); const [abortScroll, setAbort] = useState(false);
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message; const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
const textEditor = useRef(null); const textEditor = useRef(null);
// const { convos } = useSelector((state) => state.convo);
const last = !message?.children?.length; const last = !message?.children?.length;
const edit = message.messageId == currentEditId; const edit = message.messageId == currentEditId;
const { ask } = useMessageHandler(); const { ask } = useMessageHandler();
const dispatch = useDispatch(); const dispatch = useDispatch();
// const currentConvo = convoMap[message.conversationId];
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user'; // const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
// const blinker = submitting && isSubmitting && last && !isCreatedByUser; // const blinker = submitting && isSubmitting && last && !isCreatedByUser;
@ -62,7 +61,7 @@ export default function Message({
} }
}, [last, message]); }, [last, message]);
const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId); const enterEdit = cancel => setCurrentEditId(cancel ? -1 : message.messageId);
const handleWheel = () => { const handleWheel = () => {
if (blinker) { if (blinker) {
@ -92,11 +91,10 @@ export default function Message({
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]'; 'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
if (message.bg && searchResult) { if (message.bg && searchResult) {
props.className = message.bg + ' cursor-pointer'; props.className = message.bg.split('hover')[0];
props.titleClass = message.bg.split(props.className)[1] + ' cursor-pointer';
} }
// const wrapText = (text) => <TextWrapper text={text} generateCursor={generateCursor}/>;
const resubmitMessage = () => { const resubmitMessage = () => {
const text = textEditor.current.innerText; const text = textEditor.current.innerText;
@ -135,11 +133,11 @@ export default function Message({
<div <div
{...props} {...props}
onWheel={handleWheel} onWheel={handleWheel}
onClick={clickSearchResult} // onClick={clickSearchResult}
> >
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm"> <div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( {typeof icon === 'string' && icon.match(/[^\\x00-\\x7F]+/) ? (
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span> <span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
) : ( ) : (
icon icon
@ -153,6 +151,13 @@ export default function Message({
</div> </div>
</div> </div>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]"> <div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
{searchResult && (
<SubRow
classes={props.titleClass + ' rounded'}
subclasses="switch-result pl-2 pb-2"
onClick={clickSearchResult}
>{`${message.title} | ${message.sender}`}</SubRow>
)}
<div className="flex flex-grow flex-col gap-3"> <div className="flex flex-grow flex-col gap-3">
{error ? ( {error ? (
<div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500"> <div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
@ -207,15 +212,13 @@ export default function Message({
visible={!error && isCreatedByUser && !edit && !searchResult} visible={!error && isCreatedByUser && !edit && !searchResult}
onClick={() => enterEdit()} onClick={() => enterEdit()}
/> />
<div className="sibling-switch-container flex justify-between"> <SubRow subclasses="switch-container">
<div className="flex items-center justify-center gap-1 self-center pt-2 text-xs"> <SiblingSwitch
<SiblingSwitch siblingIdx={siblingIdx}
siblingIdx={siblingIdx} siblingCount={siblingCount}
siblingCount={siblingCount} setSiblingIdx={setSiblingIdx}
setSiblingIdx={setSiblingIdx} />
/> </SubRow>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,38 @@
import React, { useRef, useEffect, useState } from 'react';
const MessageBar = ({ children, dynamicProps, handleWheel, clickSearchResult }) => {
const ref = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(ref.current);
}
},
{ threshold: 0.1 }
);
observer.observe(ref.current);
return () => {
observer.unobserve(ref.current);
};
}, []);
return (
<div
{...dynamicProps}
onWheel={handleWheel}
// onClick={clickSearchResult}
ref={ref}
>
{isVisible ? children : null}
</div>
);
};
export default MessageBar;

View file

@ -75,7 +75,7 @@ export default function Messages({ messages, messageTree }) {
<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} {customModel ? `(${customModel})` : null} Model: {modelName} {customModel ? `(${customModel})` : null}
</div> </div>
{(messageTree.length === 0) ? ( {(messageTree.length === 0 || messages.length === 0 || !messages) ? (
<Spinner /> <Spinner />
) : ( ) : (
<> <>

View file

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice'; import { setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice'; import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice'; import { setSubmission, setDisabled } from '~/store/submitSlice';
import { setText } from '~/store/textSlice'; import { setText } from '~/store/textSlice';
import { setInputValue, setQuery } from '~/store/searchSlice';
export default function NewChat() { export default function NewChat() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -12,7 +13,11 @@ export default function NewChat() {
dispatch(setText('')); dispatch(setText(''));
dispatch(setMessages([])); dispatch(setMessages([]));
dispatch(setNewConvo()); dispatch(setNewConvo());
dispatch(refreshConversation());
dispatch(setSubmission({})); dispatch(setSubmission({}));
dispatch(setDisabled(false));
dispatch(setInputValue(''));
dispatch(setQuery(''));
}; };
return ( return (

View file

@ -1,12 +1,13 @@
import React, { useState, useCallback } from 'react'; import React, { useCallback } from 'react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { setQuery } from '~/store/searchSlice'; import { setInputValue, setQuery } from '~/store/searchSlice';
export default function SearchBar({ fetch, clearSearch }) { export default function SearchBar({ fetch, clearSearch }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [inputValue, setInputValue] = useState(''); const { inputValue } = useSelector((state) => state.search);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback( const debouncedChangeHandler = useCallback(
debounce((q) => { debounce((q) => {
@ -28,10 +29,9 @@ export default function SearchBar({ fetch, clearSearch }) {
} }
}; };
const changeHandler = (e) => { const changeHandler = (e) => {
let q = e.target.value; let q = e.target.value;
setInputValue(q); dispatch(setInputValue(q));
q = q.trim(); q = q.trim();
if (q === '') { if (q === '') {

View file

@ -54,8 +54,10 @@ export default function Nav({ navVisible, setNavVisible }) {
const clearSearch = () => { const clearSearch = () => {
setPage(1); setPage(1);
dispatch(refreshConversation()); dispatch(refreshConversation());
dispatch(setNewConvo()); if (!conversationId) {
dispatch(setMessages([])); dispatch(setNewConvo());
dispatch(setMessages([]));
}
dispatch(setDisabled(false)); dispatch(setDisabled(false));
}; };

View file

@ -22,11 +22,17 @@
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.sibling-switch-container { .switch-container {
display: none; display: none;
} }
} }
.switch-result {
display: block !important;
visibility: visible;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
/* .sibling-switch { /* .sibling-switch {
left: 114px; left: 114px;

View file

@ -17,14 +17,15 @@ const initialState = {
refreshConvoHint: 0, refreshConvoHint: 0,
search: false, search: false,
latestMessage: null, latestMessage: null,
convos: [] convos: [],
convoMap: {},
}; };
const currentSlice = createSlice({ const currentSlice = createSlice({
name: 'convo', name: 'convo',
initialState, initialState,
reducers: { reducers: {
refreshConversation: (state, action) => { refreshConversation: (state) => {
state.refreshConvoHint = state.refreshConvoHint + 1; state.refreshConvoHint = state.refreshConvoHint + 1;
}, },
setConversation: (state, action) => { setConversation: (state, action) => {
@ -69,6 +70,13 @@ const currentSlice = createSlice({
} else { } else {
state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
} }
// state.convoMap = convos.reduce((acc, curr) => {
// acc[curr.conversationId] = { ...curr };
// delete acc[curr.conversationId].conversationId;
// return acc;
// }, {});
}, },
setPages: (state, action) => { setPages: (state, action) => {
state.pages = action.payload; state.pages = action.payload;

View file

@ -4,12 +4,16 @@ const initialState = {
searchEnabled: false, searchEnabled: false,
search: false, search: false,
query: '', query: '',
inputValue: '',
}; };
const currentSlice = createSlice({ const currentSlice = createSlice({
name: 'search', name: 'search',
initialState, initialState,
reducers: { reducers: {
setInputValue: (state, action) => {
state.inputValue = action.payload;
},
setSearchState: (state, action) => { setSearchState: (state, action) => {
state.searchEnabled = action.payload; state.searchEnabled = action.payload;
}, },
@ -26,6 +30,6 @@ const currentSlice = createSlice({
} }
}); });
export const { setSearchState, setQuery } = currentSlice.actions; export const { setInputValue, setSearchState, setQuery } = currentSlice.actions;
export default currentSlice.reducer; export default currentSlice.reducer;

View file

@ -21,31 +21,32 @@ export default function buildTree(messages, groupAll = false) {
} }
// Group all messages into one tree // Group all messages into one tree
// let parentId = null; let parentId = null;
// messages.forEach((message, i) => { messages.forEach((message, i) => {
// messageMap[message.messageId] = { ...message, bg: i % 2 === 0 ? even : odd, children: [] }; messageMap[message.messageId] = { ...message, bg: i % 2 === 0 ? even : odd, children: [] };
// const currentMessage = messageMap[message.messageId]; const currentMessage = messageMap[message.messageId];
// const parentMessage = messageMap[parentId]; const parentMessage = messageMap[parentId];
// if (parentMessage) parentMessage.children.push(currentMessage); if (parentMessage) parentMessage.children.push(currentMessage);
// else rootMessages.push(currentMessage); else rootMessages.push(currentMessage);
// parentId = message.messageId; parentId = message.messageId;
// });
// return rootMessages;
// Group all messages by conversation
// Traverse the messages array and store each element in messageMap.
rootMessages = {};
let parents = 0;
messages.forEach(message => {
if (message.conversationId in messageMap) {
messageMap[message.conversationId].children.push(message);
} else {
messageMap[message.conversationId] = { ...message, bg: parents % 2 === 0 ? even : odd, children: [] };
rootMessages[message.conversationId] = messageMap[message.conversationId];
parents++;
}
}); });
return Object.values(rootMessages); return rootMessages;
// Group all messages by conversation, doesn't look great
// Traverse the messages array and store each element in messageMap.
// rootMessages = {};
// let parents = 0;
// messages.forEach(message => {
// if (message.conversationId in messageMap) {
// messageMap[message.conversationId].children.push(message);
// } else {
// messageMap[message.conversationId] = { ...message, bg: parents % 2 === 0 ? even : odd, children: [] };
// rootMessages.push(messageMap[message.conversationId]);
// parents++;
// }
// });
// // return Object.values(rootMessages);
// return rootMessages;
} }