Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 125 additions & 3 deletions app/api/analyze-code/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ interface CodeAnalysis {
detailedFeedback: string
}

const LOW_SIGNAL_KO_PATTERNS = [
"전반적으로",
"일반적으로",
"좋은 시도",
"조금 더 개선",
]

const LOW_SIGNAL_EN_PATTERNS = [
"overall",
"generally",
"good attempt",
"could be improved",
]

function clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) {
return min
Expand Down Expand Up @@ -96,6 +110,106 @@ function deriveEfficiencyLevel(efficiency: number): CodeAnalysis["efficiencyLeve
return "Low"
}

function isLowSignalText(text: string, language: MentorLanguage): boolean {
const normalized = text.trim().toLowerCase()
if (!normalized) {
return true
}

const hasConcreteAction = /(replace|change|trace|check|use|reduce|split|추적|바꿔|확인|사용|줄이|분리)/i.test(
text
)
const hasTestReference = /(test|케이스|입력|expected|actual|pass|fail|통과|실패)/i.test(text)
const matchedPattern =
language === "ko"
? LOW_SIGNAL_KO_PATTERNS.some((pattern) => normalized.includes(pattern))
: LOW_SIGNAL_EN_PATTERNS.some((pattern) => normalized.includes(pattern))

if (text.length < 24 && !hasConcreteAction) {
return true
}
if (matchedPattern && !hasConcreteAction) {
return true
}

return !hasConcreteAction && !hasTestReference
}

function buildDeterministicSuggestion(
language: MentorLanguage,
testResults: AnalyzeRequest["testResults"],
allTestsPassed: boolean,
code: string
): string {
const firstFailed = testResults.find((result) => !result.passed)
const hasNestedLoop = /for\s*\(.*\)\s*{[\s\S]*for\s*\(/.test(code)

if (language === "ko") {
if (!allTestsPassed && firstFailed) {
return `실패 케이스(${firstFailed.input}) 기준으로 첫 오분기 지점 1곳만 수정해보세요.`
}
return hasNestedLoop
? "중첩 순회를 Map/Set 조회로 바꿔 반복 스캔을 줄여보세요."
: "경계값 처리(빈 입력/최소 길이)를 early return으로 분리해보세요."
}

if (!allTestsPassed && firstFailed) {
return `Patch one wrong branch first using the failing case (${firstFailed.input}).`
}
return hasNestedLoop
? "Replace nested scans with Map/Set lookups to reduce repeated passes."
: "Extract boundary checks (empty/min length) into early returns."
}

function buildDeterministicDetailedFeedback(
language: MentorLanguage,
testResults: AnalyzeRequest["testResults"],
allTestsPassed: boolean,
code: string
): string {
const passed = testResults.filter((result) => result.passed).length
const total = testResults.length
const firstFailed = testResults.find((result) => !result.passed)
const hasNestedLoop = /for\s*\(.*\)\s*{[\s\S]*for\s*\(/.test(code)

if (language === "ko") {
if (!allTestsPassed && firstFailed) {
return `${passed}/${total} 통과 상태입니다. 실패 케이스 입력(${firstFailed.input})에서 조건문 분기 순서를 위에서 아래로 추적해 기대값(${firstFailed.expected})과 달라지는 첫 지점을 수정하세요.`
}
return hasNestedLoop
? `${passed}/${total} 통과입니다. 정답은 맞으니 다음 단계는 중첩 루프를 줄여 시간복잡도를 개선하는 것입니다.`
: `${passed}/${total} 통과입니다. 현재 로직은 안정적이며, 다음 개선은 경계값 처리와 함수 책임 분리입니다.`
}

if (!allTestsPassed && firstFailed) {
return `${passed}/${total} tests pass. Start with the failing input (${firstFailed.input}), trace branch flow top-down, and fix the first divergence from expected output (${firstFailed.expected}).`
}
return hasNestedLoop
? `${passed}/${total} tests pass. Correctness is stable; next step is reducing nested loops for better time complexity.`
: `${passed}/${total} tests pass. Logic is stable; next step is clearer boundary handling and smaller function responsibilities.`
}

function normalizeAnalysisFeedback(
analysis: CodeAnalysis,
language: MentorLanguage,
testResults: AnalyzeRequest["testResults"],
allTestsPassed: boolean,
code: string
): CodeAnalysis {
const suggestion = isLowSignalText(analysis.suggestion, language)
? buildDeterministicSuggestion(language, testResults, allTestsPassed, code)
: analysis.suggestion
const detailedFeedback = isLowSignalText(analysis.detailedFeedback, language)
? buildDeterministicDetailedFeedback(language, testResults, allTestsPassed, code)
: analysis.detailedFeedback

return {
...analysis,
suggestion,
detailedFeedback,
}
}

function parseAnalysisJson(
content: string,
testResults: AnalyzeRequest["testResults"]
Expand Down Expand Up @@ -187,7 +301,7 @@ Return ONLY the JSON object, no other text.`
model: getLanguageModel("claude", model, apiKey),
prompt,
maxOutputTokens,
temperature: 0.7,
temperature: 0.35,
})
const content = result.text

Expand Down Expand Up @@ -256,7 +370,7 @@ Return ONLY the JSON object, no other text.`
system: "You are a helpful coding mentor. Always respond with valid JSON only.",
prompt,
maxOutputTokens,
temperature: 0.7,
temperature: 0.35,
})
const content = result.text

Expand Down Expand Up @@ -324,7 +438,7 @@ Return ONLY the JSON object, no other text.`
model: getLanguageModel("gemini", model, apiKey),
prompt,
maxOutputTokens,
temperature: 0.7,
temperature: 0.35,
})
const content = result.text

Expand Down Expand Up @@ -432,6 +546,14 @@ export async function POST(req: NextRequest) {
)
}

analysis = normalizeAnalysisFeedback(
analysis,
language,
testResults,
allTestsPassed,
code
)

return NextResponse.json(analysis)
} catch (error) {
console.error("Error analyzing code:", error)
Expand Down
92 changes: 88 additions & 4 deletions app/api/review-code/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,89 @@ const REVIEW_REPLY_FORMAT_RULES = `Reply format:
- Avoid rhetorical questions and repetitive praise.
- Keep it to one short acknowledgement + one concrete action point whenever possible.`

const LOW_SIGNAL_KO_PATTERNS = [
"좋은 시도",
"좋은 접근",
"잘하고 있어",
"전반적으로",
"일반적으로",
"조금 더 개선",
]

const LOW_SIGNAL_EN_PATTERNS = [
"good attempt",
"good job",
"overall",
"generally",
"could be improved",
"looks fine",
]

function resolveResponseTokenLimit(maxOutputTokens: number): number {
return Math.max(256, maxOutputTokens)
}

function isLowSignalReview(text: string, language: MentorLanguage): boolean {
const normalized = text.trim().toLowerCase()
if (!normalized) {
return true
}

const lineCount = text
.split("\n")
.map((line) => line.trim())
.filter(Boolean).length
const hasTestReference = /(test|케이스|입력|expected|actual|pass|fail|통과|실패)/i.test(text)
const hasConcreteAction = /(replace|change|trace|check|use|추적|바꿔|확인|사용|적용)/i.test(text)

const matchedPattern =
language === "ko"
? LOW_SIGNAL_KO_PATTERNS.some((pattern) => normalized.includes(pattern))
: LOW_SIGNAL_EN_PATTERNS.some((pattern) => normalized.includes(pattern))

if (lineCount <= 1 && !hasTestReference) {
return true
}
if (matchedPattern && !hasConcreteAction) {
return true
}
return !hasTestReference && !hasConcreteAction
}

function buildDeterministicReviewFallback(
language: MentorLanguage,
testResults: TestResult[],
allTestsPassed: boolean,
code: string
): string {
const firstFailed = testResults.find((result) => !result.passed)
const hasNestedLoop = /for\s*\(.*\)\s*{[\s\S]*for\s*\(/.test(code)

if (language === "ko") {
if (!allTestsPassed && firstFailed) {
return `지금은 ${testResults.filter((r) => r.passed).length}/${testResults.length} 통과야. 먼저 실패한 케이스 1개부터 고치자.
입력: ${firstFailed.input}
기대값: ${firstFailed.expected}, 실제값: ${firstFailed.actual}
다음 액션: 이 케이스를 기준으로 조건문 분기 순서를 위에서 아래로 한 줄씩 추적하고, 기대값과 다른 첫 분기 지점만 수정해봐.`
}

return hasNestedLoop
? "테스트는 통과했어. 다음으로 중첩 루프를 줄일 수 있는지부터 보자. Map/Set으로 조회를 O(1)로 바꾸면 반복 스캔을 줄일 가능성이 커."
: "테스트는 통과했어. 다음 액션은 함수 책임을 한 단계만 분리하는 거야. 경계값 처리(빈 입력/최소 길이)를 early return으로 먼저 두면 유지보수가 쉬워져."
}

if (!allTestsPassed && firstFailed) {
return `You are at ${testResults.filter((r) => r.passed).length}/${testResults.length}. Fix one failing case first.
Input: ${firstFailed.input}
Expected: ${firstFailed.expected}, Actual: ${firstFailed.actual}
Next action: trace the branch order top-down for this case and patch only the first branch where behavior diverges from expected output.`
}

return hasNestedLoop
? "Your tests pass. Next action: reduce nested scans first. Consider replacing repeated lookups with Map/Set for O(1) access."
: "Your tests pass. Next action: isolate one responsibility. Put boundary checks (empty/min length) as early returns to simplify maintenance."
}

function finalizeMentorResponse(
text: string,
language: MentorLanguage,
Expand Down Expand Up @@ -160,6 +239,8 @@ ${!r.passed ? ` Input: ${r.input}\n Expected: ${r.expected}\n Got: ${r.actual
}
- Suggest algorithm alternatives naturally when relevant.
- Do not dump a full solution unless explicitly requested.
- Reference at least one concrete test detail (input/expected/actual or pass count).
- Include exactly one next action that the learner can execute immediately.
- ${REVIEW_REPLY_FORMAT_RULES}

Provide your supportive feedback now:
Expand Down Expand Up @@ -192,7 +273,7 @@ async function reviewWithClaude(
model: getLanguageModel("claude", model, apiKey),
prompt,
maxOutputTokens: resolveResponseTokenLimit(maxOutputTokens),
temperature: 0.7,
temperature: 0.35,
})
return result.text
}
Expand Down Expand Up @@ -223,7 +304,7 @@ async function reviewWithGPT(
"You are a helpful coding mentor who provides constructive feedback and guides students to learn.",
prompt,
maxOutputTokens: resolveResponseTokenLimit(maxOutputTokens),
temperature: 0.7,
temperature: 0.35,
})
return result.text
}
Expand Down Expand Up @@ -252,7 +333,7 @@ async function reviewWithGemini(
model: getLanguageModel("gemini", model, apiKey),
prompt,
maxOutputTokens: resolveResponseTokenLimit(maxOutputTokens),
temperature: 0.7,
temperature: 0.35,
})
return result.text
}
Expand Down Expand Up @@ -337,7 +418,10 @@ export async function POST(req: NextRequest) {
}

