Jelajahi Sumber

Merge pull request #9835 from weseek/feat/164347-send-only-selected-markdown-range-to-server

feat: Send only selected markdown range to server
Shun Miyazawa 1 tahun lalu
induk
melakukan
b943ed7e24

+ 8 - 9
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -3,8 +3,6 @@ import {
   type FC, memo, useRef, useEffect, useState, useCallback, useMemo,
 } from 'react';
 
-import { GlobalCodeMirrorEditorKey } from '@growi/editor';
-import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useForm, Controller } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { Collapse, UncontrolledTooltip } from 'reactstrap';
@@ -75,11 +73,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
   const { data: growiCloudUri } = useGrowiCloudUri();
   const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
   const { trigger: mutateMessageData } = useSWRMUTxMessages(aiAssistantData?._id, threadData?.threadId);
-  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
   const { postMessage: postMessageForKnowledgeAssistant, processMessage: processMessageForKnowledgeAssistant } = useKnowledgeAssistant();
   const {
-    postMessage: postMessageForEditorAssistant, processMessage: processMessageForEditorAssistant, accept, reject,
+    postMessage: postMessageForEditorAssistant,
+    processMessage: processMessageForEditorAssistant,
+    accept,
+    reject,
   } = useEditorAssistant();
 
   const form = useForm<FormData>({
@@ -197,8 +197,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
       const response = await (async() => {
         if (isEditorAssistant) {
-          const markdown = codeMirrorEditor?.getDoc();
-          return postMessageForEditorAssistant(currentThreadId_, data.input, markdown ?? '');
+          return postMessageForEditorAssistant(currentThreadId_, data.input);
         }
         if (aiAssistantData?._id != null) {
           return postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId_, data.input, data.summaryMode);
@@ -301,7 +300,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     }
 
   // eslint-disable-next-line max-len
-  }, [isGenerating, messageLogs, form, currentThreadId, isEditorAssistant, selectedAiAssistant?._id, aiAssistantData?._id, mutateThreadData, t, codeMirrorEditor, postMessageForEditorAssistant, postMessageForKnowledgeAssistant, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
+  }, [isGenerating, messageLogs, form, currentThreadId, isEditorAssistant, selectedAiAssistant?._id, aiAssistantData?._id, mutateThreadData, t, postMessageForEditorAssistant, postMessageForKnowledgeAssistant, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
 
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
     if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
@@ -504,7 +503,7 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
 
   useEffect(() => {
     const handleClickOutside = (event: MouseEvent) => {
-      if (isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
+      if (isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node) && !isEditorAssistant) {
         closeAiAssistantSidebar();
       }
     };
@@ -513,7 +512,7 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
     return () => {
       document.removeEventListener('mousedown', handleClickOutside);
     };
-  }, [closeAiAssistantSidebar, isOpened]);
+  }, [closeAiAssistantSidebar, isEditorAssistant, isOpened]);
 
   useEffect(() => {
     if (!aiAssistantSidebarData?.isOpened) {

+ 93 - 53
apps/app/src/features/openai/client/services/editor-assistant.ts

@@ -25,7 +25,7 @@ import { useIsEnableUnifiedMergeView } from '~/stores-universal/context';
 import { useCurrentPageId } from '~/stores/page';
 
 interface PostMessage {
-  (threadId: string, userMessage: string, markdown: string): Promise<Response>;
+  (threadId: string, userMessage: string): Promise<Response>;
 }
 interface ProcessMessage {
   (data: unknown, handler: {
@@ -41,78 +41,68 @@ type DetectedDiff = Array<{
   id: string,
 }>
 
-const insertTextAtLine = (ytext: YText, lineNumber: number, textToInsert: string): void => {
-  // Get the entire text content
-  const content = ytext.toString();
-
-  // Split by newlines to get all lines
-  const lines = content.split('\n');
-
-  // Calculate the index position for insertion
-  let insertPosition = 0;
-
-  // Sum the length of all lines before the target line (plus newline characters)
-  for (let i = 0; i < lineNumber && i < lines.length; i++) {
-    insertPosition += lines[i].length + 1; // +1 for the newline character
-  }
-
-  // Insert the text at the calculated position
-  ytext.insert(insertPosition, textToInsert);
-};
-
-
-const getLineInfo = (ytext: YText, lineNumber: number): { text: string, startIndex: number } | null => {
-  // Get the entire text content
-  const content = ytext.toString();
-
-  // Split by newlines to get all lines
-  const lines = content.split('\n');
-
-  // Check if the requested line exists
-  if (lineNumber < 0 || lineNumber >= lines.length) {
-    return null; // Line doesn't exist
-  }
-
-  // Get the text of the specified line
-  const text = lines[lineNumber];
-
-  // Calculate the start index of the line
-  let startIndex = 0;
-  for (let i = 0; i < lineNumber; i++) {
-    startIndex += lines[i].length + 1; // +1 for the newline character
-  }
-
-  // Return comprehensive line information
-  return {
-    text,
-    startIndex,
-  };
-};
-
+type UseEditorAssistant = () => {
+  postMessage: PostMessage,
+  processMessage: ProcessMessage,
+  accept: () => void,
+  reject: () => void,
+}
 
-export const useEditorAssistant = (): {postMessage: PostMessage, processMessage: ProcessMessage, accept: () => void, reject: () => void } => {
+export const useEditorAssistant: UseEditorAssistant = () => {
+  // Refs
   // const positionRef = useRef<number>(0);
   const lineRef = useRef<number>(0);
 
+  // States
   const [detectedDiff, setDetectedDiff] = useState<DetectedDiff>();
 
+  // SWR Hooks
   const { data: currentPageId } = useCurrentPageId();
   const { data: isEnableUnifiedMergeView, mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   const ydocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
 
-  const postMessage: PostMessage = useCallback(async(threadId, userMessage, markdown) => {
+  // Functions
+  const getSelectedText = useCallback(() => {
+    const view = codeMirrorEditor?.view;
+    if (view == null) {
+      return;
+    }
+
+    return view.state.sliceDoc(
+      view.state.selection.main.from,
+      view.state.selection.main.to,
+    );
+  }, [codeMirrorEditor?.view]);
+
+  const getSelectedTextFirstLineNumber = useCallback(() => {
+    const view = codeMirrorEditor?.view;
+    if (view == null) {
+      return;
+    }
+
+    const selectionStart = view.state.selection.main.from;
+
+    const lineInfo = view.state.doc.lineAt(selectionStart);
+
+    return lineInfo.number;
+  }, [codeMirrorEditor?.view]);
+
+  const postMessage: PostMessage = useCallback(async(threadId, userMessage) => {
+    lineRef.current = getSelectedTextFirstLineNumber() ?? 0;
+
+    const selectedMarkdown = getSelectedText();
     const response = await fetch('/_api/v3/openai/edit', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         threadId,
         userMessage,
-        markdown,
+        markdown: selectedMarkdown,
       }),
     });
     return response;
-  }, []);
+  }, [getSelectedText, getSelectedTextFirstLineNumber]);
 
   const processMessage: ProcessMessage = useCallback((data, handler) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
@@ -134,6 +124,55 @@ export const useEditorAssistant = (): {postMessage: PostMessage, processMessage:
     });
   }, [mutateIsEnableUnifiedMergeView]);
 
+  const insertTextAtLine = (ytext: YText, lineNumber: number, textToInsert: string): void => {
+    // Get the entire text content
+    const content = ytext.toString();
+
+    // Split by newlines to get all lines
+    const lines = content.split('\n');
+
+    // Calculate the index position for insertion
+    let insertPosition = 0;
+
+    // Sum the length of all lines before the target line (plus newline characters)
+    for (let i = 0; i < lineNumber && i < lines.length; i++) {
+      insertPosition += lines[i].length + 1; // +1 for the newline character
+    }
+
+    // Insert the text at the calculated position
+    ytext.insert(insertPosition, textToInsert);
+  };
+
+
+  const getLineInfo = (ytext: YText, lineNumber: number): { text: string, startIndex: number } | null => {
+    // Get the entire text content
+    const content = ytext.toString();
+
+    // Split by newlines to get all lines
+    const lines = content.split('\n');
+
+    // Check if the requested line exists
+    if (lineNumber < 0 || lineNumber >= lines.length) {
+      return null; // Line doesn't exist
+    }
+
+    // Get the text of the specified line
+    const text = lines[lineNumber];
+
+    // Calculate the start index of the line
+    let startIndex = 0;
+    for (let i = 0; i < lineNumber; i++) {
+      startIndex += lines[i].length + 1; // +1 for the newline character
+    }
+
+    // Return comprehensive line information
+    return {
+      text,
+      startIndex,
+    };
+  };
+
+
   const accept = useCallback(() => {
     acceptChange(codeMirrorEditor?.view);
     mutateIsEnableUnifiedMergeView(false);
@@ -144,6 +183,7 @@ export const useEditorAssistant = (): {postMessage: PostMessage, processMessage:
     mutateIsEnableUnifiedMergeView(false);
   }, [codeMirrorEditor?.view, mutateIsEnableUnifiedMergeView]);
 
+  // Effects
   useEffect(() => {
 
     const pendingDetectedDiff: DetectedDiff | undefined = detectedDiff?.filter(diff => diff.applied === false);