# Project export: Socratical

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: An intelligent whiteboard for visual problem solving.
- Devpost: https://devpost.com/software/socratical
- GitHub: https://github.com/vinngo/cruzhacks
- Demo: https://cruzhacks-seven.vercel.app/
- Video: https://www.youtube.com/embed/bhKILEuOIHw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Vercel] Best Use of Vercel)
- Team: 2 GitHub contributor(s) — vinngo (74 commits), Claude Sonnet 4.5 (19 commits)

## Devpost submission (written by the team)

### Inspiration

In subjects like math or physics, working through practice problems is often crucial for learning concepts effectively. This works sometimes without external help but eventually students will get stuck or be left wondering if they're on the right track. A tutor could help, but tutors are expensive and aren't always available. ChatGPT exists, but all it can do is give students the answers, there is no learning process involved by just being given a solution. Socratical brings the following to the table: 1. A pedagogy conducive to all learning Socratical makes the Socratic method scalable. Most students may never get a tutor that's this patient or accessible. Now they can work through any problem visually, on a canvas that feels natural, while an AI guides their thinking rather than just checking their answers. Unlike human tutors, Socratical isn't limited to math. Any problem that can be worked out on a whiteboard is fair game with Socratical: calculus, physics, biology, discrete math, Leetcode, System Design, the same method still applies. 2. Why vs What The turn-based dialogue the interface provides creates a richer experience than ChatGPT. The tutor can ask direct questions in the chat window and also directly manipulate the canvas to simulate an actual live tutoring session. The tutor doesn't stop at the answer however, it constantly probes for the user's input on edge cases and real-life applications. The user will understand why the problem solving process works rather than just knowing that it works. 3. Performance anxiety A student can work at their own pace, make mistakes privately, and get helpful guided questions instead of being told that they're wrong. That's psychologically different from a student raising their hand in class. How I built it I started by extending an open-source lightweight interactive canvas library called tldraw. The goal of the implementation was to enable the AI to see and interact with the canvas to provide the best learning experience for the user using the canvas' APIs. I used Claude Sonnet 4 as the multi-modal model along with Vercel's AI SDK to implement features such as streaming and tool calls seamlessly. I hosted it on Vercel making use of Vercel functions and Fluid Compute to make API calls feel instant. Challenges I ran into Working with a canvas library and its APIs was a first for me. I had to design helper functions to create screenshots and calculate annotation positions on the canvas. I also had issues with synchronization and timing LLM responses with rendering. Thankfully the Vercel AI SDK abstracted away some elements such as streaming but I had to implement my own custom hooks and components to render annotations. Accomplishments that I'm proud of This is the first hackathon I've ever done solo and so, this is a project that I completely own and am proud of in so many ways. To me this is a totally new interaction model that no chat bot like ChatGPT can easily replicate. Technically speaking, I'm proud of the decisions I made regarding the tech stack and architecture. This project was complex but scoped down enough to fit inside a weekend-long hackathon and I'm glad I made the right calls so that the project didn't become an insurmountable task. What I learned Even though I'd say I have a decent amount of experience in web dev, interfacing with a non-standard interaction model (that being a drawable canvas) was a new experience for sure. I had to consider rendering strategies, complex state management, and finally understanding what useRef was good for.

### What's next

Right now, Socratical exists on a session by session basis, meaning conversations and previous work aren't stored anywhere. Data persistence was intentionally left out for the sake of time, but is an important feature to implement in the future. As frontier models improve, their 'vision' may get better but I imagine messy handwriting or complex diagrams may overwhelm Socratical as of now. I hope to come up with ways to inject more context (perhaps making use of tldraw's APIs) into Socratical so that it could understand more complex problems. I also want to use Vercel's AI SDK to its fullest by implementing agentic workflows that could help the AI make better decisions such as using Desmos to plot graphs or an external calculator to validate answers.

## README (from the GitHub repository)

# Socratical

## Philosophize your toughest subjects.

### 🏆 Winner of the Vercel Track at Cruzhacks 2026 

