mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
refactor: basic message and send message. as well as model
THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
parent
de8f519742
commit
c7c30d8bb5
24 changed files with 1057 additions and 1035 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
205
client/src/components/Input/Models/ModelMenu.jsx
Normal file
205
client/src/components/Input/Models/ModelMenu.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
};
|
||||
454
client/src/components/Input/index.jsx
Normal file
454
client/src/components/Input/index.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue