mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2026-05-31 10:18:52 +00:00
feat(chat): add problem context tools
This commit is contained in:
parent
4d97ed602d
commit
65edacea8c
@ -1,11 +1,17 @@
|
|||||||
import { streamText } from "ai";
|
import { streamText, tool } from "ai";
|
||||||
|
import { z } from "zod";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
import { model } from "@/lib/ai";
|
import { model } from "@/lib/ai";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { Locale, ProblemContentType } from "@/generated/client";
|
||||||
|
|
||||||
// Allow streaming responses up to 30 seconds
|
// Allow streaming responses up to 30 seconds
|
||||||
export const maxDuration = 30;
|
export const maxDuration = 30;
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const { messages } = await req.json();
|
const { messages, problemId, locale, submissionId } = await req.json();
|
||||||
|
const userId = (await auth())?.user?.id;
|
||||||
|
const activeLocale = locale === Locale.en ? Locale.en : Locale.zh;
|
||||||
|
|
||||||
const system = `You are a patient programming tutor for Judge4c, a platform for learning C and C++.
|
const system = `You are a patient programming tutor for Judge4c, a platform for learning C and C++.
|
||||||
|
|
||||||
@ -13,6 +19,8 @@ Help users understand problems, debug code, and improve solutions with clear, re
|
|||||||
|
|
||||||
Prefer concise answers for simple questions. For debugging or refactoring, be specific and practical: point to the likely cause, suggest a minimal fix, and mention any important trade-offs. Avoid insults, sarcasm, profanity, or comments that shame the user.
|
Prefer concise answers for simple questions. For debugging or refactoring, be specific and practical: point to the likely cause, suggest a minimal fix, and mention any important trade-offs. Avoid insults, sarcasm, profanity, or comments that shame the user.
|
||||||
|
|
||||||
|
Use tools when the user's question depends on current problem context. Use getCurrentCode for the latest editor code, getEditorDiagnostics for current editor errors and warnings, getProblemDescription for the statement, getProblemSolution for the official solution, getTestcases for configured test cases, getSubmissionHistory for the user's submissions, and getSubmissionDetail for one submission's code and judge result.
|
||||||
|
|
||||||
When providing code, keep it directly relevant to the user's question and avoid unnecessary rewrites. If the user is working on an algorithmic problem, do not reveal the full solution immediately unless they ask for it; start with hints or targeted guidance.
|
When providing code, keep it directly relevant to the user's question and avoid unnecessary rewrites. If the user is working on an algorithmic problem, do not reveal the full solution immediately unless they ask for it; start with hints or targeted guidance.
|
||||||
|
|
||||||
Reply in the user's language.`;
|
Reply in the user's language.`;
|
||||||
@ -21,7 +29,166 @@ Reply in the user's language.`;
|
|||||||
model,
|
model,
|
||||||
system: system,
|
system: system,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
|
tools: {
|
||||||
|
getCurrentCode: {
|
||||||
|
description:
|
||||||
|
"Get the latest code and language from the user's current editor.",
|
||||||
|
parameters: z.object({}),
|
||||||
|
},
|
||||||
|
getEditorDiagnostics: {
|
||||||
|
description:
|
||||||
|
"Get the latest errors and warnings from the user's current editor diagnostics.",
|
||||||
|
parameters: z.object({}),
|
||||||
|
},
|
||||||
|
getProblemDescription: tool({
|
||||||
|
description: "Get the current problem statement.",
|
||||||
|
parameters: z.object({}),
|
||||||
|
execute: async () =>
|
||||||
|
getProblemContent(problemId, activeLocale, ProblemContentType.DESCRIPTION),
|
||||||
|
}),
|
||||||
|
getProblemSolution: tool({
|
||||||
|
description: "Get the current problem solution or editorial.",
|
||||||
|
parameters: z.object({}),
|
||||||
|
execute: async () =>
|
||||||
|
getProblemContent(problemId, activeLocale, ProblemContentType.SOLUTION),
|
||||||
|
}),
|
||||||
|
getTestcases: tool({
|
||||||
|
description: "Get the current problem's configured test cases.",
|
||||||
|
parameters: z.object({}),
|
||||||
|
execute: async () => {
|
||||||
|
if (!problemId) return { error: "Missing problem id." };
|
||||||
|
|
||||||
|
const testcases = await prisma.testcase.findMany({
|
||||||
|
where: { problemId },
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
expectedOutput: true,
|
||||||
|
inputs: {
|
||||||
|
orderBy: { index: "asc" },
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { testcases };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getSubmissionHistory: tool({
|
||||||
|
description: "Get the current user's submission history for this problem.",
|
||||||
|
parameters: z.object({
|
||||||
|
limit: z.number().int().min(1).max(20).optional(),
|
||||||
|
}),
|
||||||
|
execute: async ({ limit = 10 }) => {
|
||||||
|
if (!userId) return { error: "User is not authenticated." };
|
||||||
|
if (!problemId) return { error: "Missing problem id." };
|
||||||
|
|
||||||
|
const submissions = await prisma.submission.findMany({
|
||||||
|
where: { userId, problemId },
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
language: true,
|
||||||
|
status: true,
|
||||||
|
message: true,
|
||||||
|
timeUsage: true,
|
||||||
|
memoryUsage: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { submissions };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getSubmissionDetail: tool({
|
||||||
|
description:
|
||||||
|
"Get code, compile output, and judge runs for one of the current user's submissions.",
|
||||||
|
parameters: z.object({
|
||||||
|
submissionId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Defaults to the submission currently open in the URL."),
|
||||||
|
}),
|
||||||
|
execute: async ({ submissionId: requestedSubmissionId }) => {
|
||||||
|
if (!userId) return { error: "User is not authenticated." };
|
||||||
|
|
||||||
|
const targetSubmissionId = requestedSubmissionId ?? submissionId;
|
||||||
|
if (!targetSubmissionId) return { error: "Missing submission id." };
|
||||||
|
|
||||||
|
const submission = await prisma.submission.findFirst({
|
||||||
|
where: { id: targetSubmissionId, userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
language: true,
|
||||||
|
content: true,
|
||||||
|
status: true,
|
||||||
|
message: true,
|
||||||
|
timeUsage: true,
|
||||||
|
memoryUsage: true,
|
||||||
|
createdAt: true,
|
||||||
|
judge: {
|
||||||
|
select: {
|
||||||
|
status: true,
|
||||||
|
compileOutput: true,
|
||||||
|
judgeRuns: {
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: {
|
||||||
|
status: true,
|
||||||
|
timeUsage: true,
|
||||||
|
memoryUsage: true,
|
||||||
|
stdin: true,
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
testcase: {
|
||||||
|
select: {
|
||||||
|
expectedOutput: true,
|
||||||
|
inputs: {
|
||||||
|
orderBy: { index: "asc" },
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return submission ?? { error: "Submission not found." };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.toDataStreamResponse();
|
return result.toDataStreamResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getProblemContent(
|
||||||
|
problemId: string | undefined,
|
||||||
|
locale: Locale,
|
||||||
|
type: ProblemContentType
|
||||||
|
) {
|
||||||
|
if (!problemId) return { error: "Missing problem id." };
|
||||||
|
|
||||||
|
const localizations = await prisma.problemLocalization.findMany({
|
||||||
|
where: { problemId, type },
|
||||||
|
select: {
|
||||||
|
locale: true,
|
||||||
|
content: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const content =
|
||||||
|
localizations.find((item) => item.locale === locale)?.content ??
|
||||||
|
localizations[0]?.content;
|
||||||
|
|
||||||
|
return content ? { locale, content } : { error: "Content not found." };
|
||||||
|
}
|
||||||
|
|||||||
@ -23,10 +23,14 @@ const getLocalizedDescription = (
|
|||||||
|
|
||||||
interface BotContentProps {
|
interface BotContentProps {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
|
submissionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BotContent = async ({ problemId }: BotContentProps) => {
|
export const BotContent = async ({
|
||||||
const locale = await getLocale();
|
problemId,
|
||||||
|
submissionId,
|
||||||
|
}: BotContentProps) => {
|
||||||
|
const locale = (await getLocale()) as Locale;
|
||||||
|
|
||||||
const descriptions = await prisma.problemLocalization.findMany({
|
const descriptions = await prisma.problemLocalization.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -35,9 +39,16 @@ export const BotContent = async ({ problemId }: BotContentProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const description = getLocalizedDescription(descriptions, locale as Locale);
|
const description = getLocalizedDescription(descriptions, locale);
|
||||||
|
|
||||||
return <BotForm description={description} />;
|
return (
|
||||||
|
<BotForm
|
||||||
|
description={description}
|
||||||
|
locale={locale}
|
||||||
|
problemId={problemId}
|
||||||
|
submissionId={submissionId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BotContentSkeleton = () => {
|
export const BotContentSkeleton = () => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
|
import { MarkerSeverity } from "monaco-editor";
|
||||||
import {
|
import {
|
||||||
ChatBubble,
|
ChatBubble,
|
||||||
ChatBubbleMessage,
|
ChatBubbleMessage,
|
||||||
@ -17,25 +18,66 @@ import { TooltipButton } from "@/components/tooltip-button";
|
|||||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||||
import { MdxComponents } from "@/components/content/mdx-components";
|
import { MdxComponents } from "@/components/content/mdx-components";
|
||||||
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
||||||
|
import type { Locale } from "@/generated/client";
|
||||||
|
|
||||||
interface BotFormProps {
|
interface BotFormProps {
|
||||||
description: string;
|
description: string;
|
||||||
|
locale: Locale;
|
||||||
|
problemId: string;
|
||||||
|
submissionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BotForm = ({ description }: BotFormProps) => {
|
export const BotForm = ({
|
||||||
|
description,
|
||||||
|
locale,
|
||||||
|
problemId,
|
||||||
|
submissionId,
|
||||||
|
}: BotFormProps) => {
|
||||||
const t = useTranslations("Bot");
|
const t = useTranslations("Bot");
|
||||||
const { problem, language, value } = useProblemEditorStore();
|
const { problem, language, value, markers } = useProblemEditorStore();
|
||||||
|
|
||||||
const { messages, input, handleInputChange, setMessages, handleSubmit } =
|
const { messages, input, handleInputChange, handleSubmit, status } = useChat({
|
||||||
useChat({
|
initialMessages: [
|
||||||
initialMessages: [
|
{
|
||||||
{
|
id: problem?.problemId || "",
|
||||||
id: problem?.problemId || "",
|
role: "system",
|
||||||
role: "system",
|
content: `Problem description:\n${description}`,
|
||||||
content: `Problem description:\n${description}`,
|
},
|
||||||
},
|
],
|
||||||
],
|
body: {
|
||||||
});
|
locale,
|
||||||
|
problemId,
|
||||||
|
submissionId,
|
||||||
|
},
|
||||||
|
maxSteps: 3,
|
||||||
|
onToolCall: async ({ toolCall }) => {
|
||||||
|
if (toolCall.toolName === "getCurrentCode") {
|
||||||
|
return {
|
||||||
|
language,
|
||||||
|
code: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolCall.toolName === "getEditorDiagnostics") {
|
||||||
|
const diagnostics = markers.map((marker) => ({
|
||||||
|
severity: getMarkerSeverityLabel(marker.severity),
|
||||||
|
message: marker.message,
|
||||||
|
source: marker.source,
|
||||||
|
code: marker.code,
|
||||||
|
startLineNumber: marker.startLineNumber,
|
||||||
|
startColumn: marker.startColumn,
|
||||||
|
endLineNumber: marker.endLineNumber,
|
||||||
|
endColumn: marker.endColumn,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors: diagnostics.filter((item) => item.severity === "error"),
|
||||||
|
warnings: diagnostics.filter((item) => item.severity === "warning"),
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleFormSubmit = useCallback(
|
const handleFormSubmit = useCallback(
|
||||||
(e: React.FormEvent) => {
|
(e: React.FormEvent) => {
|
||||||
@ -46,16 +88,9 @@ export const BotForm = ({ description }: BotFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCodeMessage = {
|
|
||||||
id: problem?.problemId || "",
|
|
||||||
role: "system" as const,
|
|
||||||
content: `Current code:\n\`\`\`${language}\n${value}\n\`\`\``,
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages((prev) => [...prev, currentCodeMessage]);
|
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
},
|
},
|
||||||
[handleSubmit, input, language, problem?.problemId, setMessages, value]
|
[handleSubmit, input]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -72,20 +107,30 @@ export const BotForm = ({ description }: BotFormProps) => {
|
|||||||
(message) =>
|
(message) =>
|
||||||
message.role === "user" || message.role === "assistant"
|
message.role === "user" || message.role === "assistant"
|
||||||
)
|
)
|
||||||
.map((message) => (
|
.map((message) => {
|
||||||
<ChatBubble
|
const isEmptyAssistantMessage =
|
||||||
key={message.id}
|
message.role === "assistant" &&
|
||||||
layout="ai"
|
!message.content.trim() &&
|
||||||
className="border-b pb-4"
|
status !== "ready";
|
||||||
>
|
|
||||||
<ChatBubbleMessage layout="ai">
|
return (
|
||||||
<MdxPreview
|
<ChatBubble
|
||||||
source={message.content}
|
key={message.id}
|
||||||
components={{ ...MdxComponents, pre: PreDetail }}
|
layout="ai"
|
||||||
/>
|
className="border-b pb-4"
|
||||||
</ChatBubbleMessage>
|
>
|
||||||
</ChatBubble>
|
<ChatBubbleMessage
|
||||||
))}
|
layout="ai"
|
||||||
|
isLoading={isEmptyAssistantMessage}
|
||||||
|
>
|
||||||
|
<MdxPreview
|
||||||
|
source={message.content}
|
||||||
|
components={{ ...MdxComponents, pre: PreDetail }}
|
||||||
|
/>
|
||||||
|
</ChatBubbleMessage>
|
||||||
|
</ChatBubble>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ChatMessageList>
|
</ChatMessageList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
@ -131,3 +176,18 @@ export const BotForm = ({ description }: BotFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMarkerSeverityLabel = (severity: MarkerSeverity) => {
|
||||||
|
switch (severity) {
|
||||||
|
case MarkerSeverity.Error:
|
||||||
|
return "error";
|
||||||
|
case MarkerSeverity.Warning:
|
||||||
|
return "warning";
|
||||||
|
case MarkerSeverity.Info:
|
||||||
|
return "info";
|
||||||
|
case MarkerSeverity.Hint:
|
||||||
|
return "hint";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -7,13 +7,14 @@ import { PanelLayout } from "@/features/problems/layouts/panel-layout";
|
|||||||
|
|
||||||
interface BotPanelProps {
|
interface BotPanelProps {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
|
submissionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BotPanel = ({ problemId }: BotPanelProps) => {
|
export const BotPanel = ({ problemId, submissionId }: BotPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<PanelLayout isScroll={false}>
|
<PanelLayout isScroll={false}>
|
||||||
<Suspense fallback={<BotContentSkeleton />}>
|
<Suspense fallback={<BotContentSkeleton />}>
|
||||||
<BotContent problemId={problemId} />
|
<BotContent problemId={problemId} submissionId={submissionId} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</PanelLayout>
|
</PanelLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const ProblemView = ({ problemId, submissionId }: ProblemViewProps) => {
|
|||||||
detail: <DetailPanel submissionId={submissionId} />,
|
detail: <DetailPanel submissionId={submissionId} />,
|
||||||
code: <CodePanel problemId={problemId} />,
|
code: <CodePanel problemId={problemId} />,
|
||||||
testcase: <TestcasePanel problemId={problemId} />,
|
testcase: <TestcasePanel problemId={problemId} />,
|
||||||
bot: <BotPanel problemId={problemId} />,
|
bot: <BotPanel problemId={problemId} submissionId={submissionId} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user