mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 23:12:23 +00:00
feat(ai): 添加 AI 代码优化功能
- 实现了 AI代码优化的功能,包括语法错误修复、性能提升、代码可读性增强等 - 新增 AIProblemEditor 组件,用于展示和编辑优化后的代码 - 在问题页面集成 AI 优化功能,用户可以切换普通编辑器和 AI 优化编辑器 - 添加了优化代码的输入输出类型定义和验证
This commit is contained in:
parent
443adf055b
commit
e1b5231f90
104
src/actions/ai-improve.ts
Normal file
104
src/actions/ai-improve.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OptimizeCodeInput,
|
||||||
|
OptimizeCodeOutput,
|
||||||
|
OptimizeCodeOutputSchema,
|
||||||
|
} from "@/types/ai-improve";
|
||||||
|
import { openai } from "@/lib/ai";
|
||||||
|
import { CoreMessage, generateText } from "ai";
|
||||||
|
import { prisma } from "@/lib/prisma"; // Prisma客户端
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用AI优化代码
|
||||||
|
* @param input 包含代码、错误信息、题目ID的输入
|
||||||
|
* @returns 优化后的代码和说明
|
||||||
|
*/
|
||||||
|
export const optimizeCode = async (
|
||||||
|
input: OptimizeCodeInput
|
||||||
|
): Promise<OptimizeCodeOutput> => {
|
||||||
|
const model = openai("gpt-4o-mini");
|
||||||
|
|
||||||
|
// 获取题目详情(如果提供了problemId)
|
||||||
|
let problemDetails = "";
|
||||||
|
if (input.problemId) {
|
||||||
|
try {
|
||||||
|
const problem = await prisma.problem.findUnique({
|
||||||
|
where: { problemId: input.problemId },
|
||||||
|
});
|
||||||
|
if (problem) {
|
||||||
|
problemDetails = `
|
||||||
|
Problem Requirements:
|
||||||
|
-------------------
|
||||||
|
Description: ${problem.description}
|
||||||
|
Input: ${problem.inputSpec}
|
||||||
|
Output: ${problem.outputSpec}
|
||||||
|
Test Cases: ${JSON.stringify(problem.testCases)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch problem details:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建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"]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 发送请求给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");
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResult.data;
|
||||||
|
};
|
@ -13,6 +13,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Dockview from "@/components/dockview";
|
import Dockview from "@/components/dockview";
|
||||||
import { useDockviewStore } from "@/stores/dockview";
|
import { useDockviewStore } from "@/stores/dockview";
|
||||||
|
import { AIProblemEditor } from "@/components/ai-optimized-editor";
|
||||||
|
|
||||||
interface ProblemPageProps {
|
interface ProblemPageProps {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
@ -38,11 +39,57 @@ export default function ProblemPage({
|
|||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const { setApi } = useDockviewStore();
|
const { setApi } = useDockviewStore();
|
||||||
const t = useTranslations("ProblemPage");
|
const t = useTranslations("ProblemPage");
|
||||||
|
const pathname = usePathname();
|
||||||
|
const problemId = pathname.split("/").pop(); // 从URL提取problemId
|
||||||
|
|
||||||
|
// AI优化相关状态
|
||||||
|
const [showAIEditor, setShowAIEditor] = useState(false);
|
||||||
|
const [userCode, setUserCode] = useState(`function example() {
|
||||||
|
// 初始代码
|
||||||
|
return "Hello World";
|
||||||
|
}`);
|
||||||
|
|
||||||
|
// 修改Code面板内容以包含切换功能
|
||||||
|
const CodeWithToggle = (
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIEditor(!showAIEditor)}
|
||||||
|
className="px-3 py-1 bg-primary text-primary-foreground rounded-md text-sm"
|
||||||
|
>
|
||||||
|
{showAIEditor ? "普通编辑器" : "AI优化编辑器"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showAIEditor && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAIEditor(false)}
|
||||||
|
className="px-3 py-1 bg-secondary text-secondary-foreground rounded-md text-sm"
|
||||||
|
>
|
||||||
|
返回编辑
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAIEditor ? (
|
||||||
|
<AIProblemEditor
|
||||||
|
initialCode={userCode}
|
||||||
|
problemId={problemId}
|
||||||
|
onCodeChange={setUserCode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// 原始Code组件保持不变
|
||||||
|
<div className="h-[500px]">
|
||||||
|
{Code}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKey((prevKey) => prevKey + 1);
|
setKey((prevKey) => prevKey + 1);
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
|
// 修改Dockview配置:更新Code面板引用
|
||||||
return (
|
return (
|
||||||
<Dockview
|
<Dockview
|
||||||
key={key}
|
key={key}
|
||||||
@ -106,7 +153,7 @@ export default function ProblemPage({
|
|||||||
tabComponent: "Code",
|
tabComponent: "Code",
|
||||||
params: {
|
params: {
|
||||||
icon: SquarePenIcon,
|
icon: SquarePenIcon,
|
||||||
content: Code,
|
content: CodeWithToggle, // 替换为带切换功能的容器
|
||||||
title: t("Code"),
|
title: t("Code"),
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
|
153
src/components/ai-optimized-editor.tsx
Normal file
153
src/components/ai-optimized-editor.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { DiffEditor } from "@monaco-editor/react";
|
||||||
|
import { optimizeCode } from "@/actions/ai-improve";
|
||||||
|
import { OptimizeCodeInput } from "@/types/ai-improve";
|
||||||
|
import { Loading } from "@/components/loading";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// 动态导入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: () => <Loading />,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function AIProblemEditor({
|
||||||
|
initialCode = "",
|
||||||
|
problemId = "",
|
||||||
|
onCodeChange
|
||||||
|
}: {
|
||||||
|
initialCode?: string;
|
||||||
|
problemId?: string;
|
||||||
|
onCodeChange?: (code: string) => void;
|
||||||
|
}) {
|
||||||
|
const [showDiff, setShowDiff] = useState(false);
|
||||||
|
const [optimizedCode, setOptimizedCode] = useState("");
|
||||||
|
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||||
|
const [currentCode, setCurrentCode] = useState(initialCode);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleCodeChange = useCallback((value: string | undefined) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setCurrentCode(value);
|
||||||
|
if (onCodeChange) {
|
||||||
|
onCodeChange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onCodeChange]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
onClick={handleOptimizeCode}
|
||||||
|
disabled={isOptimizing || !currentCode}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{isOptimizing ? "优化中..." : "AI优化代码"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showDiff && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDiff(!showDiff)}
|
||||||
|
className="px-4 py-2 bg-secondary text-secondary-foreground rounded-md"
|
||||||
|
>
|
||||||
|
{"隐藏对比"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-destructive/10 text-destructive rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDiff ? (
|
||||||
|
<DiffEditor
|
||||||
|
original={currentCode}
|
||||||
|
modified={optimizedCode}
|
||||||
|
language="typescript"
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Editor
|
||||||
|
height="500px"
|
||||||
|
language="typescript"
|
||||||
|
theme="vs-dark"
|
||||||
|
value={currentCode}
|
||||||
|
onChange={handleCodeChange}
|
||||||
|
options={{
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 14
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
8
src/lib/ai.ts
Normal file
8
src/lib/ai.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { createOpenAI } from "@ai-sdk/openai";
|
||||||
|
|
||||||
|
export const openai = createOpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY || "",
|
||||||
|
baseURL: process.env.OPENAI_BASE_URL || "",
|
||||||
|
});
|
19
src/types/ai-improve.ts
Normal file
19
src/types/ai-improve.ts
Normal file
@ -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<typeof OptimizeCodeInputSchema>;
|
||||||
|
|
||||||
|
// 优化代码的输出类型
|
||||||
|
export const OptimizeCodeOutputSchema = z.object({
|
||||||
|
optimizedCode: z.string(), // 优化后的代码
|
||||||
|
explanation: z.string(), // 优化说明
|
||||||
|
issuesFixed: z.array(z.string()).optional(), // 修复的问题列表
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OptimizeCodeOutput = z.infer<typeof OptimizeCodeOutputSchema>;
|
Loading…
Reference in New Issue
Block a user