diff --git a/SECURITY.md b/SECURITY.md index 12584d5c..114b2655 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ The codebase is split into a FastAPI backend (`backend/`) and a Next.js frontend ### 1.1 Google OAuth 2.0 with PKCE -**Files:** `backend/routes/auth.py`, `frontend/src/components/SignInModal.tsx`, `frontend/src/app/auth/callback/page.tsx`, `frontend/src/app/api/auth/session/route.ts` +**Files:** `backend/routes/auth.py`, `frontend/src/components/marketing/SignInModal.tsx`, `frontend/src/app/auth/callback/page.tsx`, `frontend/src/app/api/auth/session/route.ts` Sign-in is a full-page redirect to `${API_URL}/api/auth/google`. The backend drives the OAuth flow and the frontend never touches Google directly. diff --git a/docs/frontend-testids.md b/docs/frontend-testids.md index 32cb4c4e..541dbfbb 100644 --- a/docs/frontend-testids.md +++ b/docs/frontend-testids.md @@ -66,12 +66,12 @@ renders the element. | Surface | Prefix | Owning file(s) | | --- | --- | --- | -| Sign-in | `signin` | `frontend/src/components/SignInModal.tsx` (+ the trigger in `src/app/(public)/page.tsx`) | +| Sign-in | `signin` | `frontend/src/components/marketing/SignInModal.tsx` (+ the trigger in `src/app/(public)/page.tsx`) | | Approval gate | `pending` | `frontend/src/app/pending/page.tsx` | | Upload modal | `upload-modal` | `frontend/src/components/DocumentUploadModal.tsx` | -| Tutor | `tutor` | `frontend/src/components/ChatPanel.tsx` (rendered by `screens/Learn.tsx`; + the session-resume rows in `src/components/screens/Learn.tsx` itself) | +| Tutor | `tutor` | `frontend/src/components/chat/ChatPanel.tsx` (rendered by `screens/Learn.tsx`; + the session-resume rows in `src/components/screens/Learn.tsx` itself) | | Quiz | `quiz` | `frontend/src/components/QuizPanel.tsx` (rendered by `screens/Quiz.tsx`) | -| Knowledge graph | `graph` | `frontend/src/components/KnowledgeGraph.tsx` (wrapper: container root + mode toggle) plus `KnowledgeGraph2D.tsx`/`KnowledgeGraph3D.tsx` (the render/data-layer seam — hidden a11y node list, SVG node/edge marks, zoom controls — added with the #395 graph-integrity journey) | +| Knowledge graph | `graph` | `frontend/src/components/graph/KnowledgeGraph.tsx` (wrapper: container root + mode toggle) plus `KnowledgeGraph2D.tsx`/`KnowledgeGraph3D.tsx` (the render/data-layer seam — hidden a11y node list, SVG node/edge marks, zoom controls — added with the #395 graph-integrity journey) | | App shell | `app` | `frontend/src/components/ShellFrame.tsx` (the authed layout frame every `(shell)` route renders inside) | | Study rooms | `social` | `frontend/src/components/screens/Social.tsx` (rooms sidebar, chat, overview, study match, directory — added with the #394 two-context journey) | | Dashboard | `dashboard` | `frontend/src/components/screens/Dashboard.tsx` (rendered by `(shell)/dashboard/page.tsx`) | diff --git a/frontend/e2e/graph.spec.ts b/frontend/e2e/graph.spec.ts index c737cdd0..2c5e1e36 100644 --- a/frontend/e2e/graph.spec.ts +++ b/frontend/e2e/graph.spec.ts @@ -63,7 +63,7 @@ function tierFor(score: number): Tier { /** * Mirror of KnowledgeGraph2D's `masteryOpacity` — how the 2D graph ENCODES * the mastery class on the `graph-node-circle` mark. Kept in lockstep with - * frontend/src/components/KnowledgeGraph2D.tsx (a redesign that retunes + * frontend/src/components/graph/KnowledgeGraph2D.tsx (a redesign that retunes * these constants updates this table in the same PR). */ const TIER_OPACITY: Record = { diff --git a/frontend/eslint-suppressions.json b/frontend/eslint-suppressions.json index a4db8d29..b3158058 100644 --- a/frontend/eslint-suppressions.json +++ b/frontend/eslint-suppressions.json @@ -50,42 +50,42 @@ "count": 2 } }, - "src/components/KnowledgeGraph2D.tsx": { - "react-hooks/exhaustive-deps": { + "src/components/SessionFeedbackGlobal.tsx": { + "react-hooks/set-state-in-effect": { "count": 1 - }, - "react-hooks/refs": { - "count": 12 } }, - "src/components/KnowledgeGraph3D.test.tsx": { + "src/components/TopNav.test.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 6 + "count": 3 } }, - "src/components/ModelToggle.tsx": { + "src/components/chat/ModelToggle.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/SessionFeedbackGlobal.tsx": { + "src/components/chat/SharedContextToggle.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/SharedContextToggle.tsx": { - "react-hooks/set-state-in-effect": { + "src/components/graph/KnowledgeGraph2D.tsx": { + "react-hooks/exhaustive-deps": { "count": 1 + }, + "react-hooks/refs": { + "count": 12 } }, - "src/components/SignInModal.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 + "src/components/graph/KnowledgeGraph3D.test.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 6 } }, - "src/components/TopNav.test.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 3 + "src/components/marketing/SignInModal.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/components/screens/Admin.tsx": { @@ -165,4 +165,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 2e9cdcd6..9da0a352 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -52,14 +52,14 @@ const eslintConfig = [ // the browser suite, add its owning file to `files` below. { files: [ - "src/components/SignInModal.tsx", + "src/components/marketing/SignInModal.tsx", "src/app/pending/page.tsx", "src/components/DocumentUploadModal.tsx", - "src/components/ChatPanel.tsx", + "src/components/chat/ChatPanel.tsx", "src/components/QuizPanel.tsx", - "src/components/KnowledgeGraph.tsx", - "src/components/KnowledgeGraph2D.tsx", - "src/components/KnowledgeGraph3D.tsx", + "src/components/graph/KnowledgeGraph.tsx", + "src/components/graph/KnowledgeGraph2D.tsx", + "src/components/graph/KnowledgeGraph3D.tsx", "src/components/ShellFrame.tsx", "src/components/screens/Social.tsx", "src/components/screens/Dashboard.tsx", diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index baa86e55..c97a3245 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { useUser } from '@/context/UserContext'; import { useScrollLock } from '@/lib/useScrollLock'; import { Network, Sparkles, FilePlus2, Brain, CalendarClock, Users, PenSquare } from 'lucide-react'; -import SignInModal from '@/components/SignInModal'; -import { HeroCard } from '@/components/HeroCard'; +import SignInModal from '@/components/marketing/SignInModal'; +import { HeroCard } from '@/components/marketing/HeroCard'; import { BRAND_FOREST } from '@/lib/brand'; import { Button } from "@/components/ui"; import { IS_TEST_MODE, random, now } from '@/lib/testMode'; @@ -18,7 +18,7 @@ import { IS_TEST_MODE, random, now } from '@/lib/testMode'; // ssr:false would drop it from the HTML crawlers see — the one page where // that matters). The placeholder mirrors the section's own 340vh scroll // height so layout below doesn't shift during client-side chunk loads. -const HowItWorks = dynamic(() => import('@/components/HowItWorks'), { +const HowItWorks = dynamic(() => import('@/components/marketing/HowItWorks'), { loading: () =>
, }); diff --git a/frontend/src/components/AIDisclaimerChip.tsx b/frontend/src/components/chat/AIDisclaimerChip.tsx similarity index 98% rename from frontend/src/components/AIDisclaimerChip.tsx rename to frontend/src/components/chat/AIDisclaimerChip.tsx index 2deb89e1..702efea4 100644 --- a/frontend/src/components/AIDisclaimerChip.tsx +++ b/frontend/src/components/chat/AIDisclaimerChip.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; -import { DisclaimerModal } from "./DisclaimerModal"; +import { DisclaimerModal } from "../DisclaimerModal"; export function AIDisclaimerChip() { const [tooltipVisible, setTooltipVisible] = useState(false); diff --git a/frontend/src/components/ChatPanel.test.tsx b/frontend/src/components/chat/ChatPanel.test.tsx similarity index 98% rename from frontend/src/components/ChatPanel.test.tsx rename to frontend/src/components/chat/ChatPanel.test.tsx index ecef45de..68761b6c 100644 --- a/frontend/src/components/ChatPanel.test.tsx +++ b/frontend/src/components/chat/ChatPanel.test.tsx @@ -17,7 +17,7 @@ import { render, screen, cleanup, fireEvent } from "@testing-library/react"; import { ChatPanel, type ChatMsg } from "./ChatPanel"; -vi.mock("./Icon", () => ({ Icon: () => null })); +vi.mock("../Icon", () => ({ Icon: () => null })); // MarkdownChat is lazy-loaded via next/dynamic (heavy markdown stack); render // plain text in its place so assertions see the message content synchronously. vi.mock("next/dynamic", () => ({ diff --git a/frontend/src/components/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx similarity index 99% rename from frontend/src/components/ChatPanel.tsx rename to frontend/src/components/chat/ChatPanel.tsx index a5112d1c..1b2afbaa 100644 --- a/frontend/src/components/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import dynamic from "next/dynamic"; -import { Icon } from "./Icon"; +import { Icon } from "../Icon"; // MarkdownChat statically imports mermaid, katex, highlight.js, and the // remark/rehype stack. Static-importing it from a client component still diff --git a/frontend/src/components/FunctionPlot.tsx b/frontend/src/components/chat/FunctionPlot.tsx similarity index 100% rename from frontend/src/components/FunctionPlot.tsx rename to frontend/src/components/chat/FunctionPlot.tsx diff --git a/frontend/src/components/MarkdownChat.tsx b/frontend/src/components/chat/MarkdownChat.tsx similarity index 100% rename from frontend/src/components/MarkdownChat.tsx rename to frontend/src/components/chat/MarkdownChat.tsx diff --git a/frontend/src/components/MermaidBlock.tsx b/frontend/src/components/chat/MermaidBlock.tsx similarity index 100% rename from frontend/src/components/MermaidBlock.tsx rename to frontend/src/components/chat/MermaidBlock.tsx diff --git a/frontend/src/components/ModelToggle.tsx b/frontend/src/components/chat/ModelToggle.tsx similarity index 100% rename from frontend/src/components/ModelToggle.tsx rename to frontend/src/components/chat/ModelToggle.tsx diff --git a/frontend/src/components/SessionSummary.tsx b/frontend/src/components/chat/SessionSummary.tsx similarity index 100% rename from frontend/src/components/SessionSummary.tsx rename to frontend/src/components/chat/SessionSummary.tsx diff --git a/frontend/src/components/SharedContextToggle.test.tsx b/frontend/src/components/chat/SharedContextToggle.test.tsx similarity index 100% rename from frontend/src/components/SharedContextToggle.test.tsx rename to frontend/src/components/chat/SharedContextToggle.test.tsx diff --git a/frontend/src/components/SharedContextToggle.tsx b/frontend/src/components/chat/SharedContextToggle.tsx similarity index 100% rename from frontend/src/components/SharedContextToggle.tsx rename to frontend/src/components/chat/SharedContextToggle.tsx diff --git a/frontend/src/components/KnowledgeGraph.tsx b/frontend/src/components/graph/KnowledgeGraph.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph.tsx rename to frontend/src/components/graph/KnowledgeGraph.tsx diff --git a/frontend/src/components/KnowledgeGraph2D.testmode.test.tsx b/frontend/src/components/graph/KnowledgeGraph2D.testmode.test.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph2D.testmode.test.tsx rename to frontend/src/components/graph/KnowledgeGraph2D.testmode.test.tsx diff --git a/frontend/src/components/KnowledgeGraph2D.tsx b/frontend/src/components/graph/KnowledgeGraph2D.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph2D.tsx rename to frontend/src/components/graph/KnowledgeGraph2D.tsx diff --git a/frontend/src/components/KnowledgeGraph3D.test.tsx b/frontend/src/components/graph/KnowledgeGraph3D.test.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph3D.test.tsx rename to frontend/src/components/graph/KnowledgeGraph3D.test.tsx diff --git a/frontend/src/components/KnowledgeGraph3D.testmode.test.tsx b/frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph3D.testmode.test.tsx rename to frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx diff --git a/frontend/src/components/KnowledgeGraph3D.tsx b/frontend/src/components/graph/KnowledgeGraph3D.tsx similarity index 100% rename from frontend/src/components/KnowledgeGraph3D.tsx rename to frontend/src/components/graph/KnowledgeGraph3D.tsx diff --git a/frontend/src/components/HeroCard.test.tsx b/frontend/src/components/marketing/HeroCard.test.tsx similarity index 97% rename from frontend/src/components/HeroCard.test.tsx rename to frontend/src/components/marketing/HeroCard.test.tsx index 50abd27d..0b899e9e 100644 --- a/frontend/src/components/HeroCard.test.tsx +++ b/frontend/src/components/marketing/HeroCard.test.tsx @@ -16,7 +16,8 @@ import { render, screen, cleanup } from '@testing-library/react'; import { HeroCard } from './HeroCard'; -const SRC = path.resolve(__dirname, '..'); +// __dirname is components/marketing/, so src/ is two levels up. +const SRC = path.resolve(__dirname, '../..'); const GLOBALS = path.join(SRC, 'app/globals.css'); /** The literal that used to be inlined at all five sites. */ diff --git a/frontend/src/components/HeroCard.tsx b/frontend/src/components/marketing/HeroCard.tsx similarity index 100% rename from frontend/src/components/HeroCard.tsx rename to frontend/src/components/marketing/HeroCard.tsx diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/marketing/HowItWorks.tsx similarity index 100% rename from frontend/src/components/HowItWorks.tsx rename to frontend/src/components/marketing/HowItWorks.tsx diff --git a/frontend/src/components/SignInModal.tsx b/frontend/src/components/marketing/SignInModal.tsx similarity index 99% rename from frontend/src/components/SignInModal.tsx rename to frontend/src/components/marketing/SignInModal.tsx index 6c4d9ad9..e71ccb65 100644 --- a/frontend/src/components/SignInModal.tsx +++ b/frontend/src/components/marketing/SignInModal.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useUser } from "@/context/UserContext"; import { useScrollLock } from "@/lib/useScrollLock"; -import { HeroCard } from "@/components/HeroCard"; +import { HeroCard } from "@/components/marketing/HeroCard"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? ""; const POPUP_TIMEOUT_MS = 3 * 60 * 1000; diff --git a/frontend/src/components/screens/Dashboard.test.tsx b/frontend/src/components/screens/Dashboard.test.tsx index 84004c7f..2548bc67 100644 --- a/frontend/src/components/screens/Dashboard.test.tsx +++ b/frontend/src/components/screens/Dashboard.test.tsx @@ -39,7 +39,7 @@ vi.mock("@/lib/useIsMobile", () => ({ useIsMobile: () => false })); const layoutState = vi.hoisted(() => ({ pref: "topnav" })); vi.mock("@/lib/useLayoutPref", () => ({ useLayoutPref: () => [layoutState.pref, vi.fn()] })); -vi.mock("../KnowledgeGraph", () => ({ KnowledgeGraph: () => null })); +vi.mock("../graph/KnowledgeGraph", () => ({ KnowledgeGraph: () => null })); vi.mock("../ManageCoursesModal", () => ({ ManageCoursesModal: () => null })); vi.mock("../Skeleton", () => ({ DashboardSkeleton: () => null })); vi.mock("../MiniStat", () => ({ MiniStat: () => null })); diff --git a/frontend/src/components/screens/Dashboard.tsx b/frontend/src/components/screens/Dashboard.tsx index d8ac46d0..eda4d920 100644 --- a/frontend/src/components/screens/Dashboard.tsx +++ b/frontend/src/components/screens/Dashboard.tsx @@ -5,7 +5,7 @@ import { TopBar } from "../TopBar"; import { Icon } from "../Icon"; import { useScrollLock } from "@/lib/useScrollLock"; import { MiniStat } from "../MiniStat"; -import { KnowledgeGraph } from "../KnowledgeGraph"; +import { KnowledgeGraph } from "../graph/KnowledgeGraph"; import { ManageCoursesModal } from "../ManageCoursesModal"; import { DashboardSkeleton } from "../Skeleton"; import { useUser } from "@/context/UserContext"; diff --git a/frontend/src/components/screens/Learn.resume.test.ts b/frontend/src/components/screens/Learn.resume.test.ts index aa5e0afe..d0c6fba7 100644 --- a/frontend/src/components/screens/Learn.resume.test.ts +++ b/frontend/src/components/screens/Learn.resume.test.ts @@ -14,7 +14,7 @@ import { describe, it, expect } from "vitest"; import { readResumeParam, removeInterruptedTurn } from "./Learn"; -import type { ChatMsg } from "../ChatPanel"; +import type { ChatMsg } from "../chat/ChatPanel"; describe("readResumeParam (#164)", () => { it("reads ?resume=", () => { diff --git a/frontend/src/components/screens/Learn.tsx b/frontend/src/components/screens/Learn.tsx index 8395a7fa..60c4c484 100644 --- a/frontend/src/components/screens/Learn.tsx +++ b/frontend/src/components/screens/Learn.tsx @@ -6,15 +6,15 @@ import { ChevronLeft } from "lucide-react"; import { TopBar } from "../TopBar"; import { Icon } from "../Icon"; import { CustomSelect } from "../CustomSelect"; -import { ChatPanel, type ChatMsg } from "../ChatPanel"; +import { ChatPanel, type ChatMsg } from "../chat/ChatPanel"; import { FullHeightScreen } from "../FullHeightScreen"; -import { SessionSummary } from "../SessionSummary"; -import { SharedContextToggle, useSharedContext } from "../SharedContextToggle"; -import { ModelToggle, useModelPref } from "../ModelToggle"; +import { SessionSummary } from "../chat/SessionSummary"; +import { SharedContextToggle, useSharedContext } from "../chat/SharedContextToggle"; +import { ModelToggle, useModelPref } from "../chat/ModelToggle"; import { Toggle } from "@/components/ui"; import { DisclaimerModal } from "../DisclaimerModal"; -import { AIDisclaimerChip } from "../AIDisclaimerChip"; -import { KnowledgeGraph } from "../KnowledgeGraph"; +import { AIDisclaimerChip } from "../chat/AIDisclaimerChip"; +import { KnowledgeGraph } from "../graph/KnowledgeGraph"; import { useToast } from "../ToastProvider"; import { useConfirm } from "@/lib/useConfirm"; import { useIsMobile } from "@/lib/useIsMobile"; diff --git a/frontend/src/components/screens/Library.tsx b/frontend/src/components/screens/Library.tsx index 0d0cd689..4b33e45c 100644 --- a/frontend/src/components/screens/Library.tsx +++ b/frontend/src/components/screens/Library.tsx @@ -12,7 +12,7 @@ import { LibraryGridSkeleton, LibraryListSkeleton } from "../Skeleton"; // Lazy-load to keep mermaid/katex/highlight.js out of the OpenNext // server bundle. See ChatPanel.tsx for the rationale. const MarkdownChat = dynamic( - () => import("../MarkdownChat").then((m) => m.MarkdownChat), + () => import("../chat/MarkdownChat").then((m) => m.MarkdownChat), { ssr: false, loading: () => null }, ); import { useToast } from "../ToastProvider"; diff --git a/frontend/src/components/screens/Quiz.tsx b/frontend/src/components/screens/Quiz.tsx index c9703ad4..037602e5 100644 --- a/frontend/src/components/screens/Quiz.tsx +++ b/frontend/src/components/screens/Quiz.tsx @@ -4,7 +4,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { TopBar } from "../TopBar"; import { FullHeightScreen } from "../FullHeightScreen"; -import { AIDisclaimerChip } from "../AIDisclaimerChip"; +import { AIDisclaimerChip } from "../chat/AIDisclaimerChip"; import { DisclaimerModal } from "../DisclaimerModal"; import { QuizPanel } from "../QuizPanel"; import { useUser } from "@/context/UserContext"; diff --git a/frontend/src/components/screens/Study.tsx b/frontend/src/components/screens/Study.tsx index c858f1e0..1e9726ad 100644 --- a/frontend/src/components/screens/Study.tsx +++ b/frontend/src/components/screens/Study.tsx @@ -3,7 +3,7 @@ import React from "react"; import dynamic from "next/dynamic"; import { useSearchParams } from "next/navigation"; import { TopBar } from "../TopBar"; -import { AIDisclaimerChip } from "../AIDisclaimerChip"; +import { AIDisclaimerChip } from "../chat/AIDisclaimerChip"; import { Icon } from "../Icon"; import { FilterPills } from "@/components/ui"; import { CustomSelect } from "../CustomSelect"; @@ -11,7 +11,7 @@ import { CustomSelect } from "../CustomSelect"; // Lazy-load to keep mermaid/katex/highlight.js out of the OpenNext // server bundle. See ChatPanel.tsx for the rationale. const MarkdownChat = dynamic( - () => import("../MarkdownChat").then((m) => m.MarkdownChat), + () => import("../chat/MarkdownChat").then((m) => m.MarkdownChat), { ssr: false, loading: () => null }, ); diff --git a/frontend/src/components/screens/Tree.tsx b/frontend/src/components/screens/Tree.tsx index 4880a7cf..f972f32b 100644 --- a/frontend/src/components/screens/Tree.tsx +++ b/frontend/src/components/screens/Tree.tsx @@ -6,7 +6,7 @@ import { Icon } from "../Icon"; import { useScrollLock } from "@/lib/useScrollLock"; import { Pill } from "../Pill"; import { FilterPills } from "@/components/ui"; -import { KnowledgeGraph } from "../KnowledgeGraph"; +import { KnowledgeGraph } from "../graph/KnowledgeGraph"; import { FullHeightScreen } from "../FullHeightScreen"; import { GraphPanelSkeleton } from "../Skeleton"; import { useUser } from "@/context/UserContext";