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

Merge pull request #9897 from weseek/imprv/165077-hide-summary-mode-switch-in-editor-assistant-mode

imprv: Hide summary mode switch in editor assistant mode
mergify[bot] 11 месяцев назад
Родитель
Сommit
77857eb33f

+ 58 - 48
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -3,9 +3,9 @@ import {
   type FC, memo, useRef, useEffect, useState, useCallback, useMemo,
   type FC, memo, useRef, useEffect, useState, useCallback, useMemo,
 } from 'react';
 } from 'react';
 
 
-import { useForm, Controller } from 'react-hook-form';
+import { Controller } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { Collapse, UncontrolledTooltip } from 'reactstrap';
+import { Collapse } from 'reactstrap';
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
 
 
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
@@ -19,11 +19,14 @@ import type { IThreadRelationHasId } from '../../../../interfaces/thread-relatio
 import {
 import {
   useEditorAssistant,
   useEditorAssistant,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForEditorAssistant,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForEditorAssistant,
+  isEditorAssistantFormData,
+  type FormData as FormDataForEditorAssistant,
 } from '../../../services/editor-assistant';
 } from '../../../services/editor-assistant';
 import {
 import {
   useKnowledgeAssistant,
   useKnowledgeAssistant,
   useFetchAndSetMessageDataEffect,
   useFetchAndSetMessageDataEffect,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForKnowledgeAssistant,
   useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForKnowledgeAssistant,
+  type FormData as FormDataForKnowledgeAssistant,
 } from '../../../services/knowledge-assistant';
 } from '../../../services/knowledge-assistant';
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
 
 
@@ -36,10 +39,7 @@ const logger = loggerFactory('growi:openai:client:components:AiAssistantSidebar'
 
 
 const moduleClass = styles['grw-ai-assistant-sidebar'] ?? '';
 const moduleClass = styles['grw-ai-assistant-sidebar'] ?? '';
 
 
-export type FormData = {
-  input: string;
-  summaryMode?: boolean;
-};
+type FormData = FormDataForEditorAssistant | FormDataForKnowledgeAssistant;
 
 
 type AiAssistantSidebarSubstanceProps = {
 type AiAssistantSidebarSubstanceProps = {
   isEditorAssistant: boolean;
   isEditorAssistant: boolean;
@@ -71,10 +71,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     createThread: createThreadForKnowledgeAssistant,
     createThread: createThreadForKnowledgeAssistant,
     postMessage: postMessageForKnowledgeAssistant,
     postMessage: postMessageForKnowledgeAssistant,
     processMessage: processMessageForKnowledgeAssistant,
     processMessage: processMessageForKnowledgeAssistant,
+    form: formForKnowledgeAssistant,
+    resetForm: resetFormForKnowledgeAssistant,
 
 
     // Views
     // Views
     initialView: initialViewForKnowledgeAssistant,
     initialView: initialViewForKnowledgeAssistant,
     generateMessageCard: generateMessageCardForKnowledgeAssistant,
     generateMessageCard: generateMessageCardForKnowledgeAssistant,
+    generateSummaryModeSwitch: generateSummaryModeSwitchForKnowledgeAssistant,
     headerIcon: headerIconForKnowledgeAssistant,
     headerIcon: headerIconForKnowledgeAssistant,
     headerText: headerTextForKnowledgeAssistant,
     headerText: headerTextForKnowledgeAssistant,
     placeHolder: placeHolderForKnowledgeAssistant,
     placeHolder: placeHolderForKnowledgeAssistant,
@@ -84,6 +87,9 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     createThread: createThreadForEditorAssistant,
     createThread: createThreadForEditorAssistant,
     postMessage: postMessageForEditorAssistant,
     postMessage: postMessageForEditorAssistant,
     processMessage: processMessageForEditorAssistant,
     processMessage: processMessageForEditorAssistant,
+    form: formForEditorAssistant,
+    resetForm: resetFormEditorAssistant,
+    isTextSelected,
 
 
     // Views
     // Views
     generateInitialView: generateInitialViewForEditorAssistant,
     generateInitialView: generateInitialViewForEditorAssistant,
@@ -93,17 +99,20 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     placeHolder: placeHolderForEditorAssistant,
     placeHolder: placeHolderForEditorAssistant,
   } = useEditorAssistant();
   } = useEditorAssistant();
 
 
-  const form = useForm<FormData>({
-    defaultValues: {
-      input: '',
-      summaryMode: true,
-    },
-  });
+  const form = isEditorAssistant ? formForEditorAssistant : formForKnowledgeAssistant;
 
 
   // Effects
   // Effects
   useFetchAndSetMessageDataEffect(setMessageLogs, threadData?.threadId);
   useFetchAndSetMessageDataEffect(setMessageLogs, threadData?.threadId);
 
 
   // Functions
   // Functions
+  const resetForm = useCallback(() => {
+    if (isEditorAssistant) {
+      resetFormEditorAssistant();
+    }
+
+    resetFormForKnowledgeAssistant();
+  }, [isEditorAssistant, resetFormEditorAssistant, resetFormForKnowledgeAssistant]);
+
   const createThread = useCallback(async(initialUserMessage: string) => {
   const createThread = useCallback(async(initialUserMessage: string) => {
     if (isEditorAssistant) {
     if (isEditorAssistant) {
       const thread = await createThreadForEditorAssistant();
       const thread = await createThreadForEditorAssistant();
@@ -117,19 +126,22 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     return thread;
     return thread;
   }, [aiAssistantData, createThreadForEditorAssistant, createThreadForKnowledgeAssistant, isEditorAssistant]);
   }, [aiAssistantData, createThreadForEditorAssistant, createThreadForKnowledgeAssistant, isEditorAssistant]);
 
 
-  const postMessage = useCallback(async(currentThreadId: string, input: string, summaryMode?: boolean) => {
+  const postMessage = useCallback(async(currentThreadId: string, formData: FormData) => {
     if (isEditorAssistant) {
     if (isEditorAssistant) {
-      const response = await postMessageForEditorAssistant(currentThreadId, input);
-      return response;
+      if (isEditorAssistantFormData(formData)) {
+        const response = await postMessageForEditorAssistant(currentThreadId, formData);
+        return response;
+      }
+      return;
     }
     }
     if (aiAssistantData?._id != null) {
     if (aiAssistantData?._id != null) {
-      const response = postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId, input, summaryMode);
+      const response = await postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId, formData);
       return response;
       return response;
     }
     }
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
 
 
   const isGenerating = generatingAnswerMessage != null;
   const isGenerating = generatingAnswerMessage != null;
-  const submit = useCallback(async(data: FormData) => {
+  const submitSubstance = useCallback(async(data: FormData) => {
     // do nothing when the assistant is generating an answer
     // do nothing when the assistant is generating an answer
     if (isGenerating) {
     if (isGenerating) {
       return;
       return;
@@ -146,8 +158,8 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
     const newUserMessage = { id: logLength.toString(), content: data.input, isUserMessage: true };
     setMessageLogs(msgs => [...msgs, newUserMessage]);
     setMessageLogs(msgs => [...msgs, newUserMessage]);
 
 
-    // reset form
-    form.reset({ input: '', summaryMode: data.summaryMode });
+    resetForm();
+
     setErrorMessage(undefined);
     setErrorMessage(undefined);
 
 
     // add an empty assistant message
     // add an empty assistant message
@@ -178,7 +190,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
         return;
         return;
       }
       }
 
 
-      const response = await postMessage(currentThreadId_, data.input, data.summaryMode);
+      const response = await postMessage(currentThreadId_, data);
       if (response == null) {
       if (response == null) {
         return;
         return;
       }
       }
@@ -275,7 +287,23 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     }
     }
 
 
   // eslint-disable-next-line max-len
   // 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 submit = useCallback((data: FormData) => {
+    if (isEditorAssistant) {
+      const markdownType = (() => {
+        if (isEditorAssistantFormData(data) && data.markdownType != null) {
+          return data.markdownType;
+        }
+
+        return isTextSelected ? 'selected' : 'none';
+      })();
+
+      return submitSubstance({ ...data, markdownType });
+    }
+
+    return submitSubstance(data);
+  }, [isEditorAssistant, isTextSelected, submitSubstance]);
 
 
   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)) {
@@ -313,6 +341,14 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     return initialViewForKnowledgeAssistant;
     return initialViewForKnowledgeAssistant;
   }, [generateInitialViewForEditorAssistant, initialViewForKnowledgeAssistant, isEditorAssistant, submit]);
   }, [generateInitialViewForEditorAssistant, initialViewForKnowledgeAssistant, isEditorAssistant, submit]);
 
 
