style: normalize quotes and indentation

This commit is contained in:
cfngc4594 2025-06-20 22:25:07 +08:00
parent e85b8e967b
commit cd1127e051
13 changed files with 1046 additions and 869 deletions

View File

@ -1,9 +1,9 @@
"use server"; "use server";
import { import {
OptimizeCodeInput, OptimizeCodeInput,
OptimizeCodeOutput, OptimizeCodeOutput,
OptimizeCodeOutputSchema, OptimizeCodeOutputSchema,
} from "@/types/ai-improve"; } from "@/types/ai-improve";
import { deepseek } from "@/lib/ai"; import { deepseek } from "@/lib/ai";
import { CoreMessage, generateText } from "ai"; import { CoreMessage, generateText } from "ai";
@ -15,71 +15,77 @@ import prisma from "@/lib/prisma";
* @returns * @returns
*/ */
export const optimizeCode = async ( export const optimizeCode = async (
input: OptimizeCodeInput input: OptimizeCodeInput
): Promise<OptimizeCodeOutput> => { ): Promise<OptimizeCodeOutput> => {
const model = deepseek("chat"); const model = deepseek("chat");
// 获取题目详情如果提供了problemId // 获取题目详情如果提供了problemId
let problemDetails = ""; let problemDetails = "";
if (input.problemId) { if (input.problemId) {
try { try {
// 尝试获取英文描述 // 尝试获取英文描述
const problemLocalizationEn = await prisma.problemLocalization.findUnique({ const problemLocalizationEn = await prisma.problemLocalization.findUnique(
where: { {
problemId_locale_type: { where: {
problemId: input.problemId, problemId_locale_type: {
locale: "en", problemId: input.problemId,
type: "DESCRIPTION", locale: "en",
}, type: "DESCRIPTION",
}, },
include: { },
problem: true, include: {
}, problem: true,
}); },
}
);
if (problemLocalizationEn) { if (problemLocalizationEn) {
problemDetails = ` problemDetails = `
Problem Requirements: Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationEn.content} Description: ${problemLocalizationEn.content}
`; `;
} else { } else {
// 回退到中文描述 // 回退到中文描述
const problemLocalizationZh = await prisma.problemLocalization.findUnique({ const problemLocalizationZh =
where: { await prisma.problemLocalization.findUnique({
problemId_locale_type: { where: {
problemId: input.problemId, problemId_locale_type: {
locale: "zh", problemId: input.problemId,
type: "DESCRIPTION", locale: "zh",
}, type: "DESCRIPTION",
}, },
include: { },
problem: true, include: {
}, problem: true,
}); },
});
if (problemLocalizationZh) { if (problemLocalizationZh) {
problemDetails = ` problemDetails = `
Problem Requirements: Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationZh.content} Description: ${problemLocalizationZh.content}
`; `;
console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); console.warn(
} else { `Fallback to Chinese description for problemId: ${input.problemId}`
problemDetails = "Problem description not found in any language."; );
console.warn(`No description found for problemId: ${input.problemId}`); } else {
} problemDetails = "Problem description not found in any language.";
} console.warn(
} catch (error) { `No description found for problemId: ${input.problemId}`
console.error("Failed to fetch problem details:", error); );
problemDetails = "Error fetching problem description.";
} }
}
} catch (error) {
console.error("Failed to fetch problem details:", error);
problemDetails = "Error fetching problem description.";
} }
}
// 构建AI提示词
// 构建AI提示词 const prompt = `
const prompt = `
Analyze the following programming code for potential errors, inefficiencies or code style issues. Analyze the following programming code for potential errors, inefficiencies or code style issues.
Provide an optimized version of the code with explanations. Focus on: Provide an optimized version of the code with explanations. Focus on:
1. Fixing any syntax errors 1. Fixing any syntax errors
@ -104,40 +110,40 @@ Format:
"issuesFixed": ["list of issues fixed"] "issuesFixed": ["list of issues fixed"]
} }
`; `;
console.log("Prompt:", prompt); console.log("Prompt:", prompt);
// 发送请求给OpenAI // 发送请求给OpenAI
const messages: CoreMessage[] = [{ role: "user", content: prompt }]; const messages: CoreMessage[] = [{ role: "user", content: prompt }];
let text; let text;
try { try {
const response = await generateText({ const response = await generateText({
model: model, model: model,
messages: messages, messages: messages,
}); });
text = response.text; text = response.text;
} catch (error) { } catch (error) {
console.error("Error generating text with OpenAI:", error); console.error("Error generating text with OpenAI:", error);
throw new Error("Failed to generate response from OpenAI"); throw new Error("Failed to generate response from OpenAI");
} }
// 解析LLM响应 // 解析LLM响应
let llmResponseJson; let llmResponseJson;
try { try {
const cleanedText = text.trim(); const cleanedText = text.trim();
llmResponseJson = JSON.parse(cleanedText); llmResponseJson = JSON.parse(cleanedText);
} catch (error) { } catch (error) {
console.error("Failed to parse LLM response as JSON:", error); console.error("Failed to parse LLM response as JSON:", error);
console.error("LLM raw output:", text); console.error("LLM raw output:", text);
throw new Error("Invalid JSON response from LLM"); throw new Error("Invalid JSON response from LLM");
} }
// 验证响应格式 // 验证响应格式
const validationResult = OptimizeCodeOutputSchema.safeParse(llmResponseJson); const validationResult = OptimizeCodeOutputSchema.safeParse(llmResponseJson);
if (!validationResult.success) { if (!validationResult.success) {
console.error("Zod validation failed:", validationResult.error.format()); console.error("Zod validation failed:", validationResult.error.format());
throw new Error("Response validation failed"); throw new Error("Response validation failed");
} }
console.log("LLM response:", llmResponseJson); console.log("LLM response:", llmResponseJson);
return validationResult.data; return validationResult.data;
}; };

View File

@ -1,83 +1,91 @@
"use server"; "use server";
import {AITestCaseInput, AITestCaseOutput, AITestCaseOutputSchema} from "@/types/ai-testcase"; import {
AITestCaseInput,
AITestCaseOutput,
AITestCaseOutputSchema,
} from "@/types/ai-testcase";
import { deepseek } from "@/lib/ai"; import { deepseek } from "@/lib/ai";
import { CoreMessage, generateText } from "ai"; import { CoreMessage, generateText } from "ai";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
/** /**
* *
* @param input * @param input
* @returns * @returns
*/ */
export const generateAITestcase = async ( export const generateAITestcase = async (
input: AITestCaseInput input: AITestCaseInput
): Promise<AITestCaseOutput> => { ): Promise<AITestCaseOutput> => {
const model = deepseek("deepseek-chat"); const model = deepseek("deepseek-chat");
let problemDetails = ""; let problemDetails = "";
if (input.problemId) { if (input.problemId) {
try { try {
// 尝试获取英文描述 // 尝试获取英文描述
const problemLocalizationEn = await prisma.problemLocalization.findUnique({ const problemLocalizationEn = await prisma.problemLocalization.findUnique(
where: { {
problemId_locale_type: { where: {
problemId: input.problemId, problemId_locale_type: {
locale: "en", problemId: input.problemId,
type: "DESCRIPTION", locale: "en",
}, type: "DESCRIPTION",
}, },
include: { },
problem: true, include: {
}, problem: true,
}); },
}
);
if (problemLocalizationEn) { if (problemLocalizationEn) {
problemDetails = ` problemDetails = `
Problem Requirements: Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationEn.content} Description: ${problemLocalizationEn.content}
`; `;
} else { } else {
// 回退到中文描述 // 回退到中文描述
const problemLocalizationZh = await prisma.problemLocalization.findUnique({ const problemLocalizationZh =
where: { await prisma.problemLocalization.findUnique({
problemId_locale_type: { where: {
problemId: input.problemId, problemId_locale_type: {
locale: "zh", problemId: input.problemId,
type: "DESCRIPTION", locale: "zh",
}, type: "DESCRIPTION",
}, },
include: { },
problem: true, include: {
}, problem: true,
}); },
});
if (problemLocalizationZh) { if (problemLocalizationZh) {
problemDetails = ` problemDetails = `
Problem Requirements: Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationZh.content} Description: ${problemLocalizationZh.content}
`; `;
console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); console.warn(
} else { `Fallback to Chinese description for problemId: ${input.problemId}`
problemDetails = "Problem description not found in any language."; );
console.warn(`No description found for problemId: ${input.problemId}`); } else {
} problemDetails = "Problem description not found in any language.";
} console.warn(
} catch (error) { `No description found for problemId: ${input.problemId}`
console.error("Failed to fetch problem details:", error); );
problemDetails = "Error fetching problem description.";
} }
}
} catch (error) {
console.error("Failed to fetch problem details:", error);
problemDetails = "Error fetching problem description.";
} }
}
// 构建AI提示词
const prompt = `
// 构建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: 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. Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers.
@ -111,40 +119,37 @@ Respond **ONLY** with this JSON structure.
`; `;
// 发送请求给OpenAI // 发送请求给OpenAI
const messages: CoreMessage[] = [{ role: "user", content: prompt }]; const messages: CoreMessage[] = [{ role: "user", content: prompt }];
let text; let text;
try { try {
const response = await generateText({ const response = await generateText({
model: model, model: model,
messages: messages, messages: messages,
}); });
text = response.text; text = response.text;
} catch (error) { } catch (error) {
console.error("Error generating text with OpenAI:", error); console.error("Error generating text with OpenAI:", error);
throw new Error("Failed to generate response from OpenAI"); throw new Error("Failed to generate response from OpenAI");
} }
// 解析LLM响应 // 解析LLM响应
let llmResponseJson; let llmResponseJson;
try { try {
llmResponseJson = JSON.parse(text) 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");
}
} catch (error) { console.log("LLM response:", llmResponseJson);
console.error("Failed to parse LLM response as JSON:", error); return validationResult.data;
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;
};

