Merge pull request #41 from massbug/fix-pr-38

Fix pr 38
This commit is contained in:
cfngc4594 2025-06-21 00:42:15 +08:00 committed by GitHub
commit 66403dbdb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1884 additions and 72 deletions

View File

@ -1,3 +1,4 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ProblemHeader } from "@/features/problems/components/header"; import { ProblemHeader } from "@/features/problems/components/header";
@ -13,6 +14,19 @@ const Layout = async ({ children, params }: LayoutProps) => {
return notFound(); return notFound();
} }
const problem = await prisma.problem.findUnique({
select: {
isPublished: true,
},
where: {
id: problemId,
},
});
if (!problem?.isPublished) {
return notFound();
}
return ( return (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
<ProblemHeader /> <ProblemHeader />

View File

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

View File

@ -0,0 +1,155 @@
"use server";
import {
AITestCaseInput,
AITestCaseOutput,
AITestCaseOutputSchema,
} from "@/types/ai-testcase";
import { deepseek } from "@/lib/ai";
import { CoreMessage, generateText } from "ai";
import prisma from "@/lib/prisma";
/**
*
* @param input
* @returns
*/
export const generateAITestcase = async (
input: AITestCaseInput
): Promise<AITestCaseOutput> => {
const model = deepseek("deepseek-chat");
let problemDetails = "";
if (input.problemId) {
try {
// 尝试获取英文描述
const problemLocalizationEn = await prisma.problemLocalization.findUnique(
{
where: {
problemId_locale_type: {
problemId: input.problemId,
locale: "en",
type: "DESCRIPTION",
},
},
include: {
problem: true,
},
}
);
if (problemLocalizationEn) {
problemDetails = `
Problem Requirements:
-------------------
Description: ${problemLocalizationEn.content}
`;
} else {
// 回退到中文描述
const problemLocalizationZh =
await prisma.problemLocalization.findUnique({
where: {
problemId_locale_type: {
problemId: input.problemId,
locale: "zh",
type: "DESCRIPTION",
},
},
include: {
problem: true,
},
});
if (problemLocalizationZh) {
problemDetails = `
Problem Requirements:
-------------------
Description: ${problemLocalizationZh.content}
`;
console.warn(
`Fallback to Chinese description for problemId: ${input.problemId}`
);
} else {
problemDetails = "Problem description not found in any language.";
console.warn(
`No description found for problemId: ${input.problemId}`
);
}
}
} catch (error) {
console.error("Failed to fetch problem details:", error);
problemDetails = "Error fetching problem description.";
}
}
// 构建AI提示词
const prompt = `
Analyze the problem statement to get the expected input structure, constraints, and output logic. Generate **novel, randomized** inputs/outputs that strictly adhere to the problem's requirements. Focus on:
Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers.
1. **Input Data Structure**: Identify required formats (e.g., arrays, integers, strings).
2. **Input Constraints**: Determine valid ranges (e.g., array length: 2100, integers: -1000 to 1000) and edge cases.
3. **Output Logic**: Ensure outputs correctly reflect problem-specific operations.
4. **Randomization**:
Vary input magnitudes (mix min/max/-edge values with mid-range randomness)
Use diverse data distributions (e.g., sorted/unsorted, negative/positive values)
Avoid patterns from existing examples
Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers.
Here is the problem description:
${problemDetails}
Respond **ONLY** with this JSON structure.
***Do not wrap the json codes in JSON markers*** :
{
"expectedOutput": "Randomized output (e.g., [-5, 100] instead of [1, 2])",
"inputs": [
{
"name": "Parameter 1",
"value": <RANDOMIZED_DATA> // Use string to wrap actual JSON types (arrays/numbers/strings)
},
... // Add parameters as needed
]
}
`;
// 发送请求给OpenAI
const messages: CoreMessage[] = [{ role: "user", content: prompt }];
let text;
try {
const response = await generateText({
model: model,
messages: messages,
});
text = response.text;
} catch (error) {
console.error("Error generating text with OpenAI:", error);
throw new Error("Failed to generate response from OpenAI");
}
// 解析LLM响应
let llmResponseJson;
try {
llmResponseJson = JSON.parse(text);
} catch (error) {
console.error("Failed to parse LLM response as JSON:", error);
console.error("LLM raw output:", text);
throw new Error("Invalid JSON response from LLM");
}
// 验证响应格式
const validationResult = AITestCaseOutputSchema.safeParse(llmResponseJson);
if (!validationResult.success) {
console.error("Zod validation failed:", validationResult.error.format());
throw new Error("Response validation failed");
}
console.log("LLM response:", llmResponseJson);
return validationResult.data;
};

View File

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

View File

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

View File

@ -0,0 +1,127 @@
"use client";
import { useCallback, useState } from "react";
import { DiffEditor } from "@monaco-editor/react";
import { optimizeCode } from "@/app/actions/ai-improve";
import type { OptimizeCodeInput } from "@/types/ai-improve";
import { CoreEditor } from "./core-editor"; // 引入你刚刚的组件
// import { Loading } from "@/components/loading";
import type { LanguageServerConfig } from "@/generated/client";
interface AIEditorWrapperProps {
language?: string;
value?: string;
path?: string;
problemId?: string;
languageServerConfigs?: LanguageServerConfig[];
onChange?: (value: string) => void;
className?: string;
}
export const AIEditorWrapper = ({
language,
value,
path,
problemId,
languageServerConfigs,
onChange,
}: // className,
AIEditorWrapperProps) => {
const [currentCode, setCurrentCode] = useState(value ?? "");
const [optimizedCode, setOptimizedCode] = useState("");
const [isOptimizing, setIsOptimizing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showDiff, setShowDiff] = useState(false);
const handleCodeChange = useCallback(
(val: string) => {
setCurrentCode(val);
onChange?.(val);
},
[onChange]
);
const handleOptimize = useCallback(async () => {
if (!problemId || !currentCode) return;
setIsOptimizing(true);
setError(null);
try {
const input: OptimizeCodeInput = {
code: currentCode,
problemId,
};
const result = await optimizeCode(input);
setOptimizedCode(result.optimizedCode);
setShowDiff(true);
} catch (err) {
setError("AI 优化失败,请稍后重试");
console.error(err);
} finally {
setIsOptimizing(false);
}
}, [currentCode, problemId]);
const handleApplyOptimized = useCallback(() => {
setCurrentCode(optimizedCode);
onChange?.(optimizedCode);
setShowDiff(false);
}, [optimizedCode, onChange]);
return (
<div className="flex flex-col h-full w-full">
<div className="flex items-center justify-between p-4">
<button
onClick={handleOptimize}
disabled={isOptimizing}
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90"
>
{isOptimizing ? "优化中..." : "AI优化代码"}
</button>
{showDiff && (
<div className="space-x-2">
<button
onClick={() => setShowDiff(false)}
className="px-4 py-2 bg-secondary text-white rounded"
>
</button>
<button
onClick={handleApplyOptimized}
className="px-4 py-2 bg-green-500 text-white rounded"
>
</button>
</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

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

View File

@ -0,0 +1,217 @@
"use client";
import { toast } from "sonner";
import { Locale } from "@/generated/client";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
updateProblemDescription,
updateProblemTitle,
} from "@/components/creater/problem-maintain";
import { Button } from "@/components/ui/button";
import MdxPreview from "@/components/mdx-preview";
import React, { useEffect, useState } from "react";
import { Accordion } from "@/components/ui/accordion";
import { CoreEditor } from "@/components/core-editor";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getProblemData } from "@/app/actions/getProblem";
import { VideoEmbed } from "@/components/content/video-embed";
import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function EditDescriptionPanel({
problemId,
}: {
problemId: string;
}) {
const [locales, setLocales] = useState<string[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>("");
const [customLocale, setCustomLocale] = useState("");
const [description, setDescription] = useState({ title: "", content: "" });
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">(
"edit"
);
useEffect(() => {
async function fetchLocales() {
try {
const langs = await getProblemLocales(problemId);
setLocales(langs);
if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) {
console.error(err);
toast.error("获取语言列表失败");
}
}
fetchLocales();
}, [problemId]);
useEffect(() => {
if (!currentLocale) return;
async function fetchProblem() {
try {
const data = await getProblemData(problemId, currentLocale);
setDescription({
title: data?.title || "",
content: data?.description || "",
});
} catch (err) {
console.error(err);
toast.error("加载题目描述失败");
}
}
fetchProblem();
}, [problemId, currentLocale]);
const handleAddCustomLocale = () => {
if (customLocale && !locales.includes(customLocale)) {
const newLocales = [...locales, customLocale];
setLocales(newLocales);
setCurrentLocale(customLocale);
setCustomLocale("");
setDescription({ title: "", content: "" });
}
};
const handleSave = async (): Promise<void> => {
if (!currentLocale) {
toast.error("请选择语言");
return;
}
try {
const locale = currentLocale as Locale;
const resTitle = await updateProblemTitle(
problemId,
locale,
description.title
);
const resDesc = await updateProblemDescription(
problemId,
locale,
description.content
);
if (resTitle.success && resDesc.success) {
toast.success("保存成功");
} else {
toast.error("保存失败");
}
} catch (err) {
console.error(err);
toast.error("保存异常");
}
};
return (
<PanelLayout>
<ScrollArea className="h-full">
<Card className="w-full rounded-none border-none bg-background">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 语言切换 */}
<div className="space-y-2">
<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 items-center justify-between">
<div className="flex items-center space-x-2">
<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="flex items-center">
<Button onClick={handleSave}></Button>
</div>
</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>
</CardContent>
</Card>
</ScrollArea>
</PanelLayout>
);
}

View File

@ -0,0 +1,169 @@
"use client";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Difficulty } from "@/generated/client";
import React, { useState, useEffect } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getProblemData } from "@/app/actions/getProblem";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { updateProblemDetail } from "@/components/creater/problem-maintain";
export default function EditDetailPanel({ problemId }: { problemId: string }) {
const [problemDetails, setProblemDetails] = useState({
displayId: 1000,
difficulty: "EASY" as Difficulty,
timeLimit: 1000,
memoryLimit: 134217728,
isPublished: false,
});
useEffect(() => {
async function fetchData() {
try {
const problemData = await getProblemData(problemId);
setProblemDetails({
displayId: problemData.displayId,
difficulty: problemData.difficulty,
timeLimit: problemData.timeLimit,
memoryLimit: problemData.memoryLimit,
isPublished: problemData.isPublished,
});
} catch (error) {
console.error("获取题目信息失败:", error);
toast.error("加载详情失败");
}
}
fetchData();
}, [problemId]);
const handleNumberInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
field: keyof typeof problemDetails
) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value)) {
setProblemDetails({
...problemDetails,
[field]: value,
});
}
};
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as Difficulty;
setProblemDetails({ ...problemDetails, difficulty: value });
};
const handleSave = async () => {
try {
const res = await updateProblemDetail(problemId, {
displayId: problemDetails.displayId,
difficulty: problemDetails.difficulty,
timeLimit: problemDetails.timeLimit,
memoryLimit: problemDetails.memoryLimit,
isPublished: problemDetails.isPublished,
});
if (res.success) {
toast.success("保存成功");
} else {
toast.error("保存失败");
}
} catch (err) {
console.error("保存异常:", err);
toast.error("保存异常");
}
};
return (
<PanelLayout>
<ScrollArea className="h-full">
<Card className="w-full rounded-none border-none bg-background">
<CardHeader className="px-6 py-4">
<div className="flex items-center justify-between">
<span></span>
<Button type="button" onClick={handleSave}>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="display-id">ID</Label>
<Input
id="display-id"
type="number"
value={problemDetails.displayId}
onChange={(e) => handleNumberInputChange(e, "displayId")}
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 justify-between">
<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>
</CardContent>
</Card>
</ScrollArea>
</PanelLayout>
);
}

