The window is basically complete, but there are some issues need to fix.

1. The window won't scroll when complete.
2. The AI needs the context to generate new inputs, I don't know
how to write.
This commit is contained in:
Dioxide 2025-06-18 21:14:50 +08:00 committed by cfngc4594
parent 79d56204ce
commit 22ce35ca7d
3 changed files with 385 additions and 154 deletions

View 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: 2100, integers: -1000 to 1000) and edge cases.
3. **Output Logic**: Ensure outputs correctly reflect problem-specific operations.
4. **Randomization**:
Vary input magnitudes (mix min/max/-edge values with mid-range randomness)
Use diverse data distributions (e.g., sorted/unsorted, negative/positive values)
Avoid patterns from existing examples
Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers.
Here is the problem description:
${problemDetails}
Respond **ONLY** with this JSON structure.
***Do not wrap the json codes in JSON markers*** :
{
"expectedOutput": "Randomized output (e.g., [-5, 100] instead of [1, 2])",
"inputs": [
{
"name": "Parameter 1",
"value": <RANDOMIZED_DATA> // Use string to wrap actual JSON types (arrays/numbers/strings)
},
... // Add parameters as needed
]
}
`;
// 发送请求给OpenAI
const messages: CoreMessage[] = [{ role: "user", content: prompt }];
let text;
try {
const response = await generateText({
model: model,
messages: messages,
});
text = response.text;
} catch (error) {
console.error("Error generating text with OpenAI:", error);
throw new Error("Failed to generate response from OpenAI");
}
// 解析LLM响应
let llmResponseJson;
try {
llmResponseJson = JSON.parse(text)
} catch (error) {
console.error("Failed to parse LLM response as JSON:", error);
console.error("LLM raw output:", text);
throw new Error("Invalid JSON response from LLM");
}
// 验证响应格式
const validationResult = AITestCaseOutputSchema.safeParse(llmResponseJson);
if (!validationResult.success) {
console.error("Zod validation failed:", validationResult.error.format());
throw new Error("Response validation failed");
}
console.log("LLM response:", llmResponseJson);
return validationResult.data;
};

View File

@ -1,11 +1,12 @@
"use client";
import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getProblemData } from "@/app/actions/getProblem";
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";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {getProblemData} from "@/app/actions/getProblem";
export default function EditTestcasePanel({
problemId,
@ -37,6 +38,7 @@ export default function EditTestcasePanel({
setTestcases([]);
}
}
fetchData();
}, [problemId]);
@ -46,11 +48,36 @@ export default function EditTestcasePanel({
{
id: `new-${Date.now()}`,
expectedOutput: "",
inputs: [{ name: "input1", value: "" }],
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);
}
}
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>
);
}

21
src/types/ai-testcase.ts Normal file
View 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>