View File

@ -1,63 +1,63 @@
// app/actions/get-problem-data.ts // app/actions/get-problem-data.ts
'use server'; "use server";
import prisma from '@/lib/prisma'; import prisma from "@/lib/prisma";
import { Locale } from '@/generated/client'; import { Locale } from "@/generated/client";
import { serialize } from 'next-mdx-remote/serialize'; import { serialize } from "next-mdx-remote/serialize";
export async function getProblemData(problemId: string, locale?: string) { export async function getProblemData(problemId: string, locale?: string) {
const selectedLocale = locale as Locale; const selectedLocale = locale as Locale;
const problem = await prisma.problem.findUnique({ const problem = await prisma.problem.findUnique({
where: { id: problemId }, where: { id: problemId },
include: { include: {
templates: true, templates: true,
testcases: { testcases: {
include: { inputs: true } include: { inputs: true },
}, },
localizations: { localizations: {
where: { where: {
locale: selectedLocale, locale: selectedLocale,
},
},
}, },
}); },
},
});
if (!problem) { if (!problem) {
throw new Error('Problem not found'); throw new Error("Problem not found");
} }
const getContent = (type: string) => const getContent = (type: string) =>
problem.localizations.find(loc => loc.type === type)?.content || ''; problem.localizations.find((loc) => loc.type === type)?.content || "";
const rawDescription = getContent('DESCRIPTION'); const rawDescription = getContent("DESCRIPTION");
const mdxDescription = await serialize(rawDescription, { const mdxDescription = await serialize(rawDescription, {
parseFrontmatter: false, parseFrontmatter: false,
}); });
return { return {
id: problem.id, id: problem.id,
displayId: problem.displayId, displayId: problem.displayId,
difficulty: problem.difficulty, difficulty: problem.difficulty,
isPublished: problem.isPublished, isPublished: problem.isPublished,
timeLimit: problem.timeLimit, timeLimit: problem.timeLimit,
memoryLimit: problem.memoryLimit, memoryLimit: problem.memoryLimit,
title: getContent('TITLE'), title: getContent("TITLE"),
description: rawDescription, description: rawDescription,
mdxDescription, mdxDescription,
solution: getContent('SOLUTION'), solution: getContent("SOLUTION"),
templates: problem.templates.map(t => ({ templates: problem.templates.map((t) => ({
language: t.language, language: t.language,
content: t.content, content: t.content,
})), })),
testcases: problem.testcases.map(tc => ({ testcases: problem.testcases.map((tc) => ({
id: tc.id, id: tc.id,
expectedOutput: tc.expectedOutput, expectedOutput: tc.expectedOutput,
inputs: tc.inputs.map(input => ({ inputs: tc.inputs.map((input) => ({
name: input.name, name: input.name,
value: input.value, value: input.value,
})), })),
})), })),
}; };
} }

View File

@ -1,14 +1,14 @@
// src/app/actions/getProblemLocales.ts // src/app/actions/getProblemLocales.ts
'use server'; "use server";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
export async function getProblemLocales(problemId: string): Promise<string[]> { export async function getProblemLocales(problemId: string): Promise<string[]> {
const locales = await prisma.problemLocalization.findMany({ const locales = await prisma.problemLocalization.findMany({
where: { problemId }, where: { problemId },
select: { locale: true }, select: { locale: true },
distinct: ['locale'], distinct: ["locale"],
}); });
return locales.map(l => l.locale); return locales.map((l) => l.locale);
} }

View File

