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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
6 changes: 3 additions & 3 deletions docs/frontend-testids.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/graph.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Tier, number> = {
Expand Down
36 changes: 18 additions & 18 deletions frontend/eslint-suppressions.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": {
Expand DownExpand Up@@ -165,4 +165,4 @@
"count": 1
}
}
}
}
10 changes: 5 additions & 5 deletions frontend/eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(public)/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,8 +6,8 @@
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';
Expand All@@ -18,7 +18,7 @@
// 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: () => <section id="how-it-works" className="landing-section relative" style={{ height: '340vh' }} />,
});

Expand DownExpand Up@@ -534,7 +534,7 @@
>
<div className="max-w-[88%] mx-auto flex items-center justify-between w-full">
<button type="button" aria-label="Sapling — scroll to top" className="flex items-center cursor-pointer group" style={{ gap: '4px' }} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<img src="/sapling-icon.svg" alt="" style={{ width: '26px', height: '26px', flexShrink: 0, position: 'relative', top: '-2px' }} />

Check warning on line 537 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: '20px', color: 'var(--brand-forest)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>Sapling</span>
</button>
<div className="flex items-center">
Expand DownExpand Up@@ -750,7 +750,7 @@
<footer className="landing-section border-t border-white/35 py-12 px-8 relative z-10">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2">
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: '20px', height: '20px' }} />

Check warning on line 753 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span className="text-sm font-light tracking-wide text-[var(--text-dim)]">Sapling · © 2026</span>
</div>
<div className="flex flex-wrap justify-center gap-6">
Expand DownExpand Up@@ -833,7 +833,7 @@
display: 'flex', flexDirection: 'column', gap: 22,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<img src="/sapling-icon.svg" alt="Sapling" style={{ width: 22, height: 22 }} />

Check warning on line 836 in frontend/src/app/(public)/page.tsx

View workflow job for this annotation

GitHub Actions/ Frontend (lint + tsc + vitest)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<span style={{ fontFamily: "var(--font-spectral), 'Spectral', Georgia, serif", fontWeight: 700, fontSize: 17, color: 'var(--brand-forest)', letterSpacing: '-0.02em' }}>Sapling</span>
</div>
<div>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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", () => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Dashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Learn.resume.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=", () => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/screens/Learn.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Library.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Quiz.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/screens/Study.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,15 +3,15 @@ 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";

// 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 },
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/screens/Tree.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand Down
Loading