= {
Easy: 0,
Medium: 1,
@@ -123,6 +146,14 @@ export default function HomePage() {
return source.filter((problem) => problem.difficulty === difficulty).length
}
+ const handleExternalFeedbackOpen = () => {
+ if (!isMentorReady) {
+ setIsMentorAlertOpen(true)
+ return
+ }
+ router.push("/external-feedback")
+ }
+
return (
@@ -204,10 +235,37 @@ export default function HomePage() {
+
+ {copy.home.externalFeedbackTitle}
+ {copy.home.externalFeedbackDescription}
+
+
+
+
+
+ {copy.problem.mentorSetupTitle}
+
+ {copy.problem.mentorSetupDescription}
+
+
+
+ {copy.problem.mentorSetupCancel}
+ router.push("/settings")}>
+ {copy.problem.mentorSetupGoToSettings}
+
+
+
+
)
}
diff --git a/app/problem/[id]/problem-page-client.tsx b/app/problem/[id]/problem-page-client.tsx
index 9f0d92c..05dc526 100644
--- a/app/problem/[id]/problem-page-client.tsx
+++ b/app/problem/[id]/problem-page-client.tsx
@@ -37,6 +37,7 @@ import {
} from "@/lib/local-progress";
import { useAppLanguage } from "@/lib/use-app-language";
import { useIsMobile } from "@/components/ui/use-mobile";
+import { isMentorConfigured as resolveMentorConfigured } from "@/lib/mentor-access";
interface ProblemPageClientProps {
problem: Problem;
@@ -90,11 +91,7 @@ export function ProblemPageClient({ problem }: ProblemPageClientProps) {
useEffect(() => {
const sync = () => {
- const settings = getApiSettings();
- const provider = settings.provider;
- const hasModel = Boolean(settings.models[provider]?.trim());
- const hasApiKey = Boolean(settings.apiKeys[provider]?.trim());
- setIsMentorConfigured(hasModel && hasApiKey);
+ setIsMentorConfigured(resolveMentorConfigured(getApiSettings()));
};
sync();
return subscribeToProgressUpdates(sync);
diff --git a/components/external-problem-feedback.tsx b/components/external-problem-feedback.tsx
new file mode 100644
index 0000000..f65cbd3
--- /dev/null
+++ b/components/external-problem-feedback.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import { useState } from "react"
+import Editor from "@monaco-editor/react"
+import { RotateCcw } from "lucide-react"
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import { useIsMobile } from "@/components/ui/use-mobile"
+import { CodeAssistantChat } from "@/components/code-assistant-chat"
+import { useAppLanguage } from "@/lib/use-app-language"
+
+export function ExternalProblemFeedback() {
+ const { copy } = useAppLanguage()
+ const isMobile = useIsMobile()
+ const text = copy.externalFeedback
+ const [problemTitle, setProblemTitle] = useState("")
+ const [problemText, setProblemText] = useState("")
+ const [code, setCode] = useState("")
+
+ const normalizedProblemDescription =
+ problemText.trim() || text.chatMissingProblemContext
+ const normalizedProblemTitle = problemTitle.trim() || text.defaultProblemTitle
+ const handleReset = () => {
+ setCode("")
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{text.title}
+
{text.description}
+
+
+
+ setProblemTitle(event.target.value)}
+ placeholder={text.titlePlaceholder}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ solution.js
+
+
+
+
+
+ setCode(value ?? "")}
+ options={{
+ minimap: { enabled: false },
+ fontSize: 14,
+ lineNumbers: "on",
+ scrollBeyondLastLine: false,
+ wordWrap: "on",
+ automaticLayout: true,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/with-mentor-access-guard.tsx b/components/with-mentor-access-guard.tsx
new file mode 100644
index 0000000..a5dc675
--- /dev/null
+++ b/components/with-mentor-access-guard.tsx
@@ -0,0 +1,81 @@
+"use client"
+
+import { useEffect, useState, type ComponentType } from "react"
+import { useRouter } from "next/navigation"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { getApiSettings, subscribeToProgressUpdates } from "@/lib/local-progress"
+import { isMentorConfigured } from "@/lib/mentor-access"
+import { useAppLanguage } from "@/lib/use-app-language"
+
+interface MentorAccessGuardOptions {
+ fallbackPath?: string
+}
+
+export function withMentorAccessGuard(
+ WrappedComponent: ComponentType
,
+ options?: MentorAccessGuardOptions
+) {
+ const fallbackPath = options?.fallbackPath ?? "/"
+
+ const GuardedComponent = (props: P) => {
+ const router = useRouter()
+ const { copy } = useAppLanguage()
+ const [ready, setReady] = useState(false)
+ const [allowed, setAllowed] = useState(false)
+
+ useEffect(() => {
+ const sync = () => {
+ setAllowed(isMentorConfigured(getApiSettings()))
+ setReady(true)
+ }
+
+ sync()
+ return subscribeToProgressUpdates(sync)
+ }, [])
+
+ if (ready && allowed) {
+ return
+ }
+
+ return (
+ {
+ if (!open) {
+ router.push(fallbackPath)
+ }
+ }}
+ >
+
+
+ {copy.problem.mentorSetupTitle}
+
+ {copy.problem.mentorSetupDescription}
+
+
+
+ router.push(fallbackPath)}>
+ {copy.problem.mentorSetupCancel}
+
+ router.push("/settings")}>
+ {copy.problem.mentorSetupGoToSettings}
+
+
+
+
+ )
+ }
+
+ GuardedComponent.displayName = `withMentorAccessGuard(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`
+
+ return GuardedComponent
+}
diff --git a/lib/i18n.ts b/lib/i18n.ts
index 6b7851a..01beafc 100644
--- a/lib/i18n.ts
+++ b/lib/i18n.ts
@@ -21,6 +21,34 @@ export interface LocaleCopy {
allChallenges: string;
challenges: string;
problems: string;
+ externalFeedbackTitle: string;
+ externalFeedbackDescription: string;
+ externalFeedbackAction: string;
+ };
+ externalFeedback: {
+ backHome: string;
+ title: string;
+ description: string;
+ titleLabel: string;
+ titlePlaceholder: string;
+ problemLabel: string;
+ problemPlaceholder: string;
+ codeLabel: string;
+ codePlaceholder: string;
+ submit: string;
+ loading: string;
+ resultTitle: string;
+ errorRequired: string;
+ errorGeneric: string;
+ errorTimeout: string;
+ defaultProblemTitle: string;
+ mentorPrompt: string;
+ editorTitle: string;
+ editorDescription: string;
+ chatTitle: string;
+ chatDescription: string;
+ chatMissingProblem: string;
+ chatMissingProblemContext: string;
};
settings: {
backHome: string;
@@ -55,6 +83,10 @@ export interface LocaleCopy {
translationReady: string;
fallbackEnglish: string;
mentorSetupNotice: string;
+ mentorSetupTitle: string;
+ mentorSetupDescription: string;
+ mentorSetupCancel: string;
+ mentorSetupGoToSettings: string;
};
problemCard: {
solved: string;
@@ -95,6 +127,38 @@ const localeCopy: Record = {
allChallenges: "All Challenges",
challenges: "Challenges",
problems: "problems",
+ externalFeedbackTitle: "Practice More Problems",
+ externalFeedbackDescription:
+ "Paste problems from any source and solve them with AI mentor chat.",
+ externalFeedbackAction: "Open Page",
+ },
+ externalFeedback: {
+ backHome: "Back to Home",
+ title: "Practice More Problems",
+ description:
+ "Paste any problem statement and solve with code + always-on AI mentor chat.",
+ titleLabel: "Problem title (optional)",
+ titlePlaceholder: "e.g. Longest Substring Without Repeating Characters",
+ problemLabel: "Problem statement",
+ problemPlaceholder: "Paste full statement, input/output format, and constraints.",
+ codeLabel: "Your code (optional)",
+ codePlaceholder: "Paste your current solution if you want code-level feedback.",
+ submit: "Get Feedback",
+ loading: "Analyzing...",
+ resultTitle: "AI Feedback",
+ errorRequired: "Paste the problem statement first.",
+ errorGeneric: "Failed to request feedback. Please try again.",
+ errorTimeout: "The request timed out. Please try again.",
+ defaultProblemTitle: "External Problem",
+ mentorPrompt:
+ "Based on the pasted problem, give a short review with approach direction, common pitfalls, and one next action.",
+ editorTitle: "Code Editor",
+ editorDescription: "Write or paste your solution and ask the mentor to review specific parts.",
+ chatTitle: "AI Mentor Chat",
+ chatDescription: "Ask for hints, approach checks, debugging, or code review in chat form.",
+ chatMissingProblem: "Paste the problem statement first for better guidance.",
+ chatMissingProblemContext:
+ "Problem statement is not provided yet. Ask the user to paste it first, then continue with focused guidance.",
},
settings: {
backHome: "Back to Home",
@@ -132,6 +196,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",
@@ -170,6 +239,39 @@ const localeCopy: Record = {
allChallenges: "전체 챌린지",
challenges: "챌린지",
problems: "문제",
+ externalFeedbackTitle: "다양한 문제 풀어보기",
+ externalFeedbackDescription:
+ "원하는 문제를 붙여넣고 AI 멘토와 함께 해결해보세요.",
+ externalFeedbackAction: "페이지 열기",
+ },
+ externalFeedback: {
+ backHome: "홈으로 돌아가기",
+ title: "다양한 문제 풀어보기",
+ description:
+ "원하는 문제를 붙여넣고 코드 작성과 AI 멘토 채팅으로 바로 풀어보세요.",
+ titleLabel: "문제 제목 (선택)",
+ titlePlaceholder: "예: Longest Substring Without Repeating Characters",
+ problemLabel: "문제 설명",
+ problemPlaceholder:
+ "문제 본문, 입력/출력 형식, 제약사항을 그대로 붙여넣어 주세요.",
+ codeLabel: "내 코드 (선택)",
+ codePlaceholder: "작성한 코드가 있다면 함께 붙여넣어 주세요.",
+ submit: "피드백 받기",
+ loading: "분석 중...",
+ resultTitle: "AI 피드백",
+ errorRequired: "문제 설명을 먼저 붙여넣어 주세요.",
+ errorGeneric: "피드백 요청에 실패했습니다. 잠시 후 다시 시도해 주세요.",
+ errorTimeout: "요청 시간이 초과되었습니다. 다시 시도해 주세요.",
+ defaultProblemTitle: "외부 문제",
+ mentorPrompt:
+ "붙여넣은 문제를 기준으로 접근 방향, 자주 틀리는 포인트, 다음 액션 1개를 짧게 피드백해줘.",
+ editorTitle: "코드 에디터",
+ editorDescription: "코드를 작성하거나 붙여넣고, 특정 부분에 대한 리뷰를 채팅으로 요청하세요.",
+ chatTitle: "AI 멘토 채팅",
+ chatDescription: "힌트, 접근 검토, 디버깅, 코드 리뷰를 채팅으로 바로 받을 수 있어요.",
+ chatMissingProblem: "더 정확한 도움을 위해 문제 설명을 먼저 붙여넣어 주세요.",
+ chatMissingProblemContext:
+ "문제 설명이 아직 입력되지 않았습니다. 먼저 문제 본문을 붙여넣도록 안내한 뒤에 구체적으로 도와주세요.",
},
settings: {
backHome: "홈으로 돌아가기",
@@ -205,6 +307,11 @@ const localeCopy: Record = {
translationReady: "영어 + 한국어",
fallbackEnglish: "영어 기본값 (fallback)",
mentorSetupNotice: "모델을 추가하지 않으면 정확한 멘토링을 기대하기는 어렵습니다.",
+ mentorSetupTitle: "AI 멘토 설정 필요",
+ mentorSetupDescription:
+ "정확한 피드백을 받으려면 모델과 API Key를 설정해야 합니다.",
+ mentorSetupCancel: "취소",
+ mentorSetupGoToSettings: "설정 페이지로 이동",
},
problemCard: {
solved: "해결됨",
diff --git a/lib/mentor-access.ts b/lib/mentor-access.ts
new file mode 100644
index 0000000..fae6cfe
--- /dev/null
+++ b/lib/mentor-access.ts
@@ -0,0 +1,8 @@
+import type { ApiSettings } from "@/lib/local-progress"
+
+export function isMentorConfigured(settings: ApiSettings): boolean {
+ const provider = settings.provider
+ const hasModel = Boolean(settings.models[provider]?.trim())
+ const hasApiKey = Boolean(settings.apiKeys[provider]?.trim())
+ return hasModel && hasApiKey
+}