mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 21:43:59 +01:00
feat: regenerate response button
This commit is contained in:
parent
7f39bbf5b0
commit
710f4c3f65
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { createRef, useState } from 'react';
|
||||||
import ScrollToBottom from 'react-scroll-to-bottom';
|
import ScrollToBottom from 'react-scroll-to-bottom';
|
||||||
import useStore from '@store/store';
|
import useStore from '@store/store';
|
||||||
|
|
||||||
|
@ -13,83 +13,14 @@ import { parseEventSource } from '@api/helper';
|
||||||
|
|
||||||
import RefreshIcon from '@icon/RefreshIcon';
|
import RefreshIcon from '@icon/RefreshIcon';
|
||||||
import { MessageInterface } from '@type/chat';
|
import { MessageInterface } from '@type/chat';
|
||||||
|
import useSubmit from '@hooks/useSubmit';
|
||||||
|
|
||||||
const ChatContent = () => {
|
const ChatContent = () => {
|
||||||
const [
|
const [messages, inputRole] = useStore((state) => [
|
||||||
messages,
|
|
||||||
inputRole,
|
|
||||||
apiFree,
|
|
||||||
apiKey,
|
|
||||||
setMessages,
|
|
||||||
setGenerating,
|
|
||||||
generating,
|
|
||||||
] = useStore((state) => [
|
|
||||||
state.messages,
|
state.messages,
|
||||||
state.inputRole,
|
state.inputRole,
|
||||||
state.apiFree,
|
|
||||||
state.apiKey,
|
|
||||||
state.setMessages,
|
|
||||||
state.setGenerating,
|
|
||||||
state.generating,
|
|
||||||
]);
|
]);
|
||||||
const [error, setError] = useState<string>('');
|
const { handleSubmit, error } = useSubmit();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (generating) return;
|
|
||||||
|
|
||||||
const updatedMessages: MessageInterface[] = JSON.parse(
|
|
||||||
JSON.stringify(messages)
|
|
||||||
);
|
|
||||||
updatedMessages.push({ role: 'assistant', content: '' });
|
|
||||||
setMessages(updatedMessages);
|
|
||||||
setGenerating(true);
|
|
||||||
let stream;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (apiFree) {
|
|
||||||
stream = await getChatCompletionStreamFree(messages);
|
|
||||||
} else if (apiKey) {
|
|
||||||
stream = await getChatCompletionStreamCustom(apiKey, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stream) {
|
|
||||||
const reader = stream.getReader();
|
|
||||||
let reading = true;
|
|
||||||
while (reading) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
const result = parseEventSource(new TextDecoder().decode(value));
|
|
||||||
|
|
||||||
if (result === '[DONE]' || done) {
|
|
||||||
reading = false;
|
|
||||||
} else {
|
|
||||||
const resultString = result.reduce((output: string, curr) => {
|
|
||||||
if (typeof curr === 'string') return output;
|
|
||||||
else {
|
|
||||||
const content = curr.choices[0].delta.content;
|
|
||||||
if (content) output += content;
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
const updatedMessages: MessageInterface[] = JSON.parse(
|
|
||||||
JSON.stringify(useStore.getState().messages)
|
|
||||||
);
|
|
||||||
updatedMessages[updatedMessages.length - 1].content += resultString;
|
|
||||||
setMessages(updatedMessages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const err = (e as Error).message;
|
|
||||||
console.log(err);
|
|
||||||
setError(err);
|
|
||||||
setTimeout(() => {
|
|
||||||
setError(''), 10000;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setGenerating(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex-1 overflow-hidden'>
|
<div className='flex-1 overflow-hidden'>
|
||||||
|
@ -122,7 +53,9 @@ const ChatContent = () => {
|
||||||
<div className='text-center mt-6 flex justify-center gap-2'>
|
<div className='text-center mt-6 flex justify-center gap-2'>
|
||||||
<button
|
<button
|
||||||
className='btn relative btn-primary mt-2'
|
className='btn relative btn-primary mt-2'
|
||||||
onClick={handleSubmit}
|
onClick={() => {
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -41,6 +41,7 @@ const Message = ({
|
||||||
sticky={sticky}
|
sticky={sticky}
|
||||||
/>
|
/>
|
||||||
<MessageContent
|
<MessageContent
|
||||||
|
role={role}
|
||||||
content={content}
|
content={content}
|
||||||
messageIndex={messageIndex}
|
messageIndex={messageIndex}
|
||||||
sticky={sticky}
|
sticky={sticky}
|
||||||
|
|
|
@ -9,15 +9,20 @@ import EditIcon2 from '@icon/EditIcon2';
|
||||||
import DeleteIcon from '@icon/DeleteIcon';
|
import DeleteIcon from '@icon/DeleteIcon';
|
||||||
import TickIcon from '@icon/TickIcon';
|
import TickIcon from '@icon/TickIcon';
|
||||||
import CrossIcon from '@icon/CrossIcon';
|
import CrossIcon from '@icon/CrossIcon';
|
||||||
|
import RefreshIcon from '@icon/RefreshIcon';
|
||||||
import DownChevronArrow from '@icon/DownChevronArrow';
|
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||||
|
|
||||||
import { MessageInterface } from '@type/chat';
|
import { MessageInterface } from '@type/chat';
|
||||||
|
|
||||||
|
import useSubmit from '@hooks/useSubmit';
|
||||||
|
|
||||||
const MessageContent = ({
|
const MessageContent = ({
|
||||||
|
role,
|
||||||
content,
|
content,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
sticky = false,
|
sticky = false,
|
||||||
}: {
|
}: {
|
||||||
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
|
@ -36,6 +41,7 @@ const MessageContent = ({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ContentView
|
<ContentView
|
||||||
|
role={role}
|
||||||
content={content}
|
content={content}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
messageIndex={messageIndex}
|
messageIndex={messageIndex}
|
||||||
|
@ -46,14 +52,18 @@ const MessageContent = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContentView = ({
|
const ContentView = ({
|
||||||
|
role,
|
||||||
content,
|
content,
|
||||||
setIsEdit,
|
setIsEdit,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
}: {
|
}: {
|
||||||
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { handleSubmit, error } = useSubmit();
|
||||||
|
|
||||||
const [isDelete, setIsDelete] = useState<boolean>(false);
|
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -150,12 +160,20 @@ const ContentView = ({
|
||||||
<div className='flex justify-end gap-2 w-full mt-2'>
|
<div className='flex justify-end gap-2 w-full mt-2'>
|
||||||
{isDelete || (
|
{isDelete || (
|
||||||
<>
|
<>
|
||||||
|
{role === 'assistant' && messageIndex === messages?.length - 1 && (
|
||||||
|
<RefreshButton
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{messageIndex !== 0 && (
|
{messageIndex !== 0 && (
|
||||||
<UpButton onClick={() => handleMove('up')} />
|
<UpButton onClick={() => handleMove('up')} />
|
||||||
)}
|
)}
|
||||||
{messageIndex !== messages?.length - 1 && (
|
{messageIndex !== messages?.length - 1 && (
|
||||||
<DownButton onClick={() => handleMove('down')} />
|
<DownButton onClick={() => handleMove('down')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EditButton setIsEdit={setIsEdit} />
|
<EditButton setIsEdit={setIsEdit} />
|
||||||
<DeleteButton setIsDelete={setIsDelete} />
|
<DeleteButton setIsDelete={setIsDelete} />
|
||||||
</>
|
</>
|
||||||
|
@ -236,6 +254,14 @@ const UpButton = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RefreshButton = ({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}) => {
|
||||||
|
return <MessageButton icon={<RefreshIcon />} onClick={onClick} />;
|
||||||
|
};
|
||||||
|
|
||||||
const EditView = ({
|
const EditView = ({
|
||||||
content,
|
content,
|
||||||
setIsEdit,
|
setIsEdit,
|
||||||
|
|
96
src/hooks/useSubmit.ts
Normal file
96
src/hooks/useSubmit.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
import { MessageInterface } from '@type/chat';
|
||||||
|
import { getChatCompletionStream as getChatCompletionStreamFree } from '@api/freeApi';
|
||||||
|
import { getChatCompletionStream as getChatCompletionStreamCustom } from '@api/customApi';
|
||||||
|
import { parseEventSource } from '@api/helper';
|
||||||
|
|
||||||
|
const useSubmit = () => {
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [messages, apiFree, apiKey, setMessages, setGenerating, generating] =
|
||||||
|
useStore((state) => [
|
||||||
|
state.messages,
|
||||||
|
state.apiFree,
|
||||||
|
state.apiKey,
|
||||||
|
state.setMessages,
|
||||||
|
state.setGenerating,
|
||||||
|
state.generating,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleSubmit = async (refresh?: boolean) => {
|
||||||
|
if (generating) return;
|
||||||
|
|
||||||
|
const updatedMessages: MessageInterface[] = JSON.parse(
|
||||||
|
JSON.stringify(messages)
|
||||||
|
);
|
||||||
|
if (refresh) {
|
||||||
|
updatedMessages[updatedMessages.length - 1] = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updatedMessages.push({ role: 'assistant', content: '' });
|
||||||
|
}
|
||||||
|
setMessages(updatedMessages);
|
||||||
|
setGenerating(true);
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (apiFree) {
|
||||||
|
if (refresh)
|
||||||
|
stream = await getChatCompletionStreamFree(
|
||||||
|
updatedMessages.slice(0, updatedMessages.length - 1)
|
||||||
|
);
|
||||||
|
else stream = await getChatCompletionStreamFree(messages);
|
||||||
|
} else if (apiKey) {
|
||||||
|
if (refresh)
|
||||||
|
stream = await getChatCompletionStreamCustom(
|
||||||
|
apiKey,
|
||||||
|
updatedMessages.slice(0, updatedMessages.length - 1)
|
||||||
|
);
|
||||||
|
else stream = await getChatCompletionStreamFree(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let reading = true;
|
||||||
|
while (reading) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
const result = parseEventSource(new TextDecoder().decode(value));
|
||||||
|
|
||||||
|
if (result === '[DONE]' || done) {
|
||||||
|
reading = false;
|
||||||
|
} else {
|
||||||
|
const resultString = result.reduce((output: string, curr) => {
|
||||||
|
if (typeof curr === 'string') return output;
|
||||||
|
else {
|
||||||
|
const content = curr.choices[0].delta.content;
|
||||||
|
if (content) output += content;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
const updatedMessages: MessageInterface[] = JSON.parse(
|
||||||
|
JSON.stringify(useStore.getState().messages)
|
||||||
|
);
|
||||||
|
updatedMessages[updatedMessages.length - 1].content += resultString;
|
||||||
|
setMessages(updatedMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const err = (e as Error).message;
|
||||||
|
console.log(err);
|
||||||
|
setError(err);
|
||||||
|
setTimeout(() => {
|
||||||
|
setError(''), 10000;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setGenerating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleSubmit, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubmit;
|
Loading…
Reference in a new issue