Просмотр исходного кода

imprv: integrate react-hook-form for form handling in editor and knowledge assistants

Shun Miyazawa 11 месяцев назад
Родитель
Сommit
b9d00b84b0

+ 34 - 45
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -3,9 +3,9 @@ import {
   type FC, memo, useRef, useEffect, useState, useCallback, useMemo,
 } from 'react';
 
-import { useForm, Controller } from 'react-hook-form';
+import { Controller } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
-import { Collapse, UncontrolledTooltip } from 'reactstrap';
+import { Collapse } from 'reactstrap';
 import SimpleBar from 'simplebar-react';
 
 import { toastError } from '~/client/util/toastr';
@@ -19,11 +19,13 @@ import type { IThreadRelationHasId } from '../../../../interfaces/thread-relatio
 import {
   useEditorAssistant,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForEditorAssistant,
+  type FormData as FormDataForEditorAssistant,
 } from '../../../services/editor-assistant';
 import {
   useKnowledgeAssistant,
   useFetchAndSetMessageDataEffect,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForKnowledgeAssistant,
+  type FormData as FormDataForKnowledgeAssistant,
 } from '../../../services/knowledge-assistant';
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
 
@@ -36,10 +38,7 @@ const logger = loggerFactory('growi:openai:client:components:AiAssistantSidebar'
 
 const moduleClass = styles['grw-ai-assistant-sidebar'] ?? '';
 
-export type FormData = {
-  input: string;
-  summaryMode?: boolean;
-};
+type FormData = FormDataForEditorAssistant | FormDataForKnowledgeAssistant;
 
 type AiAssistantSidebarSubstanceProps = {
   isEditorAssistant: boolean;
@@ -71,10 +70,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     createThread: createThreadForKnowledgeAssistant,
     postMessage: postMessageForKnowledgeAssistant,
     processMessage: processMessageForKnowledgeAssistant,
+    form: formForForKnowledgeAssistant,
+    resetForm: resetFormForKnowledgeAssistant,
 
     // Views
     initialView: initialViewForKnowledgeAssistant,
     generateMessageCard: generateMessageCardForKnowledgeAssistant,
+    generateSummaryModeSwitch: generateSummaryModeSwitchForKnowledgeAssistant,
     headerIcon: headerIconForKnowledgeAssistant,
     headerText: headerTextForKnowledgeAssistant,
     placeHolder: placeHolderForKnowledgeAssistant,
@@ -84,6 +86,8 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     createThread: createThreadForEditorAssistant,
     postMessage: postMessageForEditorAssistant,
     processMessage: processMessageForEditorAssistant,
+    form: formForForEditorAssistant,
+    resetForm: resetFormEditorAssistant,
 
     // Views
     generateInitialView: generateInitialViewForEditorAssistant,
@@ -93,17 +97,20 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     placeHolder: placeHolderForEditorAssistant,
   } = useEditorAssistant();
 
-  const form = useForm<FormData>({
-    defaultValues: {
-      input: '',
-      summaryMode: true,
-    },
-  });
+  const form = isEditorAssistant ? formForForEditorAssistant : formForForKnowledgeAssistant;
 
   // Effects
   useFetchAndSetMessageDataEffect(setMessageLogs, threadData?.threadId);
 
   // Functions
+  const resetForm = useCallback(() => {
+    if (isEditorAssistant) {
+      resetFormEditorAssistant();
+    }
+
+    resetFormForKnowledgeAssistant();
+  }, [isEditorAssistant, resetFormEditorAssistant, resetFormForKnowledgeAssistant]);
+
   const createThread = useCallback(async(initialUserMessage: string) => {
     if (isEditorAssistant) {
       const thread = await createThreadForEditorAssistant();
@@ -117,13 +124,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     return thread;
   }, [aiAssistantData, createThreadForEditorAssistant, createThreadForKnowledgeAssistant, isEditorAssistant]);
 
-  const postMessage = useCallback(async(currentThreadId: string, input: string, summaryMode?: boolean) => {
+  const postMessage = useCallback(async(currentThreadId: string, input: string) => {
     if (isEditorAssistant) {
       const response = await postMessageForEditorAssistant(currentThreadId, input);
       return response;
     }
     if (aiAssistantData?._id != null) {
-      const response = postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId, input, summaryMode);
+      const response = postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId, input);
       return response;
     }
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
@@ -146,8 +153,8 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
     setMessageLogs(msgs => [...msgs, newUserMessage]);
 
-    // reset form
-    form.reset({ input: '', summaryMode: data.summaryMode });
+    resetForm();
+
     setErrorMessage(undefined);
 
     // add an empty assistant message
@@ -178,7 +185,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
         return;
       }
 
-      const response = await postMessage(currentThreadId_, data.input, data.summaryMode);
+      const response = await postMessage(currentThreadId_, data.input);
       if (response == null) {
         return;
       }
@@ -275,7 +282,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     }
 
   // eslint-disable-next-line max-len
-  }, [isGenerating, messageLogs, form, currentThreadId, createThread, t, postMessage, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
+  }, [isGenerating, messageLogs, resetForm, currentThreadId, createThread, t, postMessage, form, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
 
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
     if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
@@ -313,6 +320,14 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     return initialViewForKnowledgeAssistant;
   }, [generateInitialViewForEditorAssistant, initialViewForKnowledgeAssistant, isEditorAssistant, submit]);
 
