Browse Source

Merge pull request #9839 from weseek/feat/164399-do-not-send-edit-request-without-text-selection

feat: Request to Editor Assistant without markdown
Shun Miyazawa 11 months ago
parent
commit
f3422e66fa

+ 24 - 34
apps/app/src/features/openai/client/services/editor-assistant.ts

@@ -3,7 +3,7 @@ import {
 } from 'react';
 
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
-import { acceptChange, rejectChange } from '@growi/editor/dist/client/services/unified-merge-view';
+import { acceptChange, rejectChange, useTextSelectionEffect } 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 { type Text as YText } from 'yjs';
@@ -103,6 +103,7 @@ export const useEditorAssistant: UseEditorAssistant = () => {
 
   // States
   const [detectedDiff, setDetectedDiff] = useState<DetectedDiff>();
+  const [selectedText, setSelectedText] = useState<string>();
 
   // SWR Hooks
   const { data: currentPageId } = useCurrentPageId();
@@ -111,46 +112,21 @@ export const useEditorAssistant: UseEditorAssistant = () => {
   const ydocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
 
   // 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: selectedMarkdown,
+        markdown: selectedText,
       }),
     });
+
+    setSelectedText(undefined);
+
     return response;
-  }, [getSelectedText, getSelectedTextFirstLineNumber]);
+  }, [selectedText]);
 
   const processMessage: ProcessMessage = useCallback((data, handler) => {
     handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
@@ -173,18 +149,32 @@ export const useEditorAssistant: UseEditorAssistant = () => {
   }, [mutateIsEnableUnifiedMergeView]);
 
   const accept = useCallback(() => {
-    acceptChange(codeMirrorEditor?.view);
+    if (codeMirrorEditor?.view == null) {
+      return;
+    }
+
+    acceptChange(codeMirrorEditor.view);
     mutateIsEnableUnifiedMergeView(false);
   }, [codeMirrorEditor?.view, mutateIsEnableUnifiedMergeView]);
 
   const reject = useCallback(() => {
-    rejectChange(codeMirrorEditor?.view);
+    if (codeMirrorEditor?.view == null) {
+      return;
+    }
+
+    rejectChange(codeMirrorEditor.view);
     mutateIsEnableUnifiedMergeView(false);
   }, [codeMirrorEditor?.view, mutateIsEnableUnifiedMergeView]);
 
+  const selectTextHandler = useCallback((selectedText: string, selectedTextFirstLineNumber: number) => {
+    setSelectedText(selectedText);
+    lineRef.current = selectedTextFirstLineNumber;
+  }, []);
+
   // Effects
-  useEffect(() => {
+  useTextSelectionEffect(codeMirrorEditor, selectTextHandler);
 
+  useEffect(() => {
     const pendingDetectedDiff: DetectedDiff | undefined = detectedDiff?.filter(diff => diff.applied === false);
     if (ydocs?.secondaryDoc != null && pendingDetectedDiff != null && pendingDetectedDiff.length > 0) {
 

+ 3 - 4
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -42,7 +42,7 @@ const LlmEditorAssistantResponseSchema = z.object({
 
 type ReqBody = {
   userMessage: string,
-  markdown: string,
+  markdown?: string,
   threadId?: string,
 }
 
@@ -71,10 +71,9 @@ export const postMessageToEditHandlersFactory: PostMessageHandlersFactory = (cro
       .notEmpty()
       .withMessage('userMessage must be set'),
     body('markdown')
+      .optional()
       .isString()
-      .withMessage('markdown must be string')
-      .notEmpty()
-      .withMessage('markdown must be set'),
+      .withMessage('markdown must be string'),
     body('threadId').optional().isString().withMessage('threadId must be string'),
   ];
 

+ 40 - 1
packages/editor/src/client/services/unified-merge-view/index.ts

@@ -1,9 +1,13 @@
+import { useEffect } from 'react';
+
 import {
   acceptChunk,
   rejectChunk,
 } from '@codemirror/merge';
+import type { ViewUpdate } from '@codemirror/view';
+import { EditorView } from '@codemirror/view';
 
-import type { EditorView } from 'src';
+import type { UseCodeMirrorEditor } from '..';
 
 export const acceptChange = (view: EditorView): boolean => {
   return acceptChunk(view);
@@ -12,3 +16,38 @@ export const acceptChange = (view: EditorView): boolean => {
 export const rejectChange = (view: EditorView): boolean => {
   return rejectChunk(view);
 };
+
+
+type OnSelected = (selectedText: string, selectedTextFirstLineNumber: number) => void
+
+const processSelectedText = (editorView: EditorView | ViewUpdate, onSelected?: OnSelected) => {
+  const selection = editorView.state.selection.main;
+  const selectedText = editorView.state.sliceDoc(selection.from, selection.to);
+  const selectedTextFirstLineNumber = editorView.state.doc.lineAt(selection.from).number - 1; // 0-based line number;
+  onSelected?.(selectedText, selectedTextFirstLineNumber);
+};
+
+export const useTextSelectionEffect = (codeMirrorEditor?: UseCodeMirrorEditor, onSelected?: OnSelected): void => {
+  useEffect(() => {
+    if (codeMirrorEditor == null) {
+      return;
+    }
+
+    // To handle cases where text is already selected in the editor at the time of first effect firing
+    if (codeMirrorEditor.view != null) {
+      processSelectedText(codeMirrorEditor.view, onSelected);
+    }
+
+    const extension = EditorView.updateListener.of((update) => {
+      if (update.selectionSet) {
+        processSelectedText(update, onSelected);
+      }
+    });
+
+    const cleanup = codeMirrorEditor?.appendExtensions([extension]);
+
+    return () => {
+      cleanup?.();
+    };
+  }, [codeMirrorEditor, onSelected]);
+};