if (resolved) {
feedback = finalizeMentorResponse(resolved, language, allTestsPassed)
const refined = isLowSignalReview(resolved, language)
? buildDeterministicReviewFallback(language, testResults, allTestsPassed, code)
: resolved
feedback = finalizeMentorResponse(refined, language, allTestsPassed)
} else {
feedback = finalizeMentorResponse(
generateFallbackReviewFeedback(
Expand Down
8 changes: 4 additions & 4 deletions app/problem/[id]/problem-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,15 +508,15 @@ export function ProblemPageClient({ problem }: ProblemPageClientProps) {
<AlertDialog open={isMentorAlertOpen} onOpenChange={setIsMentorAlertOpen}>
<AlertDialogContent overlayClassName="bg-black/45">
<AlertDialogHeader>
<AlertDialogTitle>AI 멘토 설정 필요</AlertDialogTitle>
<AlertDialogTitle>{copy.problem.mentorSetupTitle}</AlertDialogTitle>
<AlertDialogDescription>
모델을 등록해야만 정확한 피드백이 가능합니다.
{copy.problem.mentorSetupDescription}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogCancel>{copy.problem.mentorSetupCancel}</AlertDialogCancel>
<AlertDialogAction onClick={() => router.push("/settings")}>
설정 페이지로 이동
{copy.problem.mentorSetupGoToSettings}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
Expand Down
14 changes: 14 additions & 0 deletions lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface LocaleCopy {
translationReady: string;
fallbackEnglish: string;
mentorSetupNotice: string;
mentorSetupTitle: string;
mentorSetupDescription: string;
mentorSetupCancel: string;
mentorSetupGoToSettings: string;
};
problemCard: {
solved: string;
Expand Down Expand Up @@ -132,6 +136,11 @@ const localeCopy: Record<AppLanguage, LocaleCopy> = {
fallbackEnglish: "English (fallback)",
mentorSetupNotice:
"If you do not add a model, it is difficult to expect accurate mentoring.",
mentorSetupTitle: "AI mentor setup required",
mentorSetupDescription:
"You need to set up a model and API key to receive accurate feedback.",
mentorSetupCancel: "Cancel",
mentorSetupGoToSettings: "Go to Settings",
},
problemCard: {
solved: "Solved",
Expand Down Expand Up @@ -205,6 +214,11 @@ const localeCopy: Record<AppLanguage, LocaleCopy> = {
translationReady: "영어 + 한국어",
fallbackEnglish: "영어 기본값 (fallback)",
mentorSetupNotice: "모델을 추가하지 않으면 정확한 멘토링을 기대하기는 어렵습니다.",
mentorSetupTitle: "AI 멘토 설정 필요",
mentorSetupDescription:
"정확한 피드백을 받으려면 모델과 API Key를 설정해야 합니다.",
mentorSetupCancel: "취소",
mentorSetupGoToSettings: "설정 페이지로 이동",
},
problemCard: {
solved: "해결됨",
Expand Down