|
@@ -1,14 +1,20 @@
|
|
|
-import React, { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
|
|
|
|
|
|
|
import { apiv3Post } from '~/client/util/apiv3-client';
|
|
import { apiv3Post } from '~/client/util/apiv3-client';
|
|
|
import { useRagSearchModal } from '~/stores/rag-search';
|
|
import { useRagSearchModal } from '~/stores/rag-search';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
|
|
|
|
+import { AutoResizeTextarea } from './AutoResizeTextarea';
|
|
|
import { MessageCard } from './MessageCard';
|
|
import { MessageCard } from './MessageCard';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+import styles from './RagSearchModal.module.scss';
|
|
|
|
|
+
|
|
|
|
|
+const moduleClass = styles['rag-search-modal'];
|
|
|
|
|
+
|
|
|
const logger = loggerFactory('growi:clinet:components:RagSearchModal');
|
|
const logger = loggerFactory('growi:clinet:components:RagSearchModal');
|
|
|
|
|
|
|
|
|
|
|
|
@@ -18,12 +24,21 @@ type Message = {
|
|
|
isUserMessage?: boolean,
|
|
isUserMessage?: boolean,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type FormData = {
|
|
|
|
|
+ input: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const RagSearchModal = (): JSX.Element => {
|
|
const RagSearchModal = (): JSX.Element => {
|
|
|
|
|
|
|
|
- const [input, setInput] = useState('');
|
|
|
|
|
|
|
+ const form = useForm<FormData>({
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ input: '',
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
const [threadId, setThreadId] = useState<string | undefined>();
|
|
const [threadId, setThreadId] = useState<string | undefined>();
|
|
|
- const [messages, setMessages] = useState<Message[]>([]);
|
|
|
|
|
|
|
+ const [messageLogs, setMessageLogs] = useState<Message[]>([]);
|
|
|
|
|
+ const [lastMessage, setLastMessage] = useState<Message>();
|
|
|
|
|
|
|
|
const { data: ragSearchModalData, close: closeRagSearchModal } = useRagSearchModal();
|
|
const { data: ragSearchModalData, close: closeRagSearchModal } = useRagSearchModal();
|
|
|
|
|
|
|
@@ -56,28 +71,52 @@ const RagSearchModal = (): JSX.Element => {
|
|
|
createThread();
|
|
createThread();
|
|
|
}, [isOpened, threadId]);
|
|
}, [isOpened, threadId]);
|
|
|
|
|
|
|
|
- const onClickSubmitUserMessageHandler = useCallback(async() => {
|
|
|
|
|
- const newUserMessage = { id: messages.length.toString(), content: input, isUserMessage: true };
|
|
|
|
|
- setMessages(msgs => [...msgs, newUserMessage]);
|
|
|
|
|
-
|
|
|
|
|
- setInput('');
|
|
|
|
|
|
|
+ const clickSubmitUserMessageHandler = form.handleSubmit(async(data) => {
|
|
|
|
|
+ const { length: logLength } = messageLogs;
|
|
|
|
|
|
|
|
// post message
|
|
// post message
|
|
|
try {
|
|
try {
|
|
|
- const res = await fetch('/_api/v3/openai/message', {
|
|
|
|
|
|
|
+ form.clearErrors();
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch('/_api/v3/openai/message', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ userMessage: input, threadId }),
|
|
|
|
|
|
|
+ body: JSON.stringify({ userMessage: data.input, threadId }),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const reader = res.body?.getReader();
|
|
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const resJson = await response.json();
|
|
|
|
|
+ if ('errors' in resJson) {
|
|
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
+ const errors = resJson.errors.map(({ message }) => message).join(', ');
|
|
|
|
|
+ form.setError('input', { type: 'manual', message: `[${response.status}] ${errors}` });
|
|
|
|
|
+ }
|
|
|
|
|
+ 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 decoder = new TextDecoder('utf-8');
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
|
|
|
|
const read = async() => {
|
|
const read = async() => {
|
|
|
if (reader == null) return;
|
|
if (reader == null) return;
|
|
|
|
|
|
|
|
const { done, value } = await reader.read();
|
|
const { done, value } = await reader.read();
|
|
|
|
|
+
|
|
|
|
|
+ // add assistant message to the logs
|
|
|
if (done) {
|
|
if (done) {
|
|
|
|
|
+ setMessageLogs(msgs => [...msgs, newAssistantMessage]);
|
|
|
|
|
+ setLastMessage(undefined);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -92,64 +131,65 @@ const RagSearchModal = (): JSX.Element => {
|
|
|
return data.content[0].text.value;
|
|
return data.content[0].text.value;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- console.log(textValues.join(''));
|
|
|
|
|
|
|
+ // append text values to the assistant message
|
|
|
|
|
+ newAssistantMessage.content += textValues.join('');
|
|
|
|
|
+ setLastMessage(newAssistantMessage);
|
|
|
|
|
|
|
|
read();
|
|
read();
|
|
|
};
|
|
};
|
|
|
read();
|
|
read();
|
|
|
-
|
|
|
|
|
- // const res = await apiv3Post('/openai/message', { userMessage: input, threadId });
|
|
|
|
|
-
|
|
|
|
|
- // if (res.data) {
|
|
|
|
|
- // const newMessages: Message[] = assistantMessageData.data.reverse()
|
|
|
|
|
- // .map((message: any) => {
|
|
|
|
|
- // return {
|
|
|
|
|
- // id: message.id,
|
|
|
|
|
- // content: message.content[0].text.value,
|
|
|
|
|
- // };
|
|
|
|
|
- // });
|
|
|
|
|
-
|
|
|
|
|
- // setMessages(msgs => [...msgs, ...newMessages]);
|
|
|
|
|
- // setThreadId(assistantMessageData.data[0].threadId);
|
|
|
|
|
- // }
|
|
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err.toString());
|
|
logger.error(err.toString());
|
|
|
|
|
+ form.setError('input', { type: 'manual', message: err.toString() });
|
|
|
}
|
|
}
|
|
|
- }, [input, messages.length, threadId]);
|
|
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <Modal size="lg" isOpen={isOpened} toggle={closeRagSearchModal} data-testid="search-modal">
|
|
|
|
|
- <ModalBody>
|
|
|
|
|
- <ModalHeader tag="h4" className="mb-3 p-0">
|
|
|
|
|
- <span className="material-symbols-outlined me-2 text-primary">psychology</span>
|
|
|
|
|
- GROWI Assistant
|
|
|
|
|
- </ModalHeader>
|
|
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Modal size="lg" isOpen={isOpened} toggle={closeRagSearchModal} className={moduleClass}>
|
|
|
|
|
+ <ModalHeader tag="h4" toggle={closeRagSearchModal} className="pe-4">
|
|
|
|
|
+ <span className="material-symbols-outlined text-primary">psychology</span>
|
|
|
|
|
+ GROWI Assistant
|
|
|
|
|
+ </ModalHeader>
|
|
|
|
|
+ <ModalBody className="px-lg-5 py-4">
|
|
|
<div className="vstack gap-4">
|
|
<div className="vstack gap-4">
|
|
|
- { messages.map(message => (
|
|
|
|
|
|
|
+ { messageLogs.map(message => (
|
|
|
<MessageCard key={message.id} right={message.isUserMessage}>{message.content}</MessageCard>
|
|
<MessageCard key={message.id} right={message.isUserMessage}>{message.content}</MessageCard>
|
|
|
)) }
|
|
)) }
|
|
|
|
|
+ { lastMessage != null && (
|
|
|
|
|
+ <MessageCard>{lastMessage.content}</MessageCard>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div className="input-group mt-5">
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- className="form-control"
|
|
|
|
|
- placeholder="お手伝いできることはありますか?"
|
|
|
|
|
- aria-label="Recipient's username"
|
|
|
|
|
- aria-describedby="button-addon2"
|
|
|
|
|
- value={input}
|
|
|
|
|
- onChange={e => setInput(e.target.value)}
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- id="button-addon2"
|
|
|
|
|
- className="btn btn-outline-secondary"
|
|
|
|
|
- onClick={onClickSubmitUserMessageHandler}
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined">arrow_upward</span>
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <form onSubmit={clickSubmitUserMessageHandler} className="hstack gap-2 align-items-end mt-4">
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ {...form.register('input')}
|
|
|
|
|
+ required
|
|
|
|
|
+ className="form-control textarea-ask"
|
|
|
|
|
+ style={{ resize: 'none' }}
|
|
|
|
|
+ rows={1}
|
|
|
|
|
+ // auto resize
|
|
|
|
|
+ // refs: https://zenn.dev/soma3134/articles/1e2fb0eab75b2d
|
|
|
|
|
+ onChange={(e) => {
|
|
|
|
|
+ e.target.style.height = 'auto';
|
|
|
|
|
+ e.target.style.height = `${e.target.scrollHeight + 4}px`;
|
|
|
|
|
+ }}
|
|
|
|
|
+ placeholder="ききたいことを入力してください"
|
|
|
|
|
+ />
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="btn btn-submit no-border"
|
|
|
|
|
+ disabled={form.formState.isSubmitting}
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined">send</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+
|
|
|
|
|
+ {form.formState.errors.input != null && (
|
|
|
|
|
+ <span className="text-danger small">{form.formState.errors.input?.message}</span>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</ModalBody>
|
|
</ModalBody>
|
|
|
</Modal>
|
|
</Modal>
|