mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 09:20:53 +00:00
commit
e6b18884d6
@ -1,121 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ProblemFlexLayout } from '@/features/problems/components/problem-flexlayout';
|
|
||||||
import EditDescriptionPanel from '@/components/creater/edit-description-panel';
|
|
||||||
import EditSolutionPanel from '@/components/creater/edit-solution-panel';
|
|
||||||
import EditTestcasePanel from '@/components/creater/edit-testcase-panel';
|
|
||||||
import EditDetailPanel from '@/components/creater/edit-detail-panel';
|
|
||||||
import EditCodePanel from '@/components/creater/edit-code-panel';
|
|
||||||
import { updateProblem } from '@/app/actions/updateProblem';
|
|
||||||
|
|
||||||
interface ProblemEditorPageProps {
|
|
||||||
params: Promise<{ problemId: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateData {
|
|
||||||
content: string;
|
|
||||||
language?: 'c' | 'cpp';
|
|
||||||
inputs?: Array<{ index: number; name: string; value: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = async (
|
|
||||||
updateFn: (data: UpdateData) => Promise<{ success: boolean }>,
|
|
||||||
data: UpdateData
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const result = await updateFn(data);
|
|
||||||
if (!result.success) {
|
|
||||||
// 这里可以添加更具体的错误处理
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新失败:', error);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function ProblemEditorPage({
|
|
||||||
params,
|
|
||||||
}: ProblemEditorPageProps) {
|
|
||||||
const { problemId } = await params;
|
|
||||||
|
|
||||||
const components: Record<string, React.ReactNode> = {
|
|
||||||
description: <EditDescriptionPanel
|
|
||||||
problemId={problemId}
|
|
||||||
onUpdate={async (data) => {
|
|
||||||
await handleUpdate(
|
|
||||||
(descriptionData) => updateProblem({
|
|
||||||
problemId,
|
|
||||||
displayId: 0,
|
|
||||||
description: descriptionData.content
|
|
||||||
}),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
solution: <EditSolutionPanel
|
|
||||||
problemId={problemId}
|
|
||||||
onUpdate={async (data) => {
|
|
||||||
await handleUpdate(
|
|
||||||
(solutionData) => updateProblem({
|
|
||||||
problemId,
|
|
||||||
displayId: 0,
|
|
||||||
solution: solutionData.content
|
|
||||||
}),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
detail: <EditDetailPanel
|
|
||||||
problemId={problemId}
|
|
||||||
onUpdate={async (data) => {
|
|
||||||
await handleUpdate(
|
|
||||||
(detailData) => updateProblem({
|
|
||||||
problemId,
|
|
||||||
displayId: 0,
|
|
||||||
detail: detailData.content
|
|
||||||
}),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
code: <EditCodePanel
|
|
||||||
problemId={problemId}
|
|
||||||
onUpdate={async (data) => {
|
|
||||||
await handleUpdate(
|
|
||||||
(codeData) => updateProblem({
|
|
||||||
problemId,
|
|
||||||
displayId: 0,
|
|
||||||
templates: [{
|
|
||||||
language: codeData.language || 'c', // 添加默认值
|
|
||||||
content: codeData.content
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
testcase: <EditTestcasePanel
|
|
||||||
problemId={problemId}
|
|
||||||
onUpdate={async (data) => {
|
|
||||||
await handleUpdate(
|
|
||||||
(testcaseData) => updateProblem({
|
|
||||||
problemId,
|
|
||||||
displayId: 0,
|
|
||||||
testcases: [{
|
|
||||||
expectedOutput: testcaseData.content,
|
|
||||||
inputs: testcaseData.inputs || [] // 添加默认空数组
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full">
|
|
||||||
<ProblemFlexLayout components={components} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,15 +1,12 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ProblemHeader } from "@/features/problems/components/header";
|
import { ProblemHeader } from "@/features/problems/components/header";
|
||||||
|
|
||||||
interface ProblemLayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{ problemId: string }>;
|
params: Promise<{ problemId: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProblemLayout({
|
const Layout = async ({ children, params }: LayoutProps) => {
|
||||||
children,
|
|
||||||
params,
|
|
||||||
}: ProblemLayoutProps) {
|
|
||||||
const { problemId } = await params;
|
const { problemId } = await params;
|
||||||
|
|
||||||
if (!problemId) {
|
if (!problemId) {
|
||||||
@ -24,4 +21,6 @@ export default async function ProblemLayout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
|
@ -1,39 +1,17 @@
|
|||||||
import { TestcasePanel } from "@/features/problems/testcase/panel";
|
import { ProblemView } from "@/features/problems/ui/views/problem-view";
|
||||||
import { BotPanel } from "@/features/problems/bot/components/panel";
|
|
||||||
import { CodePanel } from "@/features/problems/code/components/panel";
|
|
||||||
import { DetailPanel } from "@/features/problems/detail/components/panel";
|
|
||||||
import { SolutionPanel } from "@/features/problems/solution/components/panel";
|
|
||||||
import { SubmissionPanel } from "@/features/problems/submission/components/panel";
|
|
||||||
import { DescriptionPanel } from "@/features/problems/description/components/panel";
|
|
||||||
import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout";
|
|
||||||
|
|
||||||
interface ProblemPageProps {
|
interface PageProps {
|
||||||
params: Promise<{ problemId: string }>;
|
params: Promise<{ problemId: string }>;
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
submissionId: string | undefined;
|
submissionId: string | undefined;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProblemPage({
|
const Page = async ({ params, searchParams }: PageProps) => {
|
||||||
params,
|
|
||||||
searchParams,
|
|
||||||
}: ProblemPageProps) {
|
|
||||||
const { problemId } = await params;
|
const { problemId } = await params;
|
||||||
const { submissionId } = await searchParams;
|
const { submissionId } = await searchParams;
|
||||||
|
|
||||||
const components: Record<string, React.ReactNode> = {
|
return <ProblemView problemId={problemId} submissionId={submissionId} />;
|
||||||
description: <DescriptionPanel problemId={problemId} />,
|
};
|
||||||
solution: <SolutionPanel problemId={problemId} />,
|
|
||||||
submission: <SubmissionPanel problemId={problemId} />,
|
|
||||||
detail: <DetailPanel submissionId={submissionId} />,
|
|
||||||
code: <CodePanel problemId={problemId} />,
|
|
||||||
testcase: <TestcasePanel problemId={problemId} />,
|
|
||||||
bot: <BotPanel problemId={problemId} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
export default Page;
|
||||||
<div className="relative flex h-full w-full">
|
|
||||||
<ProblemFlexLayout components={components} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { ProblemEditLayout } from "@/features/admin/ui/layouts/problem-edit-layout";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ problemId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = async ({ children, params }: LayoutProps) => {
|
||||||
|
const { problemId } = await params;
|
||||||
|
|
||||||
|
if (!problemId) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ProblemEditLayout>{children}</ProblemEditLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
13
src/app/(protected)/admin/problems/[problemId]/edit/page.tsx
Normal file
13
src/app/(protected)/admin/problems/[problemId]/edit/page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ problemId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page = async ({ params }: PageProps) => {
|
||||||
|
const { problemId } = await params;
|
||||||
|
|
||||||
|
return <ProblemEditView problemId={problemId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
11
src/app/(protected)/layout.tsx
Normal file
11
src/app/(protected)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
|
return <AdminProtectedLayout>{children}</AdminProtectedLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
150
src/app/actions/ai-testcase.ts
Normal file
150
src/app/actions/ai-testcase.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"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: 2–100, integers: -1000 to 1000) and edge cases.
|
||||||
|
3. **Output Logic**: Ensure outputs correctly reflect problem-specific operations.
|
||||||
|
4. **Randomization**:
|
||||||
|
Vary input magnitudes (mix min/max/-edge values with mid-range randomness)
|
||||||
|
Use diverse data distributions (e.g., sorted/unsorted, negative/positive values)
|
||||||
|
Avoid patterns from existing examples
|
||||||
|
|
||||||
|
Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers.
|
||||||
|
|
||||||
|
Here is the problem description:
|
||||||
|
|
||||||
|
${problemDetails}
|
||||||
|
|
||||||
|
Respond **ONLY** with this JSON structure.
|
||||||
|
***Do not wrap the json codes in JSON markers*** :
|
||||||
|
{
|
||||||
|
"expectedOutput": "Randomized output (e.g., [-5, 100] instead of [1, 2])",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "Parameter 1",
|
||||||
|
"value": <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;
|
||||||
|
};
|
@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Actions } from "flexlayout-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ReactNode, useRef, useState } from "react";
|
import { ReactNode, useRef, useState } from "react";
|
||||||
import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
||||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
import { Actions } from "flexlayout-react";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
interface PreDetailProps {
|
interface PreDetailProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
@ -1,180 +1,240 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import {useState, useEffect} from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import {generateAITestcase} from "@/app/actions/ai-testcase";
|
||||||
import { Input } from "@/components/ui/input";
|
import {Label} from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import {Input} from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {Button} from "@/components/ui/button";
|
||||||
import { getProblemData } from "@/app/actions/getProblem";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {getProblemData} from "@/app/actions/getProblem";
|
||||||
|
|
||||||
export default function EditTestcasePanel({
|
export default function EditTestcasePanel({
|
||||||
problemId,
|
problemId,
|
||||||
}: {
|
}: {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
}) {
|
}) {
|
||||||
const [testcases, setTestcases] = useState<
|
const [testcases, setTestcases] = useState<
|
||||||
Array<{
|
Array<{
|
||||||
id: string;
|
id: string;
|
||||||
expectedOutput: string;
|
expectedOutput: string;
|
||||||
inputs: Array<{
|
inputs: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
const problemData = await getProblemData(problemId);
|
const problemData = await getProblemData(problemId);
|
||||||
if (problemData && problemData.testcases) {
|
if (problemData && problemData.testcases) {
|
||||||
setTestcases(problemData.testcases);
|
setTestcases(problemData.testcases);
|
||||||
} else {
|
} else {
|
||||||
setTestcases([]);
|
setTestcases([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载测试用例失败:", error);
|
||||||
|
setTestcases([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [problemId]);
|
||||||
|
|
||||||
|
const handleAddTestcase = () => {
|
||||||
|
setTestcases([
|
||||||
|
...testcases,
|
||||||
|
{
|
||||||
|
id: `new-${Date.now()}`,
|
||||||
|
expectedOutput: "",
|
||||||
|
inputs: [{name: "input1", value: ""}],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
|
||||||
|
const handleAITestcase = async () => {
|
||||||
|
setIsGenerating(true);
|
||||||
|
try {
|
||||||
|
const AIOutputParsed = await generateAITestcase({problemId: problemId});
|
||||||
|
setTestcases([
|
||||||
|
...testcases,
|
||||||
|
{
|
||||||
|
id: `new-${Date.now()}`,
|
||||||
|
expectedOutput: AIOutputParsed.expectedOutput,
|
||||||
|
inputs: AIOutputParsed.inputs
|
||||||
|
}
|
||||||
|
])
|
||||||
|
window.scrollTo({
|
||||||
|
top: document.body.scrollHeight,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("加载测试用例失败:", error);
|
|
||||||
setTestcases([]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fetchData();
|
|
||||||
}, [problemId]);
|
|
||||||
|
|
||||||
const handleAddTestcase = () => {
|
const handleRemoveTestcase = (index: number) => {
|
||||||
setTestcases([
|
const newTestcases = [...testcases];
|
||||||
...testcases,
|
newTestcases.splice(index, 1);
|
||||||
{
|
setTestcases(newTestcases);
|
||||||
id: `new-${Date.now()}`,
|
};
|
||||||
expectedOutput: "",
|
|
||||||
inputs: [{ name: "input1", value: "" }],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveTestcase = (index: number) => {
|
const handleInputChange = (
|
||||||
const newTestcases = [...testcases];
|
testcaseIndex: number,
|
||||||
newTestcases.splice(index, 1);
|
inputIndex: number,
|
||||||
setTestcases(newTestcases);
|
field: "name" | "value",
|
||||||
};
|
value: string
|
||||||
|
) => {
|
||||||
|
const newTestcases = [...testcases];
|
||||||
|
newTestcases[testcaseIndex].inputs[inputIndex][field] = value;
|
||||||
|
setTestcases(newTestcases);
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleExpectedOutputChange = (testcaseIndex: number, value: string) => {
|
||||||
testcaseIndex: number,
|
const newTestcases = [...testcases];
|
||||||
inputIndex: number,
|
newTestcases[testcaseIndex].expectedOutput = value;
|
||||||
field: "name" | "value",
|
setTestcases(newTestcases);
|
||||||
value: string
|
};
|
||||||
) => {
|
|
||||||
const newTestcases = [...testcases];
|
|
||||||
newTestcases[testcaseIndex].inputs[inputIndex][field] = value;
|
|
||||||
setTestcases(newTestcases);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExpectedOutputChange = (testcaseIndex: number, value: string) => {
|
const handleAddInput = (testcaseIndex: number) => {
|
||||||
const newTestcases = [...testcases];
|
const newTestcases = [...testcases];
|
||||||
newTestcases[testcaseIndex].expectedOutput = value;
|
newTestcases[testcaseIndex].inputs.push({
|
||||||
setTestcases(newTestcases);
|
name: `input${newTestcases[testcaseIndex].inputs.length + 1}`,
|
||||||
};
|
value: "",
|
||||||
|
});
|
||||||
|
setTestcases(newTestcases);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddInput = (testcaseIndex: number) => {
|
const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => {
|
||||||
const newTestcases = [...testcases];
|
const newTestcases = [...testcases];
|
||||||
newTestcases[testcaseIndex].inputs.push({
|
newTestcases[testcaseIndex].inputs.splice(inputIndex, 1);
|
||||||
name: `input${newTestcases[testcaseIndex].inputs.length + 1}`,
|
setTestcases(newTestcases);
|
||||||
value: "",
|
};
|
||||||
});
|
|
||||||
setTestcases(newTestcases);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => {
|
return (
|
||||||
const newTestcases = [...testcases];
|
<Card className="w-full">
|
||||||
newTestcases[testcaseIndex].inputs.splice(inputIndex, 1);
|
<CardHeader>
|
||||||
setTestcases(newTestcases);
|
<CardTitle>测试用例</CardTitle>
|
||||||
};
|
<div className="flex items-center space-x-1"> {/* space-x-1 让按钮更接近 */}
|
||||||
|
<Button type="button" onClick={handleAddTestcase}>
|
||||||
return (
|
添加测试用例
|
||||||
<Card className="w-full">
|
</Button>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>测试用例</CardTitle>
|
|
||||||
<Button type="button" onClick={handleAddTestcase}>
|
|
||||||
添加测试用例
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
{testcases.map((testcase, index) => (
|
|
||||||
<div key={testcase.id} className="border p-4 rounded-md space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h3 className="text-lg font-medium">测试用例 {index + 1}</h3>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
className="flex items-center gap-1"
|
||||||
onClick={() => handleRemoveTestcase(index)}
|
onClick={handleAITestcase}
|
||||||
|
disabled={isGenerating}
|
||||||
|
style={{
|
||||||
|
opacity: isGenerating ? 0.7 : 1,
|
||||||
|
cursor: isGenerating ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
删除
|
<svg
|
||||||
|
data-testid="geist-icon"
|
||||||
|
height="16"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{color: isGenerating ? '#888' : "currentColor"}}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 0.5V0H3.5V0.5C3.5 1.60457 4.39543 2.5 5.5 2.5H6V3V3.5H5.5C4.39543 3.5 3.5 4.39543 3.5 5.5V6H3H2.5V5.5C2.5 4.39543 1.60457 3.5 0.5 3.5H0V3V2.5H0.5C1.60457 2.5 2.5 1.60457 2.5 0.5Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
<path
|
||||||
|
d="M14.5 4.5V5H13.5V4.5C13.5 3.94772 13.0523 3.5 12.5 3.5H12V3V2.5H12.5C13.0523 2.5 13.5 2.05228 13.5 1.5V1H14H14.5V1.5C14.5 2.05228 14.9477 2.5 15.5 2.5H16V3V3.5H15.5C14.9477 3.5 14.5 3.94772 14.5 4.5Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
<path
|
||||||
|
d="M8.40706 4.92939L8.5 4H9.5L9.59294 4.92939C9.82973 7.29734 11.7027 9.17027 14.0706 9.40706L15 9.5V10.5L14.0706 10.5929C11.7027 10.8297 9.82973 12.7027 9.59294 15.0706L9.5 16H8.5L8.40706 15.0706C8.17027 12.7027 6.29734 10.8297 3.92939 10.5929L3 10.5V9.5L3.92939 9.40706C6.29734 9.17027 8.17027 7.29734 8.40706 4.92939Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
{isGenerating ? '生成中...' : '使用AI生成测试用例'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{testcases.map((testcase, index) => (
|
||||||
|
<div key={testcase.id} className="border p-4 rounded-md space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">测试用例 {index + 1}</h3>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleRemoveTestcase(index)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`expected-output-${index}`}>预期输出</Label>
|
<Label htmlFor={`expected-output-${index}`}>预期输出</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`expected-output-${index}`}
|
id={`expected-output-${index}`}
|
||||||
value={testcase.expectedOutput}
|
value={testcase.expectedOutput}
|
||||||
onChange={(e) => handleExpectedOutputChange(index, e.target.value)}
|
onChange={(e) => handleExpectedOutputChange(index, e.target.value)}
|
||||||
placeholder="输入预期输出"
|
placeholder="输入预期输出"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Label>输入参数</Label>
|
<Label>输入参数</Label>
|
||||||
<Button type="button" onClick={() => handleAddInput(index)}>
|
<Button type="button" onClick={() => handleAddInput(index)}>
|
||||||
添加输入
|
添加输入
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{testcase.inputs.map((input, inputIndex) => (
|
{testcase.inputs.map((input, inputIndex) => (
|
||||||
<div key={input.name} className="grid grid-cols-2 gap-4">
|
<div key={input.name} className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`input-name-${index}-${inputIndex}`}>
|
<Label htmlFor={`input-name-${index}-${inputIndex}`}>
|
||||||
参数名称
|
参数名称
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`input-name-${index}-${inputIndex}`}
|
id={`input-name-${index}-${inputIndex}`}
|
||||||
value={input.name}
|
value={input.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(index, inputIndex, "name", e.target.value)
|
handleInputChange(index, inputIndex, "name", e.target.value)
|
||||||
}
|
}
|
||||||
placeholder="输入参数名称"
|
placeholder="输入参数名称"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`input-value-${index}-${inputIndex}`}>
|
<Label htmlFor={`input-value-${index}-${inputIndex}`}>
|
||||||
参数值
|
参数值
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`input-value-${index}-${inputIndex}`}
|
id={`input-value-${index}-${inputIndex}`}
|
||||||
value={input.value}
|
value={input.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange(index, inputIndex, "value", e.target.value)
|
handleInputChange(index, inputIndex, "value", e.target.value)
|
||||||
}
|
}
|
||||||
placeholder="输入参数值"
|
placeholder="输入参数值"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{inputIndex > 0 && (
|
{inputIndex > 0 && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleRemoveInput(index, inputIndex)}
|
onClick={() => handleRemoveInput(index, inputIndex)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
删除输入
|
删除输入
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
26
src/features/admin/ui/components/problem-edit-flexlayout.tsx
Normal file
26
src/features/admin/ui/components/problem-edit-flexlayout.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useProblemEditFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
|
import { FlexLayout } from "@/features/problems/components/flexlayout";
|
||||||
|
|
||||||
|
interface ProblemEditFlexLayoutProps {
|
||||||
|
components: Record<string, React.ReactNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProblemEditFlexLayout = ({
|
||||||
|
components,
|
||||||
|
}: ProblemEditFlexLayoutProps) => {
|
||||||
|
const { hasHydrated, model, jsonModel, setModel, setJsonModel } =
|
||||||
|
useProblemEditFlexLayoutStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlexLayout
|
||||||
|
components={components}
|
||||||
|
hasHydrated={hasHydrated}
|
||||||
|
model={model}
|
||||||
|
jsonModel={jsonModel}
|
||||||
|
setModel={setModel}
|
||||||
|
setJsonModel={setJsonModel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
30
src/features/admin/ui/layouts/admin-protected-layout.tsx
Normal file
30
src/features/admin/ui/layouts/admin-protected-layout.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { auth, signIn } from "@/lib/auth";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface AdminProtectedLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminProtectedLayout = async ({
|
||||||
|
children,
|
||||||
|
}: AdminProtectedLayoutProps) => {
|
||||||
|
const session = await auth();
|
||||||
|
const userId = session?.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
await signIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user?.role !== "ADMIN") redirect("/unauthorized");
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
29
src/features/admin/ui/layouts/problem-edit-layout.tsx
Normal file
29
src/features/admin/ui/layouts/problem-edit-layout.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { BackButton } from "@/components/back-button";
|
||||||
|
import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar";
|
||||||
|
|
||||||
|
interface ProblemEditLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProblemEditLayout = ({ children }: ProblemEditLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen">
|
||||||
|
<header className="relative flex h-12 flex-none items-center">
|
||||||
|
<div className="container mx-auto flex h-full items-center justify-between px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<BackButton href="/problem" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Suspense fallback={<UserAvatarSkeleton />}>
|
||||||
|
<UserAvatar />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex w-full flex-grow overflow-y-hidden p-2.5 pt-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
26
src/features/admin/ui/views/problem-edit-view.tsx
Normal file
26
src/features/admin/ui/views/problem-edit-view.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import EditCodePanel from "@/components/creater/edit-code-panel";
|
||||||
|
import EditDetailPanel from "@/components/creater/edit-detail-panel";
|
||||||
|
import EditSolutionPanel from "@/components/creater/edit-solution-panel";
|
||||||
|
import EditTestcasePanel from "@/components/creater/edit-testcase-panel";
|
||||||
|
import EditDescriptionPanel from "@/components/creater/edit-description-panel";
|
||||||
|
import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout";
|
||||||
|
|
||||||
|
interface ProblemEditViewProps {
|
||||||
|
problemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => {
|
||||||
|
const components: Record<string, React.ReactNode> = {
|
||||||
|
description: <EditDescriptionPanel problemId={problemId} />,
|
||||||
|
solution: <EditSolutionPanel problemId={problemId} />,
|
||||||
|
detail: <EditDetailPanel problemId={problemId} />,
|
||||||
|
code: <EditCodePanel problemId={problemId} />,
|
||||||
|
testcase: <EditTestcasePanel problemId={problemId} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full">
|
||||||
|
<ProblemEditFlexLayout components={components} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -11,7 +11,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { Actions, DockLocation } from "flexlayout-react";
|
import { Actions, DockLocation } from "flexlayout-react";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
|
|
||||||
export const ViewBotButton = () => {
|
export const ViewBotButton = () => {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
111
src/features/problems/components/flexlayout.tsx
Normal file
111
src/features/problems/components/flexlayout.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BotIcon,
|
||||||
|
CircleCheckBigIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
FlaskConicalIcon,
|
||||||
|
SquareCheckIcon,
|
||||||
|
SquarePenIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
IJsonModel,
|
||||||
|
ITabRenderValues,
|
||||||
|
Layout,
|
||||||
|
Model,
|
||||||
|
TabNode,
|
||||||
|
} from "flexlayout-react";
|
||||||
|
import "@/styles/flexlayout.css";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
interface FlexLayoutProps {
|
||||||
|
components: Record<string, React.ReactNode>;
|
||||||
|
hasHydrated: boolean;
|
||||||
|
model: Model | null;
|
||||||
|
jsonModel: IJsonModel;
|
||||||
|
setModel: (model: Model) => void;
|
||||||
|
setJsonModel: (jsonModel: IJsonModel) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlexLayout = ({
|
||||||
|
components,
|
||||||
|
hasHydrated,
|
||||||
|
model,
|
||||||
|
jsonModel,
|
||||||
|
setModel,
|
||||||
|
setJsonModel,
|
||||||
|
}: FlexLayoutProps) => {
|
||||||
|
const t = useTranslations("ProblemPage");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasHydrated && !model) {
|
||||||
|
const model = Model.fromJson(jsonModel);
|
||||||
|
setModel(model);
|
||||||
|
}
|
||||||
|
}, [hasHydrated, jsonModel, model, setModel]);
|
||||||
|
|
||||||
|
const onModelChange = useCallback(
|
||||||
|
(model: Model) => {
|
||||||
|
const jsonModel = model.toJson();
|
||||||
|
setJsonModel(jsonModel);
|
||||||
|
},
|
||||||
|
[setJsonModel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const factory = useCallback(
|
||||||
|
(node: TabNode) => {
|
||||||
|
const component = node.getComponent();
|
||||||
|
return component ? components[component] : null;
|
||||||
|
},
|
||||||
|
[components]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRenderTab = useCallback(
|
||||||
|
(node: TabNode, renderValues: ITabRenderValues) => {
|
||||||
|
const Icon = getIconForTab(node.getId());
|
||||||
|
renderValues.leading = Icon ? (
|
||||||
|
<Icon className="opacity-60" size={16} aria-hidden="true" />
|
||||||
|
) : null;
|
||||||
|
renderValues.content = (
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t(node.getName()) || node.getName()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!model || !hasHydrated)
|
||||||
|
return <Skeleton className="h-full w-full rounded-2xl" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
model={model}
|
||||||
|
factory={factory}
|
||||||
|
onRenderTab={onRenderTab}
|
||||||
|
onModelChange={onModelChange}
|
||||||
|
realtimeResize={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconForTab = (id: string) => {
|
||||||
|
switch (id) {
|
||||||
|
case "description":
|
||||||
|
return FileTextIcon;
|
||||||
|
case "solution":
|
||||||
|
return FlaskConicalIcon;
|
||||||
|
case "submission":
|
||||||
|
return CircleCheckBigIcon;
|
||||||
|
case "detail":
|
||||||
|
return CircleCheckBigIcon;
|
||||||
|
case "code":
|
||||||
|
return SquarePenIcon;
|
||||||
|
case "testcase":
|
||||||
|
return SquareCheckIcon;
|
||||||
|
case "bot":
|
||||||
|
return BotIcon;
|
||||||
|
}
|
||||||
|
};
|
@ -10,7 +10,7 @@ import { LoaderCircleIcon, PlayIcon } from "lucide-react";
|
|||||||
import { TooltipButton } from "@/components/tooltip-button";
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||||
import { JudgeToast } from "@/features/problems/components/judge-toast";
|
import { JudgeToast } from "@/features/problems/components/judge-toast";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
|
|
||||||
interface JudgeButtonProps {
|
interface JudgeButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -1,94 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { FlexLayout } from "./flexlayout";
|
||||||
BotIcon,
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
CircleCheckBigIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
FlaskConicalIcon,
|
|
||||||
SquareCheckIcon,
|
|
||||||
SquarePenIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import "@/styles/flexlayout.css";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useCallback, useEffect } from "react";
|
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
|
||||||
import { ITabRenderValues, Layout, Model, TabNode } from "flexlayout-react";
|
|
||||||
|
|
||||||
interface ProblemFlexLayoutProps {
|
interface ProblemFlexLayoutProps {
|
||||||
components: Record<string, React.ReactNode>;
|
components: Record<string, React.ReactNode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProblemFlexLayout = ({ components }: ProblemFlexLayoutProps) => {
|
export const ProblemFlexLayout = ({ components }: ProblemFlexLayoutProps) => {
|
||||||
const t = useTranslations("ProblemPage");
|
const { hasHydrated, model, jsonModel, setModel, setJsonModel } =
|
||||||
const { model, setModel, jsonModel, setJsonModel } =
|
|
||||||
useProblemFlexLayoutStore();
|
useProblemFlexLayoutStore();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!model) {
|
|
||||||
const model = Model.fromJson(jsonModel);
|
|
||||||
setModel(model);
|
|
||||||
}
|
|
||||||
}, [jsonModel, model, setModel]);
|
|
||||||
|
|
||||||
const onModelChange = useCallback(
|
|
||||||
(model: Model) => {
|
|
||||||
const jsonModel = model.toJson();
|
|
||||||
setJsonModel(jsonModel);
|
|
||||||
},
|
|
||||||
[setJsonModel]
|
|
||||||
);
|
|
||||||
|
|
||||||
const factory = useCallback(
|
|
||||||
(node: TabNode) => {
|
|
||||||
const component = node.getComponent();
|
|
||||||
return component ? components[component] : null;
|
|
||||||
},
|
|
||||||
[components]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRenderTab = useCallback(
|
|
||||||
(node: TabNode, renderValues: ITabRenderValues) => {
|
|
||||||
const Icon = getIconForTab(node.getId());
|
|
||||||
renderValues.leading = Icon ? (
|
|
||||||
<Icon className="opacity-60" size={16} aria-hidden="true" />
|
|
||||||
) : null;
|
|
||||||
renderValues.content = (
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{t(node.getName()) || node.getName()}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!model) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<FlexLayout
|
||||||
|
components={components}
|
||||||
|
hasHydrated={hasHydrated}
|
||||||
model={model}
|
model={model}
|
||||||
factory={factory}
|
jsonModel={jsonModel}
|
||||||
onRenderTab={onRenderTab}
|
setModel={setModel}
|
||||||
onModelChange={onModelChange}
|
setJsonModel={setJsonModel}
|
||||||
realtimeResize={true}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIconForTab = (id: string) => {
|
|
||||||
switch (id) {
|
|
||||||
case "description":
|
|
||||||
return FileTextIcon;
|
|
||||||
case "solution":
|
|
||||||
return FlaskConicalIcon;
|
|
||||||
case "submission":
|
|
||||||
return CircleCheckBigIcon;
|
|
||||||
case "detail":
|
|
||||||
return CircleCheckBigIcon;
|
|
||||||
case "code":
|
|
||||||
return SquarePenIcon;
|
|
||||||
case "testcase":
|
|
||||||
return SquareCheckIcon;
|
|
||||||
case "bot":
|
|
||||||
return BotIcon;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -4,7 +4,7 @@ import { Actions } from "flexlayout-react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
export const DetailHeader = () => {
|
export const DetailHeader = () => {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { Actions } from "flexlayout-react";
|
import { Actions } from "flexlayout-react";
|
||||||
import { BookOpenIcon } from "lucide-react";
|
import { BookOpenIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
|
|
||||||
export const ViewSolutionButton = () => {
|
export const ViewSolutionButton = () => {
|
||||||
const { model } = useProblemFlexLayoutStore();
|
const { model } = useProblemFlexLayoutStore();
|
||||||
|
@ -7,7 +7,7 @@ import { Locale, Submission } from "@/generated/client";
|
|||||||
import { Actions, DockLocation } from "flexlayout-react";
|
import { Actions, DockLocation } from "flexlayout-react";
|
||||||
import { getColorClassForStatus } from "@/config/status";
|
import { getColorClassForStatus } from "@/config/status";
|
||||||
import { TableCell, TableRow } from "@/components/ui/table";
|
import { TableCell, TableRow } from "@/components/ui/table";
|
||||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { getIconForLanguage, getLabelForLanguage } from "@/config/language";
|
import { getIconForLanguage, getLabelForLanguage } from "@/config/language";
|
||||||
|
|
||||||
|
31
src/features/problems/ui/views/problem-view.tsx
Normal file
31
src/features/problems/ui/views/problem-view.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { TestcasePanel } from "@/features/problems/testcase/panel";
|
||||||
|
import { BotPanel } from "@/features/problems/bot/components/panel";
|
||||||
|
import { CodePanel } from "@/features/problems/code/components/panel";
|
||||||
|
import { DetailPanel } from "@/features/problems/detail/components/panel";
|
||||||
|
import { SolutionPanel } from "@/features/problems/solution/components/panel";
|
||||||
|
import { SubmissionPanel } from "@/features/problems/submission/components/panel";
|
||||||
|
import { DescriptionPanel } from "@/features/problems/description/components/panel";
|
||||||
|
import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout";
|
||||||
|
|
||||||
|
interface ProblemViewProps {
|
||||||
|
problemId: string;
|
||||||
|
submissionId: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProblemView = ({ problemId, submissionId }: ProblemViewProps) => {
|
||||||
|
const components: Record<string, React.ReactNode> = {
|
||||||
|
description: <DescriptionPanel problemId={problemId} />,
|
||||||
|
solution: <SolutionPanel problemId={problemId} />,
|
||||||
|
submission: <SubmissionPanel problemId={problemId} />,
|
||||||
|
detail: <DetailPanel submissionId={submissionId} />,
|
||||||
|
code: <CodePanel problemId={problemId} />,
|
||||||
|
testcase: <TestcasePanel problemId={problemId} />,
|
||||||
|
bot: <BotPanel problemId={problemId} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full">
|
||||||
|
<ProblemFlexLayout components={components} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
211
src/stores/flexlayout.ts
Normal file
211
src/stores/flexlayout.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { IJsonModel, Model } from "flexlayout-react";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
type FlexLayoutState = {
|
||||||
|
hasHydrated: boolean;
|
||||||
|
model: Model | null;
|
||||||
|
jsonModel: IJsonModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlexLayoutAction = {
|
||||||
|
setHasHydrated: (hasHydrated: boolean) => void;
|
||||||
|
setModel: (model: Model) => void;
|
||||||
|
setJsonModel: (jsonModel: IJsonModel) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlexLayoutStore = FlexLayoutState & FlexLayoutAction;
|
||||||
|
|
||||||
|
const createFlexLayoutStore = (storageKey: string, jsonModel: IJsonModel) =>
|
||||||
|
create<FlexLayoutStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
hasHydrated: false,
|
||||||
|
model: null,
|
||||||
|
jsonModel: jsonModel,
|
||||||
|
setHasHydrated: (hasHydrated) => set({ hasHydrated }),
|
||||||
|
setModel: (model) => set({ model }),
|
||||||
|
setJsonModel: (jsonModel) => set({ jsonModel }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: storageKey,
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
jsonModel: state.jsonModel,
|
||||||
|
}),
|
||||||
|
onRehydrateStorage: () => {
|
||||||
|
return (state) => {
|
||||||
|
state?.setHasHydrated(true);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialProblemFlexLayoutJsonModel: IJsonModel = {
|
||||||
|
global: {
|
||||||
|
// tabEnableClose: false,
|
||||||
|
tabEnableRename: false,
|
||||||
|
},
|
||||||
|
borders: [],
|
||||||
|
layout: {
|
||||||
|
type: "row",
|
||||||
|
weight: 100,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "1",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "description",
|
||||||
|
name: "Description",
|
||||||
|
component: "description",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "solution",
|
||||||
|
name: "Solutions",
|
||||||
|
component: "solution",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "submission",
|
||||||
|
name: "Submissions",
|
||||||
|
component: "submission",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// type: "tab",
|
||||||
|
// id: "detail",
|
||||||
|
// name: "Details",
|
||||||
|
// component: "detail",
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "2",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "code",
|
||||||
|
name: "Code",
|
||||||
|
component: "code",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "3",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "testcase",
|
||||||
|
name: "Testcase",
|
||||||
|
component: "testcase",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProblemFlexLayoutStore = createFlexLayoutStore(
|
||||||
|
"problem-flexlayout",
|
||||||
|
initialProblemFlexLayoutJsonModel
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialProblemEditFlexLayoutJsonModel: IJsonModel = {
|
||||||
|
global: {
|
||||||
|
// tabEnableClose: false,
|
||||||
|
tabEnableRename: false,
|
||||||
|
},
|
||||||
|
borders: [],
|
||||||
|
layout: {
|
||||||
|
type: "row",
|
||||||
|
weight: 100,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "1",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "description",
|
||||||
|
name: "Description",
|
||||||
|
component: "description",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "solution",
|
||||||
|
name: "Solutions",
|
||||||
|
component: "solution",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "detail",
|
||||||
|
name: "Details",
|
||||||
|
component: "detail",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "2",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "code",
|
||||||
|
name: "Code",
|
||||||
|
component: "code",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tabset",
|
||||||
|
id: "3",
|
||||||
|
weight: 50,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "tab",
|
||||||
|
id: "testcase",
|
||||||
|
name: "Testcase",
|
||||||
|
component: "testcase",
|
||||||
|
enableClose: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProblemEditFlexLayoutStore = createFlexLayoutStore(
|
||||||
|
"problem-edit-flexlayout",
|
||||||
|
initialProblemEditFlexLayoutJsonModel
|
||||||
|
);
|
@ -1,115 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { IJsonModel, Model } from "flexlayout-react";
|
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
|
||||||
|
|
||||||
const initialJsonModel: IJsonModel = {
|
|
||||||
global: {
|
|
||||||
// tabEnableClose: false,
|
|
||||||
tabEnableRename: false,
|
|
||||||
},
|
|
||||||
borders: [],
|
|
||||||
layout: {
|
|
||||||
type: "row",
|
|
||||||
weight: 100,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "tabset",
|
|
||||||
id: "1",
|
|
||||||
weight: 50,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "tab",
|
|
||||||
id: "description",
|
|
||||||
name: "Description",
|
|
||||||
component: "description",
|
|
||||||
enableClose: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "tab",
|
|
||||||
id: "solution",
|
|
||||||
name: "Solutions",
|
|
||||||
component: "solution",
|
|
||||||
enableClose: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "tab",
|
|
||||||
id: "submission",
|
|
||||||
name: "Submissions",
|
|
||||||
component: "submission",
|
|
||||||
enableClose: false,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// type: "tab",
|
|
||||||
// id: "detail",
|
|
||||||
// name: "Details",
|
|
||||||
// component: "detail",
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "row",
|
|
||||||
weight: 50,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "tabset",
|
|
||||||
id: "2",
|
|
||||||
weight: 50,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "tab",
|
|
||||||
id: "code",
|
|
||||||
name: "Code",
|
|
||||||
component: "code",
|
|
||||||
enableClose: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "tabset",
|
|
||||||
id: "3",
|
|
||||||
weight: 50,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "tab",
|
|
||||||
id: "testcase",
|
|
||||||
name: "Testcase",
|
|
||||||
component: "testcase",
|
|
||||||
enableClose: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProblemFlexLayoutState = {
|
|
||||||
model: Model | null;
|
|
||||||
jsonModel: IJsonModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProblemFlexLayoutAction = {
|
|
||||||
setModel: (model: Model) => void;
|
|
||||||
setJsonModel: (jsonModel: IJsonModel) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProblemFlexLayoutStore = ProblemFlexLayoutState & ProblemFlexLayoutAction;
|
|
||||||
|
|
||||||
export const useProblemFlexLayoutStore = create<ProblemFlexLayoutStore>()(
|
|
||||||
persist(
|
|
||||||
(set) => ({
|
|
||||||
model: null,
|
|
||||||
jsonModel: initialJsonModel,
|
|
||||||
setModel: (model) => set({ model }),
|
|
||||||
setJsonModel: (jsonModel) => set({ jsonModel }),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "problem-flexlayout",
|
|
||||||
storage: createJSONStorage(() => localStorage),
|
|
||||||
partialize: (state) => ({
|
|
||||||
jsonModel: state.jsonModel,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
21
src/types/ai-testcase.ts
Normal file
21
src/types/ai-testcase.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user