View File

@ -0,0 +1,210 @@
"use client";
import { toast } from "sonner";
import { Locale } from "@/generated/client";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import MdxPreview from "@/components/mdx-preview";
import React, { useEffect, useState } from "react";
import { Accordion } from "@/components/ui/accordion";
import { CoreEditor } from "@/components/core-editor";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getProblemData } from "@/app/actions/getProblem";
import { VideoEmbed } from "@/components/content/video-embed";
import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { updateProblemSolution } from "@/components/creater/problem-maintain";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function EditSolutionPanel({
problemId,
}: {
problemId: string;
}) {
const [locales, setLocales] = useState<string[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>("");
const [customLocale, setCustomLocale] = useState("");
const [solution, setSolution] = useState({ title: "", content: "" });
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">(
"edit"
);
useEffect(() => {
async function fetchLocales() {
try {
const langs = await getProblemLocales(problemId);
setLocales(langs);
if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) {
console.error(err);
toast.error("获取语言列表失败");
}
}
fetchLocales();
}, [problemId]);
useEffect(() => {
if (!currentLocale) return;
async function fetchSolution() {
try {
const data = await getProblemData(problemId, currentLocale);
setSolution({
title: (data?.title || "") + " 解析",
content: data?.solution || "",
});
} catch (err) {
console.error(err);
toast.error("加载题目解析失败");
}
}
fetchSolution();
}, [problemId, currentLocale]);
const handleAddCustomLocale = () => {
if (customLocale && !locales.includes(customLocale)) {
setLocales((prev) => [...prev, customLocale]);
setCurrentLocale(customLocale);
setCustomLocale("");
setSolution({ title: "", content: "" });
}
};
const handleSave = async (): Promise<void> => {
if (!currentLocale) {
toast.error("请选择语言");
return;
}
try {
const locale = currentLocale as Locale;
const res = await updateProblemSolution(
problemId,
locale,
solution.content
);
if (res.success) {
toast.success("保存成功");
} else {
toast.error("保存失败");
}
} catch (err) {
console.error(err);
toast.error("保存异常");
}
};
return (
<PanelLayout>
<ScrollArea className="h-full">
<Card className="w-full rounded-none border-none bg-background">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 语言切换 */}
<div className="space-y-2">
<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">
<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>
</ScrollArea>
</PanelLayout>
);
}

