diff --git a/app/(main)/external-feedback/page.tsx b/app/(main)/external-feedback/page.tsx new file mode 100644 index 0000000..9a7b6af --- /dev/null +++ b/app/(main)/external-feedback/page.tsx @@ -0,0 +1,20 @@ +"use client" + +import { ExternalProblemFeedback } from "@/components/external-problem-feedback" +import { withMentorAccessGuard } from "@/components/with-mentor-access-guard" +import { PageTransition } from "@/components/page-transition" +import { usePageEntryAnimation } from "@/lib/use-page-entry-animation" + +function ExternalFeedbackPage() { + const shouldAnimateOnMount = usePageEntryAnimation() + + return ( + + + + ) +} + +export default withMentorAccessGuard(ExternalFeedbackPage, { + fallbackPath: "/", +}) diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 378f213..e197763 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -1,19 +1,32 @@ "use client" import { useEffect, useMemo, useState } from "react" +import { useRouter } from "next/navigation" import { ChevronDown } from "lucide-react" import { HeroSection } from "@/components/hero-section" import { VirtualizedProblemList } from "@/components/virtualized-problem-list" import { StreakWidget } from "@/components/streak-widget" import { StatsWidget } from "@/components/stats-widget" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" import { PageTransition } from "@/components/page-transition" import { problems } from "@/lib/problems" import { localizeCategory } from "@/lib/problems" import type { Difficulty, Problem } from "@/lib/problems" import { + getApiSettings, getSolvedProblemIds, subscribeToProgressUpdates, } from "@/lib/local-progress" +import { isMentorConfigured as resolveMentorConfigured } from "@/lib/mentor-access" import { useAppLanguage } from "@/lib/use-app-language" import { usePageEntryAnimation } from "@/lib/use-page-entry-animation" import { useRestoreScroll } from "@/lib/use-restore-scroll" @@ -49,6 +62,7 @@ function parseHomeViewState(value: unknown): HomeViewState | null { } export default function HomePage() { + const router = useRouter() const { language, copy } = useAppLanguage() const shouldAnimateOnMount = usePageEntryAnimation() const [viewState, setViewState] = useRestoreScroll({ @@ -62,6 +76,8 @@ export default function HomePage() { }) const { selectedCategory, selectedDifficulty, sortBy } = viewState const [solvedIds, setSolvedIds] = useState>(new Set()) + const [isMentorReady, setIsMentorReady] = useState(false) + const [isMentorAlertOpen, setIsMentorAlertOpen] = useState(false) useEffect(() => { const sync = () => setSolvedIds(getSolvedProblemIds()) @@ -69,6 +85,14 @@ export default function HomePage() { return subscribeToProgressUpdates(sync) }, []) + useEffect(() => { + const sync = () => { + setIsMentorReady(resolveMentorConfigured(getApiSettings())) + } + sync() + return subscribeToProgressUpdates(sync) + }, []) + const categories = useMemo(() => { const map = new Map() for (const problem of problems) { @@ -87,7 +111,6 @@ export default function HomePage() { language === "ko" ? "난이도 오름차순" : "Difficulty Ascending" const sortDifficultyDescLabel = language === "ko" ? "난이도 내림차순" : "Difficulty Descending" - const difficultyRank: Record = { 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} + /> +
+
+ +