Yuki Takei vor 9 Monaten
Ursprung
Commit
70d06e76ff

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

@@ -136,13 +136,20 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
     if (isEditorAssistant) {
       if (isEditorAssistantFormData(formData)) {
-        const response = await postMessageForEditorAssistant(threadId, formData);
+        const response = await postMessageForEditorAssistant({
+          threadId,
+          formData,
+        });
         return response;
       }
       return;
     }
     if (aiAssistantData?._id != null) {
-      const response = await postMessageForKnowledgeAssistant(aiAssistantData._id, threadId, formData);
+      const response = await postMessageForKnowledgeAssistant({
+        aiAssistantId: aiAssistantData._id,
+        threadId,
+        formData,
+      });
       return response;
     }
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);

+ 9 - 3
apps/app/src/features/openai/client/services/editor-assistant/use-editor-assistant.tsx

@@ -41,8 +41,14 @@ import { performSearchReplace } from './search-replace-engine';
 interface CreateThread {
   (): Promise<IThreadRelationHasId>;
 }
+
+type PostMesageArgs = {
+  threadId: string;
+  formData: FormData;
+}
+
 interface PostMessage {
-  (threadId: string, formData: FormData): Promise<Response>;
+  (args: PostMesageArgs): Promise<Response>;
 }
 interface ProcessMessage {
   (data: unknown, handler: {
@@ -162,7 +168,7 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     return response.data;
   }, [selectedAiAssistant?._id]);
 
-  const postMessage: PostMessage = useCallback(async(threadId, formData) => {
+  const postMessage: PostMessage = useCallback(async({ threadId, formData }) => {
     // Clear partial content info on new request
     setPartialContentInfo(null);
 
@@ -290,7 +296,7 @@ export const useEditorAssistant: UseEditorAssistant = () => {
     });
   }, [isGeneratingEditorText, mutateIsEnableUnifiedMergeView, clientEngine, yDocs]);
 
-  const selectTextHandler = useCallback((selectedText: string, selectedTextFirstLineNumber: number) => {
+  const selectTextHandler = useCallback(({ selectedText, selectedTextIndex, selectedTextFirstLineNumber }) => {
     setSelectedText(selectedText);
     lineRef.current = selectedTextFirstLineNumber;
   }, []);

+ 8 - 2
apps/app/src/features/openai/client/services/knowledge-assistant.tsx

@@ -27,8 +27,14 @@ interface CreateThread {
   (aiAssistantId: string, initialUserMessage: string): Promise<IThreadRelationHasId>;
 }
 
+type PostMessageArgs = {
+  aiAssistantId: string;
+  threadId: string;
+  formData: FormData;
+};
+
 interface PostMessage {
-  (aiAssistantId: string, threadId: string, formData: FormData): Promise<Response>;
+  (args: PostMessageArgs): Promise<Response>;
 }
 
 interface ProcessMessage {
@@ -106,7 +112,7 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
     return thread;
   }, [mutateThreadData]);
 
-  const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, formData) => {
+  const postMessage: PostMessage = useCallback(async({ aiAssistantId, threadId, formData }) => {
     const response = await fetch('/_api/v3/openai/message', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },

+ 123 - 0
apps/app/src/features/openai/interfaces/editor-assistant/sse-schemas.spec.ts

@@ -2,9 +2,11 @@ import {
   SseMessageSchema,
   SseDetectedDiffSchema,
   SseFinalizedSchema,
+  EditRequestBodySchema,
   type SseMessage,
   type SseDetectedDiff,
   type SseFinalized,
+  type EditRequestBody,
 } from './sse-schemas';
 
 describe('sse-schemas', () => {
@@ -216,7 +218,128 @@ describe('sse-schemas', () => {
     });
   });
 
+  describe('EditRequestBodySchema', () => {
+    test('should validate valid edit request body with all required fields', () => {
+      const validRequest = {
+        threadId: 'thread-123',
+        userMessage: 'Please update this code',
+        pageBody: 'function example() { return true; }',
+      };
+
+      const result = EditRequestBodySchema.safeParse(validRequest);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.threadId).toBe(validRequest.threadId);
+        expect(result.data.userMessage).toBe(validRequest.userMessage);
+        expect(result.data.pageBody).toBe(validRequest.pageBody);
+      }
+    });
+
+    test('should validate edit request with optional fields', () => {
+      const validRequest = {
+        threadId: 'thread-456',
+        aiAssistantId: 'assistant-789',
+        userMessage: 'Add logging functionality',
+        pageBody: 'const data = getData();',
+        selectedText: 'const data',
+        selectedPosition: 5,
+        isPageBodyPartial: true,
+        partialPageBodyStartIndex: 10,
+      };
+
+      const result = EditRequestBodySchema.safeParse(validRequest);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.aiAssistantId).toBe(validRequest.aiAssistantId);
+        expect(result.data.selectedText).toBe(validRequest.selectedText);
+        expect(result.data.selectedPosition).toBe(validRequest.selectedPosition);
+        expect(result.data.isPageBodyPartial).toBe(validRequest.isPageBodyPartial);
+        expect(result.data.partialPageBodyStartIndex).toBe(validRequest.partialPageBodyStartIndex);
+      }
+    });
+
+    test('should fail when threadId is missing', () => {
+      const invalidRequest = {
+        userMessage: 'Update code',
+        pageBody: 'code here',
+      };
+
+      const result = EditRequestBodySchema.safeParse(invalidRequest);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].path).toEqual(['threadId']);
+      }
+    });
+
+    test('should fail when userMessage is missing', () => {
+      const invalidRequest = {
+        threadId: 'thread-123',
+        pageBody: 'code here',
+      };
+
+      const result = EditRequestBodySchema.safeParse(invalidRequest);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].path).toEqual(['userMessage']);
+      }
+    });
+
+    test('should fail when pageBody is missing', () => {
+      const invalidRequest = {
+        threadId: 'thread-123',
+        userMessage: 'Update code',
+      };
+
+      const result = EditRequestBodySchema.safeParse(invalidRequest);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].path).toEqual(['pageBody']);
+      }
+    });
+
+    test('should validate when optional fields are omitted', () => {
+      const validRequest = {
+        threadId: 'thread-123',
+        userMessage: 'Simple update',
+        pageBody: 'function test() {}',
+      };
+
+      const result = EditRequestBodySchema.safeParse(validRequest);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.aiAssistantId).toBeUndefined();
+        expect(result.data.selectedText).toBeUndefined();
+        expect(result.data.selectedPosition).toBeUndefined();
+        expect(result.data.isPageBodyPartial).toBeUndefined();
+        expect(result.data.partialPageBodyStartIndex).toBeUndefined();
+      }
+    });
+
+    test('should allow extra fields (non-strict mode)', () => {
+      const validRequest = {
+        threadId: 'thread-123',
+        userMessage: 'Update code',
+        pageBody: 'code here',
+        extraField: 'ignored',
+      };
+
+      const result = EditRequestBodySchema.safeParse(validRequest);
+      expect(result.success).toBe(true);
+    });
+  });
+
   describe('Type inference', () => {
+    test('EditRequestBody type should match schema', () => {
+      const editRequest: EditRequestBody = {
+        threadId: 'thread-123',
+        userMessage: 'Test message',
+        pageBody: 'const test = true;',
+      };
+
+      const result = EditRequestBodySchema.safeParse(editRequest);
+      expect(result.success).toBe(true);
+    });
+
     test('SseMessage type should match schema', () => {
       const message: SseMessage = {
         appendedMessage: 'Test message',

+ 4 - 3
apps/app/src/features/openai/interfaces/editor-assistant/sse-schemas.ts

@@ -8,15 +8,16 @@ import { LlmEditorAssistantDiffSchema } from './llm-response-schemas';
 
 // Request schemas
 export const EditRequestBodySchema = z.object({
+  threadId: z.string(),
+  aiAssistantId: z.string().optional(),
   userMessage: z.string(),
   pageBody: z.string(),
+  selectedText: z.string().optional(),
+  selectedPosition: z.number().optional(),
   isPageBodyPartial: z.boolean().optional()
     .describe('Whether the page body is a partial content'),
   partialPageBodyStartIndex: z.number().optional()
     .describe('0-based index for the start of the partial page body'),
-  selectedText: z.string().optional(),
-  selectedPosition: z.number().optional(),
-  threadId: z.string().optional(),
 });
 
 // Type definitions

+ 1 - 1
apps/app/src/features/openai/interfaces/thread-relation.ts

@@ -12,7 +12,7 @@ export type ThreadType = typeof ThreadType[keyof typeof ThreadType];
 
 export interface IThreadRelation {
   userId: Ref<IUser>
-  aiAssistant: Ref<AiAssistant>
+  aiAssistant?: Ref<AiAssistant>
   threadId: string;
   title?: string;
   type: ThreadType;

+ 8 - 2
packages/editor/src/client/services/unified-merge-view/index.ts

@@ -24,14 +24,20 @@ export const acceptAllChunks = (view: EditorView): void => {
   }
 };
 
+type OnSelectedArgs = {
+  selectedText: string;
+  selectedTextIndex: number; // 0-based index in the selected text
+  selectedTextFirstLineNumber: number; // 0-based line number
+}
 
-type OnSelected = (selectedText: string, selectedTextFirstLineNumber: number) => void
+type OnSelected = (args: OnSelectedArgs) => void
 
 const processSelectedText = (editorView: EditorView | ViewUpdate, onSelected?: OnSelected) => {
   const selection = editorView.state.selection.main;
   const selectedText = editorView.state.sliceDoc(selection.from, selection.to);
+  const selectedTextIndex = selection.from;
   const selectedTextFirstLineNumber = editorView.state.doc.lineAt(selection.from).number - 1; // 0-based line number;
-  onSelected?.(selectedText, selectedTextFirstLineNumber);
+  onSelected?.({ selectedText, selectedTextIndex, selectedTextFirstLineNumber });
 };
 
 export const useTextSelectionEffect = (codeMirrorEditor?: UseCodeMirrorEditor, onSelected?: OnSelected): void => {