View File

@ -0,0 +1,278 @@
"use client";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
addProblemTestcase,
updateProblemTestcase,
deleteProblemTestcase,
} from "@/components/creater/problem-maintain";
import { Button } from "@/components/ui/button";
import React, { useState, useEffect } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getProblemData } from "@/app/actions/getProblem";
import { generateAITestcase } from "@/app/actions/ai-testcase";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface Testcase {
id: string;
expectedOutput: string;
inputs: { name: string; value: string }[];
}
export default function EditTestcasePanel({
problemId,
}: {
problemId: string;
}) {
const [testcases, setTestcases] = useState<Testcase[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
// 加载测试用例
useEffect(() => {
async function fetch() {
try {
const data = await getProblemData(problemId);
setTestcases(data.testcases || []);
} catch (err) {
console.error("加载测试用例失败:", err);
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;
}
if (tc.id.startsWith("new-")) {
const res = await addProblemTestcase(
problemId,
tc.expectedOutput,
tc.inputs
);
if (res.success) {
toast.success(`新增测试用例 ${i + 1} 成功`);
} else {
toast.error(`新增测试用例 ${i + 1} 失败`);
}
} else {
const res = await updateProblemTestcase(
problemId,
tc.id,
tc.expectedOutput,
tc.inputs
);
if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`);
else toast.error(`更新测试用例 ${i + 1} 失败`);
}
}
// 保存完成后刷新最新测试用例
const data = await getProblemData(problemId);
setTestcases(data.testcases || []);
toast.success("测试用例保存并刷新成功");
} catch (err) {
console.error(err);
toast.error("保存测试用例异常");
}
};
return (
<PanelLayout>
<ScrollArea className="h-full">
<Card className="w-full rounded-none border-none bg-background">
<CardHeader className="px-6 py-4">
<div className="flex items-center justify-between">
<span></span>
<div className="flex items-center space-x-2">
<Button onClick={handleAITestcase} disabled={isGenerating}>
{isGenerating ? "生成中..." : "AI生成"}
</Button>
<Button onClick={handleAddTestcase}></Button>
<Button variant="secondary" onClick={handleSaveAll}>
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{testcases.map((tc, idx) => (
<div key={tc.id} className="border p-4 rounded space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium"> {idx + 1}</h3>
<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>
</ScrollArea>
</PanelLayout>
);
}

View File

@ -0,0 +1,268 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
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;
}
) {
try {
const updatedProblem = await prisma.problem.update({
where: { id: problemId },
data: {
displayId: data.displayId,
difficulty: data.difficulty,
timeLimit: data.timeLimit,
memoryLimit: data.memoryLimit,
isPublished: data.isPublished,
},
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, problem: updatedProblem };
} catch (error) {
console.error("Failed to update problem detail:", error);
return { success: false, error: "Failed to update problem detail" };
}
}
export async function updateProblemDescription(
problemId: string,
locale: Locale,
content: string
) {
try {
const updatedLocalization = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId: problemId,
locale: locale,
type: ProblemContentType.DESCRIPTION,
},
},
create: {
problemId: problemId,
locale: locale,
type: ProblemContentType.DESCRIPTION,
content: content,
},
update: {
content: content,
},
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updatedLocalization };
} catch (error) {
console.error("Failed to update problem description:", error);
return { success: false, error: "Failed to update problem description" };
}
}
export async function updateProblemSolution(
problemId: string,
locale: Locale,
content: string
) {
try {
const updatedLocalization = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId: problemId,
locale: locale,
type: ProblemContentType.SOLUTION,
},
},
create: {
problemId: problemId,
locale: locale,
type: ProblemContentType.SOLUTION,
content: content,
},
update: {
content: content,
},
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updatedLocalization };
} catch (error) {
console.error("Failed to update problem solution:", error);
return { success: false, error: "Failed to update problem solution" };
}
}
export async function updateProblemTemplate(
problemId: string,
language: Language,
content: string
) {
try {
const updatedTemplate = await prisma.template.upsert({
where: {
problemId_language: {
problemId: problemId,
language: language,
},
},
create: {
problemId: problemId,
language: language,
content: content,
},
update: {
content: content,
},
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, template: updatedTemplate };
} catch (error) {
console.error("Failed to update problem template:", error);
return { success: false, error: "Failed to update problem template" };
}
}
export async function updateProblemTestcase(
problemId: string,
testcaseId: string,
expectedOutput: string,
inputs: { name: string; value: string }[]
) {
try {
// Update testcase
const updatedTestcase = await prisma.testcase.update({
where: { id: testcaseId },
data: {
expectedOutput: expectedOutput,
},
});
// Delete old inputs
await prisma.testcaseInput.deleteMany({
where: { testcaseId: testcaseId },
});
// Create new inputs
const createdInputs = await prisma.testcaseInput.createMany({
data: inputs.map((input, index) => ({
testcaseId: testcaseId,
index: index,
name: input.name,
value: input.value,
})),
});
revalidatePath(`/problem-editor/${problemId}`);
return {
success: true,
testcase: updatedTestcase,
inputs: createdInputs,
};
} catch (error) {
console.error("Failed to update problem testcase:", error);
return { success: false, error: "Failed to update problem testcase" };
}
}
export async function addProblemTestcase(
problemId: string,
expectedOutput: string,
inputs: { name: string; value: string }[]
) {
try {
// Create testcase
const newTestcase = await prisma.testcase.create({
data: {
problemId: problemId,
expectedOutput: expectedOutput,
},
});
// Create inputs
const createdInputs = await prisma.testcaseInput.createMany({
data: inputs.map((input, index) => ({
testcaseId: newTestcase.id,
index: index,
name: input.name,
value: input.value,
})),
});
revalidatePath(`/problem-editor/${problemId}`);
return {
success: true,
testcase: newTestcase,
inputs: createdInputs,
};
} catch (error) {
console.error("Failed to add problem testcase:", error);
return { success: false, error: "Failed to add problem testcase" };
}
}
export async function deleteProblemTestcase(
problemId: string,
testcaseId: string
) {
try {
const deletedTestcase = await prisma.testcase.delete({
where: { id: testcaseId },
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, testcase: deletedTestcase };
} catch (error) {
console.error("Failed to delete problem testcase:", error);
return { success: false, error: "Failed to delete problem testcase" };
}
}
/**
* TITLE
*/
export async function updateProblemTitle(
problemId: string,
locale: Locale,
title: string
) {
try {
const updated = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId,
locale,
type: ProblemContentType.TITLE,
},
},
create: {
problemId,
locale,
type: ProblemContentType.TITLE,
content: title,
},
update: {
content: title,
},
});
// 重新缓存编辑页
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updated };
} catch (error) {
console.error("更新题目标题失败:", error);
return { success: false, error: "更新题目标题失败" };
}
}

View File

@ -11,9 +11,9 @@ interface ProblemEditViewProps {
export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => { export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => {
const components: Record<string, React.ReactNode> = { const components: Record<string, React.ReactNode> = {
detail: <EditDetailPanel problemId={problemId} />,
description: <EditDescriptionPanel problemId={problemId} />, description: <EditDescriptionPanel problemId={problemId} />,
solution: <EditSolutionPanel problemId={problemId} />, solution: <EditSolutionPanel problemId={problemId} />,
detail: <EditDetailPanel problemId={problemId} />,
code: <EditCodePanel problemId={problemId} />, code: <EditCodePanel problemId={problemId} />,
testcase: <EditTestcasePanel problemId={problemId} />, testcase: <EditTestcasePanel problemId={problemId} />,
}; };

View File

@ -37,13 +37,7 @@ export const BotContent = async ({ problemId }: BotContentProps) => {
const description = getLocalizedDescription(descriptions, locale as Locale); const description = getLocalizedDescription(descriptions, locale as Locale);
return ( return <BotForm description={description} />;
<div className="relative flex-1">
<div className="absolute h-full w-full">
<BotForm description={description} />
</div>
</div>
);
}; };
export const BotContentSkeleton = () => { export const BotContentSkeleton = () => {

View File

@ -3,6 +3,7 @@ import {
BotContent, BotContent,
BotContentSkeleton, BotContentSkeleton,
} from "@/features/problems/bot/components/content"; } from "@/features/problems/bot/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface BotPanelProps { interface BotPanelProps {
problemId: string; problemId: string;
@ -10,10 +11,10 @@ interface BotPanelProps {
export const BotPanel = ({ problemId }: BotPanelProps) => { export const BotPanel = ({ problemId }: BotPanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<Suspense fallback={<BotContentSkeleton />}> <Suspense fallback={<BotContentSkeleton />}>
<BotContent problemId={problemId} /> <BotContent problemId={problemId} />
</Suspense> </Suspense>
</div> </PanelLayout>
); );
}; };

View File

@ -3,6 +3,7 @@ import {
CodeContent, CodeContent,
CodeContentSkeleton, CodeContentSkeleton,
} from "@/features/problems/code/components/content"; } from "@/features/problems/code/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { CodeFooter } from "@/features/problems/code/components/footer"; import { CodeFooter } from "@/features/problems/code/components/footer";
import { CodeToolbar } from "@/features/problems/code/components/toolbar/code-toolbar"; import { CodeToolbar } from "@/features/problems/code/components/toolbar/code-toolbar";
@ -12,16 +13,14 @@ interface CodePanelProps {
export const CodePanel = ({ problemId }: CodePanelProps) => { export const CodePanel = ({ problemId }: CodePanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<CodeToolbar className="border-b" /> <div className="h-full flex flex-col">
<div className="relative flex-1"> <CodeToolbar className="border-b" />
<div className="absolute h-full w-full"> <Suspense fallback={<CodeContentSkeleton />}>
<Suspense fallback={<CodeContentSkeleton />}> <CodeContent problemId={problemId} />
<CodeContent problemId={problemId} /> </Suspense>
</Suspense> <CodeFooter />
</div>
</div> </div>
<CodeFooter /> </PanelLayout>
</div>
); );
}; };

View File

@ -3,6 +3,7 @@ import {
DescriptionContent, DescriptionContent,
DescriptionContentSkeleton, DescriptionContentSkeleton,
} from "@/features/problems/description/components/content"; } from "@/features/problems/description/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface DescriptionPanelProps { interface DescriptionPanelProps {
problemId: string; problemId: string;
@ -10,14 +11,10 @@ interface DescriptionPanelProps {
export const DescriptionPanel = ({ problemId }: DescriptionPanelProps) => { export const DescriptionPanel = ({ problemId }: DescriptionPanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<div className="relative flex-1"> <Suspense fallback={<DescriptionContentSkeleton />}>
<div className="absolute h-full w-full"> <DescriptionContent problemId={problemId} />
<Suspense fallback={<DescriptionContentSkeleton />}> </Suspense>
<DescriptionContent problemId={problemId} /> </PanelLayout>
</Suspense>
</div>
</div>
</div>
); );
}; };

View File

@ -3,6 +3,7 @@ import {
DetailContent, DetailContent,
DetailContentSkeleton, DetailContentSkeleton,
} from "@/features/problems/detail/components/content"; } from "@/features/problems/detail/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { DetailHeader } from "@/features/problems/detail/components/header"; import { DetailHeader } from "@/features/problems/detail/components/header";
interface DetailPanelProps { interface DetailPanelProps {
@ -15,15 +16,11 @@ export const DetailPanel = ({ submissionId }: DetailPanelProps) => {
} }
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<DetailHeader /> <DetailHeader />
<div className="relative flex-1"> <Suspense fallback={<DetailContentSkeleton />}>
<div className="absolute h-full w-full"> <DetailContent submissionId={submissionId} />
<Suspense fallback={<DetailContentSkeleton />}> </Suspense>
<DetailContent submissionId={submissionId} /> </PanelLayout>
</Suspense>
</div>
</div>
</div>
); );
}; };

View File

@ -0,0 +1,13 @@
interface PanelLayoutProps {
children: React.ReactNode;
}
export const PanelLayout = ({ children }: PanelLayoutProps) => {
return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
<div className="relative flex-1">
<div className="absolute h-full w-full">{children}</div>
</div>
</div>
);
};

View File

@ -3,6 +3,7 @@ import {
SolutionContent, SolutionContent,
SolutionContentSkeleton, SolutionContentSkeleton,
} from "@/features/problems/solution/components/content"; } from "@/features/problems/solution/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface SolutionPanelProps { interface SolutionPanelProps {
problemId: string; problemId: string;
@ -10,14 +11,10 @@ interface SolutionPanelProps {
export const SolutionPanel = ({ problemId }: SolutionPanelProps) => { export const SolutionPanel = ({ problemId }: SolutionPanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<div className="relative flex-1"> <Suspense fallback={<SolutionContentSkeleton />}>
<div className="absolute h-full w-full"> <SolutionContent problemId={problemId} />
<Suspense fallback={<SolutionContentSkeleton />}> </Suspense>
<SolutionContent problemId={problemId} /> </PanelLayout>
</Suspense>
</div>
</div>
</div>
); );
}; };

View File

@ -3,6 +3,7 @@ import {
SubmissionContent, SubmissionContent,
SubmissionContentSkeleton, SubmissionContentSkeleton,
} from "@/features/problems/submission/components/content"; } from "@/features/problems/submission/components/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface SubmissionPanelProps { interface SubmissionPanelProps {
problemId: string; problemId: string;
@ -10,14 +11,10 @@ interface SubmissionPanelProps {
export const SubmissionPanel = ({ problemId }: SubmissionPanelProps) => { export const SubmissionPanel = ({ problemId }: SubmissionPanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<div className="relative flex-1"> <Suspense fallback={<SubmissionContentSkeleton />}>
<div className="absolute h-full w-full"> <SubmissionContent problemId={problemId} />
<Suspense fallback={<SubmissionContentSkeleton />}> </Suspense>
<SubmissionContent problemId={problemId} /> </PanelLayout>
</Suspense>
</div>
</div>
</div>
); );
}; };

View File

@ -3,6 +3,7 @@ import {
TestcaseContent, TestcaseContent,
TestcaseContentSkeleton, TestcaseContentSkeleton,
} from "@/features/problems/testcase/content"; } from "@/features/problems/testcase/content";
import { PanelLayout } from "@/features/problems/layouts/panel-layout";
interface TestcasePanelProps { interface TestcasePanelProps {
problemId: string; problemId: string;
@ -10,14 +11,10 @@ interface TestcasePanelProps {
export const TestcasePanel = ({ problemId }: TestcasePanelProps) => { export const TestcasePanel = ({ problemId }: TestcasePanelProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <PanelLayout>
<div className="relative flex-1"> <Suspense fallback={<TestcaseContentSkeleton />}>
<div className="absolute h-full w-full"> <TestcaseContent problemId={problemId} />
<Suspense fallback={<TestcaseContentSkeleton />}> </Suspense>
<TestcaseContent problemId={problemId} /> </PanelLayout>
</Suspense>
</div>
</div>
</div>
); );
}; };

View File

@ -144,6 +144,13 @@ const initialProblemEditFlexLayoutJsonModel: IJsonModel = {
id: "1", id: "1",
weight: 50, weight: 50,
children: [ children: [
{
type: "tab",
id: "detail",
name: "Details",
component: "detail",
enableClose: false,
},
{ {
type: "tab", type: "tab",
id: "description", id: "description",
@ -158,13 +165,6 @@ const initialProblemEditFlexLayoutJsonModel: IJsonModel = {
component: "solution", component: "solution",
enableClose: false, enableClose: false,
}, },
{
type: "tab",
id: "detail",
name: "Details",
component: "detail",
enableClose: false,
},
], ],
}, },
{ {

19
src/types/ai-improve.ts Normal file
View File

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

19
src/types/ai-testcase.ts Normal file
View File

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