@ -19,24 +19,27 @@ interface AIEditorWrapperProps {
} }
export const AIEditorWrapper = ({ export const AIEditorWrapper = ({
language, language,
value, value,
path, path,
problemId, problemId,
languageServerConfigs, languageServerConfigs,
onChange, onChange,
// className, }: // className,
}: AIEditorWrapperProps) => { AIEditorWrapperProps) => {
const [currentCode, setCurrentCode] = useState(value ?? ""); const [currentCode, setCurrentCode] = useState(value ?? "");
const [optimizedCode, setOptimizedCode] = useState(""); const [optimizedCode, setOptimizedCode] = useState("");
const [isOptimizing, setIsOptimizing] = useState(false); const [isOptimizing, setIsOptimizing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showDiff, setShowDiff] = useState(false); const [showDiff, setShowDiff] = useState(false);
const handleCodeChange = useCallback((val: string) => { const handleCodeChange = useCallback(
setCurrentCode(val); (val: string) => {
onChange?.(val); setCurrentCode(val);
}, [onChange]); onChange?.(val);
},
[onChange]
);
const handleOptimize = useCallback(async () => { const handleOptimize = useCallback(async () => {
if (!problemId || !currentCode) return; if (!problemId || !currentCode) return;
@ -66,59 +69,59 @@ export const AIEditorWrapper = ({
}, [optimizedCode, onChange]); }, [optimizedCode, onChange]);
return ( return (
<div className="flex flex-col h-full w-full"> <div className="flex flex-col h-full w-full">
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<button <button
onClick={handleOptimize} onClick={handleOptimize}
disabled={isOptimizing} disabled={isOptimizing}
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90" className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90"
> >
{isOptimizing ? "优化中..." : "AI优化代码"} {isOptimizing ? "优化中..." : "AI优化代码"}
</button> </button>
{showDiff && ( {showDiff && (
<div className="space-x-2"> <div className="space-x-2">
<button <button
onClick={() => setShowDiff(false)} onClick={() => setShowDiff(false)}
className="px-4 py-2 bg-secondary text-white rounded" className="px-4 py-2 bg-secondary text-white rounded"
> >
</button> </button>
<button <button
onClick={handleApplyOptimized} onClick={handleApplyOptimized}
className="px-4 py-2 bg-green-500 text-white rounded" className="px-4 py-2 bg-green-500 text-white rounded"
> >
</button> </button>
</div> </div>
)}
</div>
{error && (
<div className="p-3 bg-red-100 text-red-600 rounded-md">{error}</div>
)} )}
<div className="flex-grow overflow-hidden">
{showDiff ? (
<DiffEditor
original={currentCode}
modified={optimizedCode}
language={language}
theme="vs-dark"
className="h-full w-full"
options={{ readOnly: true, minimap: { enabled: false } }}
/>
) : (
<CoreEditor
language={language}
value={currentCode}
path={path}
languageServerConfigs={languageServerConfigs}
onChange={handleCodeChange}
className="h-full w-full"
/>
)}
</div>
</div> </div>
{error && (
<div className="p-3 bg-red-100 text-red-600 rounded-md">{error}</div>
)}
<div className="flex-grow overflow-hidden">
{showDiff ? (
<DiffEditor
original={currentCode}
modified={optimizedCode}
language={language}
theme="vs-dark"
className="h-full w-full"
options={{ readOnly: true, minimap: { enabled: false } }}
/>
) : (
<CoreEditor
language={language}
value={currentCode}
path={path}
languageServerConfigs={languageServerConfigs}
onChange={handleCodeChange}
className="h-full w-full"
/>
)}
</div>
</div>
); );
}; };

View File

@ -1,14 +1,14 @@
"use client" "use client";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import { getProblemData } from '@/app/actions/getProblem'; import { getProblemData } from "@/app/actions/getProblem";
import { updateProblemTemplate } from '@/components/creater/problem-maintain'; import { updateProblemTemplate } from "@/components/creater/problem-maintain";
import { Label } from '@/components/ui/label'; import { Label } from "@/components/ui/label";
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CoreEditor } from '@/components/core-editor'; import { CoreEditor } from "@/components/core-editor";
import { Language } from '@/generated/client'; import { Language } from "@/generated/client";
import { toast } from 'sonner'; import { toast } from "sonner";
interface Template { interface Template {
language: string; language: string;
@ -21,7 +21,7 @@ interface EditCodePanelProps {
export default function EditCodePanel({ problemId }: EditCodePanelProps) { export default function EditCodePanel({ problemId }: EditCodePanelProps) {
const [codeTemplate, setCodeTemplate] = useState<Template>({ const [codeTemplate, setCodeTemplate] = useState<Template>({
language: 'cpp', language: "cpp",
content: `// 默认代码模板 for Problem ${problemId}`, content: `// 默认代码模板 for Problem ${problemId}`,
}); });
const [templates, setTemplates] = useState<Template[]>([]); const [templates, setTemplates] = useState<Template[]>([]);
@ -31,79 +31,81 @@ export default function EditCodePanel({ problemId }: EditCodePanelProps) {
try { try {
const problem = await getProblemData(problemId); const problem = await getProblemData(problemId);
setTemplates(problem.templates); setTemplates(problem.templates);
const sel = problem.templates.find(t => t.language === 'cpp') || problem.templates[0]; const sel =
problem.templates.find((t) => t.language === "cpp") ||
problem.templates[0];
if (sel) setCodeTemplate(sel); if (sel) setCodeTemplate(sel);
} catch (err) { } catch (err) {
console.error('加载问题数据失败:', err); console.error("加载问题数据失败:", err);
toast.error('加载问题数据失败'); toast.error("加载问题数据失败");
} }
} }
fetchTemplates(); fetchTemplates();
}, [problemId]); }, [problemId]);
const handleLanguageChange = (language: string) => { const handleLanguageChange = (language: string) => {
const sel = templates.find(t => t.language === language); const sel = templates.find((t) => t.language === language);
if (sel) setCodeTemplate(sel); if (sel) setCodeTemplate(sel);
}; };
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
try { try {
const res = await updateProblemTemplate( const res = await updateProblemTemplate(
problemId, problemId,
codeTemplate.language as Language, codeTemplate.language as Language,
codeTemplate.content codeTemplate.content
); );
if (res.success) { if (res.success) {
toast.success('保存成功'); toast.success("保存成功");
} else { } else {
toast.error('保存失败'); toast.error("保存失败");
} }
} catch (error) { } catch (error) {
console.error('保存异常:', error); console.error("保存异常:", error);
toast.error('保存异常'); toast.error("保存异常");
} }
}; };
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="language-select"></Label> <Label htmlFor="language-select"></Label>
<select <select
id="language-select" id="language-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700" className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={codeTemplate.language} value={codeTemplate.language}
onChange={e => handleLanguageChange(e.target.value)} onChange={(e) => handleLanguageChange(e.target.value)}
> >
{templates.map(t => ( {templates.map((t) => (
<option key={t.language} value={t.language}> <option key={t.language} value={t.language}>
{t.language.toUpperCase()} {t.language.toUpperCase()}
</option> </option>
))} ))}
</select> </select>
</div>
<div className="space-y-2">
<Label htmlFor="code-editor"></Label>
<div className="border rounded-md h-[500px]">
<CoreEditor
language={codeTemplate.language}
value={codeTemplate.content}
path={`/${problemId}.${codeTemplate.language}`}
onChange={value =>
setCodeTemplate({ ...codeTemplate, content: value || '' })
}
/>
</div>
</div>
<Button onClick={handleSave}></Button>
</div> </div>
</CardContent>
</Card> <div className="space-y-2">
<Label htmlFor="code-editor"></Label>
<div className="border rounded-md h-[500px]">
<CoreEditor
language={codeTemplate.language}
value={codeTemplate.content}
path={`/${problemId}.${codeTemplate.language}`}
onChange={(value) =>
setCodeTemplate({ ...codeTemplate, content: value || "" })
}
/>
</div>
</div>
<Button onClick={handleSave}></Button>
</div>
</CardContent>
</Card>
); );
} }

View File

