refactor: basic message and send message. as well as model

THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
Wentao Lyu 2023-03-28 22:39:27 +08:00
parent de8f519742
commit c7c30d8bb5
24 changed files with 1057 additions and 1035 deletions

View file

@ -1,7 +1,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 Chat from './routes/Chat';
import store from './store';
import userAuth from './utils/userAuth';
import { useRecoilState, useSetRecoilState } from 'recoil';
@ -24,7 +24,7 @@ const router = createBrowserRouter([
},
{
path: 'chat/:conversationId',
element: null //<Chat />
element: <Chat />
}
]
}
@ -33,14 +33,45 @@ const router = createBrowserRouter([
const App = () => {
const [user, setUser] = useRecoilState(store.user);
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
const setModelsFilter = useSetRecoilState(store.modelsFilter);
useEffect(() => {
axios.get('/api/search/enable').then(res => {
setIsSearchEnabled(res.data);
});
// fetch if seatch enabled
axios
.get('/api/search/enable', {
timeout: 1000,
withCredentials: true
})
.then(res => {
setIsSearchEnabled(res.data);
});
// fetch user
userAuth()
.then(user => setUser(user))
.catch(err => console.log(err));
// fetch models
axios
.get('/api/models', {
timeout: 1000,
withCredentials: true
})
.then(({ data }) => {
const filter = {
chatgpt: data?.hasOpenAI,
chatgptCustom: data?.hasOpenAI,
bingai: data?.hasBing,
sydney: data?.hasBing,
chatgptBrowser: data?.hasChatGpt
};
setModelsFilter(filter);
})
.catch(error => {
console.error(error);
console.log('Not login!');
window.location.href = '/auth/login';
});
}, []);
if (user)

View file

@ -15,6 +15,7 @@ export default function Conversation({ conversation, retainView }) {
const resetLatestMessage = useResetRecoilState(store.latestMessage);
const { refreshConversations } = store.useConversations();
const { switchToConversation } = store.useConversation();
const [renaming, setRenaming] = useState(false);
const [titleInput, setTitleInput] = useState(title);
@ -56,9 +57,7 @@ export default function Conversation({ conversation, retainView }) {
setSubmission(null);
// set conversation to the new conversation
setCurrentConversation(conversation);
setMessages(null);
resetLatestMessage();
switchToConversation(conversation);
// if (!stopStream) {
// dispatch(setStopStream(true));

View file

@ -4,16 +4,12 @@ import ModelItem from './ModelItem';
export default function MenuItems({ models, onSelect }) {
return (
<>
{models.map((modelItem) => (
{models.map(modelItem => (
<ModelItem
key={modelItem._id}
id={modelItem._id}
modelName={modelItem.name}
value={modelItem.value}
model={modelItem.model || 'chatgptCustom'}
onSelect={onSelect}
chatGptLabel={modelItem.chatGptLabel}
promptPrefix={modelItem.promptPrefix}
model={modelItem}
/>
))}
</>

View file

@ -1,12 +1,9 @@
import React, { useState, useRef } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { useSelector, useDispatch } from 'react-redux';
import { setSubmission, setModel, setCustomGpt } from '~/store/submitSlice';
import { setNewConvo } from '~/store/convoSlice';
import manualSWR from '~/utils/fetchers';
import { Button } from '../ui/Button.tsx';
import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import { Button } from '../../ui/Button.tsx';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import {
DialogClose,
@ -15,11 +12,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle
} from '../ui/Dialog.tsx';
} from '../../ui/Dialog.tsx';
import store from '~/store';
export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
const dispatch = useDispatch();
const { modelMap, initial } = useSelector((state) => state.models);
const { newConversation } = store.useConversation();
const [chatGptLabel, setChatGptLabel] = useState('');
const [promptPrefix, setPromptPrefix] = useState('');
const [saveText, setSaveText] = useState('Save');
@ -27,22 +26,25 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
const inputRef = useRef(null);
const updateCustomGpt = manualSWR(`/api/customGpts/`, 'post');
const selectHandler = (e) => {
const selectHandler = e => {
if (chatGptLabel.length === 0) {
e.preventDefault();
setRequired(true);
inputRef.current.focus();
return;
}
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
handleSaveState(chatGptLabel.toLowerCase());
// Set new conversation
dispatch(setNewConvo());
dispatch(setSubmission({}));
newConversation({
model: 'chatgptCustom',
chatGptLabel,
promptPrefix
});
};
const saveHandler = (e) => {
const saveHandler = e => {
e.preventDefault();
setModelSave(true);
const value = chatGptLabel.toLowerCase();
@ -56,26 +58,30 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
updateCustomGpt.trigger({ value, chatGptLabel, promptPrefix });
mutate();
setSaveText((prev) => prev + 'd!');
setSaveText(prev => prev + 'd!');
setTimeout(() => {
setSaveText('Save');
}, 2500);
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
// dispatch(setDisabled(false));
// dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
newConversation({
model: 'chatgptCustom',
chatGptLabel,
promptPrefix
});
};
if (
chatGptLabel !== 'chatgptCustom' &&
modelMap[chatGptLabel.toLowerCase()] &&
!initial[chatGptLabel.toLowerCase()] &&
saveText === 'Save'
) {
setSaveText('Update');
} else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') {
setSaveText('Save');
}
// Commented by wtlyu
// if (
// chatGptLabel !== 'chatgptCustom' &&
// modelMap[chatGptLabel.toLowerCase()] &&
// !initial[chatGptLabel.toLowerCase()] &&
// saveText === 'Save'
// ) {
// setSaveText('Update');
// } else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') {
// setSaveText('Save');
// }
const requiredProp = required ? { required: true } : {};
@ -84,8 +90,7 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
<DialogHeader>
<DialogTitle className="text-gray-800 dark:text-white">Customize ChatGPT</DialogTitle>
<DialogDescription className="text-gray-600 dark:text-gray-300">
Note: important instructions are often better placed in your message rather than the
prefix.{' '}
Note: important instructions are often better placed in your message rather than the prefix.{' '}
<a
href="https://platform.openai.com/docs/guides/chat/instructing-chat-models"
target="_blank"
@ -107,7 +112,7 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
id="chatGptLabel"
value={chatGptLabel}
ref={inputRef}
onChange={(e) => setChatGptLabel(e.target.value)}
onChange={e => setChatGptLabel(e.target.value)}
placeholder="Set a custom name for ChatGPT"
className=" col-span-3 shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600 invalid:placeholder-opacity-70 invalid:ring-opacity-10 focus:ring-0 focus:invalid:border-red-400 focus:invalid:ring-red-300 dark:border-none dark:bg-gray-700
dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:invalid:border-red-600 dark:invalid:text-red-300 dark:invalid:placeholder-opacity-80 dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0 dark:focus:invalid:ring-red-600 dark:focus:invalid:ring-opacity-50"
@ -124,7 +129,7 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
<TextareaAutosize
id="promptPrefix"
value={promptPrefix}
onChange={(e) => setPromptPrefix(e.target.value)}
onChange={e => setPromptPrefix(e.target.value)}
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
className="col-span-3 flex h-20 w-full resize-none rounded-md border border-gray-300 bg-transparent py-2 px-3 text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-none dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
/>

View file

@ -1,36 +1,61 @@
import React, { useState, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DropdownMenuRadioItem } from '../ui/DropdownMenu.tsx';
import { setModels } from '~/store/modelSlice';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx';
import { Circle } from 'lucide-react';
import { DialogTrigger } from '../ui/Dialog.tsx';
import RenameButton from '../Conversations/RenameButton';
import TrashIcon from '../svg/TrashIcon';
import { DialogTrigger } from '../../ui/Dialog.tsx';
import RenameButton from '../../Conversations/RenameButton';
import TrashIcon from '../../svg/TrashIcon';
import manualSWR from '~/utils/fetchers';
import { getIconOfModel } from '../../utils';
import { getIconOfModel } from '~/utils';
import store from '~/store';
export default function ModelItem({ model: _model, value, onSelect }) {
const { name, model, _id: id, chatGptLabel = null, promptPrefix = null } = _model;
const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
const currentConversation = useRecoilValue(store.conversation) || {};
export default function ModelItem({ modelName, value, model, onSelect, id, chatGptLabel, promptPrefix }) {
const dispatch = useDispatch();
const { customModel } = useSelector((state) => state.submit);
const { initial } = useSelector((state) => state.models);
const [isHovering, setIsHovering] = useState(false);
const [renaming, setRenaming] = useState(false);
const [currentName, setCurrentName] = useState(modelName);
const [modelInput, setModelInput] = useState(modelName);
const [currentName, setCurrentName] = useState(name);
const [modelInput, setModelInput] = useState(name);
const inputRef = useRef(null);
const rename = manualSWR(`/api/customGpts`, 'post');
const deleteCustom = manualSWR(`/api/customGpts/delete`, 'post', (res) => {
const fetchedModels = res.data.map((modelItem) => ({
const rename = manualSWR(`/api/customGpts`, 'post', res => {});
const deleteCustom = manualSWR(`/api/customGpts/delete`, 'post', res => {
const fetchedModels = res.data.map(modelItem => ({
...modelItem,
name: modelItem.chatGptLabel
name: modelItem.chatGptLabel,
model: 'chatgptCustom'
}));
dispatch(setModels(fetchedModels));
setCustomGPTModels(fetchedModels);
});
const icon = getIconOfModel({ size: 20, sender: modelName, isCreatedByUser: false, model, chatGptLabel, promptPrefix, error: false, className: "mr-2" });
const icon = getIconOfModel({
size: 20,
sender: chatGptLabel || model,
isCreatedByUser: false,
model,
chatGptLabel,
promptPrefix,
error: false,
className: 'mr-2'
});
if (value === 'chatgptCustom') {
if (model !== 'chatgptCustom')
// regular model
return (
<DropdownMenuRadioItem
value={value}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{name}
{model === 'chatgpt' && <sup>$</sup>}
</DropdownMenuRadioItem>
);
else if (model === 'chatgptCustom' && chatGptLabel === null && promptPrefix === null)
// base chatgptCustom model, click to add new chatgptCustom.
return (
<DialogTrigger className="w-full">
<DropdownMenuRadioItem
@ -38,26 +63,13 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{modelName}
{name}
<sup>$</sup>
</DropdownMenuRadioItem>
</DialogTrigger>
);
}
if (initial[value])
return (
<DropdownMenuRadioItem
value={value}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{modelName}
{value === 'chatgpt' && <sup>$</sup>}
</DropdownMenuRadioItem>
);
// else: a chatgptCustom model
const handleMouseOver = () => {
setIsHovering(true);
};
@ -66,7 +78,7 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
setIsHovering(false);
};
const renameHandler = (e) => {
const renameHandler = e => {
e.preventDefault();
e.stopPropagation();
setRenaming(true);
@ -75,10 +87,10 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
}, 25);
};
const onRename = (e) => {
const onRename = e => {
e.preventDefault();
setRenaming(false);
if (modelInput === modelName) {
if (modelInput === name) {
return;
}
rename.trigger({
@ -89,13 +101,13 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
setCurrentName(modelInput);
};
const onDelete = async (e) => {
const onDelete = async e => {
e.preventDefault();
await deleteCustom.trigger({ _id: id });
onSelect('chatgpt', true);
onSelect('chatgpt');
};
const handleKeyDown = (e) => {
const handleKeyDown = e => {
if (e.key === 'Enter') {
onRename(e);
}
@ -115,14 +127,14 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
<span
value={value}
className={itemClass.className}
onClick={(e) => {
onClick={e => {
if (isHovering) {
return;
}
onSelect(value, true);
onSelect('chatgptCustom', value);
}}
>
{customModel === value && (
{currentConversation?.chatGptLabel === value && (
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Circle className="h-2 w-2 fill-current" />
</span>
@ -137,8 +149,8 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
type="text"
className="pointer-events-auto z-50 m-0 mr-2 w-3/4 border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={modelInput}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setModelInput(e.target.value)}
onClick={e => e.stopPropagation()}
onChange={e => setModelInput(e.target.value)}
// onBlur={onRename}
onKeyDown={handleKeyDown}
/>

View file

@ -0,0 +1,205 @@
import React, { useState, useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import axios from 'axios';
import ModelDialog from './ModelDialog';
import MenuItems from './MenuItems';
import { swr } from '~/utils/fetchers';
import { getIconOfModel } from '~/utils';
import { Button } from '../../ui/Button.tsx';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../../ui/DropdownMenu.tsx';
import { Dialog } from '../../ui/Dialog.tsx';
import store from '~/store';
export default function ModelMenu() {
const [modelSave, setModelSave] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const models = useRecoilValue(store.models);
const availableModels = useRecoilValue(store.availableModels);
const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
const conversation = useRecoilValue(store.conversation) || {};
const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
const { newConversation } = store.useConversation();
// fetch the list of saved chatgptCustom
const { data, isLoading, mutate } = swr(`/api/customGpts`, res => {
const fetchedModels = res.map(modelItem => ({
...modelItem,
name: modelItem.chatGptLabel,
model: 'chatgptCustom'
}));
setCustomGPTModels(fetchedModels);
});
// useEffect(() => {
// mutate();
// try {
// const lastSelected = JSON.parse(localStorage.getItem('model'));
// if (lastSelected === 'chatgptCustom') {
// return;
// } else if (initial[lastSelected]) {
// dispatch(setModel(lastSelected));
// }
// } catch (err) {
// console.log(err);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, []);
// update the default model when availableModels changes
// typically, availableModels changes => modelsFilter or customGPTModels changes
useEffect(() => {
if (conversationId == 'new') {
newConversation();
}
}, [availableModels]);
// save selected model to localstoreage
useEffect(() => {
if (model) localStorage.setItem('model', JSON.stringify({ model, chatGptLabel, promptPrefix }));
}, [model]);
// set the current model
const onChange = (newModel, value = null) => {
setMenuOpen(false);
if (!newModel) {
return;
} else if (newModel === model && value === chatGptLabel) {
// bypass if not changed
return;
} else if (newModel === 'chatgptCustom' && value === null) {
// return;
} else if (newModel !== 'chatgptCustom') {
newConversation({
model: newModel,
chatGptLabel: null,
promptPrefix: null
});
} else if (newModel === 'chatgptCustom') {
const targetModel = models.find(element => element.value == value);
if (targetModel) {
const chatGptLabel = targetModel?.chatGptLabel;
const promptPrefix = targetModel?.promptPrefix;
newConversation({
model: newModel,
chatGptLabel,
promptPrefix
});
}
}
};
const onOpenChange = open => {
mutate();
if (!open) {
setModelSave(false);
}
};
const handleSaveState = value => {
if (!modelSave) {
return;
}
setCustomGPTModels(value);
setModelSave(false);
};
const defaultColorProps = [
'text-gray-500',
'hover:bg-gray-100',
'hover:bg-opacity-20',
'disabled:hover:bg-transparent',
'dark:data-[state=open]:bg-gray-800',
'dark:hover:bg-opacity-20',
'dark:hover:bg-gray-900',
'dark:hover:text-gray-400',
'dark:disabled:hover:bg-transparent'
];
const chatgptColorProps = [
'text-green-700',
'data-[state=open]:bg-green-100',
'dark:text-emerald-300',
'hover:bg-green-100',
'disabled:hover:bg-transparent',
'dark:data-[state=open]:bg-green-900',
'dark:hover:bg-opacity-50',
'dark:hover:bg-green-900',
'dark:hover:text-gray-100',
'dark:disabled:hover:bg-transparent'
];
const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps;
const icon = getIconOfModel({
size: 32,
sender: chatGptLabel || model,
isCreatedByUser: false,
model,
chatGptLabel,
promptPrefix,
error: false,
button: true
});
return (
<Dialog onOpenChange={onOpenChange}>
<DropdownMenu
open={menuOpen}
onOpenChange={setMenuOpen}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
className={`absolute top-[0.25px] mb-0 ml-1 items-center rounded-md border-0 p-1 outline-none md:ml-0 ${colorProps.join(
' '
)} focus:ring-0 focus:ring-offset-0 disabled:top-[0.25px] dark:data-[state=open]:bg-opacity-50 md:top-1 md:left-1 md:pl-1 md:disabled:top-1`}
>
{icon}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 dark:bg-gray-700"
onCloseAutoFocus={event => event.preventDefault()}
>
<DropdownMenuLabel className="dark:text-gray-300">Select a Model</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={chatGptLabel || model}
onValueChange={onChange}
className="overflow-y-auto"
>
{availableModels.length ? (
<MenuItems
models={availableModels}
onSelect={onChange}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">No model available.</DropdownMenuLabel>
)}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<ModelDialog
mutate={mutate}
setModelSave={setModelSave}
handleSaveState={handleSaveState}
/>
</Dialog>
);
}

View file

@ -1,11 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
export default function SubmitButton({ submitMessage, disabled }) {
const { isSubmitting } = useSelector((state) => state.submit);
const { error, latestMessage } = useSelector((state) => state.convo);
const clickHandler = (e) => {
export default function SubmitButton({ submitMessage, disabled, isSubmitting }) {
const clickHandler = e => {
e.preventDefault();
submitMessage();
};

View file

@ -0,0 +1,454 @@
import React, { useEffect, useRef, useState } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
// import { SSE } from '~/utils/sse';
import SubmitButton from './SubmitButton';
// import AdjustToneButton from './AdjustToneButton';
// import BingStyles from './BingStyles';
import ModelMenu from './Models/ModelMenu';
import Footer from './Footer';
import TextareaAutosize from 'react-textarea-autosize';
import createPayload from '~/utils/createPayload';
import RegenerateIcon from '../svg/RegenerateIcon';
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
// import { setConversation, setError, refreshConversation } from '~/store/convoSlice';
// import { setMessages } from '~/store/messageSlice';
// import { setSubmitState, toggleCursor } from '~/store/submitSlice';
// import { setText } from '~/store/textSlice';
import { useMessageHandler } from '../../utils/handleSubmit';
import store from '~/store';
export default function TextChat() {
const inputRef = useRef(null);
const isComposing = useRef(false);
const conversation = useRecoilValue(store.conversation);
const latestMessage = useRecoilValue(store.latestMessage);
const messages = useRecoilValue(store.messages);
const isSubmitting = useRecoilValue(store.isSubmitting);
// TODO: do we need this?
const disabled = false;
const [text, setText] = useState('');
const { ask, regenerate, stopGenerating } = useMessageHandler();
const bingStylesRef = useRef(null);
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
const isNotAppendable = latestMessage?.cancelled || latestMessage?.error;
// auto focus to input, when enter a conversation.
useEffect(() => {
inputRef.current?.focus();
setText('');
}, [conversation?.conversationId]);
// controls the height of Bing tone style tabs
useEffect(() => {
if (!inputRef.current) {
return; // wait for the ref to be available
}
const resizeObserver = new ResizeObserver(() => {
const newHeight = inputRef.current.clientHeight;
if (newHeight >= 24) {
// 24 is the default height of the input
bingStylesRef.current.style.bottom = 15 + newHeight + 'px';
}
});
resizeObserver.observe(inputRef.current);
return () => resizeObserver.disconnect();
}, [inputRef]);
// const messageHandler = (data, currentState, currentMsg) => {
// const { messages, message, sender, isRegenerate } = currentState;
// if (isRegenerate)
// dispatch(
// setMessages([
// ...messages,
// {
// sender,
// text: data,
// parentMessageId: message?.overrideParentMessageId,
// messageId: message?.overrideParentMessageId + '_',
// submitting: true
// }
// ])
// );
// else
// dispatch(
// setMessages([
// ...messages,
// currentMsg,
// {
// sender,
// text: data,
// parentMessageId: currentMsg?.messageId,
// messageId: currentMsg?.messageId + '_',
// submitting: true
// }
// ])
// );
// };
// const cancelHandler = (data, currentState, currentMsg) => {
// const { messages, message, sender, isRegenerate } = currentState;
// if (isRegenerate)
// dispatch(
// setMessages([
// ...messages,
// {
// sender,
// text: data,
// parentMessageId: message?.overrideParentMessageId,
// messageId: message?.overrideParentMessageId + '_',
// cancelled: true
// }
// ])
// );
// else
// dispatch(
// setMessages([
// ...messages,
// currentMsg,
// {
// sender,
// text: data,
// parentMessageId: currentMsg?.messageId,
// messageId: currentMsg?.messageId + '_',
// cancelled: true
// }
// ])
// );
// };
// const createdHandler = (data, currentState, currentMsg) => {
// const { conversationId } = currentMsg;
// dispatch(
// setConversation({
// conversationId,
// latestMessage: null
// })
// );
// };
// const convoHandler = (data, currentState) => {
// const { requestMessage, responseMessage } = data;
// const { messages, message, isCustomModel, isRegenerate } = currentState;
// const { model, chatGptLabel, promptPrefix } = message;
// if (isRegenerate) dispatch(setMessages([...messages, responseMessage]));
// else dispatch(setMessages([...messages, requestMessage, responseMessage]));
// dispatch(setSubmitState(false));
// const isBing = model === 'bingai' || model === 'sydney';
// // refresh title
// if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
// setTimeout(() => {
// dispatch(refreshConversation());
// }, 2000);
// // in case it takes too long.
// setTimeout(() => {
// dispatch(refreshConversation());
// }, 5000);
// }
// if (!isBing && convo.conversationId === null && convo.parentMessageId === null) {
// const { title } = data;
// const { conversationId, messageId } = responseMessage;
// dispatch(
// setConversation({
// title,
// conversationId,
// parentMessageId: messageId,
// jailbreakConversationId: null,
// conversationSignature: null,
// clientId: null,
// invocationId: null,
// chatGptLabel: model === isCustomModel ? chatGptLabel : null,
// promptPrefix: model === isCustomModel ? promptPrefix : null,
// latestMessage: null
// })
// );
// } else if (model === 'bingai') {
// console.log('Bing data:', data);
// const { title } = data;
// const { conversationSignature, clientId, conversationId, invocationId, parentMessageId } =
// responseMessage;
// dispatch(
// setConversation({
// title,
// parentMessageId,
// conversationSignature,
// clientId,
// conversationId,
// invocationId,
// latestMessage: null
// })
// );
// } else if (model === 'sydney') {
// const { title } = data;
// const {
// jailbreakConversationId,
// parentMessageId,
// conversationSignature,
// clientId,
// conversationId,
// invocationId
// } = responseMessage;
// dispatch(
// setConversation({
// title,
// jailbreakConversationId,
// parentMessageId,
// conversationSignature,
// clientId,
// conversationId,
// invocationId,
// latestMessage: null
// })
// );
// }
// };
// const errorHandler = (data, currentState, currentMsg) => {
// const { messages, message } = currentState;
// console.log('Error:', data);
// const errorResponse = {
// ...data,
// error: true,
// parentMessageId: currentMsg?.messageId
// };
// dispatch(setSubmitState(false));
// dispatch(setMessages([...messages, currentMsg, errorResponse]));
// dispatch(setText(message?.text));
// dispatch(setError(true));
// return;
// };
const submitMessage = () => {
ask({ text });
setText('');
};
// useEffect(() => {
// inputRef.current?.focus();
// if (Object.keys(submission).length === 0) {
// return;
// }
// const currentState = submission;
// let currentMsg = { ...currentState.message };
// let latestResponseText = '';
// const { server, payload } = createPayload(submission);
// const onMessage = e => {
// if (stopStream) {
// return;
// }
// const data = JSON.parse(e.data);
// if (data.final) {
// convoHandler(data, currentState);
// dispatch(toggleCursor());
// console.log('final', data);
// }
// if (data.created) {
// currentMsg = data.message;
// createdHandler(data, currentState, currentMsg);
// } else {
// let text = data.text || data.response;
// if (data.initial) {
// dispatch(toggleCursor());
// }
// if (data.message) {
// latestResponseText = text;
// messageHandler(text, currentState, currentMsg);
// }
// // console.log('dataStream', data);
// }
// };
// const events = new SSE(server, {
// payload: JSON.stringify(payload),
// headers: { 'Content-Type': 'application/json' }
// });
// events.onopen = function () {
// console.log('connection is opened');
// };
// events.onmessage = onMessage;
// events.oncancel = () => {
// dispatch(toggleCursor(true));
// cancelHandler(latestResponseText, currentState, currentMsg);
// };
// events.onerror = function (e) {
// console.log('error in opening conn.');
// events.close();
// const data = JSON.parse(e.data);
// dispatch(toggleCursor(true));
// errorHandler(data, currentState, currentMsg);
// };
// events.stream();
// return () => {
// events.removeEventListener('message', onMessage);
// dispatch(toggleCursor(true));
// const isCancelled = events.readyState <= 1;
// events.close();
// if (isCancelled) {
// const e = new Event('cancel');
// events.dispatchEvent(e);
// }
// };
// }, [submission]);
const handleRegenerate = () => {
if (latestMessage && !latestMessage?.isCreatedByUser) regenerate(latestMessage);
};
const handleStopGenerating = () => {
stopGenerating();
};
const handleKeyDown = e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
if (e.key === 'Enter' && !e.shiftKey) {
if (!isComposing?.current) submitMessage();
}
};
const handleKeyUp = e => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
setText(e.target.value);
}
if (e.key === 'Enter' && e.shiftKey) {
return console.log('Enter + Shift');
}
if (isSubmitting) {
return;
}
};
const handleCompositionStart = () => {
isComposing.current = true;
};
const handleCompositionEnd = () => {
isComposing.current = false;
};
const changeHandler = e => {
const { value } = e.target;
setText(value);
};
const isSearchView = messages?.[0]?.searchResult === true;
const getPlaceholderText = () => {
if (isSearchView) {
return 'Click a message title to open its conversation.';
}
if (disabled) {
return 'Choose another model or customize GPT again';
}
if (isNotAppendable) {
return 'Edit your message or Regenerate.';
}
return '';
};
const handleBingToneSetting = () => {
setShowBingToneSetting(show => !show);
};
if (isSearchView) return <></>;
return (
<>
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient fixed bottom-0 left-0 w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:absolute md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
<form className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
<div className="relative flex h-full flex-1 md:flex-col">
<span className="order-last ml-1 flex justify-center gap-0 md:order-none md:m-auto md:mb-2 md:w-full md:gap-2">
{/* <BingStyles
ref={bingStylesRef}
show={showBingToneSetting}
/> */}
{isSubmitting ? (
<button
onClick={handleStopGenerating}
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
type="button"
>
<StopGeneratingIcon />
<span className="hidden md:block">Stop generating</span>
</button>
) : latestMessage && !latestMessage?.isCreatedByUser ? (
<button
onClick={handleRegenerate}
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
type="button"
>
<RegenerateIcon />
<span className="hidden md:block">Regenerate response</span>
</button>
) : null}
</span>
<div
className={`relative flex flex-grow flex-col rounded-md border border-black/10 ${
disabled ? 'bg-gray-100' : 'bg-white'
} py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 ${
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
>
<ModelMenu />
<TextareaAutosize
tabIndex="0"
autoFocus
ref={inputRef}
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
rows="1"
value={disabled || isNotAppendable ? '' : text}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={changeHandler}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
placeholder={getPlaceholderText()}
disabled={disabled || isNotAppendable}
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-12 pr-8 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-8"
/>
<SubmitButton
submitMessage={submitMessage}
disabled={disabled || isNotAppendable}
/>
{/* {messages?.length && model === 'sydney' ? (
<AdjustToneButton onClick={handleBingToneSetting} />
) : null} */}
</div>
</div>
</form>
<Footer />
</div>
</>
);
}

View file

@ -1,35 +0,0 @@
import React from 'react';
import RegenerateIcon from '../svg/RegenerateIcon';
export default function Regenerate({ submitMessage, tryAgain, errorMessage }) {
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
};
return (
<>
<span className="mb-2 block flex justify-center text-xs text-black dark:text-white/50 md:mb-2">
There was an error generating a response
</span>
<span className="m-auto flex justify-center">
{!errorMessage.includes('short') && (
<button
onClick={clickHandler}
className="btn btn-primary m-auto flex justify-center gap-2"
>
<RegenerateIcon />
Regenerate response
</button>
)}
<button
onClick={tryAgain}
className="btn btn-neutral flex justify-center gap-2 border-0 md:border"
>
<RegenerateIcon />
Try another message
</button>
</span>
</>
);
}

View file

@ -1,446 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { SSE } from '~/utils/sse';
import SubmitButton from './SubmitButton';
// import Regenerate from './Regenerate'; // not used as of Wentao's update
import BingStyles from './BingStyles';
import ModelMenu from '../Models/ModelMenu';
import Footer from './Footer';
import TextareaAutosize from 'react-textarea-autosize';
import createPayload from '~/utils/createPayload';
import RegenerateIcon from '../svg/RegenerateIcon';
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
import { setConversation, setError, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmitState, toggleCursor } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { useMessageHandler } from '../../utils/handleSubmit';
import AdjustToneButton from './AdjustToneButton';
export default function TextChat({ messages }) {
const inputRef = useRef(null);
const bingStylesRef = useRef(null);
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
const isComposing = useRef(false);
const dispatch = useDispatch();
const convo = useSelector(state => state.convo);
const { isSubmitting, stopStream, submission, disabled, model } = useSelector(state => state.submit);
const { text } = useSelector(state => state.text);
const { latestMessage } = convo;
const { ask, regenerate, stopGenerating } = useMessageHandler();
const isNotAppendable = latestMessage?.cancelled || latestMessage?.error;
// auto focus to input, when enter a conversation.
useEffect(() => {
inputRef.current?.focus();
}, [convo?.conversationId]);
// controls the height of Bing tone style tabs
useEffect(() => {
if (!inputRef.current) {
return; // wait for the ref to be available
}
const resizeObserver = new ResizeObserver(() => {
const newHeight = inputRef.current.clientHeight;
if (newHeight >= 24) { // 24 is the default height of the input
bingStylesRef.current.style.bottom = 15 + newHeight + 'px';
}
});
resizeObserver.observe(inputRef.current);
return () => resizeObserver.disconnect();
}, [inputRef]);
const messageHandler = (data, currentState, currentMsg) => {
const { messages, message, sender, isRegenerate } = currentState;
if (isRegenerate)
dispatch(
setMessages([
...messages,
{
sender,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
])
);
else
dispatch(
setMessages([
...messages,
currentMsg,
{
sender,
text: data,
parentMessageId: currentMsg?.messageId,
messageId: currentMsg?.messageId + '_',
submitting: true
}
])
);
};
const cancelHandler = (data, currentState, currentMsg) => {
const { messages, message, sender, isRegenerate } = currentState;
if (isRegenerate)
dispatch(
setMessages([
...messages,
{
sender,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
cancelled: true
}
])
);
else
dispatch(
setMessages([
...messages,
currentMsg,
{
sender,
text: data,
parentMessageId: currentMsg?.messageId,
messageId: currentMsg?.messageId + '_',
cancelled: true
}
])
);
};
const createdHandler = (data, currentState, currentMsg) => {
const { conversationId } = currentMsg;
dispatch(
setConversation({
conversationId,
latestMessage: null
})
);
};
const convoHandler = (data, currentState) => {
const { requestMessage, responseMessage } = data;
const { messages, message, isCustomModel, isRegenerate } = currentState;
const { model, chatGptLabel, promptPrefix } = message;
if (isRegenerate) dispatch(setMessages([...messages, responseMessage]));
else dispatch(setMessages([...messages, requestMessage, responseMessage]));
dispatch(setSubmitState(false));
const isBing = model === 'bingai' || model === 'sydney';
// refresh title
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
dispatch(refreshConversation());
}, 2000);
// in case it takes too long.
setTimeout(() => {
dispatch(refreshConversation());
}, 5000);
}
if (!isBing && convo.conversationId === null && convo.parentMessageId === null) {
const { title } = data;
const { conversationId, messageId } = responseMessage;
dispatch(
setConversation({
title,
conversationId,
parentMessageId: messageId,
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
chatGptLabel: model === isCustomModel ? chatGptLabel : null,
promptPrefix: model === isCustomModel ? promptPrefix : null,
latestMessage: null
})
);
} else if (model === 'bingai') {
console.log('Bing data:', data);
const { title } = data;
const { conversationSignature, clientId, conversationId, invocationId, parentMessageId } =
responseMessage;
dispatch(
setConversation({
title,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId,
latestMessage: null
})
);
} else if (model === 'sydney') {
const { title } = data;
const {
jailbreakConversationId,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId
} = responseMessage;
dispatch(
setConversation({
title,
jailbreakConversationId,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId,
latestMessage: null
})
);
}
};
const errorHandler = (data, currentState, currentMsg) => {
const { messages, message } = currentState;
console.log('Error:', data);
const errorResponse = {
...data,
error: true,
parentMessageId: currentMsg?.messageId
};
dispatch(setSubmitState(false));
dispatch(setMessages([...messages, currentMsg, errorResponse]));
dispatch(setText(message?.text));
dispatch(setError(true));
return;
};
const submitMessage = () => {
ask({ text });
};
useEffect(() => {
inputRef.current?.focus();
if (Object.keys(submission).length === 0) {
return;
}
const currentState = submission;
let currentMsg = { ...currentState.message };
let latestResponseText = '';
const { server, payload } = createPayload(submission);
const onMessage = e => {
if (stopStream) {
return;
}
const data = JSON.parse(e.data);
if (data.final) {
convoHandler(data, currentState);
dispatch(toggleCursor());
console.log('final', data);
}
if (data.created) {
currentMsg = data.message;
createdHandler(data, currentState, currentMsg);
} else {
let text = data.text || data.response;
if (data.initial) {
dispatch(toggleCursor());
}
if (data.message) {
latestResponseText = text;
messageHandler(text, currentState, currentMsg);
}
// console.log('dataStream', data);
}
};
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
events.onopen = function () {
console.log('connection is opened');
};
events.onmessage = onMessage;
events.oncancel = () => {
dispatch(toggleCursor(true));
cancelHandler(latestResponseText, currentState, currentMsg);
};
events.onerror = function (e) {
console.log('error in opening conn.');
events.close();
const data = JSON.parse(e.data);
dispatch(toggleCursor(true));
errorHandler(data, currentState, currentMsg);
};
events.stream();
return () => {
events.removeEventListener('message', onMessage);
dispatch(toggleCursor(true));
const isCancelled = events.readyState <= 1;
events.close();
if (isCancelled) {
const e = new Event('cancel');
events.dispatchEvent(e);
}
};
}, [submission]);
const handleRegenerate = () => {
if (latestMessage && !latestMessage?.isCreatedByUser) regenerate(latestMessage);
};
const handleStopGenerating = () => {
stopGenerating();
};
const handleKeyDown = e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
if (e.key === 'Enter' && !e.shiftKey) {
if (!isComposing.current) submitMessage();
}
};
const handleKeyUp = e => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
dispatch(setText(e.target.value));
}
if (e.key === 'Enter' && e.shiftKey) {
return console.log('Enter + Shift');
}
if (isSubmitting) {
return;
}
};
const handleCompositionStart = () => {
isComposing.current = true;
};
const handleCompositionEnd = () => {
isComposing.current = false;
};
const changeHandler = e => {
const { value } = e.target;
// if (isSubmitting && (value === '' || value === '\n')) {
// return;
// }
dispatch(setText(value));
};
// const tryAgain = (e) => {
// e.preventDefault();
// dispatch(setError(false));
// };
const isSearchView = messages?.[0]?.searchResult === true;
const getPlaceholderText = () => {
if (isSearchView) {
return 'Click a message title to open its conversation.';
}
if (disabled) {
return 'Choose another model or customize GPT again';
}
if (isNotAppendable) {
return 'Edit your message or Regenerate.';
}
return '';
};
const handleBingToneSetting = () => {
setShowBingToneSetting((show) => !show)
}
return (
<>
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient fixed bottom-0 left-0 w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:absolute md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
<form className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
<div className="relative flex h-full flex-1 md:flex-col">
<span className="order-last ml-1 flex justify-center gap-0 md:order-none md:m-auto md:mb-2 md:w-full md:gap-2">
<BingStyles ref={bingStylesRef} show={showBingToneSetting}/>
{isSubmitting && !isSearchView ? (
<button
onClick={handleStopGenerating}
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
type="button"
>
<StopGeneratingIcon />
<span className="hidden md:block">Stop generating</span>
</button>
) : latestMessage && !latestMessage?.isCreatedByUser && !isSearchView ? (
<button
onClick={handleRegenerate}
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
type="button"
>
<RegenerateIcon />
<span className="hidden md:block">Regenerate response</span>
</button>
) : null}
</span>
<div
className={`relative flex flex-grow flex-col rounded-md border border-black/10 ${
disabled ? 'bg-gray-100' : 'bg-white'
} py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 ${
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
>
<ModelMenu />
<TextareaAutosize
tabIndex="0"
autoFocus
ref={inputRef}
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
rows="1"
value={disabled || isNotAppendable ? '' : text}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={changeHandler}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
placeholder={getPlaceholderText()}
disabled={disabled || isNotAppendable}
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-12 pr-8 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-8"
/>
<SubmitButton
submitMessage={submitMessage}
disabled={disabled || isNotAppendable}
/>
{messages?.length && model === 'sydney' ?
<AdjustToneButton onClick={handleBingToneSetting} /> :
null}
</div>
</div>
</form>
<Footer />
</div>
</>
);
}

View file

@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState } from "react";
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
import { SSE } from "~/utils/sse";
import { useMessageHandler } from "../../utils/handleSubmit";
import createPayload from "~/utils/createPayload";
import React, { useEffect, useRef, useState } from 'react';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { SSE } from '~/utils/sse';
import { useMessageHandler } from '../../utils/handleSubmit';
import createPayload from '~/utils/createPayload';
import store from "~/store";
import store from '~/store';
export default function MessageHandler({ messages }) {
const [submission, setSubmission] = useRecoilState(store.submission);
@ -16,12 +16,7 @@ export default function MessageHandler({ messages }) {
const { refreshConversations } = store.useConversations();
const messageHandler = (data, submission) => {
const {
messages,
message,
initialResponse,
isRegenerate = false,
} = submission;
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
@ -30,9 +25,9 @@ export default function MessageHandler({ messages }) {
...initialResponse,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + "_",
submitting: true,
},
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
]);
else
setMessages([
@ -42,19 +37,14 @@ export default function MessageHandler({ messages }) {
...initialResponse,
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + "_",
submitting: true,
},
messageId: message?.messageId + '_',
submitting: true
}
]);
};
const cancelHandler = (data, submission) => {
const {
messages,
message,
initialResponse,
isRegenerate = false,
} = submission;
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
@ -63,9 +53,9 @@ export default function MessageHandler({ messages }) {
...initialResponse,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + "_",
cancelled: true,
},
messageId: message?.overrideParentMessageId + '_',
cancelled: true
}
]);
else
setMessages([
@ -75,19 +65,14 @@ export default function MessageHandler({ messages }) {
...initialResponse,
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + "_",
cancelled: true,
},
messageId: message?.messageId + '_',
cancelled: true
}
]);
};
const createdHandler = (data, submission) => {
const {
messages,
message,
initialResponse,
isRegenerate = false,
} = submission;
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
@ -95,9 +80,9 @@ export default function MessageHandler({ messages }) {
{
...initialResponse,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + "_",
submitting: true,
},
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
]);
else
setMessages([
@ -106,27 +91,21 @@ export default function MessageHandler({ messages }) {
{
...initialResponse,
parentMessageId: message?.messageId,
messageId: message?.messageId + "_",
submitting: true,
},
messageId: message?.messageId + '_',
submitting: true
}
]);
const { conversationId } = message;
setConversation((prevState) => ({
setConversation(prevState => ({
...prevState,
conversationId,
conversationId
}));
resetLatestMessage();
};
const finalHandler = (data, submission) => {
const {
conversation,
messages,
message,
initialResponse,
isRegenerate = false,
} = submission;
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
const { requestMessage, responseMessage } = data;
const { conversationId } = requestMessage;
@ -137,9 +116,7 @@ export default function MessageHandler({ messages }) {
setIsSubmitting(false);
// refresh title
if (
requestMessage.parentMessageId == "00000000-0000-0000-0000-000000000000"
) {
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
refreshConversations();
}, 2000);
@ -151,12 +128,12 @@ export default function MessageHandler({ messages }) {
}
const { model, chatGptLabel, promptPrefix } = conversation;
const isBing = model === "bingai" || model === "sydney";
const isBing = model === 'bingai' || model === 'sydney';
if (!isBing) {
const { title } = data;
const { conversationId } = responseMessage;
setConversation((prevState) => ({
setConversation(prevState => ({
...prevState,
title,
conversationId,
@ -166,13 +143,12 @@ export default function MessageHandler({ messages }) {
invocationId: null,
chatGptLabel,
promptPrefix,
latestMessage: null,
latestMessage: null
}));
} else if (model === "bingai") {
} else if (model === 'bingai') {
const { title } = data;
const { conversationSignature, clientId, conversationId, invocationId } =
responseMessage;
setConversation((prevState) => ({
const { conversationSignature, clientId, conversationId, invocationId } = responseMessage;
setConversation(prevState => ({
...prevState,
title,
conversationId,
@ -182,9 +158,9 @@ export default function MessageHandler({ messages }) {
invocationId,
chatGptLabel,
promptPrefix,
latestMessage: null,
latestMessage: null
}));
} else if (model === "sydney") {
} else if (model === 'sydney') {
const { title } = data;
const {
jailbreakConversationId,
@ -192,9 +168,9 @@ export default function MessageHandler({ messages }) {
conversationSignature,
clientId,
conversationId,
invocationId,
invocationId
} = responseMessage;
setConversation((prevState) => ({
setConversation(prevState => ({
...prevState,
title,
conversationId,
@ -204,25 +180,19 @@ export default function MessageHandler({ messages }) {
invocationId,
chatGptLabel,
promptPrefix,
latestMessage: null,
latestMessage: null
}));
}
};
const errorHandler = (data, submission) => {
const {
conversation,
messages,
message,
initialResponse,
isRegenerate = false,
} = submission;
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
console.log("Error:", data);
console.log('Error:', data);
const errorResponse = {
...data,
error: true,
parentMessageId: message?.messageId,
parentMessageId: message?.messageId
};
setIsSubmitting(false);
setMessages([...messages, message, errorResponse]);
@ -240,16 +210,16 @@ export default function MessageHandler({ messages }) {
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' }
});
let latestResponseText = "";
events.onmessage = (e) => {
let latestResponseText = '';
events.onmessage = e => {
const data = JSON.parse(e.data);
if (data.final) {
finalHandler(data, { ...submission, message });
console.log("final", data);
console.log('final', data);
}
if (data.created) {
message = {
@ -257,10 +227,10 @@ export default function MessageHandler({ messages }) {
model: message?.model,
chatGptLabel: message?.chatGptLabel,
promptPrefix: message?.promptPrefix,
overrideParentMessageId: message?.overrideParentMessageId,
overrideParentMessageId: message?.overrideParentMessageId
};
createdHandler(data, { ...submission, message });
console.log("created", message);
console.log('created', message);
} else {
let text = data.text || data.response;
if (data.initial) console.log(data);
@ -273,13 +243,12 @@ export default function MessageHandler({ messages }) {
}
};
events.onopen = () => console.log("connection is opened");
events.onopen = () => console.log('connection is opened');
events.oncancel = (e) =>
cancelHandler(latestResponseText, { ...submission, message });
events.oncancel = e => cancelHandler(latestResponseText, { ...submission, message });
events.onerror = function (e) {
console.log("error in opening conn.");
console.log('error in opening conn.');
events.close();
const data = JSON.parse(e.data);
@ -294,7 +263,7 @@ export default function MessageHandler({ messages }) {
const isCancelled = events.readyState <= 1;
events.close();
if (isCancelled) {
const e = new Event("cancel");
const e = new Event('cancel');
events.dispatchEvent(e);
}
setIsSubmitting(false);

View file

@ -1,20 +1,19 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
import SubRow from './Content/SubRow';
import Content from './Content/Content';
import MultiMessage from './MultiMessage';
import HoverButtons from './HoverButtons';
import SiblingSwitch from './SiblingSwitch';
import { setConversation, setLatestMessage } from '~/store/convoSlice';
import { setModel, setCustomModel, setCustomGpt, setDisabled } from '~/store/submitSlice';
import { setMessages } from '~/store/messageSlice';
import { fetchById } from '~/utils/fetchers';
import { getIconOfModel } from '~/utils';
import { useMessageHandler } from '~/utils/handleSubmit';
import store from '~/store';
export default function Message({
conversation,
message,
messages,
scrollToBottom,
currentEditId,
setCurrentEditId,
@ -22,30 +21,24 @@ export default function Message({
siblingCount,
setSiblingIdx
}) {
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(state => state.submit);
const isSubmitting = useRecoilValue(store.isSubmitting);
const setLatestMessage = useSetRecoilState(store.latestMessage);
const { model, chatGptLabel, promptPrefix } = conversation;
const [abortScroll, setAbort] = useState(false);
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
const textEditor = useRef(null);
const last = !message?.children?.length;
const edit = message.messageId == currentEditId;
const { ask } = useMessageHandler();
const dispatch = useDispatch();
// const currentConvo = convoMap[message.conversationId];
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
// const blinker = submitting && isSubmitting && last && !isCreatedByUser;
const { switchToConversation } = store.useConversation();
const blinker = submitting && isSubmitting;
const generateCursor = useCallback(() => {
if (!blinker) {
return '';
}
if (!cursor) {
return '';
}
return <span className="result-streaming"></span>;
}, [blinker, cursor]);
}, [blinker]);
useEffect(() => {
if (blinker && !abortScroll) {
@ -55,9 +48,7 @@ export default function Message({
useEffect(() => {
if (last) {
// TODO: stop using conversation.parentMessageId and remove it.
dispatch(setConversation({ parentMessageId: message?.messageId }));
dispatch(setLatestMessage({ ...message }));
setLatestMessage({ ...message });
}
}, [last, message]);
@ -110,22 +101,23 @@ export default function Message({
const clickSearchResult = async () => {
if (!searchResult) return;
dispatch(setMessages([]));
// dispatch(setMessages([]));
const convoResponse = await fetchById('convos', message.conversationId);
const convo = convoResponse.data;
if (convo?.chatGptLabel) {
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(convo.chatGptLabel.toLowerCase()));
} else {
dispatch(setModel(convo.model));
dispatch(setCustomModel(null));
}
// if (convo?.chatGptLabel) {
// // dispatch(setModel('chatgptCustom'));
// // dispatch(setCustomModel(convo.chatGptLabel.toLowerCase()));
// } else {
// // dispatch(setModel(convo.model));
// // dispatch(setCustomModel(null));
// }
dispatch(setCustomGpt(convo));
dispatch(setConversation(convo));
const { data } = await fetchById('messages', message.conversationId);
dispatch(setMessages(data));
dispatch(setDisabled(false));
// dispatch(setCustomGpt(convo));
switchToConversation(convo);
// dispatch(setConversation(convo));
// const { data } = await fetchById('messages', message.conversationId);
// dispatch(setMessages(data));
// dispatch(setDisabled(false));
};
return (
@ -133,7 +125,6 @@ export default function Message({
<div
{...props}
onWheel={handleWheel}
// 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 flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
@ -199,17 +190,13 @@ export default function Message({
<div className="flex min-h-[20px] flex-grow flex-col items-start gap-4 whitespace-pre-wrap">
{/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */}
<div className="markdown prose dark:prose-invert light w-full break-words">
{!isCreatedByUser ?
<>
<Content
content={text}
/>
{generateCursor()}
</> :
<>
{text}
</>
}
{!isCreatedByUser ? (
<>
<Content content={text} />
</>
) : (
<>{text}</>
)}
</div>
</div>
)}
@ -230,8 +217,8 @@ export default function Message({
</div>
</div>
<MultiMessage
messageList={message.children}
messages={messages}
conversation={conversation}
messagesTree={message.children}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}

View file

@ -2,44 +2,44 @@ import React, { useEffect, useState } from 'react';
import Message from './Message';
export default function MultiMessage({
messageList,
messages,
conversation,
messagesTree,
scrollToBottom,
currentEditId,
setCurrentEditId,
setCurrentEditId
}) {
const [siblingIdx, setSiblingIdx] = useState(0);
const setSiblingIdxRev = (value) => {
setSiblingIdx(messageList?.length - value - 1);
const setSiblingIdxRev = value => {
setSiblingIdx(messagesTree?.length - value - 1);
};
useEffect(() => {
// reset siblingIdx when changes, mostly a new message is submitting.
setSiblingIdx(0);
}, [messageList?.length])
}, [messagesTree?.length]);
// if (!messageList?.length) return null;
if (!(messageList && messageList.length)) {
if (!(messagesTree && messagesTree.length)) {
return null;
}
if (siblingIdx >= messageList?.length) {
if (siblingIdx >= messagesTree?.length) {
setSiblingIdx(0);
return null;
}
const message = messageList[messageList.length - siblingIdx - 1];
const message = messagesTree[messagesTree.length - siblingIdx - 1];
return (
<Message
key={message.messageId}
conversation={conversation}
message={message}
messages={messages}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
siblingIdx={messageList.length - siblingIdx - 1}
siblingCount={messageList.length}
siblingIdx={messagesTree.length - siblingIdx - 1}
siblingCount={messagesTree.length}
setSiblingIdx={setSiblingIdxRev}
/>
);

View file

@ -1,27 +1,30 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import Spinner from '../svg/Spinner';
import { throttle } from 'lodash';
import { CSSTransition } from 'react-transition-group';
import ScrollToBottom from './ScrollToBottom';
import MultiMessage from './MultiMessage';
import { useSelector } from 'react-redux';
export default function Messages({ messages, messageTree }) {
import store from '~/store';
export default function Messages() {
const [currentEditId, setCurrentEditId] = useState(-1);
const { conversationId } = useSelector((state) => state.convo);
const { model, customModel } = useSelector((state) => state.submit);
const { models } = useSelector((state) => state.models);
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 modelName = models.find((element) => element.model == model)?.name;
const modelName = models.find(element => element.model == model)?.name;
useEffect(() => {
const timeoutId = setTimeout(() => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff ) / clientHeight;
const percent = Math.abs(clientHeight - diff) / clientHeight;
const hasScrollbar = scrollHeight > clientHeight && percent > 0.2;
setShowScrollButton(hasScrollbar);
}, 650);
@ -33,17 +36,24 @@ export default function Messages({ messages, messageTree }) {
clearTimeout(timeoutId);
window.removeEventListener('scroll', handleScroll);
};
}, [messages]);
}, [messagesTree]);
const scrollToBottom = useCallback(throttle(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false);
}, 750, { leading: true }), [messagesEndRef]);
const scrollToBottom = useCallback(
throttle(
() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false);
},
750,
{ leading: true }
),
[messagesEndRef]
);
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff ) / clientHeight;
const percent = Math.abs(clientHeight - diff) / clientHeight;
if (percent <= 0.2) {
setShowScrollButton(false);
} else {
@ -57,7 +67,7 @@ export default function Messages({ messages, messageTree }) {
timeoutId = setTimeout(handleScroll, 100);
};
const scrollHandler = (e) => {
const scrollHandler = e => {
e.preventDefault();
scrollToBottom();
};
@ -68,20 +78,19 @@ export default function Messages({ messages, messageTree }) {
ref={scrollableRef}
onScroll={debouncedHandleScroll}
>
{/* <div className="flex-1 overflow-hidden"> */}
<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} {customModel ? `(${customModel})` : null}
Model: {modelName} {chatGptLabel ? `(${chatGptLabel})` : null}
</div>
{(messageTree.length === 0 || !messages) ? (
{messagesTree === null ? (
<Spinner />
) : (
<>
<MultiMessage
key={conversationId} // avoid internal state mixture
messageList={messageTree}
messages={messages}
conversation={conversation}
messagesTree={messagesTree}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
@ -103,7 +112,6 @@ export default function Messages({ messages, messageTree }) {
/>
</div>
</div>
{/* </div> */}
</div>
);
}

View file

@ -1,223 +0,0 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useSelector, useDispatch } from 'react-redux';
import {
setSubmission,
setModel,
setDisabled,
setCustomGpt,
setCustomModel
} from '~/store/submitSlice';
import { setNewConvo } from '~/store/convoSlice';
import ModelDialog from './ModelDialog';
import MenuItems from './MenuItems';
import { swr } from '~/utils/fetchers';
import { setModels, setInitial } from '~/store/modelSlice';
import { setMessages } from '~/store/messageSlice';
import { setText } from '~/store/textSlice';
import { Button } from '../ui/Button.tsx';
import { getIconOfModel } from '../../utils';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/DropdownMenu.tsx';
import { Dialog } from '../ui/Dialog.tsx';
export default function ModelMenu() {
const dispatch = useDispatch();
const [modelSave, setModelSave] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const { model, customModel, promptPrefix, chatGptLabel } = useSelector((state) => state.submit);
const { models, modelMap, initial } = useSelector((state) => state.models);
const { data, isLoading, mutate } = swr(`/api/customGpts`, (res) => {
const fetchedModels = res.map((modelItem) => ({
...modelItem,
name: modelItem.chatGptLabel,
model: 'chatgptCustom'
}));
dispatch(setModels(fetchedModels));
});
useEffect(() => {
mutate();
try {
const lastSelected = JSON.parse(localStorage.getItem('model'));
if (lastSelected === 'chatgptCustom') {
return;
} else if (initial[lastSelected]) {
dispatch(setModel(lastSelected));
}
} catch (err) {
console.log(err);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
axios.get('/api/models', {
timeout: 1000,
withCredentials: true
}).then((res) => {
return res.data
}).then((data) => {
const initial = {chatgpt: data?.hasOpenAI, chatgptCustom: data?.hasOpenAI, bingai: data?.hasBing, sydney: data?.hasBing, chatgptBrowser: data?.hasChatGpt}
dispatch(setInitial(initial))
// TODO, auto reset default model
if (data?.hasOpenAI) {
dispatch(setModel('chatgpt'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (data?.hasBing) {
dispatch(setModel('bingai'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (data?.hasChatGpt) {
dispatch(setModel('chatgptBrowser'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else {
dispatch(setDisabled(true));
}
}).catch((error) => {
console.error(error)
console.log('Not login!')
window.location.href = "/auth/login";
})
}, [])
useEffect(() => {
localStorage.setItem('model', JSON.stringify(model));
}, [model]);
const filteredModels = models.filter(({model, _id }) => initial[model] );
const onChange = (value) => {
if (!value) {
return;
} else if (value === model) {
return;
} else if (value === 'chatgptCustom') {
// return;
} else if (initial[value]) {
dispatch(setModel(value));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (!initial[value]) {
const chatGptLabel = modelMap[value]?.chatGptLabel;
const promptPrefix = modelMap[value]?.promptPrefix;
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(value));
setMenuOpen(false);
} else if (!modelMap[value]) {
dispatch(setCustomModel(null));
}
// Set new conversation
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
};
const onOpenChange = (open) => {
mutate();
if (!open) {
setModelSave(false);
}
};
const handleSaveState = (value) => {
if (!modelSave) {
return;
}
dispatch(setCustomModel(value));
setModelSave(false);
};
const defaultColorProps = [
'text-gray-500',
'hover:bg-gray-100',
'hover:bg-opacity-20',
'disabled:hover:bg-transparent',
'dark:data-[state=open]:bg-gray-800',
'dark:hover:bg-opacity-20',
'dark:hover:bg-gray-900',
'dark:hover:text-gray-400',
'dark:disabled:hover:bg-transparent'
];
const chatgptColorProps = [
'text-green-700',
'data-[state=open]:bg-green-100',
'dark:text-emerald-300',
'hover:bg-green-100',
'disabled:hover:bg-transparent',
'dark:data-[state=open]:bg-green-900',
'dark:hover:bg-opacity-50',
'dark:hover:bg-green-900',
'dark:hover:text-gray-100',
'dark:disabled:hover:bg-transparent'
];
const isBing = model === 'bingai' || model === 'sydney';
const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps;
const icon = getIconOfModel({ size: 32, sender: chatGptLabel || model, isCreatedByUser: false, model, chatGptLabel, promptPrefix, error: false, button: true});
return (
<Dialog onOpenChange={onOpenChange}>
<DropdownMenu
open={menuOpen}
onOpenChange={setMenuOpen}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
className={`absolute top-[0.25px] items-center mb-0 rounded-md border-0 p-1 ml-1 md:ml-0 outline-none ${colorProps.join(
' '
)} focus:ring-0 focus:ring-offset-0 disabled:top-[0.25px] dark:data-[state=open]:bg-opacity-50 md:top-1 md:left-1 md:pl-1 md:disabled:top-1`}
>
{icon}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 dark:bg-gray-700" onCloseAutoFocus={(event) => event.preventDefault()}>
<DropdownMenuLabel className="dark:text-gray-300">Select a Model</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={customModel ? customModel : model}
onValueChange={onChange}
className="overflow-y-auto"
>
{filteredModels.length?
<MenuItems
models={filteredModels}
onSelect={onChange}
/>:<DropdownMenuLabel className="dark:text-gray-300">No model available.</DropdownMenuLabel>
}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<ModelDialog
mutate={mutate}
modelMap={modelMap}
setModelSave={setModelSave}
handleSaveState={handleSaveState}
/>
</Dialog>
);
}

View file

@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { setText } from '~/store/textSlice';
import { useRecoilValue } from 'recoil';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import Templates from '../Prompts/Templates';
import SunIcon from '../svg/SunIcon';
@ -8,27 +7,31 @@ import LightningIcon from '../svg/LightningIcon';
import CautionIcon from '../svg/CautionIcon';
import ChatIcon from '../svg/ChatIcon';
export default function Landing({ title }) {
import store from '~/store';
export default function Landing() {
const [showingTemplates, setShowingTemplates] = useState(false);
const dispatch = useDispatch();
const conversation = useRecoilValue(store.conversation);
const { title = 'New Chat' } = conversation || {};
useDocumentTitle(title);
const clickHandler = (e) => {
const clickHandler = e => {
e.preventDefault();
const { innerText } = e.target;
const quote = innerText.split('"')[1].trim();
dispatch(setText(quote));
// dispatch(setText(quote));
};
const showTemplates = (e) => {
const showTemplates = e => {
e.preventDefault();
setShowingTemplates(!showingTemplates);
};
return (
<div className="flex pt-10 md:pt-0 h-full flex-col items-center overflow-y-auto text-sm dark:bg-gray-800">
<div className="flex h-full flex-col items-center overflow-y-auto pt-10 text-sm dark:bg-gray-800 md:pt-0">
<div className="w-full px-6 text-gray-800 dark:text-gray-100 md:flex md:max-w-2xl md:flex-col lg:max-w-3xl">
<h1 className="mt-6 ml-auto mr-auto mb-10 flex items-center justify-center gap-2 text-center text-4xl font-semibold md:mt-[20vh] sm:mb-16">
<h1 className="mt-6 ml-auto mr-auto mb-10 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[20vh]">
ChatGPT Clone
</h1>
<div className="items-start gap-3.5 text-center md:flex">

View file

@ -1,13 +1,13 @@
import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import React, { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import Landing from "../components/ui/Landing";
import Messages from "../components/Messages";
import TextChat from "../components/Input";
import Landing from '../components/ui/Landing';
import Messages from '../components/Messages';
import TextChat from '../components/Input';
import store from "~/store";
import manualSWR from "~/utils/fetchers";
import store from '~/store';
import manualSWR from '~/utils/fetchers';
// import TextChat from './components/Main/TextChat';
// {/* <TextChat messages={messages} /> */}
@ -20,28 +20,22 @@ export default function Chat() {
const { conversationId } = useParams();
const navigate = useNavigate();
const { trigger: messagesTrigger } = manualSWR(
`/api/messages/${conversation?.conversationId}`,
"get"
);
const { trigger: messagesTrigger } = manualSWR(`/api/messages/${conversation?.conversationId}`, 'get');
const { trigger: conversationTrigger } = manualSWR(
`/api/convos/${conversationId}`,
"get"
);
const { trigger: conversationTrigger } = manualSWR(`/api/convos/${conversationId}`, 'get');
// when conversation changed or conversationId (in url) changed
useEffect(() => {
if (conversation === null) {
// no current conversation, we need to do something
if (conversationId == "new") {
if (conversationId == 'new') {
// create new
newConversation();
} else {
// fetch it from server
conversationTrigger().then(setConversation);
setMessages(null);
console.log("NEED TO FETCH DATA");
console.log('NEED TO FETCH DATA');
}
} else if (conversation?.conversationId !== conversationId)
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
@ -60,7 +54,7 @@ export default function Chat() {
return (
<>
{conversationId == "new" ? <Landing /> : <Messages />}
{conversationId == 'new' ? <Landing /> : <Messages />}
<TextChat />
</>
);

View file

@ -1,5 +1,13 @@
import models from './models';
import { atom, selector, useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
import {
atom,
selector,
useRecoilValue,
useSetRecoilState,
useResetRecoilState,
useRecoilCallback,
useRecoilState
} from 'recoil';
import buildTree from '~/utils/buildTree';
// current conversation, can be null (need to be fetched from server)
@ -44,19 +52,45 @@ const latestMessage = atom({
});
const useConversation = () => {
const modelsFilter = useRecoilValue(models.modelsFilter);
const setConversation = useSetRecoilState(conversation);
const setMessages = useSetRecoilState(messages);
const resetLatestMessage = useResetRecoilState(latestMessage);
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
const switchToConversation = useRecoilCallback(
({ snapshot }) =>
async (_conversation, messages = null) => {
const prevConversation = await snapshot.getPromise(conversation);
const prevModelsFilter = await snapshot.getPromise(models.modelsFilter);
_switchToConversation(_conversation, messages, { prevModelsFilter, prevConversation });
},
[]
);
const _switchToConversation = (
conversation,
messages = null,
{ prevModelsFilter = {}, prev_conversation = {} }
) => {
let { model = null, chatGptLabel = null, promptPrefix = null } = conversation;
const getDefaultModel = () => {
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;
promptPrefix = _promptPrefix;
return;
}
} catch (error) {}
try {
// try to read latest selected model from local storage
const lastSelected = JSON.parse(localStorage.getItem('model'));
const { model: _model, chatGptLabel: _chatGptLabel, promptPrefix: _promptPrefix } = lastSelected;
if (modelsFilter[_model]) {
if (prevModelsFilter[_model]) {
model = _model;
chatGptLabel = _chatGptLabel;
promptPrefix = _promptPrefix;
@ -65,9 +99,9 @@ const useConversation = () => {
} catch (error) {}
// if anything happens, reset to default model
if (modelsFilter?.chatgpt) model = 'chatgpt';
else if (modelsFilter?.bingai) model = 'bingai';
else if (modelsFilter?.chatgptBrowser) model = 'chatgptBrowser';
if (prevModelsFilter?.chatgpt) model = 'chatgpt';
else if (prevModelsFilter?.bingai) model = 'bingai';
else if (prevModelsFilter?.chatgptBrowser) model = 'chatgptBrowser';
chatGptLabel = null;
promptPrefix = null;
};
@ -77,24 +111,36 @@ const useConversation = () => {
getDefaultModel();
setConversation({
conversationId: 'new',
title: 'New Chat',
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
...conversation,
model: model,
chatGptLabel: chatGptLabel,
promptPrefix: promptPrefix,
user: null,
suggestions: [],
toneStyle: null
promptPrefix: promptPrefix
});
setMessages([]);
setMessages(messages);
resetLatestMessage();
};
return { newConversation };
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
switchToConversation(
{
conversationId: 'new',
title: 'New Chat',
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
model: model,
chatGptLabel: chatGptLabel,
promptPrefix: promptPrefix,
user: null,
suggestions: [],
toneStyle: null
},
[]
);
};
return { newConversation, switchToConversation };
};
export default {

View file

@ -4,6 +4,8 @@ const odd =
'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] hover:bg-gray-100/40 hover:text-gray-700 dark:hover:bg-[#3b3d49] dark:hover:text-gray-200';
export default function buildTree(messages, groupAll = false) {
if (messages === null) return null;
let messageMap = {};
let rootMessages = [];

View file

@ -1,31 +1,50 @@
export default function createPayload({ convo, message }) {
const endpoint = `/api/ask`;
let payload = { ...message };
const { model } = message;
export default function createPayload(submission) {
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
if (!payload.conversationId)
if (convo?.conversationId && convo?.parentMessageId) {
payload = {
...payload,
conversationId: convo.conversationId,
parentMessageId: convo.parentMessageId || '00000000-0000-0000-0000-000000000000'
};
const endpoint = `/api/ask`;
const {
model,
chatGptLabel,
promptPrefix,
jailbreakConversationId,
conversationId,
conversationSignature,
clientId,
invocationId,
toneStyle
} = conversation;
let payload = {
...message,
...{
model,
chatGptLabel,
promptPrefix,
conversationId
}
};
// if (!payload.conversationId)
// if (convo?.conversationId && convo?.parentMessageId) {
// payload = {
// ...payload,
// conversationId: convo.conversationId,
// parentMessageId: convo.parentMessageId || '00000000-0000-0000-0000-000000000000'
// };
// }
const isBing = model === 'bingai' || model === 'sydney';
if (isBing && !convo?.conversationId) {
payload.toneStyle = convo.toneStyle || 'fast';
if (isBing && !conversationId) {
payload.toneStyle = toneStyle || 'fast';
}
if (isBing && convo?.conversationId) {
if (isBing && conversationId) {
payload = {
...payload,
jailbreakConversationId: convo.jailbreakConversationId,
conversationId: convo.conversationId,
conversationSignature: convo.conversationSignature,
clientId: convo.clientId,
invocationId: convo.invocationId,
toneStyle: convo.toneStyle,
jailbreakConversationId,
conversationSignature,
clientId,
invocationId
};
}