+  const additionalInputControl = useMemo(() => {
+    if (isEditorAssistant) {
+      return <></>;
+    }
+
+    return generateSummaryModeSwitchForKnowledgeAssistant(isGenerating);
+  }, [generateSummaryModeSwitchForKnowledgeAssistant, isEditorAssistant, isGenerating]);
+
   const messageCard = useCallback(
     (role: MessageCardRole, children: string, messageId?: string, messageLogs?: MessageLog[], generatingAnswerMessage?: MessageLog) => {
       if (isEditorAssistant) {
@@ -396,33 +411,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
                   <span className="material-symbols-outlined">send</span>
                 </button>
               </div>
-              <div className="form-check form-switch">
-                <input
-                  id="swSummaryMode"
-                  type="checkbox"
-                  role="switch"
-                  className="form-check-input"
-                  {...form.register('summaryMode')}
-                  disabled={form.formState.isSubmitting || isGenerating}
-                />
-                <label className="form-check-label" htmlFor="swSummaryMode">
-                  {t('sidebar_ai_assistant.summary_mode_label')}
-                </label>
-
-                {/* Help */}
-                <a
-                  id="tooltipForHelpOfSummaryMode"
-                  role="button"
-                  className="ms-1"
-                >
-                  <span className="material-symbols-outlined fs-6" style={{ lineHeight: 'unset' }}>help</span>
-                </a>
-                <UncontrolledTooltip
-                  target="tooltipForHelpOfSummaryMode"
-                >
-                  {t('sidebar_ai_assistant.summary_mode_help')}
-                </UncontrolledTooltip>
-              </div>
+              { additionalInputControl }
             </form>
 
             {form.formState.errors.input != null && (

+ 20 - 1
apps/app/src/features/openai/client/services/editor-assistant.tsx

@@ -8,6 +8,7 @@ import {
 } from '@growi/editor/dist/client/services/unified-merge-view';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useSecondaryYdocs } from '@growi/editor/dist/client/stores/use-secondary-ydocs';
+import { useForm, type UseFormReturn } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { type Text as YText } from 'yjs';
 
@@ -34,7 +35,7 @@ import type { MessageLog } from '../../interfaces/message';
 import type { IThreadRelationHasId } from '../../interfaces/thread-relation';
 import { ThreadType } from '../../interfaces/thread-relation';
 import { AiAssistantDropdown } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantDropdown';
-import { type FormData } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar';
+// import { type FormData } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar';
 import { MessageCard, type MessageCardRole } from '../components/AiAssistant/AiAssistantSidebar/MessageCard';
 import { QuickMenuList } from '../components/AiAssistant/AiAssistantSidebar/QuickMenuList';
 import { useAiAssistantSidebar } from '../stores/ai-assistant';
@@ -59,6 +60,10 @@ interface GenerateInitialView {
 interface GenerateMessageCard {
   (role: MessageCardRole, children: string, messageId: string, messageLogs: MessageLog[], generatingAnswerMessage?: MessageLog): JSX.Element;
 }
+export interface FormData {
+  input: string
+  markdown?: string
+}
 
 type DetectedDiff = Array<{
   data: SseDetectedDiff,
@@ -70,6 +75,8 @@ type UseEditorAssistant = () => {
   createThread: CreateThread,
   postMessage: PostMessage,
   processMessage: ProcessMessage,
+  form: UseFormReturn<FormData>
+  resetForm: () => void
 
   // Views
   generateInitialView: GenerateInitialView,
@@ -150,7 +157,17 @@ export const useEditorAssistant: UseEditorAssistant = () => {
   const yDocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
   const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
 
+  const form = useForm<FormData>({
+    defaultValues: {
+      input: '',
+    },
+  });
+
   // Functions
+  const resetForm = useCallback(() => {
+    form.reset({ input: '', markdown: undefined });
+  }, [form]);
+
   const createThread: CreateThread = useCallback(async() => {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
       type: ThreadType.EDITOR,
@@ -365,6 +382,8 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     createThread,
     postMessage,
     processMessage,
+    form,
+    resetForm,
 
     // Views
     generateInitialView,

+ 69 - 5
apps/app/src/features/openai/client/services/knowledge-assistant.tsx

@@ -3,6 +3,10 @@ import {
   useCallback, useMemo, useState, useEffect,
 } from 'react';
 
+import { useForm, type UseFormReturn } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
 import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
@@ -21,7 +25,7 @@ interface CreateThread {
 }
 
 interface PostMessage {
-  (aiAssistantId: string, threadId: string, userMessage: string, summaryMode?: boolean): Promise<Response>;
+  (aiAssistantId: string, threadId: string, userMessage: string): Promise<Response>;
 }
 
 interface ProcessMessage {
@@ -34,14 +38,26 @@ interface GenerateMessageCard {
   (role: MessageCardRole, children: string): JSX.Element;
 }
 
+interface GenerateSummaryModeSwitch {
+  (isGenerating: boolean): JSX.Element
+}
+
+export interface FormData {
+  input: string
+  summaryMode?: boolean
+}
+
 type UseKnowledgeAssistant = () => {
   createThread: CreateThread
   postMessage: PostMessage
   processMessage: ProcessMessage
+  form: UseFormReturn<FormData>
+  resetForm: () => void
 
   // Views
   initialView: JSX.Element
-  generateMessageCard: GenerateMessageCard,
+  generateMessageCard: GenerateMessageCard
+  generateSummaryModeSwitch: GenerateSummaryModeSwitch
   headerIcon: JSX.Element
   headerText: JSX.Element
   placeHolder: string
@@ -53,11 +69,24 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
   const { aiAssistantData } = aiAssistantSidebarData ?? {};
   const { threadData } = aiAssistantSidebarData ?? {};
   const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
+  const { t } = useTranslation();
+
+  const form = useForm<FormData>({
+    defaultValues: {
+      input: '',
+      summaryMode: true,
+    },
+  });
 
   // States
   const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title);
 
   // Functions
+  const resetForm = useCallback(() => {
+    const summaryMode = form.getValues('summaryMode');
+    form.reset({ input: '', summaryMode });
+  }, [form]);
+
   const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
       type: ThreadType.KNOWLEDGE,
@@ -74,7 +103,7 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
     return thread;
   }, [mutateThreadData]);
 
-  const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, userMessage, summaryMode) => {
+  const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, userMessage) => {
     const response = await fetch('/_api/v3/openai/message', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
@@ -82,11 +111,11 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
         aiAssistantId,
         threadId,
         userMessage,
-        summaryMode,
+        summaryMode: form.getValues('summaryMode'),
       }),
     });
     return response;
-  }, []);
+  }, [form]);
 
   const processMessage: ProcessMessage = useCallback((data, handler) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
@@ -129,14 +158,49 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
     );
   }, []);
 
