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 { ProblemHeader } from "@/features/problems/components/header";
|
||||
|
||||
interface ProblemLayoutProps {
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ problemId: string }>;
|
||||
}
|
||||
|
||||
export default async function ProblemLayout({
|
||||
children,
|
||||
params,
|
||||
}: ProblemLayoutProps) {
|
||||
const Layout = async ({ children, params }: LayoutProps) => {
|
||||
const { problemId } = await params;
|
||||
|
||||
if (!problemId) {
|
||||
@ -24,4 +21,6 @@ export default async function ProblemLayout({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
@ -1,39 +1,17 @@
|
||||
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";
|
||||
import { ProblemView } from "@/features/problems/ui/views/problem-view";
|
||||
|
||||
interface ProblemPageProps {
|
||||
interface PageProps {
|
||||
params: Promise<{ problemId: string }>;
|
||||
searchParams: Promise<{
|
||||
submissionId: string | undefined;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function ProblemPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: ProblemPageProps) {
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const { problemId } = await params;
|
||||
const { submissionId } = await searchParams;
|
||||
|
||||
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 <ProblemView problemId={problemId} submissionId={submissionId} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full">
|
||||
<ProblemFlexLayout components={components} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Page;
|
||||
|
@ -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";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ReactNode, useRef, useState } from "react";
|
||||
import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
|
||||
interface PreDetailProps {
|
||||
children?: ReactNode;
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {useState, useEffect} from "react";
|
||||
import {generateAITestcase} from "@/app/actions/ai-testcase";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Button} from "@/components/ui/button";
|
||||
@ -37,6 +38,7 @@ export default function EditTestcasePanel({
|
||||
setTestcases([]);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [problemId]);
|
||||
|
||||
@ -51,6 +53,31 @@ export default function EditTestcasePanel({
|
||||
]);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveTestcase = (index: number) => {
|
||||
const newTestcases = [...testcases];
|
||||
newTestcases.splice(index, 1);
|
||||
@ -93,9 +120,41 @@ export default function EditTestcasePanel({
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>测试用例</CardTitle>
|
||||
<div className="flex items-center space-x-1"> {/* space-x-1 让按钮更接近 */}
|
||||
<Button type="button" onClick={handleAddTestcase}>
|
||||
添加测试用例
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex items-center gap-1"
|
||||
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>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
@ -176,5 +235,6 @@ export default function EditTestcasePanel({
|
||||
</div>
|
||||
</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 { Toggle } from "@/components/ui/toggle";
|
||||
import { Actions, DockLocation } from "flexlayout-react";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
|
||||
export const ViewBotButton = () => {
|
||||
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 { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { JudgeToast } from "@/features/problems/components/judge-toast";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
|
||||
interface JudgeButtonProps {
|
||||
className?: string;
|
||||
|
@ -1,94 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
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";
|
||||
import { FlexLayout } from "./flexlayout";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
|
||||
interface ProblemFlexLayoutProps {
|
||||
components: Record<string, React.ReactNode>;
|
||||
}
|
||||
|
||||
export const ProblemFlexLayout = ({ components }: ProblemFlexLayoutProps) => {
|
||||
const t = useTranslations("ProblemPage");
|
||||
const { model, setModel, jsonModel, setJsonModel } =
|
||||
const { hasHydrated, model, jsonModel, setModel, setJsonModel } =
|
||||
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 (
|
||||
<Layout
|
||||
<FlexLayout
|
||||
components={components}
|
||||
hasHydrated={hasHydrated}
|
||||
model={model}
|
||||
factory={factory}
|
||||
onRenderTab={onRenderTab}
|
||||
onModelChange={onModelChange}
|
||||
realtimeResize={true}
|
||||
jsonModel={jsonModel}
|
||||
setModel={setModel}
|
||||
setJsonModel={setJsonModel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 { ArrowLeftIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export const DetailHeader = () => {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/flexlayout";
|
||||
|
||||
export const ViewSolutionButton = () => {
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
|
@ -7,7 +7,7 @@ import { Locale, Submission } from "@/generated/client";
|
||||
import { Actions, DockLocation } from "flexlayout-react";
|
||||
import { getColorClassForStatus } from "@/config/status";
|
||||
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 { 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