@ -12,16 +12,25 @@ import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { Accordion } from "@/components/ui/accordion"; import { Accordion } from "@/components/ui/accordion";
import { VideoEmbed } from "@/components/content/video-embed"; import { VideoEmbed } from "@/components/content/video-embed";
import { toast } from "sonner"; import { toast } from "sonner";
import { updateProblemDescription, updateProblemTitle } from '@/components/creater/problem-maintain'; import {
updateProblemDescription,
updateProblemTitle,
} from "@/components/creater/problem-maintain";
import { Locale } from "@/generated/client"; import { Locale } from "@/generated/client";
export default function EditDescriptionPanel({ problemId }: { problemId: string }) { export default function EditDescriptionPanel({
problemId,
}: {
problemId: string;
}) {
const [locales, setLocales] = useState<string[]>([]); const [locales, setLocales] = useState<string[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>(""); const [currentLocale, setCurrentLocale] = useState<string>("");
const [customLocale, setCustomLocale] = useState(""); const [customLocale, setCustomLocale] = useState("");
const [description, setDescription] = useState({ title: "", content: "" }); const [description, setDescription] = useState({ title: "", content: "" });
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit"); const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">(
"edit"
);
useEffect(() => { useEffect(() => {
async function fetchLocales() { async function fetchLocales() {
@ -31,7 +40,7 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
if (langs.length > 0) setCurrentLocale(langs[0]); if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('获取语言列表失败'); toast.error("获取语言列表失败");
} }
} }
fetchLocales(); fetchLocales();
@ -42,10 +51,13 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
async function fetchProblem() { async function fetchProblem() {
try { try {
const data = await getProblemData(problemId, currentLocale); const data = await getProblemData(problemId, currentLocale);
setDescription({ title: data?.title || "", content: data?.description || "" }); setDescription({
title: data?.title || "",
content: data?.description || "",
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('加载题目描述失败'); toast.error("加载题目描述失败");
} }
} }
fetchProblem(); fetchProblem();
@ -63,111 +75,134 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
if (!currentLocale) { if (!currentLocale) {
toast.error('请选择语言'); toast.error("请选择语言");
return; return;
} }
try { try {
const locale = currentLocale as Locale; const locale = currentLocale as Locale;
const resTitle = await updateProblemTitle(problemId, locale, description.title); const resTitle = await updateProblemTitle(
const resDesc = await updateProblemDescription(problemId, locale, description.content); problemId,
locale,
description.title
);
const resDesc = await updateProblemDescription(
problemId,
locale,
description.content
);
if (resTitle.success && resDesc.success) { if (resTitle.success && resDesc.success) {
toast.success('保存成功'); toast.success("保存成功");
} else { } else {
toast.error('保存失败'); toast.error("保存失败");
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('保存异常'); toast.error("保存异常");
} }
}; };
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* 语言切换 */} {/* 语言切换 */}
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<div className="flex space-x-2">
<select
value={currentLocale}
onChange={(e) => setCurrentLocale(e.target.value)}
className="border rounded-md px-3 py-2"
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
<Input
placeholder="添加新语言"
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button onClick={handleAddCustomLocale}></Button>
</div>
</div>
{/* 标题输入 */}
<div className="space-y-2">
<Label htmlFor="description-title"></Label>
<Input
id="description-title"
value={description.title}
onChange={(e) => setDescription({ ...description, title: e.target.value })}
placeholder="输入题目标题"
/>
</div>
{/* 编辑/预览切换 */}
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <select
type="button" value={currentLocale}
variant={viewMode === "edit" ? "default" : "outline"} onChange={(e) => setCurrentLocale(e.target.value)}
onClick={() => setViewMode("edit")} className="border rounded-md px-3 py-2"
> >
{locales.map((locale) => (
</Button> <option key={locale} value={locale}>
<Button {locale}
type="button" </option>
variant={viewMode === "preview" ? "default" : "outline"} ))}
onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")} </select>
> <Input
{viewMode === "preview" ? "取消" : "预览"} placeholder="添加新语言"
</Button> value={customLocale}
<Button onChange={(e) => setCustomLocale(e.target.value)}
type="button" />
variant={viewMode === "compare" ? "default" : "outline"} <Button onClick={handleAddCustomLocale}></Button>
onClick={() => setViewMode("compare")}
>
</Button>
</div> </div>
</div>
{/* 编辑/预览区域 */} {/* 标题输入 */}
<div className={viewMode === "compare" ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div className="space-y-2">
{(viewMode === "edit" || viewMode === "compare") && ( <Label htmlFor="description-title"></Label>
<div className="relative h-[600px]"> <Input
<CoreEditor id="description-title"
value={description.content} value={description.title}
onChange={(newVal) => setDescription({ ...description, content: newVal || "" })} onChange={(e) =>
language="markdown" setDescription({ ...description, title: e.target.value })
className="absolute inset-0 rounded-md border border-input" }
/> placeholder="输入题目标题"
</div> />
)} </div>
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview source={description.content} components={{ Accordion, VideoEmbed }} />
</div>
)}
</div>
<Button onClick={handleSave}></Button> {/* 编辑/预览切换 */}
</CardContent> <div className="flex space-x-2">
</Card> <Button
type="button"
variant={viewMode === "edit" ? "default" : "outline"}
onClick={() => setViewMode("edit")}
>
</Button>
<Button
type="button"
variant={viewMode === "preview" ? "default" : "outline"}
onClick={() =>
setViewMode(viewMode === "preview" ? "edit" : "preview")
}
>
{viewMode === "preview" ? "取消" : "预览"}
</Button>
<Button
type="button"
variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode("compare")}
>
</Button>
</div>
{/* 编辑/预览区域 */}
<div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]">
<CoreEditor
value={description.content}
onChange={(newVal) =>
setDescription({ ...description, content: newVal || "" })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
)}
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview
source={description.content}
components={{ Accordion, VideoEmbed }}
/>
</div>
)}
</div>
<Button onClick={handleSave}></Button>
</CardContent>
</Card>
); );
} }

View File

@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { toast } from "sonner"; import { toast } from "sonner";
import { updateProblemDetail } from '@/components/creater/problem-maintain'; import { updateProblemDetail } from "@/components/creater/problem-maintain";
import { Difficulty } from "@/generated/client"; import { Difficulty } from "@/generated/client";
export default function EditDetailPanel({ problemId }: { problemId: string }) { export default function EditDetailPanel({ problemId }: { problemId: string }) {
@ -32,15 +32,15 @@ export default function EditDetailPanel({ problemId }: { problemId: string }) {
}); });
} catch (error) { } catch (error) {
console.error("获取题目信息失败:", error); console.error("获取题目信息失败:", error);
toast.error('加载详情失败'); toast.error("加载详情失败");
} }
} }
fetchData(); fetchData();
}, [problemId]); }, [problemId]);
const handleNumberInputChange = ( const handleNumberInputChange = (
e: React.ChangeEvent<HTMLInputElement>, e: React.ChangeEvent<HTMLInputElement>,
field: keyof typeof problemDetails field: keyof typeof problemDetails
) => { ) => {
const value = parseInt(e.target.value, 10); const value = parseInt(e.target.value, 10);
if (!isNaN(value)) { if (!isNaN(value)) {
@ -66,92 +66,95 @@ export default function EditDetailPanel({ problemId }: { problemId: string }) {
isPublished: problemDetails.isPublished, isPublished: problemDetails.isPublished,
}); });
if (res.success) { if (res.success) {
toast.success('保存成功'); toast.success("保存成功");
} else { } else {
toast.error('保存失败'); toast.error("保存失败");
} }
} catch (err) { } catch (err) {
console.error('保存异常:', err); console.error("保存异常:", err);
toast.error('保存异常'); toast.error("保存异常");
} }
}; };
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="display-id">ID</Label> <Label htmlFor="display-id">ID</Label>
<Input <Input
id="display-id" id="display-id"
type="number" type="number"
value={problemDetails.displayId} value={problemDetails.displayId}
onChange={(e) => handleNumberInputChange(e, "displayId")} onChange={(e) => handleNumberInputChange(e, "displayId")}
placeholder="输入显示ID" placeholder="输入显示ID"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty-select"></Label>
<select
id="difficulty-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={problemDetails.difficulty}
onChange={handleDifficultyChange}
>
<option value="EASY"></option>
<option value="MEDIUM"></option>
<option value="HARD"></option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="time-limit"> (ms)</Label>
<Input
id="time-limit"
type="number"
value={problemDetails.timeLimit}
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
placeholder="输入时间限制"
/>
</div>
<div className="space-y-2">
<Label htmlFor="memory-limit"> ()</Label>
<Input
id="memory-limit"
type="number"
value={problemDetails.memoryLimit}
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
placeholder="输入内存限制"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<input
id="is-published"
type="checkbox"
checked={problemDetails.isPublished}
onChange={(e) =>
setProblemDetails({ ...problemDetails, isPublished: e.target.checked })
}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2"
/> />
<Label htmlFor="is-published" className="text-sm font-medium">
</Label>
</div> </div>
<div className="space-y-2">
<Button type="button" onClick={handleSave}> <Label htmlFor="difficulty-select"></Label>
<select
</Button> id="difficulty-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={problemDetails.difficulty}
onChange={handleDifficultyChange}
>
<option value="EASY"></option>
<option value="MEDIUM"></option>
<option value="HARD"></option>
</select>
</div>
</div> </div>
</CardContent>
</Card> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="time-limit"> (ms)</Label>
<Input
id="time-limit"
type="number"
value={problemDetails.timeLimit}
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
placeholder="输入时间限制"
/>
</div>
<div className="space-y-2">
<Label htmlFor="memory-limit"> ()</Label>
<Input
id="memory-limit"
type="number"
value={problemDetails.memoryLimit}
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
placeholder="输入内存限制"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<input
id="is-published"
type="checkbox"
checked={problemDetails.isPublished}
onChange={(e) =>
setProblemDetails({
...problemDetails,
isPublished: e.target.checked,
})
}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2"
/>
<Label htmlFor="is-published" className="text-sm font-medium">
</Label>
</div>
<Button type="button" onClick={handleSave}>
</Button>
</div>
</CardContent>
</Card>
); );
} }