+  const generateSummaryModeSwitch: GenerateSummaryModeSwitch = useCallback((isGenerating) => {
+    return (
+      <div className="form-check form-switch">
+        <input
+          id="swSummaryMode"
+          type="checkbox"
+          role="switch"
+          className="form-check-input"
+          {...form.register('summaryMode')}
+          disabled={form.formState.isSubmitting || isGenerating}
+        />
+        <label className="form-check-label" htmlFor="swSummaryMode">
+          {t('sidebar_ai_assistant.summary_mode_label')}
+        </label>
+
+        {/* Help */}
+        <a
+          id="tooltipForHelpOfSummaryMode"
+          role="button"
+          className="ms-1"
+        >
+          <span className="material-symbols-outlined fs-6" style={{ lineHeight: 'unset' }}>help</span>
+        </a>
+        <UncontrolledTooltip
+          target="tooltipForHelpOfSummaryMode"
+        >
+          {t('sidebar_ai_assistant.summary_mode_help')}
+        </UncontrolledTooltip>
+      </div>
+    );
+  }, [form, t]);
+
   return {
     createThread,
     postMessage,
     processMessage,
+    form,
+    resetForm,
 
     // Views
     initialView,
     generateMessageCard,
+    generateSummaryModeSwitch,
     headerIcon,
     headerText,
     placeHolder,