+  const additionalInputControl = useMemo(() => {
+    if (isEditorAssistant) {
+      return <></>;
+    }
+
+    return generateSummaryModeSwitchForKnowledgeAssistant(isGenerating);
+  }, [generateSummaryModeSwitchForKnowledgeAssistant, isEditorAssistant, isGenerating]);
+
   const messageCard = useCallback(
   const messageCard = useCallback(
     (role: MessageCardRole, children: string, messageId?: string, messageLogs?: MessageLog[], generatingAnswerMessage?: MessageLog) => {
     (role: MessageCardRole, children: string, messageId?: string, messageLogs?: MessageLog[], generatingAnswerMessage?: MessageLog) => {
       if (isEditorAssistant) {
       if (isEditorAssistant) {
@@ -396,33 +432,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
                   <span className="material-symbols-outlined">send</span>
                   <span className="material-symbols-outlined">send</span>
                 </button>
                 </button>
               </div>
               </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>
 
 
             {form.formState.errors.input != null && (
             {form.formState.errors.input != null && (

+ 66 - 23
apps/app/src/features/openai/client/services/editor-assistant.tsx

@@ -8,6 +8,7 @@ import {
 } from '@growi/editor/dist/client/services/unified-merge-view';
 } from '@growi/editor/dist/client/services/unified-merge-view';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useSecondaryYdocs } from '@growi/editor/dist/client/stores/use-secondary-ydocs';
 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 { useTranslation } from 'react-i18next';
 import { type Text as YText } from 'yjs';
 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 type { IThreadRelationHasId } from '../../interfaces/thread-relation';
 import { ThreadType } from '../../interfaces/thread-relation';
 import { ThreadType } from '../../interfaces/thread-relation';
 import { AiAssistantDropdown } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantDropdown';
 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 { MessageCard, type MessageCardRole } from '../components/AiAssistant/AiAssistantSidebar/MessageCard';
 import { QuickMenuList } from '../components/AiAssistant/AiAssistantSidebar/QuickMenuList';
 import { QuickMenuList } from '../components/AiAssistant/AiAssistantSidebar/QuickMenuList';
 import { useAiAssistantSidebar } from '../stores/ai-assistant';
 import { useAiAssistantSidebar } from '../stores/ai-assistant';
@@ -43,7 +44,7 @@ interface CreateThread {
   (): Promise<IThreadRelationHasId>;
   (): Promise<IThreadRelationHasId>;
 }
 }
 interface PostMessage {
 interface PostMessage {
-  (threadId: string, userMessage: string): Promise<Response>;
+  (threadId: string, formData: FormData): Promise<Response>;
 }
 }
 interface ProcessMessage {
 interface ProcessMessage {
   (data: unknown, handler: {
   (data: unknown, handler: {
@@ -59,6 +60,10 @@ interface GenerateInitialView {
 interface GenerateMessageCard {
 interface GenerateMessageCard {
   (role: MessageCardRole, children: string, messageId: string, messageLogs: MessageLog[], generatingAnswerMessage?: MessageLog): JSX.Element;
   (role: MessageCardRole, children: string, messageId: string, messageLogs: MessageLog[], generatingAnswerMessage?: MessageLog): JSX.Element;
 }
 }
+export interface FormData {
+  input: string,
+  markdownType?: 'full' | 'selected' | 'none'
+}
 
 
 type DetectedDiff = Array<{
 type DetectedDiff = Array<{
   data: SseDetectedDiff,
   data: SseDetectedDiff,
@@ -70,6 +75,9 @@ type UseEditorAssistant = () => {
   createThread: CreateThread,
   createThread: CreateThread,
   postMessage: PostMessage,
   postMessage: PostMessage,
   processMessage: ProcessMessage,
   processMessage: ProcessMessage,
+  form: UseFormReturn<FormData>
+  resetForm: () => void
+  isTextSelected: boolean,
 
 
   // Views
   // Views
   generateInitialView: GenerateInitialView,
   generateInitialView: GenerateInitialView,
@@ -139,8 +147,10 @@ export const useEditorAssistant: UseEditorAssistant = () => {
 
 
   // States
   // States
   const [detectedDiff, setDetectedDiff] = useState<DetectedDiff>();
   const [detectedDiff, setDetectedDiff] = useState<DetectedDiff>();
-  const [selectedText, setSelectedText] = useState<string>();
   const [selectedAiAssistant, setSelectedAiAssistant] = useState<AiAssistantHasId>();
   const [selectedAiAssistant, setSelectedAiAssistant] = useState<AiAssistantHasId>();
+  const [selectedText, setSelectedText] = useState<string>();
+
+  const isTextSelected = useMemo(() => selectedText != null && selectedText.length !== 0, [selectedText]);
 
 
   // Hooks
   // Hooks
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -150,7 +160,17 @@ export const useEditorAssistant: UseEditorAssistant = () => {
   const yDocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
   const yDocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
   const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
   const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
 
 
+  const form = useForm<FormData>({
+    defaultValues: {
+      input: '',
+    },
+  });
+
   // Functions
   // Functions
+  const resetForm = useCallback(() => {
+    form.reset({ input: '' });
+  }, [form]);
+
   const createThread: CreateThread = useCallback(async() => {
   const createThread: CreateThread = useCallback(async() => {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
       type: ThreadType.EDITOR,
       type: ThreadType.EDITOR,
@@ -159,21 +179,33 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     return response.data;
     return response.data;
   }, [selectedAiAssistant?._id]);
   }, [selectedAiAssistant?._id]);
 
 
-  const postMessage: PostMessage = useCallback(async(threadId, userMessage) => {
+  const postMessage: PostMessage = useCallback(async(threadId, formData) => {
+    const getMarkdown = (): string | undefined => {
+      if (formData.markdownType === 'none') {
+        return undefined;
+      }
+
+      if (formData.markdownType === 'selected') {
+        return selectedText;
+      }
+
+      if (formData.markdownType === 'full') {
+        return codeMirrorEditor?.getDoc();
+      }
+    };
+
     const response = await fetch('/_api/v3/openai/edit', {
     const response = await fetch('/_api/v3/openai/edit', {
       method: 'POST',
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
       body: JSON.stringify({
         threadId,
         threadId,
-        userMessage,
-        markdown: selectedText != null && selectedText.length !== 0
-          ? selectedText
-          : undefined,
+        userMessage: formData.input,
+        markdown: getMarkdown(),
       }),
       }),
     });
     });
 
 
     return response;
     return response;
-  }, [selectedText]);
+  }, [codeMirrorEditor, selectedText]);
 
 
   const processMessage: ProcessMessage = useCallback((data, handler) => {
   const processMessage: ProcessMessage = useCallback((data, handler) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
@@ -231,7 +263,7 @@ export const useEditorAssistant: UseEditorAssistant = () => {
         pendingDetectedDiff.forEach((detectedDiff) => {
         pendingDetectedDiff.forEach((detectedDiff) => {
           if (isReplaceDiff(detectedDiff.data)) {
           if (isReplaceDiff(detectedDiff.data)) {
 
 
-            if (selectedText != null && selectedText.length !== 0) {
+            if (isTextSelected) {
               const lineInfo = getLineInfo(yText, lineRef.current);
               const lineInfo = getLineInfo(yText, lineRef.current);
               if (lineInfo != null && lineInfo.text !== detectedDiff.data.diff.replace) {
               if (lineInfo != null && lineInfo.text !== detectedDiff.data.diff.replace) {
                 yText.delete(lineInfo.startIndex, lineInfo.text.length);
                 yText.delete(lineInfo.startIndex, lineInfo.text.length);
@@ -256,26 +288,29 @@ export const useEditorAssistant: UseEditorAssistant = () => {
         });
         });
       });
       });
 
 
-      // Mark as applied: true after applying to secondaryDoc
+      // Mark items as applied after applying to secondaryDoc
       setDetectedDiff((prev) => {
       setDetectedDiff((prev) => {
+        if (!prev) return prev;
         const pendingDetectedDiffIds = pendingDetectedDiff.map(diff => diff.id);
         const pendingDetectedDiffIds = pendingDetectedDiff.map(diff => diff.id);
-        prev?.forEach((diff) => {
+        return prev.map((diff) => {
           if (pendingDetectedDiffIds.includes(diff.id)) {
           if (pendingDetectedDiffIds.includes(diff.id)) {
-            diff.applied = true;
+            return { ...diff, applied: true };
           }
           }
+          return diff;
         });
         });
-        return prev;
       });
       });
+    }
+  }, [codeMirrorEditor, detectedDiff, isTextSelected, selectedText, yDocs?.secondaryDoc]);
 
 
-      // Set detectedDiff to undefined after applying all detectedDiff to secondaryDoc
-      if (detectedDiff?.filter(detectedDiff => detectedDiff.applied === false).length === 0) {
-        setSelectedText(undefined);
-        setDetectedDiff(undefined);
-        lineRef.current = 0;
-        // positionRef.current = 0;
-      }
+  // Set detectedDiff to undefined after applying all detectedDiff to secondaryDoc
+  useEffect(() => {
+    if (detectedDiff?.filter(detectedDiff => detectedDiff.applied === false).length === 0) {
+      setSelectedText(undefined);
+      setDetectedDiff(undefined);
+      lineRef.current = 0;
+      // positionRef.current = 0;
     }
     }
-  }, [codeMirrorEditor, detectedDiff, selectedText, yDocs?.secondaryDoc]);
+  }, [detectedDiff]);
 
 
 
 
   // Views
   // Views
@@ -295,7 +330,7 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     };
     };
 
 
     const clickQuickMenuHandler = async(quickMenu: string) => {
     const clickQuickMenuHandler = async(quickMenu: string) => {
-      await onSubmit({ input: quickMenu });
+      await onSubmit({ input: quickMenu, markdownType: 'full' });
     };
     };
 
 
     return (
     return (
@@ -365,6 +400,9 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     createThread,
     createThread,
     postMessage,
     postMessage,
     processMessage,
     processMessage,
+    form,
+    resetForm,
+    isTextSelected,
 
 
     // Views
     // Views
     generateInitialView,
     generateInitialView,
@@ -385,3 +423,8 @@ export const useAiAssistantSidebarCloseEffect = (): void => {
     }
     }
   }, [close, data?.isEditorAssistant, editorMode]);
   }, [close, data?.isEditorAssistant, editorMode]);
 };
 };
+
+// type guard
+export const isEditorAssistantFormData = (formData): formData is FormData => {
+  return 'markdownType' in formData;
+};

+ 70 - 6
apps/app/src/features/openai/client/services/knowledge-assistant.tsx

@@ -3,6 +3,10 @@ import {
   useCallback, useMemo, useState, useEffect,
   useCallback, useMemo, useState, useEffect,
 } from 'react';
 } 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 { apiv3Post } from '~/client/util/apiv3-client';
 import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
 import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
 import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
 import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
@@ -21,7 +25,7 @@ interface CreateThread {
 }
 }
 
 
 interface PostMessage {
 interface PostMessage {
-  (aiAssistantId: string, threadId: string, userMessage: string, summaryMode?: boolean): Promise<Response>;
+  (aiAssistantId: string, threadId: string, formData: FormData): Promise<Response>;
 }
 }
 
 
 interface ProcessMessage {
 interface ProcessMessage {
@@ -34,14 +38,26 @@ interface GenerateMessageCard {
   (role: MessageCardRole, children: string): JSX.Element;
   (role: MessageCardRole, children: string): JSX.Element;
 }
 }
 
 
+interface GenerateSummaryModeSwitch {
+  (isGenerating: boolean): JSX.Element
+}
+
+export interface FormData {
+  input: string
+  summaryMode?: boolean
+}
+
 type UseKnowledgeAssistant = () => {
 type UseKnowledgeAssistant = () => {
   createThread: CreateThread
   createThread: CreateThread
   postMessage: PostMessage
   postMessage: PostMessage
   processMessage: ProcessMessage
   processMessage: ProcessMessage
+  form: UseFormReturn<FormData>
+  resetForm: () => void
 
 
   // Views
   // Views
   initialView: JSX.Element
   initialView: JSX.Element
-  generateMessageCard: GenerateMessageCard,
+  generateMessageCard: GenerateMessageCard
+  generateSummaryModeSwitch: GenerateSummaryModeSwitch
   headerIcon: JSX.Element
   headerIcon: JSX.Element
   headerText: JSX.Element
   headerText: JSX.Element
   placeHolder: string
   placeHolder: string
@@ -53,11 +69,24 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
   const { aiAssistantData } = aiAssistantSidebarData ?? {};
   const { aiAssistantData } = aiAssistantSidebarData ?? {};
   const { threadData } = aiAssistantSidebarData ?? {};
   const { threadData } = aiAssistantSidebarData ?? {};
   const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
   const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
+  const { t } = useTranslation();
+
+  const form = useForm<FormData>({
+    defaultValues: {
+      input: '',
+      summaryMode: true,
+    },
+  });
 
 
   // States
   // States
   const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title);
   const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title);
 
 
   // Functions
   // Functions
+  const resetForm = useCallback(() => {
+    const summaryMode = form.getValues('summaryMode');
+    form.reset({ input: '', summaryMode });
+  }, [form]);
+
   const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => {
   const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
     const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
       type: ThreadType.KNOWLEDGE,
       type: ThreadType.KNOWLEDGE,
@@ -74,19 +103,19 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
     return thread;
     return thread;
   }, [mutateThreadData]);
   }, [mutateThreadData]);
 
 
-  const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, userMessage, summaryMode) => {
+  const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, formData) => {
     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' },
       body: JSON.stringify({
       body: JSON.stringify({
         aiAssistantId,
         aiAssistantId,
         threadId,
         threadId,
-        userMessage,
-        summaryMode,
+        userMessage: formData.input,
+        summaryMode: form.getValues('summaryMode'),
       }),
       }),
     });
     });
     return response;
     return response;
-  }, []);
+  }, [form]);
 
 
   const processMessage: ProcessMessage = useCallback((data, handler) => {
   const processMessage: ProcessMessage = useCallback((data, handler) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
     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 {
   return {
     createThread,
     createThread,
     postMessage,
     postMessage,
     processMessage,
     processMessage,
+    form,
+    resetForm,
 
 
     // Views
     // Views
     initialView,
     initialView,
     generateMessageCard,
     generateMessageCard,
+    generateSummaryModeSwitch,
     headerIcon,
     headerIcon,
     headerText,
     headerText,
     placeHolder,
     placeHolder,