diff --git a/src/app/(app)/problems/[problemId]/layout.tsx b/src/app/(app)/problems/[problemId]/layout.tsx index fe22fac..72db5c0 100644 --- a/src/app/(app)/problems/[problemId]/layout.tsx +++ b/src/app/(app)/problems/[problemId]/layout.tsx @@ -1,3 +1,4 @@ +import prisma from "@/lib/prisma"; import { notFound } from "next/navigation"; import { ProblemHeader } from "@/features/problems/components/header"; @@ -13,6 +14,19 @@ const Layout = async ({ children, params }: LayoutProps) => { return notFound(); } + const problem = await prisma.problem.findUnique({ + select: { + isPublished: true, + }, + where: { + id: problemId, + }, + }); + + if (!problem?.isPublished) { + return notFound(); + } + return (
diff --git a/src/app/actions/ai-improve.ts b/src/app/actions/ai-improve.ts new file mode 100644 index 0000000..d73a47a --- /dev/null +++ b/src/app/actions/ai-improve.ts @@ -0,0 +1,149 @@ +"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; +}; diff --git a/src/app/actions/ai-testcase.ts b/src/app/actions/ai-testcase.ts new file mode 100644 index 0000000..ddb6d4c --- /dev/null +++ b/src/app/actions/ai-testcase.ts @@ -0,0 +1,155 @@ +"use server"; + +import { + AITestCaseInput, + AITestCaseOutput, + AITestCaseOutputSchema, +} from "@/types/ai-testcase"; + +import { deepseek } from "@/lib/ai"; +import { CoreMessage, generateText } from "ai"; +import prisma from "@/lib/prisma"; + +/** + * + * @param input + * @returns + */ +export const generateAITestcase = async ( + input: AITestCaseInput +): Promise => { + const model = deepseek("deepseek-chat"); + + 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 problem statement to get the expected input structure, constraints, and output logic. Generate **novel, randomized** inputs/outputs that strictly adhere to the problem's requirements. Focus on: +Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers. + +1. **Input Data Structure**: Identify required formats (e.g., arrays, integers, strings). +2. **Input Constraints**: Determine valid ranges (e.g., array length: 2–100, integers: -1000 to 1000) and edge cases. +3. **Output Logic**: Ensure outputs correctly reflect problem-specific operations. +4. **Randomization**: + Vary input magnitudes (mix min/max/-edge values with mid-range randomness) + Use diverse data distributions (e.g., sorted/unsorted, negative/positive values) + Avoid patterns from existing examples + +Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers. + +Here is the problem description: + +${problemDetails} + +Respond **ONLY** with this JSON structure. +***Do not wrap the json codes in JSON markers*** : +{ + "expectedOutput": "Randomized output (e.g., [-5, 100] instead of [1, 2])", + "inputs": [ + { + "name": "Parameter 1", + "value": // Use string to wrap actual JSON types (arrays/numbers/strings) + }, + ... // Add parameters as needed + ] +} + + +`; + + // 发送请求给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 { + llmResponseJson = JSON.parse(text); + } 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 = AITestCaseOutputSchema.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; +}; diff --git a/src/app/actions/getProblem.ts b/src/app/actions/getProblem.ts new file mode 100644 index 0000000..99958c5 --- /dev/null +++ b/src/app/actions/getProblem.ts @@ -0,0 +1,63 @@ +// app/actions/get-problem-data.ts +"use server"; + +import prisma from "@/lib/prisma"; +import { Locale } from "@/generated/client"; +import { serialize } from "next-mdx-remote/serialize"; + +export async function getProblemData(problemId: string, locale?: string) { + const selectedLocale = locale as Locale; + + const problem = await prisma.problem.findUnique({ + where: { id: problemId }, + include: { + templates: true, + testcases: { + include: { inputs: true }, + }, + localizations: { + where: { + locale: selectedLocale, + }, + }, + }, + }); + + if (!problem) { + throw new Error("Problem not found"); + } + + const getContent = (type: string) => + problem.localizations.find((loc) => loc.type === type)?.content || ""; + + const rawDescription = getContent("DESCRIPTION"); + + const mdxDescription = await serialize(rawDescription, { + parseFrontmatter: false, + }); + + return { + id: problem.id, + displayId: problem.displayId, + difficulty: problem.difficulty, + isPublished: problem.isPublished, + timeLimit: problem.timeLimit, + memoryLimit: problem.memoryLimit, + title: getContent("TITLE"), + description: rawDescription, + mdxDescription, + solution: getContent("SOLUTION"), + templates: problem.templates.map((t) => ({ + language: t.language, + content: t.content, + })), + testcases: problem.testcases.map((tc) => ({ + id: tc.id, + expectedOutput: tc.expectedOutput, + inputs: tc.inputs.map((input) => ({ + name: input.name, + value: input.value, + })), + })), + }; +} diff --git a/src/app/actions/getProblemLocales.ts b/src/app/actions/getProblemLocales.ts new file mode 100644 index 0000000..57d8a46 --- /dev/null +++ b/src/app/actions/getProblemLocales.ts @@ -0,0 +1,14 @@ +// src/app/actions/getProblemLocales.ts +"use server"; + +import prisma from "@/lib/prisma"; + +export async function getProblemLocales(problemId: string): Promise { + const locales = await prisma.problemLocalization.findMany({ + where: { problemId }, + select: { locale: true }, + distinct: ["locale"], + }); + + return locales.map((l) => l.locale); +} diff --git a/src/components/ai-optimized-editor.tsx b/src/components/ai-optimized-editor.tsx new file mode 100644 index 0000000..adfd522 --- /dev/null +++ b/src/components/ai-optimized-editor.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { DiffEditor } from "@monaco-editor/react"; +import { optimizeCode } from "@/app/actions/ai-improve"; +import type { OptimizeCodeInput } from "@/types/ai-improve"; +import { CoreEditor } from "./core-editor"; // 引入你刚刚的组件 +// import { Loading } from "@/components/loading"; +import type { LanguageServerConfig } from "@/generated/client"; + +interface AIEditorWrapperProps { + language?: string; + value?: string; + path?: string; + problemId?: string; + languageServerConfigs?: LanguageServerConfig[]; + onChange?: (value: string) => void; + className?: string; +} + +export const AIEditorWrapper = ({ + language, + value, + path, + problemId, + languageServerConfigs, + onChange, +}: // className, +AIEditorWrapperProps) => { + const [currentCode, setCurrentCode] = useState(value ?? ""); + const [optimizedCode, setOptimizedCode] = useState(""); + const [isOptimizing, setIsOptimizing] = useState(false); + const [error, setError] = useState(null); + const [showDiff, setShowDiff] = useState(false); + + const handleCodeChange = useCallback( + (val: string) => { + setCurrentCode(val); + onChange?.(val); + }, + [onChange] + ); + + const handleOptimize = useCallback(async () => { + if (!problemId || !currentCode) 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("AI 优化失败,请稍后重试"); + console.error(err); + } finally { + setIsOptimizing(false); + } + }, [currentCode, problemId]); + + const handleApplyOptimized = useCallback(() => { + setCurrentCode(optimizedCode); + onChange?.(optimizedCode); + setShowDiff(false); + }, [optimizedCode, onChange]); + + return ( +
+
+ + + {showDiff && ( +
+ + +
+ )} +
+ + {error && ( +
{error}
+ )} + +
+ {showDiff ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/src/components/creater/edit-code-panel.tsx b/src/components/creater/edit-code-panel.tsx new file mode 100644 index 0000000..581a497 --- /dev/null +++ b/src/components/creater/edit-code-panel.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Language } from "@/generated/client"; +import { Button } from "@/components/ui/button"; +import React, { useEffect, useState } from "react"; +import { CoreEditor } from "@/components/core-editor"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { getProblemData } from "@/app/actions/getProblem"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { PanelLayout } from "@/features/problems/layouts/panel-layout"; +import { updateProblemTemplate } from "@/components/creater/problem-maintain"; + +interface Template { + language: string; + content: string; +} + +interface EditCodePanelProps { + problemId: string; +} + +export default function EditCodePanel({ problemId }: EditCodePanelProps) { + const [codeTemplate, setCodeTemplate] = useState