View File

@ -12,16 +12,22 @@ import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { Accordion } from "@/components/ui/accordion"; import { Accordion } from "@/components/ui/accordion";
import { VideoEmbed } from "@/components/content/video-embed"; import { VideoEmbed } from "@/components/content/video-embed";
import { toast } from "sonner"; import { toast } from "sonner";
import { updateProblemSolution } from '@/components/creater/problem-maintain'; import { updateProblemSolution } from "@/components/creater/problem-maintain";
import { Locale } from "@/generated/client"; import { Locale } from "@/generated/client";
export default function EditSolutionPanel({ problemId }: { problemId: string }) { export default function EditSolutionPanel({
problemId,
}: {
problemId: string;
}) {
const [locales, setLocales] = useState<string[]>([]); const [locales, setLocales] = useState<string[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>(""); const [currentLocale, setCurrentLocale] = useState<string>("");
const [customLocale, setCustomLocale] = useState(""); const [customLocale, setCustomLocale] = useState("");
const [solution, setSolution] = useState({ title: "", content: "" }); const [solution, setSolution] = useState({ title: "", content: "" });
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit"); const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">(
"edit"
);
useEffect(() => { useEffect(() => {
async function fetchLocales() { async function fetchLocales() {
@ -31,7 +37,7 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
if (langs.length > 0) setCurrentLocale(langs[0]); if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('获取语言列表失败'); toast.error("获取语言列表失败");
} }
} }
fetchLocales(); fetchLocales();
@ -42,10 +48,13 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
async function fetchSolution() { async function fetchSolution() {
try { try {
const data = await getProblemData(problemId, currentLocale); const data = await getProblemData(problemId, currentLocale);
setSolution({ title: (data?.title || "") + " 解析", content: data?.solution || "" }); setSolution({
title: (data?.title || "") + " 解析",
content: data?.solution || "",
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('加载题目解析失败'); toast.error("加载题目解析失败");
} }
} }
fetchSolution(); fetchSolution();
@ -53,7 +62,7 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
const handleAddCustomLocale = () => { const handleAddCustomLocale = () => {
if (customLocale && !locales.includes(customLocale)) { if (customLocale && !locales.includes(customLocale)) {
setLocales(prev => [...prev, customLocale]); setLocales((prev) => [...prev, customLocale]);
setCurrentLocale(customLocale); setCurrentLocale(customLocale);
setCustomLocale(""); setCustomLocale("");
setSolution({ title: "", content: "" }); setSolution({ title: "", content: "" });
@ -62,95 +71,134 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
if (!currentLocale) { if (!currentLocale) {
toast.error('请选择语言'); toast.error("请选择语言");
return; return;
} }
try { try {
const locale = currentLocale as Locale; const locale = currentLocale as Locale;
const res = await updateProblemSolution(problemId, locale, solution.content); const res = await updateProblemSolution(
problemId,
locale,
solution.content
);
if (res.success) { if (res.success) {
toast.success('保存成功'); toast.success("保存成功");
} else { } else {
toast.error('保存失败'); toast.error("保存失败");
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error('保存异常'); toast.error("保存异常");
} }
}; };
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* 语言切换 */} {/* 语言切换 */}
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<div className="flex space-x-2">
<select
value={currentLocale}
onChange={(e) => setCurrentLocale(e.target.value)}
className="border rounded-md px-3 py-2"
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
<Input
placeholder="添加新语言"
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button type="button" onClick={handleAddCustomLocale}></Button>
</div>
</div>
{/* 标题输入 (仅展示) */}
<div className="space-y-2">
<Label htmlFor="solution-title"></Label>
<Input
id="solution-title"
value={solution.title}
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
placeholder="输入题解标题"
disabled
/>
</div>
{/* 编辑/预览切换 */}
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button type="button" variant={viewMode === "edit" ? "default" : "outline"} onClick={() => setViewMode("edit")}></Button> <select
<Button type="button" variant={viewMode === "preview" ? "default" : "outline"} onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")}> value={currentLocale}
{viewMode === "preview" ? "取消" : "预览"} onChange={(e) => setCurrentLocale(e.target.value)}
className="border rounded-md px-3 py-2"
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
<Input
placeholder="添加新语言"
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button type="button" onClick={handleAddCustomLocale}>
</Button> </Button>
<Button type="button" variant={viewMode === "compare" ? "default" : "outline"} onClick={() => setViewMode("compare")}></Button>
</div> </div>
</div>
{/* 编辑/预览区域 */} {/* 标题输入 (仅展示) */}
<div className={viewMode === "compare" ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div className="space-y-2">
{(viewMode === "edit" || viewMode === "compare") && ( <Label htmlFor="solution-title"></Label>
<div className="relative h-[600px]"> <Input
<CoreEditor id="solution-title"
value={solution.content} value={solution.title}
onChange={(val) => setSolution({ ...solution, content: val || "" })} onChange={(e) =>
language="markdown" setSolution({ ...solution, title: e.target.value })
className="absolute inset-0 rounded-md border border-input" }
/> placeholder="输入题解标题"
</div> disabled
)} />
{viewMode !== "edit" && ( </div>
<div className="prose dark:prose-invert">
<MdxPreview source={solution.content} components={{ Accordion, VideoEmbed }} />
</div>
)}
</div>
<Button type="button" onClick={handleSave}></Button> {/* 编辑/预览切换 */}
</CardContent> <div className="flex space-x-2">
</Card> <Button
type="button"
variant={viewMode === "edit" ? "default" : "outline"}
onClick={() => setViewMode("edit")}
>
</Button>
<Button
type="button"
variant={viewMode === "preview" ? "default" : "outline"}
onClick={() =>
setViewMode(viewMode === "preview" ? "edit" : "preview")
}
>
{viewMode === "preview" ? "取消" : "预览"}
</Button>
<Button
type="button"
variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode("compare")}
>
</Button>
</div>
{/* 编辑/预览区域 */}
<div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]">
<CoreEditor
value={solution.content}
onChange={(val) =>
setSolution({ ...solution, content: val || "" })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
)}
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview
source={solution.content}
components={{ Accordion, VideoEmbed }}
/>
</div>
)}
</div>
<Button type="button" onClick={handleSave}>
</Button>
</CardContent>
</Card>
); );
} }

View File

@ -4,9 +4,9 @@ import React, { useState, useEffect } from "react";
import { generateAITestcase } from "@/app/actions/ai-testcase"; import { generateAITestcase } from "@/app/actions/ai-testcase";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { import {
addProblemTestcase, addProblemTestcase,
updateProblemTestcase, updateProblemTestcase,
deleteProblemTestcase, deleteProblemTestcase,
} from "@/components/creater/problem-maintain"; } from "@/components/creater/problem-maintain";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -15,210 +15,249 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner"; import { toast } from "sonner";
interface Testcase { interface Testcase {
id: string; id: string;
expectedOutput: string; expectedOutput: string;
inputs: { name: string; value: string }[]; inputs: { name: string; value: string }[];
} }
export default function EditTestcasePanel({ problemId }: { problemId: string }) { export default function EditTestcasePanel({
const [testcases, setTestcases] = useState<Testcase[]>([]); problemId,
const [isGenerating, setIsGenerating] = useState(false); }: {
problemId: string;
}) {
const [testcases, setTestcases] = useState<Testcase[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
// 加载测试用例 // 加载测试用例
useEffect(() => { useEffect(() => {
async function fetch() { async function fetch() {
try { try {
const data = await getProblemData(problemId); const data = await getProblemData(problemId);
setTestcases(data.testcases || []); setTestcases(data.testcases || []);
} catch (err) { } catch (err) {
console.error("加载测试用例失败:", err); console.error("加载测试用例失败:", err);
toast.error("加载测试用例失败"); toast.error("加载测试用例失败");
} }
}
fetch();
}, [problemId]);
// 本地添加测试用例
const handleAddTestcase = () =>
setTestcases((prev) => [
...prev,
{
id: `new-${Date.now()}-${Math.random()}`,
expectedOutput: "",
inputs: [{ name: "input1", value: "" }],
},
]);
// AI 生成测试用例
const handleAITestcase = async () => {
setIsGenerating(true);
try {
const ai = await generateAITestcase({ problemId });
setTestcases((prev) => [
...prev,
{
id: `new-${Date.now()}-${Math.random()}`,
expectedOutput: ai.expectedOutput,
inputs: ai.inputs,
},
]);
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
} catch (err) {
console.error(err);
toast.error("AI 生成测试用例失败");
} finally {
setIsGenerating(false);
}
};
// 删除测试用例(本地 + 服务器)
const handleRemoveTestcase = async (idx: number) => {
const tc = testcases[idx];
if (!tc.id.startsWith("new-")) {
try {
const res = await deleteProblemTestcase(problemId, tc.id);
if (res.success) toast.success("删除测试用例成功");
else toast.error("删除测试用例失败");
} catch (err) {
console.error(err);
toast.error("删除测试用例异常");
}
}
setTestcases((prev) => prev.filter((_, i) => i !== idx));
};
// 修改预期输出
const handleExpectedOutputChange = (idx: number, val: string) =>
setTestcases((prev) => {
const c = [...prev];
c[idx] = { ...c[idx], expectedOutput: val };
return c;
});
// 修改输入参数
const handleInputChange = (
tIdx: number,
iIdx: number,
field: "name" | "value",
val: string
) =>
setTestcases((prev) => {
const c = [...prev];
const newInputs = [...c[tIdx].inputs];
newInputs[iIdx] = { ...newInputs[iIdx], [field]: val };
c[tIdx] = { ...c[tIdx], inputs: newInputs };
return c;
});
// 添加输入参数
const handleAddInput = (tIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
const inputs = [
...c[tIdx].inputs,
{ name: `input${c[tIdx].inputs.length + 1}`, value: "" },
];
c[tIdx] = { ...c[tIdx], inputs };
return c;
});
// 删除输入参数
const handleRemoveInput = (tIdx: number, iIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
const inputs = c[tIdx].inputs.filter((_, i) => i !== iIdx);
c[tIdx] = { ...c[tIdx], inputs };
return c;
});
// 保存所有测试用例,并刷新最新数据
const handleSaveAll = async () => {
try {
for (let i = 0; i < testcases.length; i++) {
const tc = testcases[i];
if (
tc.expectedOutput.trim() === "" ||
tc.inputs.some((inp) => !inp.name.trim() || !inp.value.trim())
) {
toast.error(`${i + 1} 个测试用例存在空的输入或输出,保存失败`);
return;
} }
fetch();
}, [problemId]);
// 本地添加测试用例 if (tc.id.startsWith("new-")) {
const handleAddTestcase = () => const res = await addProblemTestcase(
setTestcases((prev) => [ problemId,
...prev, tc.expectedOutput,
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: "", inputs: [{ name: "input1", value: "" }] }, tc.inputs
]); );
if (res.success) {
// AI 生成测试用例 toast.success(`新增测试用例 ${i + 1} 成功`);
const handleAITestcase = async () => { } else {
setIsGenerating(true); toast.error(`新增测试用例 ${i + 1} 失败`);
try { }
const ai = await generateAITestcase({ problemId }); } else {
setTestcases((prev) => [ const res = await updateProblemTestcase(
...prev, problemId,
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs }, tc.id,
]); tc.expectedOutput,
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); tc.inputs
} catch (err) { );
console.error(err); if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`);
toast.error("AI 生成测试用例失败"); else toast.error(`更新测试用例 ${i + 1} 失败`);
} finally {
setIsGenerating(false);
} }
}; }
// 删除测试用例(本地 + 服务器) // 保存完成后刷新最新测试用例
const handleRemoveTestcase = async (idx: number) => { const data = await getProblemData(problemId);
const tc = testcases[idx]; setTestcases(data.testcases || []);
if (!tc.id.startsWith("new-")) { toast.success("测试用例保存并刷新成功");
try { } catch (err) {
const res = await deleteProblemTestcase(problemId, tc.id); console.error(err);
if (res.success) toast.success("删除测试用例成功"); toast.error("保存测试用例异常");
else toast.error("删除测试用例失败"); }
} catch (err) { };
console.error(err);
toast.error("删除测试用例异常");
}
}
setTestcases((prev) => prev.filter((_, i) => i !== idx));
};
// 修改预期输出 return (
const handleExpectedOutputChange = (idx: number, val: string) => <Card className="w-full">
setTestcases((prev) => { <CardHeader>
const c = [...prev]; <CardTitle></CardTitle>
c[idx] = { ...c[idx], expectedOutput: val }; <div className="flex items-center space-x-2">
return c; <Button onClick={handleAddTestcase}></Button>
}); <Button onClick={handleAITestcase} disabled={isGenerating}>
{isGenerating ? "生成中..." : "使用AI生成测试用例"}
// 修改输入参数 </Button>
const handleInputChange = ( <Button variant="secondary" onClick={handleSaveAll}>
tIdx: number,
iIdx: number, </Button>
field: "name" | "value", </div>
val: string </CardHeader>
) => <CardContent className="space-y-6">
setTestcases((prev) => { {testcases.map((tc, idx) => (
const c = [...prev]; <div key={tc.id} className="border p-4 rounded space-y-4">
const newInputs = [...c[tIdx].inputs]; <div className="flex justify-between items-center">
newInputs[iIdx] = { ...newInputs[iIdx], [field]: val }; <h3 className="font-medium"> {idx + 1}</h3>
c[tIdx] = { ...c[tIdx], inputs: newInputs }; <Button
return c; variant="destructive"
}); onClick={() => handleRemoveTestcase(idx)}
>
// 添加输入参数
const handleAddInput = (tIdx: number) => </Button>
setTestcases((prev) => { </div>
const c = [...prev]; <div className="space-y-2">
const inputs = [...c[tIdx].inputs, { name: `input${c[tIdx].inputs.length + 1}`, value: "" }]; <Label></Label>
c[tIdx] = { ...c[tIdx], inputs }; <Input
return c; value={tc.expectedOutput}
}); onChange={(e) =>
handleExpectedOutputChange(idx, e.target.value)
// 删除输入参数
const handleRemoveInput = (tIdx: number, iIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
const inputs = c[tIdx].inputs.filter((_, i) => i !== iIdx);
c[tIdx] = { ...c[tIdx], inputs };
return c;
});
// 保存所有测试用例,并刷新最新数据
const handleSaveAll = async () => {
try {
for (let i = 0; i < testcases.length; i++) {
const tc = testcases[i];
if (tc.expectedOutput.trim() === "" || tc.inputs.some(inp => !inp.name.trim() || !inp.value.trim())) {
toast.error(`${i + 1} 个测试用例存在空的输入或输出,保存失败`);
return;
} }
placeholder="输入预期输出"
if (tc.id.startsWith("new-")) { />
const res = await addProblemTestcase(problemId, tc.expectedOutput, tc.inputs); </div>
if (res.success) { <div className="space-y-2">
toast.success(`新增测试用例 ${i + 1} 成功`); <div className="flex justify-between items-center">
} else { <Label></Label>
toast.error(`新增测试用例 ${i + 1} 失败`); <Button onClick={() => handleAddInput(idx)}></Button>
} </div>
} else { {tc.inputs.map((inp, iIdx) => (
const res = await updateProblemTestcase(problemId, tc.id, tc.expectedOutput, tc.inputs); <div key={iIdx} className="grid grid-cols-2 gap-4">
if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`); <div>
else toast.error(`更新测试用例 ${i + 1} 失败`); <Label></Label>
} <Input
} value={inp.name}
onChange={(e) =>
// 保存完成后刷新最新测试用例 handleInputChange(idx, iIdx, "name", e.target.value)
const data = await getProblemData(problemId); }
setTestcases(data.testcases || []); placeholder="参数名称"
toast.success("测试用例保存并刷新成功"); />
} catch (err) { </div>
console.error(err); <div>
toast.error("保存测试用例异常"); <Label></Label>
} <Input
}; value={inp.value}
onChange={(e) =>
return ( handleInputChange(idx, iIdx, "value", e.target.value)
<Card className="w-full"> }
<CardHeader> placeholder="参数值"
<CardTitle></CardTitle> />
<div className="flex items-center space-x-2"> </div>
<Button onClick={handleAddTestcase}></Button> {iIdx > 0 && (
<Button onClick={handleAITestcase} disabled={isGenerating}> <Button
{isGenerating ? "生成中..." : "使用AI生成测试用例"} variant="outline"
</Button> onClick={() => handleRemoveInput(idx, iIdx)}
<Button variant="secondary" onClick={handleSaveAll}> >
</Button> </Button>
)}
</div> </div>
</CardHeader> ))}
<CardContent className="space-y-6"> </div>
{testcases.map((tc, idx) => ( </div>
<div key={tc.id} className="border p-4 rounded space-y-4"> ))}
<div className="flex justify-between items-center"> </CardContent>
<h3 className="font-medium"> {idx + 1}</h3> </Card>
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}> );
</Button>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={tc.expectedOutput}
onChange={(e) => handleExpectedOutputChange(idx, e.target.value)}
placeholder="输入预期输出"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label></Label>
<Button onClick={() => handleAddInput(idx)}></Button>
</div>
{tc.inputs.map((inp, iIdx) => (
<div key={iIdx} className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
value={inp.name}
onChange={(e) => handleInputChange(idx, iIdx, "name", e.target.value)}
placeholder="参数名称"
/>
</div>
<div>
<Label></Label>
<Input
value={inp.value}
onChange={(e) => handleInputChange(idx, iIdx, "value", e.target.value)}
placeholder="参数值"
/>
</div>
{iIdx > 0 && (
<Button variant="outline" onClick={() => handleRemoveInput(idx, iIdx)}>
</Button>
)}
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
);
} }

