editor-assistant.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {
  2. useCallback, useEffect, useState, useRef,
  3. } from 'react';
  4. import { GlobalCodeMirrorEditorKey } from '@growi/editor';
  5. import { acceptChange, rejectChange } from '@growi/editor/dist/client/services/unified-merge-view';
  6. import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
  7. import { useSecondaryYdocs } from '@growi/editor/dist/client/stores/use-secondary-ydocs';
  8. import {
  9. SseMessageSchema,
  10. SseDetectedDiffSchema,
  11. SseFinalizedSchema,
  12. isReplaceDiff,
  13. // isInsertDiff,
  14. // isDeleteDiff,
  15. // isRetainDiff,
  16. type SseMessage,
  17. type SseDetectedDiff,
  18. type SseFinalized,
  19. } from '~/features/openai/interfaces/editor-assistant/sse-schemas';
  20. import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
  21. import { useIsEnableUnifiedMergeView } from '~/stores-universal/context';
  22. import { useCurrentPageId } from '~/stores/page';
  23. interface PostMessage {
  24. (threadId: string, userMessage: string, markdown: string): Promise<Response>;
  25. }
  26. interface ProcessMessage {
  27. (data: unknown, handler: {
  28. onMessage: (data: SseMessage) => void;
  29. onDetectedDiff: (data: SseDetectedDiff) => void;
  30. onFinalized: (data: SseFinalized) => void;
  31. }): void;
  32. }
  33. type DetectedDiff = Array<{
  34. data: SseDetectedDiff,
  35. applied: boolean,
  36. id: string,
  37. }>
  38. export const useEditorAssistant = (): {postMessage: PostMessage, processMessage: ProcessMessage, accept: () => void, reject: () => void } => {
  39. const positionRef = useRef<number>(0);
  40. const [detectedDiff, setDetectedDiff] = useState<DetectedDiff>();
  41. const { data: currentPageId } = useCurrentPageId();
  42. const { data: isEnableUnifiedMergeView, mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
  43. const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
  44. const ydocs = useSecondaryYdocs(isEnableUnifiedMergeView ?? false, { pageId: currentPageId ?? undefined, useSecondary: isEnableUnifiedMergeView ?? false });
  45. const postMessage: PostMessage = useCallback(async(threadId, userMessage, markdown) => {
  46. const response = await fetch('/_api/v3/openai/edit', {
  47. method: 'POST',
  48. headers: { 'Content-Type': 'application/json' },
  49. body: JSON.stringify({
  50. threadId,
  51. userMessage,
  52. markdown,
  53. }),
  54. });
  55. return response;
  56. }, []);
  57. const processMessage: ProcessMessage = useCallback((data, handler) => {
  58. handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
  59. handler.onMessage(data);
  60. });
  61. handleIfSuccessfullyParsed(data, SseDetectedDiffSchema, (data: SseDetectedDiff) => {
  62. mutateIsEnableUnifiedMergeView(true);
  63. setDetectedDiff((prev) => {
  64. const newData = { data, applied: false, id: crypto.randomUUID() };
  65. if (prev == null) {
  66. return [newData];
  67. }
  68. return [...prev, newData];
  69. });
  70. handler.onDetectedDiff(data);
  71. });
  72. handleIfSuccessfullyParsed(data, SseFinalizedSchema, (data: SseFinalized) => {
  73. handler.onFinalized(data);
  74. });
  75. }, [mutateIsEnableUnifiedMergeView]);
  76. const accept = useCallback(() => {
  77. acceptChange(codeMirrorEditor?.view);
  78. mutateIsEnableUnifiedMergeView(false);
  79. }, [codeMirrorEditor?.view, mutateIsEnableUnifiedMergeView]);
  80. const reject = useCallback(() => {
  81. rejectChange(codeMirrorEditor?.view);
  82. mutateIsEnableUnifiedMergeView(false);
  83. }, [codeMirrorEditor?.view, mutateIsEnableUnifiedMergeView]);
  84. useEffect(() => {
  85. const pendingDetectedDiff: DetectedDiff | undefined = detectedDiff?.filter(diff => diff.applied === false);
  86. if (ydocs?.secondaryDoc != null && pendingDetectedDiff != null && pendingDetectedDiff.length > 0) {
  87. // For debug
  88. // const testDetectedDiff = [
  89. // {
  90. // data: { diff: { retain: 9 } },
  91. // applied: false,
  92. // id: crypto.randomUUID(),
  93. // },
  94. // {
  95. // data: { diff: { delete: 5 } },
  96. // applied: false,
  97. // id: crypto.randomUUID(),
  98. // },
  99. // {
  100. // data: { diff: { insert: 'growi' } },
  101. // applied: false,
  102. // id: crypto.randomUUID(),
  103. // },
  104. // ];
  105. const ytext = ydocs.secondaryDoc.getText('codemirror');
  106. ydocs.secondaryDoc.transact(() => {
  107. pendingDetectedDiff.forEach((detectedDiff) => {
  108. if (isReplaceDiff(detectedDiff.data)) {
  109. // TODO: https://redmine.weseek.co.jp/issues/164330
  110. }
  111. // if (isInsertDiff(detectedDiff.data)) {
  112. // ytext.insert(positionRef.current, detectedDiff.data.diff.insert);
  113. // }
  114. // if (isDeleteDiff(detectedDiff.data)) {
  115. // ytext.delete(positionRef.current, detectedDiff.data.diff.delete);
  116. // }
  117. // if (isRetainDiff(detectedDiff.data)) {
  118. // positionRef.current += detectedDiff.data.diff.retain;
  119. // }
  120. });
  121. });
  122. // Mark as applied: true after applying to secondaryDoc
  123. setDetectedDiff((prev) => {
  124. const pendingDetectedDiffIds = pendingDetectedDiff.map(diff => diff.id);
  125. prev?.forEach((diff) => {
  126. if (pendingDetectedDiffIds.includes(diff.id)) {
  127. diff.applied = true;
  128. }
  129. });
  130. return prev;
  131. });
  132. // Set detectedDiff to undefined after applying all detectedDiff to secondaryDoc
  133. if (detectedDiff?.filter(detectedDiff => detectedDiff.applied === false).length === 0) {
  134. setDetectedDiff(undefined);
  135. positionRef.current = 0;
  136. }
  137. }
  138. }, [codeMirrorEditor, detectedDiff, ydocs?.secondaryDoc]);
  139. return {
  140. postMessage,
  141. processMessage,
  142. accept,
  143. reject,
  144. };
  145. };