From fee5dfccdf2e38969c8189935dbb3302bdf93508 Mon Sep 17 00:00:00 2001 From: fly6516 Date: Sat, 14 Jun 2025 11:33:17 +0800 Subject: [PATCH] feat: migrate prototype of ai-optimized-editor feature --- src/app/actions/ai-improve.ts | 143 ++++++++++++++ src/components/ai-optimized-editor.tsx | 250 +++++++++++++++++++++++++ src/types/ai-improve.ts | 19 ++ 3 files changed, 412 insertions(+) create mode 100644 src/app/actions/ai-improve.ts create mode 100644 src/components/ai-optimized-editor.tsx create mode 100644 src/types/ai-improve.ts diff --git a/src/app/actions/ai-improve.ts b/src/app/actions/ai-improve.ts new file mode 100644 index 0000000..e438bf2 --- /dev/null +++ b/src/app/actions/ai-improve.ts @@ -0,0 +1,143 @@ +"use server"; + +import { + OptimizeCodeInput, + OptimizeCodeOutput, + OptimizeCodeOutputSchema, +} from "@/types/ai-improve"; +import { deepseek } from "@/lib/ai"; +import { CoreMessage, generateText } from "ai"; +import prisma from "@/lib/prisma"; + +/** + * 调用AI优化代码 + * @param input 包含代码、错误信息、题目ID的输入 + * @returns 优化后的代码和说明 + */ +export const optimizeCode = async ( + input: OptimizeCodeInput +): Promise => { + const model = deepseek("chat"); + + // 获取题目详情(如果提供了problemId) + let problemDetails = ""; + + if (input.problemId) { + try { + // 尝试获取英文描述 + const problemLocalizationEn = await prisma.problemLocalization.findUnique({ + where: { + problemId_locale_type: { + problemId: input.problemId, + locale: "en", + type: "DESCRIPTION", + }, + }, + include: { + problem: true, + }, + }); + + if (problemLocalizationEn) { + problemDetails = ` +Problem Requirements: +------------------- +Description: ${problemLocalizationEn.content} + `; + } else { + // 回退到中文描述 + const problemLocalizationZh = await prisma.problemLocalization.findUnique({ + where: { + problemId_locale_type: { + problemId: input.problemId, + locale: "zh", + type: "DESCRIPTION", + }, + }, + include: { + problem: true, + }, + }); + + if (problemLocalizationZh) { + problemDetails = ` +Problem Requirements: +------------------- +Description: ${problemLocalizationZh.content} + `; + console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); + } else { + problemDetails = "Problem description not found in any language."; + console.warn(`No description found for problemId: ${input.problemId}`); + } + } + } catch (error) { + console.error("Failed to fetch problem details:", error); + problemDetails = "Error fetching problem description."; + } + } + + + // 构建AI提示词 + const prompt = ` +Analyze the following programming code for potential errors, inefficiencies or code style issues. +Provide an optimized version of the code with explanations. Focus on: +1. Fixing any syntax errors +2. Improving performance +3. Enhancing code readability +4. Following best practices + +Original code: +\`\`\` +${input.code} +\`\`\` + +Error message (if any): ${input.error || "No error message provided"} + +${problemDetails} + +Respond ONLY with the JSON object containing the optimized code and explanations. +Format: +{ + "optimizedCode": "optimized code here", + "explanation": "explanation of changes made", + "issuesFixed": ["list of issues fixed"] +} +`; + console.log("Prompt:", prompt); + + // 发送请求给OpenAI + const messages: CoreMessage[] = [{ role: "user", content: prompt }]; + let text; + try { + const response = await generateText({ + model: model, + messages: messages, + }); + text = response.text; + } catch (error) { + console.error("Error generating text with OpenAI:", error); + throw new Error("Failed to generate response from OpenAI"); + } + + // 解析LLM响应 + let llmResponseJson; + try { + const cleanedText = text.trim(); + llmResponseJson = JSON.parse(cleanedText); + } catch (error) { + console.error("Failed to parse LLM response as JSON:", error); + console.error("LLM raw output:", text); + throw new Error("Invalid JSON response from LLM"); + } + + // 验证响应格式 + const validationResult = OptimizeCodeOutputSchema.safeParse(llmResponseJson); + if (!validationResult.success) { + console.error("Zod validation failed:", validationResult.error.format()); + throw new Error("Response validation failed"); + } + + console.log("LLM response:", llmResponseJson); + return validationResult.data; +}; \ No newline at end of file diff --git a/src/components/ai-optimized-editor.tsx b/src/components/ai-optimized-editor.tsx new file mode 100644 index 0000000..87d1c2c --- /dev/null +++ b/src/components/ai-optimized-editor.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useState } from "react"; +import { DiffEditor } from "@monaco-editor/react"; +import { optimizeCode } from "@/app/actions/ai-improve"; +import { OptimizeCodeInput } from "@/types/ai-improve"; +import dynamic from "next/dynamic"; +import { highlighter } from "@/lib/shiki"; +import type { editor } from "monaco-editor"; +import { Loading } from "@/components/loading"; +import { shikiToMonaco } from "@shikijs/monaco"; +import { useProblem } from "@/hooks/use-problem"; +import type { Monaco } from "@monaco-editor/react"; +import { useCallback, useEffect, useRef } from "react"; +import { connectToLanguageServer } from "@/lib/language-server"; +import type { MonacoLanguageClient } from "monaco-languageclient"; +import { DefaultEditorOptionConfig } from "@/config/editor-option"; + +// 动态导入Monaco Editor +const Editor = dynamic( + async () => { + await import("vscode"); + const monaco = await import("monaco-editor"); + + self.MonacoEnvironment = { + getWorker(_, label) { + if (label === "json") { + return new Worker( + new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url) + ); + } + if (label === "css" || label === "scss" || label === "less") { + return new Worker( + new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url) + ); + } + if (label === "html" || label === "handlebars" || label === "razor") { + return new Worker( + new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url) + ); + } + if (label === "typescript" || label === "javascript") { + return new Worker( + new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url) + ); + } + return new Worker( + new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url) + ); + }, + }; + const { loader } = await import("@monaco-editor/react"); + loader.config({ monaco }); + return (await import("@monaco-editor/react")).Editor; + }, + { + ssr: false, + loading: () => , + } +); + +export function AIProblemEditor({ + initialCode = "", + problemId = "", + onCodeChange + }: { + initialCode?: string; + problemId?: string; + onCodeChange?: (code: string) => void; +}) { + const { + editor, + setEditor, + setMarkers, + setWebSocket, + currentLang, + currentPath, + currentTheme, + currentValue, + changeValue, + currentEditorLanguageConfig, + currentLanguageServerConfig, + } = useProblem(); + + const monacoLanguageClientRef = useRef(null); + + // 保持原有AI优化的状态 + const [showDiff, setShowDiff] = useState(false); + const [optimizedCode, setOptimizedCode] = useState(""); + const [isOptimizing, setIsOptimizing] = useState(false); + const [error, setError] = useState(null); + + // 重用useProblem的状态管理 + const currentCode = currentValue || initialCode; + + const handleCodeChange = useCallback((value: string | undefined) => { + if (value !== undefined) { + changeValue(value); + if (onCodeChange) { + onCodeChange(value); + } + } + }, [onCodeChange, changeValue]); + + // 保持原有LSP连接逻辑 + const connectLSP = useCallback(async () => { + if (!(currentLang && editor)) return; + + if (monacoLanguageClientRef.current) { + monacoLanguageClientRef.current.stop(); + monacoLanguageClientRef.current = null; + setWebSocket(null); + } + + if (!currentEditorLanguageConfig || !currentLanguageServerConfig) return; + + try { + const { client: monacoLanguageClient, webSocket } = await connectToLanguageServer( + currentEditorLanguageConfig, + currentLanguageServerConfig + ); + monacoLanguageClientRef.current = monacoLanguageClient; + setWebSocket(webSocket); + } catch (error) { + console.error("Failed to connect to LSP:", error); + } + }, [ + currentEditorLanguageConfig, + currentLang, + currentLanguageServerConfig, + editor, + setWebSocket, + ]); + + useEffect(() => { + connectLSP(); + }, [connectLSP]); + + useEffect(() => { + return () => { + if (monacoLanguageClientRef.current) { + monacoLanguageClientRef.current.stop(); + monacoLanguageClientRef.current = null; + setWebSocket(null); + } + }; + }, [setWebSocket]); + + const handleEditorWillMount = useCallback((monaco: Monaco) => { + shikiToMonaco(highlighter, monaco); + }, []); + + const handleOnMount = useCallback( + async (editor: editor.IStandaloneCodeEditor) => { + setEditor(editor); + await connectLSP(); + }, + [setEditor, connectLSP] + ); + + const handleEditorValidation = useCallback( + (markers: editor.IMarker[]) => { + setMarkers(markers); + }, + [setMarkers] + ); + + const handleOptimizeCode = useCallback(async () => { + if (!currentCode || !problemId) return; + + setIsOptimizing(true); + setError(null); + + try { + const input: OptimizeCodeInput = { + code: currentCode, + problemId + }; + + const result = await optimizeCode(input); + setOptimizedCode(result.optimizedCode); + setShowDiff(true); + } catch (err) { + setError("代码优化失败,请重试"); + console.error(err); + } finally { + setIsOptimizing(false); + } + }, [currentCode, problemId]); + + return ( +
+ {/* 保持原有AI优化按钮 */} +
+ + + {showDiff && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + +
+ {showDiff ? ( + + ) : ( + } + className="h-full w-full" + /> + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/types/ai-improve.ts b/src/types/ai-improve.ts new file mode 100644 index 0000000..5d55afe --- /dev/null +++ b/src/types/ai-improve.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +// 优化代码的输入类型 +export const OptimizeCodeInputSchema = z.object({ + code: z.string(), // 用户输入的代码 + error: z.string().optional(), // 可选的错误信息 + problemId: z.string().optional(), // 可选的题目ID +}); + +export type OptimizeCodeInput = z.infer; + +// 优化代码的输出类型 +export const OptimizeCodeOutputSchema = z.object({ + optimizedCode: z.string(), // 优化后的代码 + explanation: z.string(), // 优化说明 + issuesFixed: z.array(z.string()).optional(), // 修复的问题列表 +}); + +export type OptimizeCodeOutput = z.infer; \ No newline at end of file