View File

@ -2,9 +2,23 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { Difficulty, Locale, ProblemContentType, Language } from "@/generated/client"; import {
Difficulty,
Locale,
ProblemContentType,
Language,
} from "@/generated/client";
export async function updateProblemDetail(problemId: string, data: { displayId?: number; difficulty?: Difficulty; timeLimit?: number; memoryLimit?: number; isPublished?: boolean }) { export async function updateProblemDetail(
problemId: string,
data: {
displayId?: number;
difficulty?: Difficulty;
timeLimit?: number;
memoryLimit?: number;
isPublished?: boolean;
}
) {
try { try {
const updatedProblem = await prisma.problem.update({ const updatedProblem = await prisma.problem.update({
where: { id: problemId }, where: { id: problemId },
@ -13,8 +27,8 @@ export async function updateProblemDetail(problemId: string, data: { displayId?:
difficulty: data.difficulty, difficulty: data.difficulty,
timeLimit: data.timeLimit, timeLimit: data.timeLimit,
memoryLimit: data.memoryLimit, memoryLimit: data.memoryLimit,
isPublished: data.isPublished isPublished: data.isPublished,
} },
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
@ -25,25 +39,29 @@ export async function updateProblemDetail(problemId: string, data: { displayId?:
} }
} }
export async function updateProblemDescription(problemId: string, locale: Locale, content: string) { export async function updateProblemDescription(
problemId: string,
locale: Locale,
content: string
) {
try { try {
const updatedLocalization = await prisma.problemLocalization.upsert({ const updatedLocalization = await prisma.problemLocalization.upsert({
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: problemId, problemId: problemId,
locale: locale, locale: locale,
type: ProblemContentType.DESCRIPTION type: ProblemContentType.DESCRIPTION,
} },
}, },
create: { create: {
problemId: problemId, problemId: problemId,
locale: locale, locale: locale,
type: ProblemContentType.DESCRIPTION, type: ProblemContentType.DESCRIPTION,
content: content content: content,
}, },
update: { update: {
content: content content: content,
} },
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
@ -54,25 +72,29 @@ export async function updateProblemDescription(problemId: string, locale: Locale
} }
} }
export async function updateProblemSolution(problemId: string, locale: Locale, content: string) { export async function updateProblemSolution(
problemId: string,
locale: Locale,
content: string
) {
try { try {
const updatedLocalization = await prisma.problemLocalization.upsert({ const updatedLocalization = await prisma.problemLocalization.upsert({
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: problemId, problemId: problemId,
locale: locale, locale: locale,
type: ProblemContentType.SOLUTION type: ProblemContentType.SOLUTION,
} },
}, },
create: { create: {
problemId: problemId, problemId: problemId,
locale: locale, locale: locale,
type: ProblemContentType.SOLUTION, type: ProblemContentType.SOLUTION,
content: content content: content,
}, },
update: { update: {
content: content content: content,
} },
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
@ -83,23 +105,27 @@ export async function updateProblemSolution(problemId: string, locale: Locale, c
} }
} }
export async function updateProblemTemplate(problemId: string, language: Language, content: string) { export async function updateProblemTemplate(
problemId: string,
language: Language,
content: string
) {
try { try {
const updatedTemplate = await prisma.template.upsert({ const updatedTemplate = await prisma.template.upsert({
where: { where: {
problemId_language: { problemId_language: {
problemId: problemId, problemId: problemId,
language: language language: language,
} },
}, },
create: { create: {
problemId: problemId, problemId: problemId,
language: language, language: language,
content: content content: content,
}, },
update: { update: {
content: content content: content,
} },
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
@ -110,19 +136,24 @@ export async function updateProblemTemplate(problemId: string, language: Languag
} }
} }
export async function updateProblemTestcase(problemId: string, testcaseId: string, expectedOutput: string, inputs: { name: string; value: string }[]) { export async function updateProblemTestcase(
problemId: string,
testcaseId: string,
expectedOutput: string,
inputs: { name: string; value: string }[]
) {
try { try {
// Update testcase // Update testcase
const updatedTestcase = await prisma.testcase.update({ const updatedTestcase = await prisma.testcase.update({
where: { id: testcaseId }, where: { id: testcaseId },
data: { data: {
expectedOutput: expectedOutput expectedOutput: expectedOutput,
} },
}); });
// Delete old inputs // Delete old inputs
await prisma.testcaseInput.deleteMany({ await prisma.testcaseInput.deleteMany({
where: { testcaseId: testcaseId } where: { testcaseId: testcaseId },
}); });
// Create new inputs // Create new inputs
@ -131,15 +162,15 @@ export async function updateProblemTestcase(problemId: string, testcaseId: strin
testcaseId: testcaseId, testcaseId: testcaseId,
index: index, index: index,
name: input.name, name: input.name,
value: input.value value: input.value,
})) })),
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
return { return {
success: true, success: true,
testcase: updatedTestcase, testcase: updatedTestcase,
inputs: createdInputs inputs: createdInputs,
}; };
} catch (error) { } catch (error) {
console.error("Failed to update problem testcase:", error); console.error("Failed to update problem testcase:", error);
@ -147,14 +178,18 @@ export async function updateProblemTestcase(problemId: string, testcaseId: strin
} }
} }
export async function addProblemTestcase(problemId: string, expectedOutput: string, inputs: { name: string; value: string }[]) { export async function addProblemTestcase(
problemId: string,
expectedOutput: string,
inputs: { name: string; value: string }[]
) {
try { try {
// Create testcase // Create testcase
const newTestcase = await prisma.testcase.create({ const newTestcase = await prisma.testcase.create({
data: { data: {
problemId: problemId, problemId: problemId,
expectedOutput: expectedOutput expectedOutput: expectedOutput,
} },
}); });
// Create inputs // Create inputs
@ -163,15 +198,15 @@ export async function addProblemTestcase(problemId: string, expectedOutput: stri
testcaseId: newTestcase.id, testcaseId: newTestcase.id,
index: index, index: index,
name: input.name, name: input.name,
value: input.value value: input.value,
})) })),
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
return { return {
success: true, success: true,
testcase: newTestcase, testcase: newTestcase,
inputs: createdInputs inputs: createdInputs,
}; };
} catch (error) { } catch (error) {
console.error("Failed to add problem testcase:", error); console.error("Failed to add problem testcase:", error);
@ -179,10 +214,13 @@ export async function addProblemTestcase(problemId: string, expectedOutput: stri
} }
} }
export async function deleteProblemTestcase(problemId: string, testcaseId: string) { export async function deleteProblemTestcase(
problemId: string,
testcaseId: string
) {
try { try {
const deletedTestcase = await prisma.testcase.delete({ const deletedTestcase = await prisma.testcase.delete({
where: { id: testcaseId } where: { id: testcaseId },
}); });
revalidatePath(`/problem-editor/${problemId}`); revalidatePath(`/problem-editor/${problemId}`);
@ -197,9 +235,9 @@ export async function deleteProblemTestcase(problemId: string, testcaseId: strin
* TITLE * TITLE
*/ */
export async function updateProblemTitle( export async function updateProblemTitle(
problemId: string, problemId: string,
locale: Locale, locale: Locale,
title: string title: string
) { ) {
try { try {
const updated = await prisma.problemLocalization.upsert({ const updated = await prisma.problemLocalization.upsert({
@ -227,4 +265,4 @@ export async function updateProblemTitle(
console.error("更新题目标题失败:", error); console.error("更新题目标题失败:", error);
return { success: false, error: "更新题目标题失败" }; return { success: false, error: "更新题目标题失败" };
} }
} }

View File

@ -2,18 +2,18 @@ import { z } from "zod";
// 优化代码的输入类型 // 优化代码的输入类型
export const OptimizeCodeInputSchema = z.object({ export const OptimizeCodeInputSchema = z.object({
code: z.string(), // 用户输入的代码 code: z.string(), // 用户输入的代码
error: z.string().optional(), // 可选的错误信息 error: z.string().optional(), // 可选的错误信息
problemId: z.string().optional(), // 可选的题目ID problemId: z.string().optional(), // 可选的题目ID
}); });
export type OptimizeCodeInput = z.infer<typeof OptimizeCodeInputSchema>; export type OptimizeCodeInput = z.infer<typeof OptimizeCodeInputSchema>;
// 优化代码的输出类型 // 优化代码的输出类型
export const OptimizeCodeOutputSchema = z.object({ export const OptimizeCodeOutputSchema = z.object({
optimizedCode: z.string(), // 优化后的代码 optimizedCode: z.string(), // 优化后的代码
explanation: z.string(), // 优化说明 explanation: z.string(), // 优化说明
issuesFixed: z.array(z.string()).optional(), // 修复的问题列表 issuesFixed: z.array(z.string()).optional(), // 修复的问题列表
}); });
export type OptimizeCodeOutput = z.infer<typeof OptimizeCodeOutputSchema>; export type OptimizeCodeOutput = z.infer<typeof OptimizeCodeOutputSchema>;

View File

@ -1,21 +1,19 @@
import {z} from "zod"; import { z } from "zod";
export const AITestCaseInputSchema = z.object({ export const AITestCaseInputSchema = z.object({
problemId: z.string(), problemId: z.string(),
}) });
export type AITestCaseInput = z.infer<typeof AITestCaseInputSchema> export type AITestCaseInput = z.infer<typeof AITestCaseInputSchema>;
const input = z.object({ const input = z.object({
name: z.string(), name: z.string(),
value: z.string() value: z.string(),
}) });
export const AITestCaseOutputSchema = z.object({ export const AITestCaseOutputSchema = z.object({
expectedOutput: z.string(), expectedOutput: z.string(),
inputs: z.array(input) inputs: z.array(input),
}) });
export type AITestCaseOutput = z.infer<typeof AITestCaseOutputSchema>
export type AITestCaseOutput = z.infer<typeof AITestCaseOutputSchema>;