try it out [here.](https://cruzhacks-seven.vercel.app)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 101 recognized source files, 503 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (109 of 109)

```
.gitignore
.planning/config.json
.planning/phases/01-foundation-canvas/01-01-PLAN.md
.planning/phases/01-foundation-canvas/01-01-SUMMARY.md
.planning/phases/01-foundation-canvas/01-02-PLAN.md
.planning/phases/01-foundation-canvas/01-02-SUMMARY.md
.planning/phases/01-foundation-canvas/01-03-PLAN.md
.planning/phases/01-foundation-canvas/01-03-SUMMARY.md
.planning/phases/01-foundation-canvas/01-04-PLAN.md
.planning/phases/01-foundation-canvas/01-04-SUMMARY.md
.planning/phases/01-foundation-canvas/01-CONTEXT.md
.planning/phases/01-foundation-canvas/01-RESEARCH.md
.planning/phases/01-foundation-canvas/01-VERIFICATION.md
.planning/phases/02-problem-input---ai-integration/02-01-PLAN.md
.planning/phases/02-problem-input---ai-integration/02-01-SUMMARY.md
.planning/phases/02-problem-input---ai-integration/02-02-PLAN.md
.planning/phases/02-problem-input---ai-integration/02-02-SUMMARY.md
.planning/phases/02-problem-input---ai-integration/02-03-CONTEXT.md
.planning/phases/02-problem-input---ai-integration/02-03-PLAN.md
.planning/phases/02-problem-input---ai-integration/02-03-SUMMARY.md
.planning/phases/02-problem-input---ai-integration/02-CONTEXT.md
.planning/phases/03-guidance-annotations/03-01-PLAN.md
.planning/phases/03-guidance-annotations/03-01-SUMMARY.md
.planning/phases/03-guidance-annotations/03-02-PLAN.md
.planning/phases/03-guidance-annotations/03-02-SUMMARY.md
.planning/phases/03-guidance-annotations/03-03-PLAN.md
.planning/phases/03-guidance-annotations/03-CONTEXT.md
.planning/PROJECT.md
.planning/REQUIREMENTS.md
.planning/ROADMAP.md
.planning/STATE.md
app/api/chat/route.ts
app/api/debug-check/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
app/template.tsx
app/workspace/page.tsx
bun.lock
components.json
components/ai-elements/artifact.tsx
components/ai-elements/canvas.tsx
components/ai-elements/chain-of-thought.tsx
components/ai-elements/checkpoint.tsx
components/ai-elements/code-block.tsx
components/ai-elements/confirmation.tsx
components/ai-elements/connection.tsx
components/ai-elements/context.tsx
components/ai-elements/controls.tsx
components/ai-elements/conversation.tsx
components/ai-elements/edge.tsx
components/ai-elements/image.tsx
components/ai-elements/inline-citation.tsx
components/ai-elements/loader.tsx
components/ai-elements/message.tsx
components/ai-elements/model-selector.tsx
components/ai-elements/node.tsx
components/ai-elements/open-in-chat.tsx
components/ai-elements/panel.tsx
components/ai-elements/plan.tsx
components/ai-elements/prompt-input.tsx
components/ai-elements/queue.tsx
components/ai-elements/reasoning.tsx
components/ai-elements/shimmer.tsx
components/ai-elements/sources.tsx
components/ai-elements/suggestion.tsx
components/ai-elements/task.tsx
components/ai-elements/tool.tsx
components/ai-elements/toolbar.tsx
components/ai-elements/web-preview.tsx
components/landing/ChatInput.tsx
components/TldrawEditor.tsx
components/ui/alert.tsx
components/ui/badge.tsx
components/ui/button-group.tsx
components/ui/button.tsx
components/ui/card.tsx
components/ui/carousel.tsx
components/ui/collapsible.tsx
components/ui/command.tsx
components/ui/dialog.tsx
components/ui/dropdown-menu.tsx
components/ui/fabula/hovercaption.tsx
components/ui/hover-card.tsx
components/ui/input-group.tsx
components/ui/input.tsx
components/ui/progress.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/separator.tsx
components/ui/textarea.tsx
components/ui/tooltip.tsx
components/workspace/AnnotationOverlay.tsx
components/workspace/CanvasPanel.tsx
components/workspace/ChatPanel.tsx
components/workspace/Navbar.tsx
components/workspace/ProblemPanel.tsx
eslint.config.mjs
hooks/useDebounce.ts
lib/ai/prompt.ts
lib/annotation-positioning.ts
lib/problem-context.tsx
lib/types/annotations.ts
lib/utils.ts
next.config.ts
package.json
postcss.config.mjs
README.md
tsconfig.json
```

### Dependencies

- package.json: @ai-sdk/react@^3.0.41, @radix-ui/react-collapsible@^1.1.12, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.16, @radix-ui/react-hover-card@^1.1.15, @radix-ui/react-progress@^1.1.8, @radix-ui/react-scroll-area@^1.2.10, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.8, @radix-ui/react-slot@^1.2.4, @radix-ui/react-tooltip@^1.2.8, @radix-ui/react-use-controllable-state@^1.2.2, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, @xyflow/react@^12.10.0, ai@^6.0.42, babel-plugin-react-compiler@1.0.0, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.1.1, embla-carousel-react@^8.6.0, eslint@^9, eslint-config-next@16.1.3, framer-motion@^12.28.1, lucide-react@^0.562.0, motion@^12.27.5, nanoid@^5.1.6, next@16.1.3, react@19.2.3, react-dom@19.2.3, react-resizable-panels@^4.4.1, shiki@^3.21.0, streamdown@^2.1.0, tailwind-merge@^3.4.0, tailwindcss@^4, tldraw@^4.2.3, tokenlens@^1.3.1, tw-animate-css@^1.4.0, typescript@^5, use-stick-to-bottom@^1.1.1

### Recent commits (newest first)

- refactor(canvas): debounce call
- improvement(ui): landing + chatpanel
- Add project demo link to README
- Add trophy emoji to project recognition
- Update README with project title and description
- improvement(ui): draggable annotations
- improvement(release: ui): annotation suggestions + website animations
- fix(release): removed ts expect error comments
- feat(release): downgrade model for prod
- improvment(ui): nav animation
- improvement(release: ui): chat panel
- chore(release): add debug mode for UI
- chore(release): init vercel ai elements + shadcn
- improvement(release): reduce debounce delay
- feat(release): how to guide
- fix: licensing key typo
- chore: add tldraw licensing key
- refactor: linting errors
- feat(03): annotation rendering
- fix(03-03): fix annotation rendering and tool call detection

## Key source files (fetched from GitHub, selected and truncated for size)

### .planning/PROJECT.md

```markdown
# Socratic Whiteboard

## What This Is

An AI-powered math tutoring app that uses the Socratic method to guide students through problem-solving on a digital canvas. Users describe a math problem, then work it out on a tldraw whiteboard while an AI agent watches their progress and asks guiding questions — both in chat and directly on the canvas — without giving away the answer.

## Core Value

The AI guides through questions, not answers. Students learn by being nudged toward discovery, not told what to do.

## Requirements

### Validated

(None yet — ship to validate)

### Active

- [ ] User can describe a math problem via text or image upload on initial screen
- [ ] User transitions to workspace with problem reference panel, canvas, and chat
- [ ] User can draw/write freely on tldraw canvas to work through problem
- [ ] AI receives canvas screenshots when user pauses (debounced)
- [ ] AI provides Socratic questions in collapsible chat sidebar
- [ ] AI proposes canvas annotations (text questions, hints/scaffolding)
- [ ] Canvas annotations appear with inline approve/dismiss buttons
- [ ] AI positions annotations to avoid overlapping existing canvas elements
- [ ] Chat and canvas annotations share conversation context
- [ ] Demo completes one full problem-solving flow with visible Socratic guidance

### Out of Scope

- Multi-subject support beyond math — keeps demo focused
- User accounts/persistence — hackathon demo doesn't need it
- Mobile optimization — desktop demo is sufficient
- Arrows/highlights on canvas — text and hints only for v1
- Error correction annotations — Socratic questioning handles this via chat

## Context

**Hackathon:** This weekend (48 hours or less)

**Target tracks:**
- Education track: Socratic method, adaptive to any math level
- UX subtrack: Canvas-first design, non-intrusive guidance, clean transitions
- Vercel subtrack: AI SDK integration, streaming responses

**User flow:**
1. Landing page with centered chat input (ChatGPT/Claude style)
2. User describes math problem (text or uploads image)
3. Workspace appears: problem panel (left) + canvas (center) + collapsible chat (right)
4. User works on canvas
5. When user pauses, AI takes screenshot, analyzes progress against original problem
6. AI responds in chat with Socratic question AND/OR proposes canvas annotation
7. User approves/dismisses annotation inline
8. Loop until problem solved

**Canvas as source of truth:** The AI uses screenshots as EYES to see work, tldraw APIs as HANDS to annotate. All meaningful interaction happens on/around the canvas.

## Constraints

- **Tech stack**: Next.js, TypeScript, Tailwind, Zustand, tldraw, Vercel AI SDK — non-negotiable
- **Timeline**: ~48 hours — must ruthlessly prioritize demo flow
- **Demo scope**: One complete problem solved with visible Socratic guidance wins

## Key Decisions

| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Canvas-first with collapsible chat | UX track requires clean, focused in
[truncated — 473 more characters]
```

### .planning/STATE.md

```markdown
# Project State

## Project Reference

See: .planning/PROJECT.md

**Core value:** The AI guides through questions, not answers. Students learn by being nudged toward discovery, not told what to do.
**Current focus:** Phase 1 — Foundation & Canvas

## Current Position

Phase: 3 of 3 (Guidance Annotations)
Plan: 2 of 3 complete
Status: Phase in progress
Last activity: 2026-01-18 — Completed 03-02-PLAN.md (Annotation overlay with smart positioning)

Progress: █████████░ 90% (9/10 plans complete)

## Performance Metrics

**Velocity:**
- Total plans completed: 9
- Average duration: ~7.5 min (automated plans only)

**By Phase:**

| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| 01-foundation-canvas | 4/4 (complete) | 39.5 min | 9.9 min |
| 02-problem-input---ai-integration | 3/3 (complete) | Manual | Manual |
| 03-guidance-annotations | 2/3 (in progress) | ~18 min | ~9 min |

## Accumulated Context

### Decisions

| Decision | Phase | Rationale |
|----------|-------|-----------|
| Tailwind CSS v4 zero-config | 01-01 | Simpler setup, faster builds with @import directive |
| Crimson Text serif for headings | 01-01 | Academic aesthetic for tutoring app |
| Google Fonts CDN delivery | 01-01 | Fast font loading without local hosting |
| System fonts for body text | 01-01 | Performance and native platform familiarity |
| ChatGPT/Claude aesthetic with centered input | 01-02 | Clean, familiar UX for initial problem entry |
| template.tsx for page transitions | 01-02 | Enables exit animations (template re-renders on navigation) |
| 300ms transition duration | 01-02 | Smooth transitions without feeling sluggish |
| react-resizable-panels Group/Separator API | 01-03 | Library exports differ from documentation (verified via Node.js) |
| Hide tldraw menus, keep toolbar | 01-03 | Minimal UI focus on canvas, toolbar needed for drawing tools |
| 500ms throttle for localStorage | 01-03 | Balance performance vs data loss risk |
| Welcome hint on empty canvas | 01-03 | Guide first-time users to start working |
| Pivot to flexbox layout from react-resizable-panels | 01-04 | Multiple rendering issues; flexbox achieves 20/80 split without complexity |
| Remove welcome hint text | 01-04 | tldraw text shape API incompatible with simple programmatic insertion |
| "Socratical" branding | 01-04 | Shorter, more distinctive than "Socratic Whiteboard" |
| Client-side object URLs for images | 02-01 | URL.createObjectURL() enables preview/display without server upload |
| React Context over Redux/Zustand | 02-01 | Simpler state management appropriate for 3-phase project scope |
| Next.js Image with object URLs | 02-01 | Works without configuration; no optimization needed for client blobs |
| Text OR image (mutually exclusive) | 02-01 | Clearer UX than allowing both simultaneously |
| useMemo excludes editorRef from dependencies | 03-02 | Prevents unnecessary recalculations when editor mounts |
| processedMessageIds Set for duplicate prevention | 03-02 | Pre
[truncated — 662 more characters]
```

### package.json

```
{
  "name": "temp-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@ai-sdk/react": "^3.0.41",
    "@radix-ui/react-collapsible": "^1.1.12",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-hover-card": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@radix-ui/react-use-controllable-state": "^1.2.2",
    "@xyflow/react": "^12.10.0",
    "ai": "^6.0.42",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "embla-carousel-react": "^8.6.0",
    "framer-motion": "^12.28.1",
    "lucide-react": "^0.562.0",
    "motion": "^12.27.5",
    "nanoid": "^5.1.6",
    "next": "16.1.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-resizable-panels": "^4.4.1",
    "shiki": "^3.21.0",
    "streamdown": "^2.1.0",
    "tailwind-merge": "^3.4.0",
    "tldraw": "^4.2.3",
    "tokenlens": "^1.3.1",
    "use-stick-to-bottom": "^1.1.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.1.3",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";
import { ProblemProvider } from "@/lib/problem-context";

export const metadata: Metadata = {
  title: "Socratical",
  description: "Philosophize your toughest subjects",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="antialiased">
        <ProblemProvider>{children}</ProblemProvider>
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
"use client";

import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import { useProblem } from "@/lib/problem-context";
import {
  PromptInput,
  PromptInputActionAddAttachments,
  PromptInputActionMenu,
  PromptInputActionMenuContent,
  PromptInputActionMenuTrigger,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputButton,
  PromptInputFooter,
  type PromptInputMessage,
  PromptInputProvider,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
} from "@/components/ai-elements/prompt-input";

export default function Home() {
  const router = useRouter();
  const { setProblem, setProblemImage } = useProblem();

  const handleSubmit = (message: PromptInputMessage) => {
    const hasText = Boolean(message.text);
    const hasAttachments = Boolean(message.files?.length);

    if (!(hasText || hasAttachments)) {
      return;
    }

    // Set problem data
    if (message.text.trim()) {
      setProblem(message.text);
    }
    if (message.files && message.files.length > 0) {
      // Convert the first file attachment to File object
      const fileUrl = message.files[0];
      if (fileUrl && fileUrl.url) {
        fetch(fileUrl.url)
          .then((res) => res.blob())
          .then((blob) => {
            const file = new File([blob], fileUrl.filename || "image.png", {
              type: fileUrl.mediaType || "image/png",
            });
            setProblemImage(file);
          })
          .catch(() => {
            // Handle error silently
          });
      }
    }

    // Navigate immediately to workspace
    router.push("/workspace");
  };

  return (
    <div className="min-h-screen w-full flex items-center justify-center bg-white">
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, ease: "easeOut" }}
        className="flex flex-col items-center justify-center gap-8 px-4 w-full max-w-150"
      >
        {/* Heading */}
        <div className="text-center space-y-4">
          <motion.h1
            layoutId="socratical-header"
            className="font-semibold text-gray-900 tracking-tight text-6xl"
          >
            Socratical
          </motion.h1>
          <p className="text-xl text-gray-600 max-w-2xl">
            Philosophize your toughest subjects
          </p>
        </div>

        {/* Chat Input */}
        <div className="w-full">
          <PromptInputProvider>
            <PromptInput globalDrop onSubmit={handleSubmit}>
              <PromptInputAttachments>
                {(attachment) => <PromptInputAttachment data={attachment} />}
              </PromptInputAttachments>
              <PromptInputBody>
                <PromptInputTextarea placeholder="Enter your problem or question..." />
              </PromptInputBody>
              <PromptInputFooter>
                <PromptInputTools>
                  <PromptInputActionMenu>
                    <PromptInputActionMenuTrigger />
                    <PromptInputActionMenuContent>
                      <PromptInputActionAddAttachments />
                    </PromptInputActionMenuContent>
                  </PromptInputActionMenu>
                </PromptInputTools>
                <PromptInputSubmit />
              </PromptInputFooter>
            </PromptInput>
          </PromptInputProvider>
        </div>

        {/* Subtle hint */}
        <p className="text-sm text-gray-400 text-center">
          Work on a digital canvas while AI asks guiding questions
        </p>
      </motion.div>
    </div>
  );
}

```

### app/workspace/page.tsx

```typescript
"use client";

import { useEffect } from "react";
import CanvasPanel from "@/components/workspace/CanvasPanel";
import { ChatPanel } from "@/components/workspace/ChatPanel";
import { useProblem } from "@/lib/problem-context";
import { useRouter } from "next/navigation";
import Navbar from "@/components/workspace/Navbar";

export default function WorkspacePage() {
  const { problemText, problemImage } = useProblem();
  const router = useRouter();

  useEffect(() => {
    if (!problemText || !problemImage) {
      router.push("/");
    }
  }, [problemText, problemImage, router]);

  return (
    <div className="h-screen w-screen flex flex-col overflow-hidden">
      {/* Top: Navbar */}
      <Navbar />

      {/* Bottom: Panels (fills remaining space) */}
      <div className="flex flex-1 overflow-hidden">
        {/* Center: Canvas Panel (flexible, expands when chat collapses) */}
        <div className="flex-1 h-full">
          <CanvasPanel />
        </div>

        {/* Right: Chat Panel (controls its own width via isCollapsed state) */}
        <div className="h-full">
          <ChatPanel />
        </div>
      </div>
    </div>
  );
}

```

### app/api/debug-check/route.ts

```typescript
export async function GET() {
  const isDebug = process.env.DEBUG === "true";

  return Response.json({
    isDebug,
  });
}

```

### app/api/chat/route.ts

```typescript
import {
  convertToModelMessages,
  streamText,
  UIMessage,
  ModelMessage,
  tool,
} from "ai";
import { z } from "zod";
import { SYSTEM_PROMPT } from "@/lib/ai/prompt";

// Define annotation tool for AI to propose canvas annotations
const proposeAnnotationTool = tool({
  description:
    "Propose an annotation (question or hint) to appear on the student's canvas. Use this when you want to guide their thinking about a specific part of their work.",
  inputSchema: z.object({
    type: z
      .enum(["question", "hint"])
      .describe(
        "Type of annotation: question for Socratic questions, hint for scaffolding/partial solutions",
      ),
    text: z
      .string()
      .describe(
        "The annotation text. Keep concise (1-2 sentences max). Questions should be open-ended. Hints should be partial (not complete answers).",
      ),
    positionHint: z
      .enum(["top-left", "top-right", "center"])
      .optional()
      .describe("Rough position preference. Algorithm will avoid overlaps."),
  }),
  execute: async ({ type, text, positionHint }) => {
    // Tool execution is handled client-side by ChatPanel.tsx
    // This just returns the input for the model's confirmation
    console.log("🔧 proposeAnnotation tool called:", {
      type,
      text,
      positionHint,
    });
    return { success: true, type, text, positionHint };
  },
});

export async function POST(req: Request) {
  try {
    const body = await req.json();

    const {
      messages,
      problem,
      screenshot,
    }: {
      messages: UIMessage[];
      problem: { text: string | undefined; imageUrl: string | undefined };
      screenshot: string | undefined;
    } = body;

    if (!messages || !Array.isArray(messages)) {
      return Response.json(
        { error: "Messages array is required" },
        { status: 400 },
      );
    }

    // Check if DEBUG mode is enabled - disable AI responses
    if (process.env.DEBUG === "true") {
      console.log("🐛 DEBUG mode enabled - AI responses disabled");
      return Response.json(
        {
          id: "debug-mode",
          role: "assistant",
          content:
            "[DEBUG MODE] AI responses are disabled. Set DEBUG=false in .env.local to enable.",
        },
        {
          status: 200,
          headers: {
            "Content-Type": "application/json",
          },
        },
      );
    }

    // Filter out empty messages (used for initial greeting trigger)
    const filteredMessages = messages.filter((msg) =>
      msg.parts.some((part) => part.type === "text" && part.text.trim()),
    );

    // Check if this is the initial greeting (no valid messages)
    const isInitialGreeting = filteredMessages.length === 0;

    // Build context-aware system message
    const systemMessage = `${SYSTEM_PROMPT}

**Current Problem:**
${problem?.text || "Image uploaded (description pending)"}${screenshot ? "\n\n**Canvas State:** The student's current work is shown in the canvas screenshot attached to the conversation." : ""}`;

    // Convert filtered messages to model messages
    const modelMessages = await convertToModelMessages(filteredMessages);

    // Add screenshot as an image message if available
    if (screenshot) {
      modelMessages.push({
        role: "user",
        content: [
          {
            type: "text",
            text: "Here is my current canvas work:",
          },
          {
            type: "image",
            image: screenshot,
          },
        ],
      } as ModelMessage);
    }

    console.log("📤 Starting AI stream:", {
      isInitialGreeting,
      messageCount: modelMessages.length,
      hasScreenshot: !!screenshot,
      hasProblem: !!(problem?.text || problem?.imageUrl),
      systemPromptLength: systemMessage.length,
    });

    // Debug: Log if we have the tool available
    console.log("🔧 Tool configuration:", {
      toolsAvailable: ["proposeAnnotation"],
      screenshotProvided: !!screenshot,
    });

    const result = isInitialGreeting
      ? streamText({
          model:
            process.env.DEV === "true"
              ? "anthropic/claude-3-haiku"
              : "anthropic/claude-haiku-4.5",
          system: systemMessage,
          prompt:
            "Greet the student and ask ONE opening Socratic question to help them start thinking about this problem. Examples: 'What information does this problem give you?' or 'What do you think would be a good first step?' Keep it encouraging and open-ended. DO NOT ATTEMPT ANY TOOL CALLS.",
          tools: {
            proposeAnnotation: proposeAnnotationTool,
          },
          temperature: 0.7,
          onStepFinish: ({ toolCalls }) => {
            console.log("🔧 Step finished, tool calls:", toolCalls);
          },
          onError: ({ error }) => {
            console.error("AI streaming error:", error);
          },
        })
      : streamText({
          model:
            process.env.DEV === "true"
              ? "anthropic/claude-3-haiku"
              : "anthropic/claude-haiku-4.5",
          system: systemMessage,
          messages: modelMessages,
          tools: {
            proposeAnnotation: proposeAnnotationTool,
          },
          toolChoice: "auto", // Let AI decide when to use tools
          temperature: 0.7,
          onStepFinish: ({ toolCalls }) => {
            console.log("🔧 Step finished, tool calls:", toolCalls);
            if (toolCalls && toolCalls.length > 0) {
              console.log(
                "✨ Tool calls detected:",
                JSON.stringify(toolCalls, null, 2),
              );
            }
          },
          onError: ({ error }) => {
            console.error("AI streaming error:", error);
          },
        });

    return result.toUIMessageStreamResponse();
  } catch (error) {
    console.error("API route error:", error);
    return Response.json(
      { error: "Failed to process chat request" },
      { status: 500 },
    );
  }
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  reactCompiler: true,
};

export default nextConfig;

```

### app/template.tsx

```typescript
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3, ease: "easeInOut" }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

```

[91 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]