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

@ -25,7 +25,8 @@ export const optimizeCode = async (
if (input.problemId) { if (input.problemId) {
try { try {
// 尝试获取英文描述 // 尝试获取英文描述
const problemLocalizationEn = await prisma.problemLocalization.findUnique({ const problemLocalizationEn = await prisma.problemLocalization.findUnique(
{
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: input.problemId, problemId: input.problemId,
@ -36,7 +37,8 @@ export const optimizeCode = async (
include: { include: {
problem: true, problem: true,
}, },
}); }
);
if (problemLocalizationEn) { if (problemLocalizationEn) {
problemDetails = ` problemDetails = `
@ -46,7 +48,8 @@ Description: ${problemLocalizationEn.content}
`; `;
} else { } else {
// 回退到中文描述 // 回退到中文描述
const problemLocalizationZh = await prisma.problemLocalization.findUnique({ const problemLocalizationZh =
await prisma.problemLocalization.findUnique({
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: input.problemId, problemId: input.problemId,
@ -65,10 +68,14 @@ Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationZh.content} Description: ${problemLocalizationZh.content}
`; `;
console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); console.warn(
`Fallback to Chinese description for problemId: ${input.problemId}`
);
} else { } else {
problemDetails = "Problem description not found in any language."; problemDetails = "Problem description not found in any language.";
console.warn(`No description found for problemId: ${input.problemId}`); console.warn(
`No description found for problemId: ${input.problemId}`
);
} }
} }
} catch (error) { } catch (error) {
@ -77,7 +84,6 @@ Description: ${problemLocalizationZh.content}
} }
} }
// 构建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.

View File

@ -1,12 +1,15 @@
"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
@ -22,7 +25,8 @@ export const generateAITestcase = async (
if (input.problemId) { if (input.problemId) {
try { try {
// 尝试获取英文描述 // 尝试获取英文描述
const problemLocalizationEn = await prisma.problemLocalization.findUnique({ const problemLocalizationEn = await prisma.problemLocalization.findUnique(
{
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: input.problemId, problemId: input.problemId,
@ -33,7 +37,8 @@ export const generateAITestcase = async (
include: { include: {
problem: true, problem: true,
}, },
}); }
);
if (problemLocalizationEn) { if (problemLocalizationEn) {
problemDetails = ` problemDetails = `
@ -43,7 +48,8 @@ Description: ${problemLocalizationEn.content}
`; `;
} else { } else {
// 回退到中文描述 // 回退到中文描述
const problemLocalizationZh = await prisma.problemLocalization.findUnique({ const problemLocalizationZh =
await prisma.problemLocalization.findUnique({
where: { where: {
problemId_locale_type: { problemId_locale_type: {
problemId: input.problemId, problemId: input.problemId,
@ -62,10 +68,14 @@ Problem Requirements:
------------------- -------------------
Description: ${problemLocalizationZh.content} Description: ${problemLocalizationZh.content}
`; `;
console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); console.warn(
`Fallback to Chinese description for problemId: ${input.problemId}`
);
} else { } else {
problemDetails = "Problem description not found in any language."; problemDetails = "Problem description not found in any language.";
console.warn(`No description found for problemId: ${input.problemId}`); console.warn(
`No description found for problemId: ${input.problemId}`
);
} }
} }
} catch (error) { } catch (error) {
@ -74,8 +84,6 @@ Description: ${problemLocalizationZh.content}
} }
} }
// 构建AI提示词 // 构建AI提示词
const prompt = ` 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:
@ -128,16 +136,13 @@ Respond **ONLY** with this JSON structure.
// 解析LLM响应 // 解析LLM响应
let llmResponseJson; let llmResponseJson;
try { try {
llmResponseJson = JSON.parse(text) llmResponseJson = JSON.parse(text);
} 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 = AITestCaseOutputSchema.safeParse(llmResponseJson); const validationResult = AITestCaseOutputSchema.safeParse(llmResponseJson);
if (!validationResult.success) { if (!validationResult.success) {

View File

@ -1,9 +1,9 @@
// 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;
@ -13,7 +13,7 @@ export async function getProblemData(problemId: string, locale?: string) {
include: { include: {
templates: true, templates: true,
testcases: { testcases: {
include: { inputs: true } include: { inputs: true },
}, },
localizations: { localizations: {
where: { where: {
@ -24,13 +24,13 @@ export async function getProblemData(problemId: string, locale?: string) {
}); });
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,
@ -43,18 +43,18 @@ export async function getProblemData(problemId: string, locale?: string) {
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,5 +1,5 @@
// src/app/actions/getProblemLocales.ts // src/app/actions/getProblemLocales.ts
'use server'; "use server";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
@ -7,8 +7,8 @@ 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

@ -25,18 +25,21 @@ export const AIEditorWrapper = ({
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(
(val: string) => {
setCurrentCode(val); setCurrentCode(val);
onChange?.(val); onChange?.(val);
}, [onChange]); },
[onChange]
);
const handleOptimize = useCallback(async () => { const handleOptimize = useCallback(async () => {
if (!problemId || !currentCode) return; if (!problemId || !currentCode) return;

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,18 +31,20 @@ 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);
}; };
@ -54,13 +56,13 @@ export default function EditCodePanel({ problemId }: EditCodePanelProps) {
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("保存异常");
} }
}; };
@ -77,9 +79,9 @@ export default function EditCodePanel({ problemId }: EditCodePanelProps) {
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>
@ -94,8 +96,8 @@ export default function EditCodePanel({ problemId }: EditCodePanelProps) {
language={codeTemplate.language} language={codeTemplate.language}
value={codeTemplate.content} value={codeTemplate.content}
path={`/${problemId}.${codeTemplate.language}`} path={`/${problemId}.${codeTemplate.language}`}
onChange={value => onChange={(value) =>
setCodeTemplate({ ...codeTemplate, content: value || '' }) setCodeTemplate({ ...codeTemplate, content: value || "" })
} }
/> />
</div> </div>

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,21 +75,29 @@ 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("保存异常");
} }
}; };
@ -117,7 +137,9 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
<Input <Input
id="description-title" id="description-title"
value={description.title} value={description.title}
onChange={(e) => setDescription({ ...description, title: e.target.value })} onChange={(e) =>
setDescription({ ...description, title: e.target.value })
}
placeholder="输入题目标题" placeholder="输入题目标题"
/> />
</div> </div>
@ -134,7 +156,9 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
<Button <Button
type="button" type="button"
variant={viewMode === "preview" ? "default" : "outline"} variant={viewMode === "preview" ? "default" : "outline"}
onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")} onClick={() =>
setViewMode(viewMode === "preview" ? "edit" : "preview")
}
> >
{viewMode === "preview" ? "取消" : "预览"} {viewMode === "preview" ? "取消" : "预览"}
</Button> </Button>
@ -148,12 +172,20 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
</div> </div>
{/* 编辑/预览区域 */} {/* 编辑/预览区域 */}
<div className={viewMode === "compare" ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && ( {(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]"> <div className="relative h-[600px]">
<CoreEditor <CoreEditor
value={description.content} value={description.content}
onChange={(newVal) => setDescription({ ...description, content: newVal || "" })} onChange={(newVal) =>
setDescription({ ...description, content: newVal || "" })
}
language="markdown" language="markdown"
className="absolute inset-0 rounded-md border border-input" className="absolute inset-0 rounded-md border border-input"
/> />
@ -161,7 +193,10 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
)} )}
{viewMode !== "edit" && ( {viewMode !== "edit" && (
<div className="prose dark:prose-invert"> <div className="prose dark:prose-invert">
<MdxPreview source={description.content} components={{ Accordion, VideoEmbed }} /> <MdxPreview
source={description.content}
components={{ Accordion, VideoEmbed }}
/>
</div> </div>
)} )}
</div> </div>

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,7 +32,7 @@ export default function EditDetailPanel({ problemId }: { problemId: string }) {
}); });
} catch (error) { } catch (error) {
console.error("获取题目信息失败:", error); console.error("获取题目信息失败:", error);
toast.error('加载详情失败'); toast.error("加载详情失败");
} }
} }
fetchData(); fetchData();
@ -66,13 +66,13 @@ 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("保存异常");
} }
}; };
@ -138,7 +138,10 @@ export default function EditDetailPanel({ problemId }: { problemId: string }) {
type="checkbox" type="checkbox"
checked={problemDetails.isPublished} checked={problemDetails.isPublished}
onChange={(e) => onChange={(e) =>
setProblemDetails({ ...problemDetails, isPublished: e.target.checked }) 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" 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"
/> />

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,20 +71,24 @@ 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("保存异常");
} }
}; };
@ -105,7 +118,9 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
value={customLocale} value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)} onChange={(e) => setCustomLocale(e.target.value)}
/> />
<Button type="button" onClick={handleAddCustomLocale}></Button> <Button type="button" onClick={handleAddCustomLocale}>
</Button>
</div> </div>
</div> </div>
@ -115,7 +130,9 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
<Input <Input
id="solution-title" id="solution-title"
value={solution.title} value={solution.title}
onChange={(e) => setSolution({ ...solution, title: e.target.value })} onChange={(e) =>
setSolution({ ...solution, title: e.target.value })
}
placeholder="输入题解标题" placeholder="输入题解标题"
disabled disabled
/> />
@ -123,20 +140,46 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
{/* 编辑/预览切换 */} {/* 编辑/预览切换 */}
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button type="button" variant={viewMode === "edit" ? "default" : "outline"} onClick={() => setViewMode("edit")}></Button> <Button
<Button type="button" variant={viewMode === "preview" ? "default" : "outline"} onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")}> 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" ? "取消" : "预览"} {viewMode === "preview" ? "取消" : "预览"}
</Button> </Button>
<Button type="button" variant={viewMode === "compare" ? "default" : "outline"} onClick={() => setViewMode("compare")}></Button> <Button
type="button"
variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode("compare")}
>
</Button>
</div> </div>
{/* 编辑/预览区域 */} {/* 编辑/预览区域 */}
<div className={viewMode === "compare" ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && ( {(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]"> <div className="relative h-[600px]">
<CoreEditor <CoreEditor
value={solution.content} value={solution.content}
onChange={(val) => setSolution({ ...solution, content: val || "" })} onChange={(val) =>
setSolution({ ...solution, content: val || "" })
}
language="markdown" language="markdown"
className="absolute inset-0 rounded-md border border-input" className="absolute inset-0 rounded-md border border-input"
/> />
@ -144,12 +187,17 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
)} )}
{viewMode !== "edit" && ( {viewMode !== "edit" && (
<div className="prose dark:prose-invert"> <div className="prose dark:prose-invert">
<MdxPreview source={solution.content} components={{ Accordion, VideoEmbed }} /> <MdxPreview
source={solution.content}
components={{ Accordion, VideoEmbed }}
/>
</div> </div>
)} )}
</div> </div>
<Button type="button" onClick={handleSave}></Button> <Button type="button" onClick={handleSave}>
</Button>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -20,7 +20,11 @@ interface Testcase {
inputs: { name: string; value: string }[]; inputs: { name: string; value: string }[];
} }
export default function EditTestcasePanel({ problemId }: { problemId: string }) { export default function EditTestcasePanel({
problemId,
}: {
problemId: string;
}) {
const [testcases, setTestcases] = useState<Testcase[]>([]); const [testcases, setTestcases] = useState<Testcase[]>([]);
const [isGenerating, setIsGenerating] = useState(false); const [isGenerating, setIsGenerating] = useState(false);
@ -42,7 +46,11 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
const handleAddTestcase = () => const handleAddTestcase = () =>
setTestcases((prev) => [ setTestcases((prev) => [
...prev, ...prev,
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: "", inputs: [{ name: "input1", value: "" }] }, {
id: `new-${Date.now()}-${Math.random()}`,
expectedOutput: "",
inputs: [{ name: "input1", value: "" }],
},
]); ]);
// AI 生成测试用例 // AI 生成测试用例
@ -52,7 +60,11 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
const ai = await generateAITestcase({ problemId }); const ai = await generateAITestcase({ problemId });
setTestcases((prev) => [ setTestcases((prev) => [
...prev, ...prev,
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs }, {
id: `new-${Date.now()}-${Math.random()}`,
expectedOutput: ai.expectedOutput,
inputs: ai.inputs,
},
]); ]);
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
} catch (err) { } catch (err) {
@ -106,7 +118,10 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
const handleAddInput = (tIdx: number) => const handleAddInput = (tIdx: number) =>
setTestcases((prev) => { setTestcases((prev) => {
const c = [...prev]; const c = [...prev];
const inputs = [...c[tIdx].inputs, { name: `input${c[tIdx].inputs.length + 1}`, value: "" }]; const inputs = [
...c[tIdx].inputs,
{ name: `input${c[tIdx].inputs.length + 1}`, value: "" },
];
c[tIdx] = { ...c[tIdx], inputs }; c[tIdx] = { ...c[tIdx], inputs };
return c; return c;
}); });
@ -125,20 +140,32 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
try { try {
for (let i = 0; i < testcases.length; i++) { for (let i = 0; i < testcases.length; i++) {
const tc = testcases[i]; const tc = testcases[i];
if (tc.expectedOutput.trim() === "" || tc.inputs.some(inp => !inp.name.trim() || !inp.value.trim())) { if (
tc.expectedOutput.trim() === "" ||
tc.inputs.some((inp) => !inp.name.trim() || !inp.value.trim())
) {
toast.error(`${i + 1} 个测试用例存在空的输入或输出,保存失败`); toast.error(`${i + 1} 个测试用例存在空的输入或输出,保存失败`);
return; return;
} }
if (tc.id.startsWith("new-")) { if (tc.id.startsWith("new-")) {
const res = await addProblemTestcase(problemId, tc.expectedOutput, tc.inputs); const res = await addProblemTestcase(
problemId,
tc.expectedOutput,
tc.inputs
);
if (res.success) { if (res.success) {
toast.success(`新增测试用例 ${i + 1} 成功`); toast.success(`新增测试用例 ${i + 1} 成功`);
} else { } else {
toast.error(`新增测试用例 ${i + 1} 失败`); toast.error(`新增测试用例 ${i + 1} 失败`);
} }
} else { } else {
const res = await updateProblemTestcase(problemId, tc.id, tc.expectedOutput, tc.inputs); const res = await updateProblemTestcase(
problemId,
tc.id,
tc.expectedOutput,
tc.inputs
);
if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`); if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`);
else toast.error(`更新测试用例 ${i + 1} 失败`); else toast.error(`更新测试用例 ${i + 1} 失败`);
} }
@ -173,7 +200,10 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
<div key={tc.id} className="border p-4 rounded space-y-4"> <div key={tc.id} className="border p-4 rounded space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="font-medium"> {idx + 1}</h3> <h3 className="font-medium"> {idx + 1}</h3>
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}> <Button
variant="destructive"
onClick={() => handleRemoveTestcase(idx)}
>
</Button> </Button>
</div> </div>
@ -181,7 +211,9 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
<Label></Label> <Label></Label>
<Input <Input
value={tc.expectedOutput} value={tc.expectedOutput}
onChange={(e) => handleExpectedOutputChange(idx, e.target.value)} onChange={(e) =>
handleExpectedOutputChange(idx, e.target.value)
}
placeholder="输入预期输出" placeholder="输入预期输出"
/> />
</div> </div>
@ -196,7 +228,9 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
<Label></Label> <Label></Label>
<Input <Input
value={inp.name} value={inp.name}
onChange={(e) => handleInputChange(idx, iIdx, "name", e.target.value)} onChange={(e) =>
handleInputChange(idx, iIdx, "name", e.target.value)
}
placeholder="参数名称" placeholder="参数名称"
/> />
</div> </div>
@ -204,12 +238,17 @@ export default function EditTestcasePanel({ problemId }: { problemId: string })
<Label></Label> <Label></Label>
<Input <Input
value={inp.value} value={inp.value}
onChange={(e) => handleInputChange(idx, iIdx, "value", e.target.value)} onChange={(e) =>
handleInputChange(idx, iIdx, "value", e.target.value)
}
placeholder="参数值" placeholder="参数值"
/> />
</div> </div>
{iIdx > 0 && ( {iIdx > 0 && (
<Button variant="outline" onClick={() => handleRemoveInput(idx, iIdx)}> <Button
variant="outline"
onClick={() => handleRemoveInput(idx, iIdx)}
>
</Button> </Button>
)} )}

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}`);

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>;