From 56b78956b79b3d80b8296aeedf6c69e78ee76871 Mon Sep 17 00:00:00 2001 From: Zero-1016 Date: Mon, 16 Feb 2026 13:04:33 +0900 Subject: [PATCH 1/2] Fix mentor setup modal i18n and improve review feedback quality --- app/api/review-code/route.ts | 92 ++++++++++++++++++++++-- app/problem/[id]/problem-page-client.tsx | 8 +-- lib/i18n.ts | 14 ++++ 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/app/api/review-code/route.ts b/app/api/review-code/route.ts index ff8669c..1c8f368 100644 --- a/app/api/review-code/route.ts +++ b/app/api/review-code/route.ts @@ -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, @@ -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: @@ -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 } @@ -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 } @@ -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 } @@ -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( diff --git a/app/problem/[id]/problem-page-client.tsx b/app/problem/[id]/problem-page-client.tsx index 9f0d92c..94f5cb7 100644 --- a/app/problem/[id]/problem-page-client.tsx +++ b/app/problem/[id]/problem-page-client.tsx @@ -508,15 +508,15 @@ export function ProblemPageClient({ problem }: ProblemPageClientProps) { - AI 멘토 설정 필요 + {copy.problem.mentorSetupTitle} - 모델을 등록해야만 정확한 피드백이 가능합니다. + {copy.problem.mentorSetupDescription} - 취소 + {copy.problem.mentorSetupCancel} router.push("/settings")}> - 설정 페이지로 이동 + {copy.problem.mentorSetupGoToSettings} diff --git a/lib/i18n.ts b/lib/i18n.ts index 6b7851a..0f4d2cd 100644 --- a/lib/i18n.ts +++ b/lib/i18n.ts @@ -55,6 +55,10 @@ export interface LocaleCopy { translationReady: string; fallbackEnglish: string; mentorSetupNotice: string; + mentorSetupTitle: string; + mentorSetupDescription: string; + mentorSetupCancel: string; + mentorSetupGoToSettings: string; }; problemCard: { solved: string; @@ -132,6 +136,11 @@ const localeCopy: Record = { 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", @@ -205,6 +214,11 @@ const localeCopy: Record = { translationReady: "영어 + 한국어", fallbackEnglish: "영어 기본값 (fallback)", mentorSetupNotice: "모델을 추가하지 않으면 정확한 멘토링을 기대하기는 어렵습니다.", + mentorSetupTitle: "AI 멘토 설정 필요", + mentorSetupDescription: + "정확한 피드백을 받으려면 모델과 API Key를 설정해야 합니다.", + mentorSetupCancel: "취소", + mentorSetupGoToSettings: "설정 페이지로 이동", }, problemCard: { solved: "해결됨", From 299057cc40757e98c4b59b64f86c8ca85ef05480 Mon Sep 17 00:00:00 2001 From: Zero-1016 Date: Mon, 16 Feb 2026 13:06:01 +0900 Subject: [PATCH 2/2] Improve analyze-code feedback quality with low-signal fallback --- app/api/analyze-code/route.ts | 128 +++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/app/api/analyze-code/route.ts b/app/api/analyze-code/route.ts index daa0334..ab8a6d2 100644 --- a/app/api/analyze-code/route.ts +++ b/app/api/analyze-code/route.ts @@ -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 @@ -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"] @@ -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 @@ -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 @@ -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 @@ -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)