|
|
@@ -1,40 +1,51 @@
|
|
|
-import type { KeyboardEvent, JSX } from 'react';
|
|
|
+import type { JSX, KeyboardEvent } from 'react';
|
|
|
import {
|
|
|
- type FC, memo, useEffect, useState, useCallback, useMemo,
|
|
|
+ type FC,
|
|
|
+ memo,
|
|
|
+ useCallback,
|
|
|
+ useEffect,
|
|
|
+ useMemo,
|
|
|
+ useState,
|
|
|
} from 'react';
|
|
|
-
|
|
|
import { Controller } from 'react-hook-form';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { Collapse } from 'reactstrap';
|
|
|
import SimpleBar from 'simplebar-react';
|
|
|
|
|
|
import { toastError } from '~/client/util/toastr';
|
|
|
-import { useGrowiCloudUri, useIsEnableUnifiedMergeView } from '~/stores-universal/context';
|
|
|
+import {
|
|
|
+ useGrowiCloudUri,
|
|
|
+ useIsEnableUnifiedMergeView,
|
|
|
+} from '~/stores-universal/context';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
import type { AiAssistantHasId } from '../../../../interfaces/ai-assistant';
|
|
|
import type { MessageLog } from '../../../../interfaces/message';
|
|
|
-import { MessageErrorCode, StreamErrorCode } from '../../../../interfaces/message-error';
|
|
|
+import {
|
|
|
+ MessageErrorCode,
|
|
|
+ StreamErrorCode,
|
|
|
+} from '../../../../interfaces/message-error';
|
|
|
import type { IThreadRelationHasId } from '../../../../interfaces/thread-relation';
|
|
|
import {
|
|
|
- useEditorAssistant,
|
|
|
- isEditorAssistantFormData,
|
|
|
type FormData as FormDataForEditorAssistant,
|
|
|
+ isEditorAssistantFormData,
|
|
|
+ useEditorAssistant,
|
|
|
} from '../../../services/editor-assistant';
|
|
|
import {
|
|
|
- useKnowledgeAssistant,
|
|
|
- useFetchAndSetMessageDataEffect,
|
|
|
type FormData as FormDataForKnowledgeAssistant,
|
|
|
+ useFetchAndSetMessageDataEffect,
|
|
|
+ useKnowledgeAssistant,
|
|
|
} from '../../../services/knowledge-assistant';
|
|
|
import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
|
|
|
import { useSWRxThreads } from '../../../stores/thread';
|
|
|
-
|
|
|
import { MessageCard } from './MessageCard/MessageCard';
|
|
|
import { ResizableTextarea } from './ResizableTextArea';
|
|
|
|
|
|
import styles from './AiAssistantSidebar.module.scss';
|
|
|
|
|
|
-const logger = loggerFactory('growi:openai:client:components:AiAssistantSidebar');
|
|
|
+const logger = loggerFactory(
|
|
|
+ 'growi:openai:client:components:AiAssistantSidebar',
|
|
|
+);
|
|
|
|
|
|
const moduleClass = styles['grw-ai-assistant-sidebar'] ?? '';
|
|
|
|
|
|
@@ -47,9 +58,11 @@ type AiAssistantSidebarSubstanceProps = {
|
|
|
onCloseButtonClicked?: () => void;
|
|
|
onNewThreadCreated?: (thread: IThreadRelationHasId) => void;
|
|
|
onMessageReceived?: () => void;
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
-const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> = (props: AiAssistantSidebarSubstanceProps) => {
|
|
|
+const AiAssistantSidebarSubstance: React.FC<
|
|
|
+ AiAssistantSidebarSubstanceProps
|
|
|
+> = (props: AiAssistantSidebarSubstanceProps) => {
|
|
|
const {
|
|
|
isEditorAssistant,
|
|
|
aiAssistantData,
|
|
|
@@ -61,9 +74,11 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
|
|
|
// States
|
|
|
const [messageLogs, setMessageLogs] = useState<MessageLog[]>([]);
|
|
|
- const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<MessageLog>();
|
|
|
+ const [generatingAnswerMessage, setGeneratingAnswerMessage] =
|
|
|
+ useState<MessageLog>();
|
|
|
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
|
|
- const [isErrorDetailCollapsed, setIsErrorDetailCollapsed] = useState<boolean>(false);
|
|
|
+ const [isErrorDetailCollapsed, setIsErrorDetailCollapsed] =
|
|
|
+ useState<boolean>(false);
|
|
|
|
|
|
// Hooks
|
|
|
const { t } = useTranslation();
|
|
|
@@ -78,7 +93,8 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
|
|
|
// Views
|
|
|
initialView: initialViewForKnowledgeAssistant,
|
|
|
- generateModeSwitchesDropdown: generateModeSwitchesDropdownForKnowledgeAssistant,
|
|
|
+ generateModeSwitchesDropdown:
|
|
|
+ generateModeSwitchesDropdownForKnowledgeAssistant,
|
|
|
headerIcon: headerIconForKnowledgeAssistant,
|
|
|
headerText: headerTextForKnowledgeAssistant,
|
|
|
placeHolder: placeHolderForKnowledgeAssistant,
|
|
|
@@ -103,7 +119,9 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
placeHolder: placeHolderForEditorAssistant,
|
|
|
} = useEditorAssistant();
|
|
|
|
|
|
- const form = isEditorAssistant ? formForEditorAssistant : formForKnowledgeAssistant;
|
|
|
+ const form = isEditorAssistant
|
|
|
+ ? formForEditorAssistant
|
|
|
+ : formForKnowledgeAssistant;
|
|
|
|
|
|
// Effects
|
|
|
useFetchAndSetMessageDataEffect(setMessageLogs, threadData?.threadId);
|
|
|
@@ -115,239 +133,304 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
}
|
|
|
|
|
|
resetFormForKnowledgeAssistant();
|
|
|
- }, [isEditorAssistant, resetFormEditorAssistant, resetFormForKnowledgeAssistant]);
|
|
|
+ }, [
|
|
|
+ isEditorAssistant,
|
|
|
+ resetFormEditorAssistant,
|
|
|
+ resetFormForKnowledgeAssistant,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const createThread = useCallback(
|
|
|
+ async (initialUserMessage: string) => {
|
|
|
+ if (isEditorAssistant) {
|
|
|
+ const thread = await createThreadForEditorAssistant();
|
|
|
+ return thread;
|
|
|
+ }
|
|
|
|
|
|
- const createThread = useCallback(async(initialUserMessage: string) => {
|
|
|
- if (isEditorAssistant) {
|
|
|
- const thread = await createThreadForEditorAssistant();
|
|
|
+ if (aiAssistantData == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const thread = await createThreadForKnowledgeAssistant(
|
|
|
+ aiAssistantData._id,
|
|
|
+ initialUserMessage,
|
|
|
+ );
|
|
|
return thread;
|
|
|
- }
|
|
|
-
|
|
|
- if (aiAssistantData == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const thread = await createThreadForKnowledgeAssistant(aiAssistantData._id, initialUserMessage);
|
|
|
- return thread;
|
|
|
- }, [aiAssistantData, createThreadForEditorAssistant, createThreadForKnowledgeAssistant, isEditorAssistant]);
|
|
|
+ },
|
|
|
+ [
|
|
|
+ aiAssistantData,
|
|
|
+ createThreadForEditorAssistant,
|
|
|
+ createThreadForKnowledgeAssistant,
|
|
|
+ isEditorAssistant,
|
|
|
+ ],
|
|
|
+ );
|
|
|
|
|
|
- const postMessage = useCallback(async(threadId: string, formData: FormData) => {
|
|
|
- if (threadId == null) {
|
|
|
- throw new Error('threadId is not set');
|
|
|
- }
|
|
|
+ const postMessage = useCallback(
|
|
|
+ async (threadId: string, formData: FormData) => {
|
|
|
+ if (threadId == null) {
|
|
|
+ throw new Error('threadId is not set');
|
|
|
+ }
|
|
|
|
|
|
- if (isEditorAssistant) {
|
|
|
- if (isEditorAssistantFormData(formData)) {
|
|
|
- const response = await postMessageForEditorAssistant({
|
|
|
+ if (isEditorAssistant) {
|
|
|
+ if (isEditorAssistantFormData(formData)) {
|
|
|
+ const response = await postMessageForEditorAssistant({
|
|
|
+ threadId,
|
|
|
+ formData,
|
|
|
+ });
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (aiAssistantData?._id != null) {
|
|
|
+ const response = await postMessageForKnowledgeAssistant({
|
|
|
+ aiAssistantId: aiAssistantData._id,
|
|
|
threadId,
|
|
|
formData,
|
|
|
});
|
|
|
return response;
|
|
|
}
|
|
|
- return;
|
|
|
- }
|
|
|
- if (aiAssistantData?._id != null) {
|
|
|
- const response = await postMessageForKnowledgeAssistant({
|
|
|
- aiAssistantId: aiAssistantData._id,
|
|
|
- threadId,
|
|
|
- formData,
|
|
|
- });
|
|
|
- return response;
|
|
|
- }
|
|
|
- }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
|
|
|
+ },
|
|
|
+ [
|
|
|
+ aiAssistantData?._id,
|
|
|
+ isEditorAssistant,
|
|
|
+ postMessageForEditorAssistant,
|
|
|
+ postMessageForKnowledgeAssistant,
|
|
|
+ ],
|
|
|
+ );
|
|
|
|
|
|
const isGenerating = generatingAnswerMessage != null;
|
|
|
- const submitSubstance = 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;
|
|
|
-
|
|
|
- // add user message to the logs
|
|
|
- const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
|
|
|
- setMessageLogs(msgs => [...msgs, newUserMessage]);
|
|
|
+ const submitSubstance = useCallback(
|
|
|
+ async (data: FormData) => {
|
|
|
+ // do nothing when the assistant is generating an answer
|
|
|
+ if (isGenerating) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- resetForm();
|
|
|
+ // do nothing when the input is empty
|
|
|
+ if (data.input.trim().length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- setErrorMessage(undefined);
|
|
|
+ const { length: logLength } = messageLogs;
|
|
|
|
|
|
- // add an empty assistant message
|
|
|
- const newAnswerMessage = { id: (logLength + 1).toString(), content: '' };
|
|
|
- setGeneratingAnswerMessage(newAnswerMessage);
|
|
|
+ // add user message to the logs
|
|
|
+ const newUserMessage = {
|
|
|
+ id: logLength.toString(),
|
|
|
+ content: data.input,
|
|
|
+ isUserMessage: true,
|
|
|
+ };
|
|
|
+ setMessageLogs((msgs) => [...msgs, newUserMessage]);
|
|
|
|
|
|
- // create thread
|
|
|
- let threadId = threadData?.threadId;
|
|
|
- if (threadId == null) {
|
|
|
- try {
|
|
|
- const newThread = await createThread(newUserMessage.content);
|
|
|
- if (newThread == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ resetForm();
|
|
|
|
|
|
- threadId = newThread.threadId;
|
|
|
+ setErrorMessage(undefined);
|
|
|
|
|
|
- onNewThreadCreated?.(newThread);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err.toString());
|
|
|
- toastError(t('sidebar_ai_assistant.failed_to_create_or_retrieve_thread'));
|
|
|
- }
|
|
|
- }
|
|
|
+ // add an empty assistant message
|
|
|
+ const newAnswerMessage = { id: (logLength + 1).toString(), content: '' };
|
|
|
+ setGeneratingAnswerMessage(newAnswerMessage);
|
|
|
|
|
|
- // post message
|
|
|
- try {
|
|
|
+ // create thread
|
|
|
+ let threadId = threadData?.threadId;
|
|
|
if (threadId == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const response = await postMessage(threadId, data);
|
|
|
- if (response == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ try {
|
|
|
+ const newThread = await createThread(newUserMessage.content);
|
|
|
+ if (newThread == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- 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}` });
|
|
|
+ threadId = newThread.threadId;
|
|
|
|
|
|
- const hasThreadIdNotSetError = resJson.errors.some(err => err.code === MessageErrorCode.THREAD_ID_IS_NOT_SET);
|
|
|
- if (hasThreadIdNotSetError) {
|
|
|
- toastError(t('sidebar_ai_assistant.failed_to_create_or_retrieve_thread'));
|
|
|
- }
|
|
|
+ onNewThreadCreated?.(newThread);
|
|
|
+ } catch (err) {
|
|
|
+ logger.error(err.toString());
|
|
|
+ toastError(
|
|
|
+ t('sidebar_ai_assistant.failed_to_create_or_retrieve_thread'),
|
|
|
+ );
|
|
|
}
|
|
|
- setGeneratingAnswerMessage(undefined);
|
|
|
- return;
|
|
|
}
|
|
|
|
|
|
- const reader = response.body?.getReader();
|
|
|
- const decoder = new TextDecoder('utf-8');
|
|
|
-
|
|
|
- const read = async() => {
|
|
|
- if (reader == null) return;
|
|
|
+ // post message
|
|
|
+ try {
|
|
|
+ if (threadId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- const { done, value } = await reader.read();
|
|
|
+ const response = await postMessage(threadId, data);
|
|
|
+ if (response == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // add assistant message to the logs
|
|
|
- if (done) {
|
|
|
- setGeneratingAnswerMessage((generatingAnswerMessage) => {
|
|
|
- if (generatingAnswerMessage == null) return;
|
|
|
- setMessageLogs(msgs => [...msgs, generatingAnswerMessage]);
|
|
|
- return undefined;
|
|
|
- });
|
|
|
+ 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}`,
|
|
|
+ });
|
|
|
|
|
|
- // refresh thread data
|
|
|
- onMessageReceived?.();
|
|
|
+ const hasThreadIdNotSetError = resJson.errors.some(
|
|
|
+ (err) => err.code === MessageErrorCode.THREAD_ID_IS_NOT_SET,
|
|
|
+ );
|
|
|
+ if (hasThreadIdNotSetError) {
|
|
|
+ toastError(
|
|
|
+ t('sidebar_ai_assistant.failed_to_create_or_retrieve_thread'),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setGeneratingAnswerMessage(undefined);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const chunk = decoder.decode(value);
|
|
|
-
|
|
|
- let isPreMessageGenerated = false;
|
|
|
- let isMainMessageGenerationStarted = false;
|
|
|
- const preMessages: string[] = [];
|
|
|
- const mainMessages: string[] = [];
|
|
|
- const lines = chunk.split('\n\n');
|
|
|
- lines.forEach((line) => {
|
|
|
- const trimmedLine = line.trim();
|
|
|
- if (trimmedLine.startsWith('data:')) {
|
|
|
- const data = JSON.parse(line.replace('data: ', ''));
|
|
|
-
|
|
|
- processMessageForKnowledgeAssistant(data, {
|
|
|
- onPreMessage: (data) => {
|
|
|
- // When main message is sent while pre-message is being transmitted
|
|
|
- if (isMainMessageGenerationStarted) {
|
|
|
- preMessages.length = 0;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (data.finished) {
|
|
|
- isPreMessageGenerated = true;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (data.text == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
- preMessages.push(data.text);
|
|
|
- },
|
|
|
- onMessage: (data) => {
|
|
|
- if (!isMainMessageGenerationStarted) {
|
|
|
- isMainMessageGenerationStarted = true;
|
|
|
- }
|
|
|
+ const reader = response.body?.getReader();
|
|
|
+ const decoder = new TextDecoder('utf-8');
|
|
|
|
|
|
- // When main message is sent while pre-message is being transmitted
|
|
|
- if (!isPreMessageGenerated) {
|
|
|
- preMessages.length = 0;
|
|
|
- }
|
|
|
- mainMessages.push(data.content[0].text.value);
|
|
|
- },
|
|
|
- });
|
|
|
+ const read = async () => {
|
|
|
+ if (reader == null) return;
|
|
|
+
|
|
|
+ const { done, value } = await reader.read();
|
|
|
|
|
|
- processMessageForEditorAssistant(data, {
|
|
|
- onMessage: (data) => {
|
|
|
- mainMessages.push(data.appendedMessage);
|
|
|
- },
|
|
|
- onDetectedDiff: (data) => {
|
|
|
- logger.debug('sse diff', { data });
|
|
|
- },
|
|
|
- onFinalized: (data) => {
|
|
|
- logger.debug('sse finalized', { data });
|
|
|
- },
|
|
|
+ // add assistant message to the logs
|
|
|
+ if (done) {
|
|
|
+ setGeneratingAnswerMessage((generatingAnswerMessage) => {
|
|
|
+ if (generatingAnswerMessage == null) return;
|
|
|
+ setMessageLogs((msgs) => [...msgs, generatingAnswerMessage]);
|
|
|
+ return undefined;
|
|
|
});
|
|
|
+
|
|
|
+ // refresh thread data
|
|
|
+ onMessageReceived?.();
|
|
|
+ return;
|
|
|
}
|
|
|
- else if (trimmedLine.startsWith('error:')) {
|
|
|
- const error = JSON.parse(line.replace('error: ', ''));
|
|
|
- logger.error(error.errorMessage);
|
|
|
- form.setError('input', { type: 'manual', message: error.message });
|
|
|
|
|
|
- if (error.code === StreamErrorCode.BUDGET_EXCEEDED) {
|
|
|
- setErrorMessage(growiCloudUri != null ? 'sidebar_ai_assistant.budget_exceeded_for_growi_cloud' : 'sidebar_ai_assistant.budget_exceeded');
|
|
|
+ const chunk = decoder.decode(value);
|
|
|
+
|
|
|
+ let isPreMessageGenerated = false;
|
|
|
+ let isMainMessageGenerationStarted = false;
|
|
|
+ const preMessages: string[] = [];
|
|
|
+ const mainMessages: string[] = [];
|
|
|
+ const lines = chunk.split('\n\n');
|
|
|
+ lines.forEach((line) => {
|
|
|
+ const trimmedLine = line.trim();
|
|
|
+ if (trimmedLine.startsWith('data:')) {
|
|
|
+ const data = JSON.parse(line.replace('data: ', ''));
|
|
|
+
|
|
|
+ processMessageForKnowledgeAssistant(data, {
|
|
|
+ onPreMessage: (data) => {
|
|
|
+ // When main message is sent while pre-message is being transmitted
|
|
|
+ if (isMainMessageGenerationStarted) {
|
|
|
+ preMessages.length = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (data.finished) {
|
|
|
+ isPreMessageGenerated = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (data.text == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ preMessages.push(data.text);
|
|
|
+ },
|
|
|
+ onMessage: (data) => {
|
|
|
+ if (!isMainMessageGenerationStarted) {
|
|
|
+ isMainMessageGenerationStarted = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // When main message is sent while pre-message is being transmitted
|
|
|
+ if (!isPreMessageGenerated) {
|
|
|
+ preMessages.length = 0;
|
|
|
+ }
|
|
|
+ mainMessages.push(data.content[0].text.value);
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ processMessageForEditorAssistant(data, {
|
|
|
+ onMessage: (data) => {
|
|
|
+ mainMessages.push(data.appendedMessage);
|
|
|
+ },
|
|
|
+ onDetectedDiff: (data) => {
|
|
|
+ logger.debug('sse diff', { data });
|
|
|
+ },
|
|
|
+ onFinalized: (data) => {
|
|
|
+ logger.debug('sse finalized', { data });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ } else if (trimmedLine.startsWith('error:')) {
|
|
|
+ const error = JSON.parse(line.replace('error: ', ''));
|
|
|
+ logger.error(error.errorMessage);
|
|
|
+ form.setError('input', {
|
|
|
+ type: 'manual',
|
|
|
+ message: error.message,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (error.code === StreamErrorCode.BUDGET_EXCEEDED) {
|
|
|
+ setErrorMessage(
|
|
|
+ growiCloudUri != null
|
|
|
+ ? 'sidebar_ai_assistant.budget_exceeded_for_growi_cloud'
|
|
|
+ : 'sidebar_ai_assistant.budget_exceeded',
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- });
|
|
|
+ });
|
|
|
|
|
|
- // append text values to the assistant message
|
|
|
- setGeneratingAnswerMessage((prevMessage) => {
|
|
|
- if (prevMessage == null) return;
|
|
|
- return {
|
|
|
- ...prevMessage,
|
|
|
- content: prevMessage.content + preMessages.join('') + mainMessages.join(''),
|
|
|
- };
|
|
|
- });
|
|
|
+ // append text values to the assistant message
|
|
|
+ setGeneratingAnswerMessage((prevMessage) => {
|
|
|
+ if (prevMessage == null) return;
|
|
|
+ return {
|
|
|
+ ...prevMessage,
|
|
|
+ content:
|
|
|
+ prevMessage.content +
|
|
|
+ preMessages.join('') +
|
|
|
+ mainMessages.join(''),
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
+ read();
|
|
|
+ };
|
|
|
read();
|
|
|
- };
|
|
|
- read();
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err.toString());
|
|
|
- form.setError('input', { type: 'manual', message: err.toString() });
|
|
|
- }
|
|
|
+ } catch (err) {
|
|
|
+ logger.error(err.toString());
|
|
|
+ form.setError('input', { type: 'manual', message: err.toString() });
|
|
|
+ }
|
|
|
|
|
|
- // eslint-disable-next-line max-len
|
|
|
- }, [isGenerating, messageLogs, resetForm, threadData?.threadId, createThread, onNewThreadCreated, t, postMessage, form, onMessageReceived, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
|
|
|
+ // eslint-disable-next-line max-len
|
|
|
+ },
|
|
|
+ [
|
|
|
+ isGenerating,
|
|
|
+ messageLogs,
|
|
|
+ resetForm,
|
|
|
+ threadData?.threadId,
|
|
|
+ createThread,
|
|
|
+ onNewThreadCreated,
|
|
|
+ t,
|
|
|
+ postMessage,
|
|
|
+ form,
|
|
|
+ onMessageReceived,
|
|
|
+ processMessageForKnowledgeAssistant,
|
|
|
+ processMessageForEditorAssistant,
|
|
|
+ growiCloudUri,
|
|
|
+ ],
|
|
|
+ );
|
|
|
|
|
|
- const submit = useCallback((data: FormData) => {
|
|
|
- if (isEditorAssistant) {
|
|
|
- const markdownType = (() => {
|
|
|
- if (isEditorAssistantFormData(data) && data.markdownType != null) {
|
|
|
- return data.markdownType;
|
|
|
- }
|
|
|
+ const submit = useCallback(
|
|
|
+ (data: FormData) => {
|
|
|
+ if (isEditorAssistant) {
|
|
|
+ const markdownType = (() => {
|
|
|
+ if (isEditorAssistantFormData(data) && data.markdownType != null) {
|
|
|
+ return data.markdownType;
|
|
|
+ }
|
|
|
|
|
|
- return isTextSelected ? 'selected' : 'none';
|
|
|
- })();
|
|
|
+ return isTextSelected ? 'selected' : 'none';
|
|
|
+ })();
|
|
|
|
|
|
- return submitSubstance({ ...data, markdownType });
|
|
|
- }
|
|
|
+ return submitSubstance({ ...data, markdownType });
|
|
|
+ }
|
|
|
|
|
|
- return submitSubstance(data);
|
|
|
- }, [isEditorAssistant, isTextSelected, submitSubstance]);
|
|
|
+ return submitSubstance(data);
|
|
|
+ },
|
|
|
+ [isEditorAssistant, isTextSelected, submitSubstance],
|
|
|
+ );
|
|
|
|
|
|
const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
// Do nothing while composing
|
|
|
@@ -366,22 +449,38 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
return isEditorAssistant
|
|
|
? headerIconForEditorAssistant
|
|
|
: headerIconForKnowledgeAssistant;
|
|
|
- }, [headerIconForEditorAssistant, headerIconForKnowledgeAssistant, isEditorAssistant]);
|
|
|
+ }, [
|
|
|
+ headerIconForEditorAssistant,
|
|
|
+ headerIconForKnowledgeAssistant,
|
|
|
+ isEditorAssistant,
|
|
|
+ ]);
|
|
|
|
|
|
const headerText = useMemo(() => {
|
|
|
return isEditorAssistant
|
|
|
? headerTextForEditorAssistant
|
|
|
: headerTextForKnowledgeAssistant;
|
|
|
- }, [isEditorAssistant, headerTextForEditorAssistant, headerTextForKnowledgeAssistant]);
|
|
|
+ }, [
|
|
|
+ isEditorAssistant,
|
|
|
+ headerTextForEditorAssistant,
|
|
|
+ headerTextForKnowledgeAssistant,
|
|
|
+ ]);
|
|
|
|
|
|
const placeHolder = useMemo(() => {
|
|
|
if (form.formState.isSubmitting) {
|
|
|
return '';
|
|
|
}
|
|
|
- return t(isEditorAssistant
|
|
|
- ? placeHolderForEditorAssistant
|
|
|
- : placeHolderForKnowledgeAssistant);
|
|
|
- }, [form.formState.isSubmitting, isEditorAssistant, placeHolderForEditorAssistant, placeHolderForKnowledgeAssistant, t]);
|
|
|
+ return t(
|
|
|
+ isEditorAssistant
|
|
|
+ ? placeHolderForEditorAssistant
|
|
|
+ : placeHolderForKnowledgeAssistant,
|
|
|
+ );
|
|
|
+ }, [
|
|
|
+ form.formState.isSubmitting,
|
|
|
+ isEditorAssistant,
|
|
|
+ placeHolderForEditorAssistant,
|
|
|
+ placeHolderForKnowledgeAssistant,
|
|
|
+ t,
|
|
|
+ ]);
|
|
|
|
|
|
const initialView = useMemo(() => {
|
|
|
if (isEditorAssistant) {
|
|
|
@@ -389,7 +488,12 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
}
|
|
|
|
|
|
return initialViewForKnowledgeAssistant;
|
|
|
- }, [generateInitialViewForEditorAssistant, initialViewForKnowledgeAssistant, isEditorAssistant, submit]);
|
|
|
+ }, [
|
|
|
+ generateInitialViewForEditorAssistant,
|
|
|
+ initialViewForKnowledgeAssistant,
|
|
|
+ isEditorAssistant,
|
|
|
+ submit,
|
|
|
+ ]);
|
|
|
|
|
|
const messageCardAdditionalItemForGeneratingMessage = useMemo(() => {
|
|
|
if (isEditorAssistant) {
|
|
|
@@ -399,17 +503,28 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
return <></>;
|
|
|
}, [generatingEditorTextLabel, isEditorAssistant]);
|
|
|
|
|
|
-
|
|
|
- const messageCardAdditionalItemForGeneratedMessage = useCallback((messageId?: string) => {
|
|
|
- if (isEditorAssistant) {
|
|
|
- if (messageId == null || messageLogs == null) {
|
|
|
- return <></>;
|
|
|
+ const messageCardAdditionalItemForGeneratedMessage = useCallback(
|
|
|
+ (messageId?: string) => {
|
|
|
+ if (isEditorAssistant) {
|
|
|
+ if (messageId == null || messageLogs == null) {
|
|
|
+ return <></>;
|
|
|
+ }
|
|
|
+ return generateActionButtons(
|
|
|
+ messageId,
|
|
|
+ messageLogs,
|
|
|
+ generatingAnswerMessage,
|
|
|
+ );
|
|
|
}
|
|
|
- return generateActionButtons(messageId, messageLogs, generatingAnswerMessage);
|
|
|
- }
|
|
|
|
|
|
- return undefined;
|
|
|
- }, [generateActionButtons, generatingAnswerMessage, isEditorAssistant, messageLogs]);
|
|
|
+ return undefined;
|
|
|
+ },
|
|
|
+ [
|
|
|
+ generateActionButtons,
|
|
|
+ generatingAnswerMessage,
|
|
|
+ isEditorAssistant,
|
|
|
+ messageLogs,
|
|
|
+ ],
|
|
|
+ );
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
@@ -429,55 +544,61 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-grow-1 overflow-hidden">
|
|
|
- <SimpleBar
|
|
|
- className="h-100"
|
|
|
- autoHide
|
|
|
- >
|
|
|
+ <SimpleBar className="h-100" autoHide>
|
|
|
{!isEditorAssistant && threadTitleViewForKnowledgeAssistant}
|
|
|
<div className="p-4">
|
|
|
<div className="d-flex flex-column gap-4 flex-grow-1">
|
|
|
- { threadData != null
|
|
|
- ? (
|
|
|
- <div className="vstack gap-4 pb-2">
|
|
|
- { messageLogs.map(message => (
|
|
|
- <>
|
|
|
- <MessageCard
|
|
|
- role={message.isUserMessage ? 'user' : 'assistant'}
|
|
|
- additionalItem={messageCardAdditionalItemForGeneratedMessage(message.id)}
|
|
|
- >
|
|
|
- {message.content}
|
|
|
- </MessageCard>
|
|
|
- </>
|
|
|
- )) }
|
|
|
- { generatingAnswerMessage != null && (
|
|
|
+ {threadData != null ? (
|
|
|
+ <div className="vstack gap-4 pb-2">
|
|
|
+ {messageLogs.map((message) => (
|
|
|
+ <>
|
|
|
<MessageCard
|
|
|
- role="assistant"
|
|
|
- additionalItem={messageCardAdditionalItemForGeneratingMessage}
|
|
|
+ role={message.isUserMessage ? 'user' : 'assistant'}
|
|
|
+ additionalItem={messageCardAdditionalItemForGeneratedMessage(
|
|
|
+ message.id,
|
|
|
+ )}
|
|
|
>
|
|
|
- {generatingAnswerMessage.content}
|
|
|
+ {message.content}
|
|
|
</MessageCard>
|
|
|
- )}
|
|
|
- { isEditorAssistant && partialContentWarnLabel }
|
|
|
- { messageLogs.length > 0 && (
|
|
|
- <div className="d-flex justify-content-center">
|
|
|
- <span className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1" style={{ fontSize: 'smaller' }}>
|
|
|
- {t('sidebar_ai_assistant.caution_against_hallucination')}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- )
|
|
|
- : (
|
|
|
- <>{ initialView }</>
|
|
|
- )
|
|
|
- }
|
|
|
+ </>
|
|
|
+ ))}
|
|
|
+ {generatingAnswerMessage != null && (
|
|
|
+ <MessageCard
|
|
|
+ role="assistant"
|
|
|
+ additionalItem={
|
|
|
+ messageCardAdditionalItemForGeneratingMessage
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {generatingAnswerMessage.content}
|
|
|
+ </MessageCard>
|
|
|
+ )}
|
|
|
+ {isEditorAssistant && partialContentWarnLabel}
|
|
|
+ {messageLogs.length > 0 && (
|
|
|
+ <div className="d-flex justify-content-center">
|
|
|
+ <span
|
|
|
+ className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1"
|
|
|
+ style={{ fontSize: 'smaller' }}
|
|
|
+ >
|
|
|
+ {t(
|
|
|
+ 'sidebar_ai_assistant.caution_against_hallucination',
|
|
|
+ )}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>{initialView}</>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</div>
|
|
|
</SimpleBar>
|
|
|
</div>
|
|
|
|
|
|
<div className="input-form-area position-sticky bg-body z-2 p-3">
|
|
|
- <form onSubmit={form.handleSubmit(submit)} className="flex-fill vstack gap-1">
|
|
|
+ <form
|
|
|
+ onSubmit={form.handleSubmit(submit)}
|
|
|
+ className="flex-fill vstack gap-1"
|
|
|
+ >
|
|
|
<Controller
|
|
|
name="input"
|
|
|
control={form.control}
|
|
|
@@ -495,8 +616,9 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
)}
|
|
|
/>
|
|
|
<div className="flex-fill hstack gap-2 justify-content-between m-0">
|
|
|
- { !isEditorAssistant && generateModeSwitchesDropdownForKnowledgeAssistant(isGenerating) }
|
|
|
- { isEditorAssistant && <div /> }
|
|
|
+ {!isEditorAssistant &&
|
|
|
+ generateModeSwitchesDropdownForKnowledgeAssistant(isGenerating)}
|
|
|
+ {isEditorAssistant && <div />}
|
|
|
<button
|
|
|
type="submit"
|
|
|
className="btn btn-submit no-border"
|
|
|
@@ -510,20 +632,32 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
{form.formState.errors.input != null && (
|
|
|
<div className="mt-4 bg-danger bg-opacity-10 rounded-3 p-2 w-100">
|
|
|
<div>
|
|
|
- <span className="material-symbols-outlined text-danger me-2">error</span>
|
|
|
- <span className="text-danger">{ errorMessage != null ? t(errorMessage) : t('sidebar_ai_assistant.error_message') }</span>
|
|
|
+ <span className="material-symbols-outlined text-danger me-2">
|
|
|
+ error
|
|
|
+ </span>
|
|
|
+ <span className="text-danger">
|
|
|
+ {errorMessage != null
|
|
|
+ ? t(errorMessage)
|
|
|
+ : t('sidebar_ai_assistant.error_message')}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
type="button"
|
|
|
className="btn btn-link text-body-secondary p-0"
|
|
|
aria-expanded={isErrorDetailCollapsed}
|
|
|
- onClick={() => setIsErrorDetailCollapsed(!isErrorDetailCollapsed)}
|
|
|
+ onClick={() =>
|
|
|
+ setIsErrorDetailCollapsed(!isErrorDetailCollapsed)
|
|
|
+ }
|
|
|
>
|
|
|
- <span className={`material-symbols-outlined mt-2 me-1 ${isErrorDetailCollapsed ? 'rotate-90' : ''}`}>
|
|
|
+ <span
|
|
|
+ className={`material-symbols-outlined mt-2 me-1 ${isErrorDetailCollapsed ? 'rotate-90' : ''}`}
|
|
|
+ >
|
|
|
chevron_right
|
|
|
</span>
|
|
|
- <span className="small">{t('sidebar_ai_assistant.show_error_detail')}</span>
|
|
|
+ <span className="small">
|
|
|
+ {t('sidebar_ai_assistant.show_error_detail')}
|
|
|
+ </span>
|
|
|
</button>
|
|
|
|
|
|
<Collapse isOpen={isErrorDetailCollapsed}>
|
|
|
@@ -543,21 +677,30 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-
|
|
|
export const AiAssistantSidebar: FC = memo((): JSX.Element => {
|
|
|
- const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar, refreshThreadData } = useAiAssistantSidebar();
|
|
|
- const { mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
|
|
|
+ const {
|
|
|
+ data: aiAssistantSidebarData,
|
|
|
+ close: closeAiAssistantSidebar,
|
|
|
+ refreshThreadData,
|
|
|
+ } = useAiAssistantSidebar();
|
|
|
+ const { mutate: mutateIsEnableUnifiedMergeView } =
|
|
|
+ useIsEnableUnifiedMergeView();
|
|
|
|
|
|
const aiAssistantData = aiAssistantSidebarData?.aiAssistantData;
|
|
|
const threadData = aiAssistantSidebarData?.threadData;
|
|
|
const isOpened = aiAssistantSidebarData?.isOpened;
|
|
|
const isEditorAssistant = aiAssistantSidebarData?.isEditorAssistant ?? false;
|
|
|
|
|
|
- const { data: threads, mutate: mutateThreads } = useSWRxThreads(aiAssistantData?._id);
|
|
|
+ const { data: threads, mutate: mutateThreads } = useSWRxThreads(
|
|
|
+ aiAssistantData?._id,
|
|
|
+ );
|
|
|
|
|
|
- const newThreadCreatedHandler = useCallback((thread: IThreadRelationHasId): void => {
|
|
|
- refreshThreadData(thread);
|
|
|
- }, [refreshThreadData]);
|
|
|
+ const newThreadCreatedHandler = useCallback(
|
|
|
+ (thread: IThreadRelationHasId): void => {
|
|
|
+ refreshThreadData(thread);
|
|
|
+ },
|
|
|
+ [refreshThreadData],
|
|
|
+ );
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!aiAssistantSidebarData?.isOpened) {
|
|
|
@@ -571,7 +714,9 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const currentThread = threads.find(t => t.threadId === threadData?.threadId);
|
|
|
+ const currentThread = threads.find(
|
|
|
+ (t) => t.threadId === threadData?.threadId,
|
|
|
+ );
|
|
|
if (currentThread != null) {
|
|
|
refreshThreadData(currentThread);
|
|
|
}
|