|
@@ -16,7 +16,7 @@ import { ResizableTextarea } from './ResizableTextArea';
|
|
|
|
|
|
|
|
import styles from './AiChatModal.module.scss';
|
|
import styles from './AiChatModal.module.scss';
|
|
|
|
|
|
|
|
-const moduleClass = styles['rag-search-modal'];
|
|
|
|
|
|
|
+const moduleClass = styles['grw-aichat-modal'] ?? '';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:clinet:components:RagSearchModal');
|
|
const logger = loggerFactory('growi:clinet:components:RagSearchModal');
|
|
|
|
|
|
|
@@ -43,7 +43,9 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
|
|
|
|
|
const [threadId, setThreadId] = useState<string | undefined>();
|
|
const [threadId, setThreadId] = useState<string | undefined>();
|
|
|
const [messageLogs, setMessageLogs] = useState<Message[]>([]);
|
|
const [messageLogs, setMessageLogs] = useState<Message[]>([]);
|
|
|
- const [lastMessage, setLastMessage] = useState<Message>();
|
|
|
|
|
|
|
+ const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<Message>();
|
|
|
|
|
+
|
|
|
|
|
+ const isGenerating = generatingAnswerMessage != null;
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
// do nothing when the modal is closed or threadId is already set
|
|
// do nothing when the modal is closed or threadId is already set
|
|
@@ -68,12 +70,31 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
}, [threadId]);
|
|
}, [threadId]);
|
|
|
|
|
|
|
|
const submit = useCallback(async(data: FormData) => {
|
|
const submit = useCallback(async(data: FormData) => {
|
|
|
|
|
+ // do nothing when the assistant is generating an answer
|
|
|
|
|
+ if (isGenerating) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // do nothing when the input is empty
|
|
|
|
|
+ if (data.input.trim().length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const { length: logLength } = messageLogs;
|
|
const { length: logLength } = messageLogs;
|
|
|
|
|
|
|
|
|
|
+ // add user message to the logs
|
|
|
|
|
+ const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
|
|
|
|
|
+ setMessageLogs(msgs => [...msgs, newUserMessage]);
|
|
|
|
|
+
|
|
|
|
|
+ // reset form
|
|
|
|
|
+ form.reset();
|
|
|
|
|
+
|
|
|
|
|
+ // add an empty assistant message
|
|
|
|
|
+ const newAnswerMessage = { id: (logLength + 1).toString(), content: '' };
|
|
|
|
|
+ setGeneratingAnswerMessage(newAnswerMessage);
|
|
|
|
|
+
|
|
|
// post message
|
|
// post message
|
|
|
try {
|
|
try {
|
|
|
- form.clearErrors();
|
|
|
|
|
-
|
|
|
|
|
const response = await fetch('/_api/v3/openai/message', {
|
|
const response = await fetch('/_api/v3/openai/message', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -87,20 +108,10 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
const errors = resJson.errors.map(({ message }) => message).join(', ');
|
|
const errors = resJson.errors.map(({ message }) => message).join(', ');
|
|
|
form.setError('input', { type: 'manual', message: `[${response.status}] ${errors}` });
|
|
form.setError('input', { type: 'manual', message: `[${response.status}] ${errors}` });
|
|
|
}
|
|
}
|
|
|
|
|
+ setGeneratingAnswerMessage(undefined);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // add user message to the logs
|
|
|
|
|
- const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
|
|
|
|
|
- setMessageLogs(msgs => [...msgs, newUserMessage]);
|
|
|
|
|
-
|
|
|
|
|
- // reset form
|
|
|
|
|
- form.reset();
|
|
|
|
|
-
|
|
|
|
|
- // add assistant message
|
|
|
|
|
- const newAssistantMessage = { id: (logLength + 1).toString(), content: '' };
|
|
|
|
|
- setLastMessage(newAssistantMessage);
|
|
|
|
|
-
|
|
|
|
|
const reader = response.body?.getReader();
|
|
const reader = response.body?.getReader();
|
|
|
const decoder = new TextDecoder('utf-8');
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
|
|
|
@@ -111,9 +122,9 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
|
|
|
|
|
// add assistant message to the logs
|
|
// add assistant message to the logs
|
|
|
if (done) {
|
|
if (done) {
|
|
|
- setLastMessage((lastMessage) => {
|
|
|
|
|
- if (lastMessage == null) return;
|
|
|
|
|
- setMessageLogs(msgs => [...msgs, lastMessage]);
|
|
|
|
|
|
|
+ setGeneratingAnswerMessage((generatingAnswerMessage) => {
|
|
|
|
|
+ if (generatingAnswerMessage == null) return;
|
|
|
|
|
+ setMessageLogs(msgs => [...msgs, generatingAnswerMessage]);
|
|
|
return undefined;
|
|
return undefined;
|
|
|
});
|
|
});
|
|
|
return;
|
|
return;
|
|
@@ -131,7 +142,7 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// append text values to the assistant message
|
|
// append text values to the assistant message
|
|
|
- setLastMessage((prevMessage) => {
|
|
|
|
|
|
|
+ setGeneratingAnswerMessage((prevMessage) => {
|
|
|
if (prevMessage == null) return;
|
|
if (prevMessage == null) return;
|
|
|
return {
|
|
return {
|
|
|
...prevMessage,
|
|
...prevMessage,
|
|
@@ -148,7 +159,7 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
form.setError('input', { type: 'manual', message: err.toString() });
|
|
form.setError('input', { type: 'manual', message: err.toString() });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- }, [form, messageLogs, threadId]);
|
|
|
|
|
|
|
+ }, [form, isGenerating, messageLogs, threadId]);
|
|
|
|
|
|
|
|
const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
@@ -163,8 +174,8 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
{ messageLogs.map(message => (
|
|
{ messageLogs.map(message => (
|
|
|
<MessageCard key={message.id} role={message.isUserMessage ? 'user' : 'assistant'}>{message.content}</MessageCard>
|
|
<MessageCard key={message.id} role={message.isUserMessage ? 'user' : 'assistant'}>{message.content}</MessageCard>
|
|
|
)) }
|
|
)) }
|
|
|
- { lastMessage != null && (
|
|
|
|
|
- <MessageCard role="assistant">{lastMessage.content}</MessageCard>
|
|
|
|
|
|
|
+ { generatingAnswerMessage != null && (
|
|
|
|
|
+ <MessageCard role="assistant">{generatingAnswerMessage.content}</MessageCard>
|
|
|
)}
|
|
)}
|
|
|
{ messageLogs.length > 0 && (
|
|
{ messageLogs.length > 0 && (
|
|
|
<div className="d-flex justify-content-center">
|
|
<div className="d-flex justify-content-center">
|
|
@@ -176,7 +187,7 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
</div>
|
|
</div>
|
|
|
</ModalBody>
|
|
</ModalBody>
|
|
|
|
|
|
|
|
- <ModalFooter className="pt-0 pb-3 pb-lg-4 px-3 px-lg-4">
|
|
|
|
|
|
|
+ <ModalFooter className="flex-column align-items-start pt-0 pb-3 pb-lg-4 px-3 px-lg-4">
|
|
|
<form onSubmit={form.handleSubmit(submit)} className="flex-fill hstack gap-2 align-items-end m-0">
|
|
<form onSubmit={form.handleSubmit(submit)} className="flex-fill hstack gap-2 align-items-end m-0">
|
|
|
<Controller
|
|
<Controller
|
|
|
name="input"
|
|
name="input"
|
|
@@ -188,15 +199,16 @@ const AiChatModalSubstance = (): JSX.Element => {
|
|
|
className="form-control textarea-ask"
|
|
className="form-control textarea-ask"
|
|
|
style={{ resize: 'none' }}
|
|
style={{ resize: 'none' }}
|
|
|
rows={1}
|
|
rows={1}
|
|
|
- placeholder={t('modal_aichat.placeholder')}
|
|
|
|
|
|
|
+ placeholder={!form.formState.isSubmitting ? t('modal_aichat.placeholder') : ''}
|
|
|
onKeyDown={keyDownHandler}
|
|
onKeyDown={keyDownHandler}
|
|
|
|
|
+ disabled={form.formState.isSubmitting}
|
|
|
/>
|
|
/>
|
|
|
)}
|
|
)}
|
|
|
/>
|
|
/>
|
|
|
<button
|
|
<button
|
|
|
type="submit"
|
|
type="submit"
|
|
|
className="btn btn-submit no-border"
|
|
className="btn btn-submit no-border"
|
|
|
- disabled={form.formState.isSubmitting}
|
|
|
|
|
|
|
+ disabled={form.formState.isSubmitting || isGenerating}
|
|
|
>
|
|
>
|
|
|
<span className="material-symbols-outlined">send</span>
|
|
<span className="material-symbols-outlined">send</span>
|
|
|
</button>
|
|
</button>
|