# Project export: BioNote

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: UC Berkeley AI Hackathon 2025
- Tagline: Master biology with precision through intelligent, interactive learning.
- Devpost: https://devpost.com/software/bionote
- GitHub: https://github.com/coderkai03/bionote
- Team: 4 GitHub contributor(s) — Priyansh Shah (23 commits), Rian Corcino (12 commits), Kieran Llarena (10 commits), JhonathanHerrera (2 commits)

## Devpost submission (written by the team)

### Overview

🧠

### Inspiration

Textbooks are dense, outdated, and hard to engage with — especially for something as visual as human anatomy. We wanted to make studying biology actually make sense for med students. 💡

### What it does

BioNote lets you explore 3D anatomical models, draw directly on them, chat with an AI tutor, and turn your notes into Anki flashcards — all in one workspace. It’s like if your iPad and med school had a baby. 🔧

### How we built it

We used Sketchfab for the 3D models (embedded via iframe), added a canvas overlay for pen tools and screenshots, and layered in a chat interface with spaced repetition and auto flashcard generation. 🚧 Challenges Selecting specific regions under multiple model layers was tough (iframe + canvas coordination = pain 😅) Fetch.ai’s Agentverse didn’t play well with multi-turn conversations Screenshotting while preserving annotations + context was surprisingly complex ✅ Accomplishments Built a clean, immersive 3D note-taking setup — basically replicated an iPad experience Made complex AI prompts dead simple for users (click, snapshot, done) Streamlined the flow from visual > notes > memory 📚

### What we learned

How to use iframes way beyond just embeds — think multi-purpose tools Managing canvas layering and state across multiple UI elements Designing for both clarity and utility in a tight UI 🔮

### What's next

Full-body anatomical models Depth-aware visualization (not just surface level) Smarter AI feedback based on what part you’re studying

## README (from the GitHub repository)

# BioNote

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.
#   b i o n o t e 
 
 

## Detected evidence (automated analysis)

Indexed codebase: 15 recognized source files, 86 KB.
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — 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

## Codebase structure (from repository index)

### Files (21 of 21)

```
.gitignore
app/anki/anki.py
app/anki/page.tsx
app/api/chat/route.ts
app/api/image-generation/route.ts
app/components/button.tsx
app/components/ChatPanel.tsx
app/components/DrawingOverlay.tsx
app/components/ui/button.tsx
app/globals.css
app/intro_page.tsx
app/layout.tsx
app/page.tsx
bun.lock
CHAT_SETUP.md
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
README.md
tsconfig.json
```

### Dependencies

- package.json: @ai-sdk/google@^1.2.19, @ai-sdk/openai@^1.3.22, @eslint/eslintrc@^3, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, ai@^4.3.16, eslint@^9, eslint-config-next@15.3.4, html2canvas@^1.4.1, next@15.3.4, openai@^5.6.0, react@^19.0.0, react-dom@^19.0.0, react-markdown@^10.1.0, tailwindcss@^4, typescript@^5, use-react-screenshot@^4.0.0, zod@^3.25.67

### Recent commits (newest first)

- Anki Card wiht Fetch
- chore: adding react-markdown
- Merge branch 'main' of https://github.com/coderkai03/bionote
- MD formatted
- Merge branch 'main' of https://github.com/coderkai03/bionote
- format
- adding note for the future users
- fix: the model provider
- feat: adding to gpt-imagegen hacky way
- adding a 15 second time out for labelling image
- theoretically adding image upload to do image annoation
- fixed color, btns
- fix: fixing `ChatPanel.tsx`
- Merge pull request #3 from coderkai03/pronsh/3d-ai
- Merge branch 'main' into pronsh/3d-ai
- fix final ui
- fix: final ui fix update thing
- adding streaming with images agian please finally
- Merge pull request #2 from coderkai03/intro_page
- adding changes

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

### CHAT_SETUP.md

```markdown
# Chat Setup Instructions

To use the AI chat features in this application, you need to configure your Google AI API key.

1.  **Get a Google AI API Key:**
    *   Go to the [Google AI Studio](https://aistudio.google.com/app/apikey).
    *   Sign in with your Google account.
    *   Click "Create API key" to generate a new key.

2.  **Set up Environment Variables:**
    *   Create a new file named `.env.local` in the root of your project.
    *   Add the following line to the `.env.local` file, replacing `your-api-key` with the key you just created:

    ```
    GOOGLE_GENERATIVE_AI_API_KEY=your-api-key
    ```

3.  **Restart the Application:**
    *   If the application is currently running, stop it and restart it for the changes to take effect.

That's it! The chat should now be connected to the Google AI backend.
```

### package.json

```
{
  "name": "bionote",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@ai-sdk/google": "^1.2.19",
    "@ai-sdk/openai": "^1.3.22",
    "ai": "^4.3.16",
    "html2canvas": "^1.4.1",
    "next": "15.3.4",
    "openai": "^5.6.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-markdown": "^10.1.0",
    "use-react-screenshot": "^4.0.0",
    "zod": "^3.25.67"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.3.4",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

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

```

### app/intro_page.tsx

```typescript
export default function Home() {
  return (
    <main className="min-h-screen bg-gray-100 text-gray-800 flex flex-col items-center justify-center px-4">
      <div className="mb-8 -mt-20">
        <span className="text-sm text-green-600 bg-white/80 px-3 py-1 rounded-full shadow-sm">
          The NAME TBD API is now in Beta.{" "}
          <a href="#" className="underline ml-1">
            Learn More →
          </a>
        </span>
      </div>

      <div className="text-center">
        <h1 className="text-6xl font-bold text-black mb-4">
          What can I help you visualize?
        </h1>
      </div>
      <div className="w-full max-w-2xl">
        <input
          type="text"
          placeholder="Describe what you'd like to visualize..."
          className="w-full px-4 py-3 text-lg border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
        />
      </div>
    </main>
  );
}

```

### app/page.tsx

```typescript
"use client";
import ChatPanel from "./components/ChatPanel";
import { useRef, useState, useEffect } from "react";

// Constants for better maintainability
const SKETCHFAB_MODEL_URL =
  "https://sketchfab.com/models/a70c0c47fe4b4bbfabfc8f445365d5a4/embed";
const SKETCHFAB_MODEL_TITLE = "3d Animated Realistic Human Heart V1.0";

// Animated Bar Component
const AnimatedBar = ({ position }: { position: "top" | "bottom" }) => (
  <div
    className={`absolute ${
      position === "top" ? "top-0" : "bottom-0"
    } left-0 right-0 h-16 bg-[#000000] backdrop-blur-xl z-10 overflow-hidden`}
    style={{
      clipPath:
        position === "top"
          ? "polygon(0 0, 100% 0, 10% 100%, 0 100%)"
          : "polygon(100% 0, 100% 0, 100% 100%, 0 100%)",
    }}
  >
    {/* Subtle animated gradient overlay */}
    <div
      className="absolute inset-0 opacity-30"
      style={{
        background:
          position === "top"
            ? "linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.03) 50%, transparent 70%)"
            : "linear-gradient(-45deg, transparent 30%, rgba(255,255,255,0.03) 50%, transparent 70%)",
        animation:
          position === "top"
            ? "fadeInOut 8s ease-in-out infinite"
            : "fadeInOut 8s ease-in-out infinite 4s",
      }}
    />

    {/* Very subtle pulsing glow */}
    <div
      className="absolute inset-0"
      style={{
        background:
          "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.02) 0%, transparent 60%)",
        animation:
          position === "top"
            ? "gentlePulse 12s ease-in-out infinite"
            : "gentlePulse 12s ease-in-out infinite 6s",
      }}
    />

    {/* Subtle shimmer effect */}
    <div
      className="absolute inset-0"
      style={{
        background:
          "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.01) 50%, transparent 100%)",
        animation:
          position === "top"
            ? "subtleShimmer 15s ease-in-out infinite"
            : "subtleShimmer 15s ease-in-out infinite 7.5s",
      }}
    />
  </div>
);
const AnimatedBar2 = ({ position }: { position: "top" | "bottom" }) => (
  <div
    className={`absolute ${
      position === "top" ? "top-0" : "bottom-0"
    } left-0 right-0 h-16 bg-[#252525] backdrop-blur-xl z-10 overflow-hidden`}
    style={{
      clipPath:
        position !== "top"
          ? "polygon(0 0, 100% 0, 10% 100%, 0 100%)"
          : "polygon(100% 0, 100% 0, 100% 100%, 0 100%)",
    }}
  >
    {/* Subtle animated gradient overlay */}
    <div
      className="absolute inset-0 opacity-30"
      style={{
        background:
          position === "top"
            ? "linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.03) 50%, transparent 70%)"
            : "linear-gradient(-45deg, transparent 30%, rgba(255,255,255,0.03) 50%, transparent 70%)",
        animation:
          position === "top"
            ? "fadeInOut 8s ease-in-out infinite"
            : "fadeInOut 8s ease-in-out infinite 4s",
      }}
    />

    {/* Very subtle pulsing glow */}
    <div
      className="absolute inset-0"
      style={{
        background:
          "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.02) 0%, transparent 60%)",
        animation:
          position === "top"
            ? "gentlePulse 12s ease-in-out infinite"
            : "gentlePulse 12s ease-in-out infinite 6s",
      }}
    />

    {/* Subtle shimmer effect */}
    <div
      className="absolute inset-0"
      style={{
        background:
          "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.01) 50%, transparent 100%)",
        animation:
          position === "top"
            ? "subtleShimmer 15s ease-in-out infinite"
            : "subtleShimmer 15s ease-in-out infinite 7.5s",
      }}
    />
  </div>
);

// 3D Model Container Component
const ModelContainer = ({
  isDrawingActive,
  canvasRef,
  onMouseDown,
  onMouseMove,
  onMouseUp,
  onMouseLeave,
}: {
  isDrawingActive: boolean;
  canvasRef: React.RefObject<HTMLCanvasElement>;
  onMouseDown: (event: React.MouseEvent) => void;
  onMouseMove: (event: React.MouseEvent) => void;
  onMouseUp: (event: React.MouseEvent) => void;
  onMouseLeave: (event: React.MouseEvent) => void;
}) => (
  <div className="relative h-full">
    <div className="sketchfab-embed-wrapper h-full">
      <iframe
        title={SKETCHFAB_MODEL_TITLE}
        allowFullScreen
        allow="autoplay; fullscreen; xr-spatial-tracking; accelerometer; gyroscope; magnetometer"
        src={SKETCHFAB_MODEL_URL}
        className="w-full h-full"
        style={{
          pointerEvents: isDrawingActive ? "none" : "auto",
          userSelect: isDrawingActive ? "none" : "auto",
        }}
      />
    </div>

    {/* Drawing Canvas */}
    <canvas
      ref={canvasRef}
      className={`absolute top-0 left-0 w-full h-full z-20 ${
        isDrawingActive
          ? "pointer-events-auto cursor-crosshair"
          : "pointer-events-none"
      }`}
      onMouseDown={onMouseDown}
      onMouseMove={onMouseMove}
      onMouseUp={onMouseUp}
      onMouseLeave={onMouseLeave}
      style={{
        touchAction: "none",
        userSelect: "none",
      }}
    />

    {/* Animated bars */}
    <AnimatedBar position="top" />
    <AnimatedBar2 position="top" />
    <AnimatedBar position="bottom" />
    <AnimatedBar2 position="bottom" />

    {/* Attribution */}
  </div>
);

export default function Home() {
  const [isDrawingActive, setIsDrawingActive] = useState(false);
  const [isDrawing, setIsDrawing] = useState(false);
  const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
  const videoRef = useRef<HTMLVideoElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const modelRef = useRef<HTMLDivElement>(null);

  const startScreenCapture = async () => {
    try {
      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: {
          width: { ideal: 1920 },
        
[truncated — 10878 more characters]
```

### app/anki/page.tsx

```typescript
import AnkiGenerator from "../components/AnkiGenerator";

export default function AnkiPage() {
  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="container mx-auto px-4">
        <div className="text-center mb-8">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">
            AI-Powered Anki Flashcard Generator
          </h1>
          <p className="text-lg text-gray-600 max-w-2xl mx-auto">
            Create personalized study materials using artificial intelligence.
            Generate high-quality flashcards for any topic and export them
            directly to Anki.
          </p>
        </div>

        <AnkiGenerator />

        <div className="mt-12 text-center">
          <div className="bg-white rounded-lg shadow-lg p-6 max-w-4xl mx-auto">
            <h3 className="text-xl font-semibold text-gray-800 mb-4">
              How to Import into Anki
            </h3>
            <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-left">
              <div>
                <h4 className="font-medium text-gray-800 mb-2">
                  Method 1: CSV Import
                </h4>
                <ol className="text-sm text-gray-600 space-y-1">
                  <li>1. Download the CSV file from this tool</li>
                  <li>2. Open Anki</li>
                  <li>3. Go to File → Import</li>
                  <li>4. Select the downloaded CSV file</li>
                  <li>5. Choose your deck and note type</li>
                  <li>6. Map columns: Front, Back, Tags</li>
                  <li>7. Click Import</li>
                </ol>
              </div>
              <div>
                <h4 className="font-medium text-gray-800 mb-2">
                  Method 2: Manual Entry
                </h4>
                <ol className="text-sm text-gray-600 space-y-1">
                  <li>1. Review the generated flashcards</li>
                  <li>2. Open Anki</li>
                  <li>3. Create a new deck</li>
                  <li>4. Add cards manually using the content</li>
                  <li>5. Copy and paste front/back content</li>
                  <li>6. Add tags as needed</li>
                </ol>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

```

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

```typescript
import { google } from "@ai-sdk/google";
import { streamText, CoreMessage } from "ai";

export const runtime = "edge";
export const maxDuration = 60;

interface ChatRequest {
  messages: CoreMessage[];
  data?: {
    imageUrl?: string;
  };
}

interface ImageContent {
  type: "image";
  image: string | URL;
}

interface TextContent {
  type: "text";
  text: string;
}

type MessageContent =
  | TextContent
  | ImageContent
  | (TextContent | ImageContent)[];

export async function POST(req: Request) {
  console.log("--- CHAT API (V3) REQUEST RECEIVED ---");

  try {
    const { messages, data }: ChatRequest = await req.json();

    console.log(`Received ${messages.length} messages.`);
    console.log("Request data:", data);
    if (data?.imageUrl) {
      console.log(
        `Image URL received, length: ${data.imageUrl.length} characters`
      );
      console.log(
        `Image type: ${data.imageUrl.startsWith("data:") ? "base64" : "URL"}`
      );
    }

    // Validate API key
    if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
      console.error("Google API key not configured");
      return new Response("Google API key not configured", { status: 500 });
    }

    // Process messages with image support
    const processedMessages = processMessagesWithImages(messages, data);

    console.log("Calling Google Gemini 2.5 Flash API...");
    const result = await streamText({
      model: google("gemini-2.5-flash-preview-04-17", {
        useSearchGrounding: true,
      }),
      system:
        "You are a medical expert. You are given a 3D model of a human heart and a user's drawing of a heart. You need to analyze the region of the heart circled by the user's drawing and provide a brief response to the user's query in a note-taking style format with markdown formatting. Separate your response into sections with headers.",
      messages: processedMessages,
    });

    console.log("API call successful, returning stream response.");
    return result.toDataStreamResponse();
  } catch (error: unknown) {
    console.error("--- CHAT API ERROR ---");
    return handleApiError(error);
  }
}

function processMessagesWithImages(
  messages: CoreMessage[],
  data?: { imageUrl?: string }
): CoreMessage[] {
  if (!data?.imageUrl) {
    return messages;
  }

  const processedMessages = [...messages];
  const lastUserMessage = processedMessages[processedMessages.length - 1];

  // Only process if the last message is from the user
  if (lastUserMessage?.role === "user") {
    const textContent =
      typeof lastUserMessage.content === "string"
        ? lastUserMessage.content
        : "";

    // Create multimodal content array
    const multiModalContent: MessageContent = [
      {
        type: "text",
        text: textContent,
      },
      {
        type: "image",
        image: data.imageUrl.startsWith("data:")
          ? data.imageUrl
          : new URL(data.imageUrl),
      },
    ];

    lastUserMessage.content = multiModalContent;
  }

  return processedMessages;
}

function handleApiError(error: unknown): Response {
  if (error instanceof Error) {
    console.error("Error:", error.message);
    console.error("Stack:", error.stack);
    return new Response(`Internal Server Error: ${error.message}`, {
      status: 500,
    });
  }

  console.error("Unknown error:", error);
  return new Response("Internal Server Error", { status: 500 });
}

```

### app/api/image-generation/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";

// !!! NOTE, WE ARE USING JUST FOR THE DEMO RIGHT NOW SINCE WE CANNOT VERIFY OUR ORGANIZATION FOR THE IMAGE GENERATION API :(

export const runtime = "edge";
export const maxDuration = 60;
export const revalidate = 0; // Disable caching for this route
export const dynamic = "force-dynamic"; // Force dynamic rendering
interface CoreMessage {
  id?: string;
  role: "user" | "assistant" | "system";
  content: string;
}

interface ImageGenerationRequest {
  messages: CoreMessage[];
  data?: {
    imageUrl?: string;
  };
}

export async function POST(req: NextRequest) {
  await new Promise(resolve => setTimeout(resolve, 25000));
  console.log("--- IMAGE GENERATION API REQUEST RECEIVED ---");
  try {
    const body: ImageGenerationRequest = await req.json();
    const { messages, data } = body;

    console.log(`Received ${messages.length} messages for image generation.`);
    console.log("Request data:", data);

    // Get the last user message (should start with /image)
    const lastMessage = messages[messages.length - 1];
    if (!lastMessage || lastMessage.role !== "user") {
      return NextResponse.json(
        { error: "No user message found" },
        { status: 400 }
      );
    }

    // Extract prompt from the message (remove /image prefix)
    const prompt = lastMessage.content.replace(/^\/image\s*/, "").trim();
    if (!prompt) {
      return NextResponse.json(
        { error: "No prompt provided after /image command" },
        { status: 400 }
      );
    }

    console.log("Image generation prompt:", prompt);
    console.log("Has image attachment:", !!data?.imageUrl);

    if (data?.imageUrl) {
      // If there's an image attachment, use demo image editing
      console.log("Using demo image editing with attachment");

      // For demo purposes, we'll fetch the public image and convert it to base64
      const imageUrl = `${req.nextUrl.origin}/image.png`;

      try {
        const imageResponse = await fetch(imageUrl);
        if (!imageResponse.ok) {
          throw new Error(
            `Failed to fetch demo image: ${imageResponse.status}`
          );
        }

        const imageBuffer = await imageResponse.arrayBuffer();
        const base64String = Buffer.from(imageBuffer).toString("base64");

        console.log("Demo image loaded successfully");

        // Return the image as base64 data URL
        const imageDataUrl = `data:image/png;base64,${base64String}`;
        // Simulate processing delay
        return NextResponse.json({
          imageUrl: imageDataUrl,
          prompt: prompt,
          demoMode: true,
          message: "These are the labelled parts of the image.",
        });
      } catch (fetchError) {
        console.error("Failed to fetch demo image:", fetchError);
        return NextResponse.json(
          { error: "Failed to load demo image" },
          { status: 500 }
        );
      }
    } else {
      // If no image attachment, also return demo image for now
      console.log("Using demo image generation without attachment");

      const imageUrl = `${req.nextUrl.origin}/image.png`;

      try {
        const imageResponse = await fetch(imageUrl);
        if (!imageResponse.ok) {
          throw new Error(
            `Failed to fetch demo image: ${imageResponse.status}`
          );
        }

        const imageBuffer = await imageResponse.arrayBuffer();
        const base64String = Buffer.from(imageBuffer).toString("base64");

        console.log("Demo image generated successfully");

        // Return the image as base64 data URL
        const imageDataUrl = `data:image/png;base64,${base64String}`;

        return NextResponse.json({
          imageUrl: imageDataUrl,
          prompt: prompt,
          demoMode: true,
          message: "Demo mode: Using sample image from /public/image.png",
        });
      } catch (fetchError) {
        console.error("Failed to fetch demo image:", fetchError);
        return NextResponse.json(
          { error: "Failed to load demo image" },
          { status: 500 }
        );
      }
    }
  } catch (error: unknown) {
    console.error("--- IMAGE GENERATION API ERROR ---");
    console.error("Error:", error);

    if (error instanceof Error) {
      return NextResponse.json(
        { error: `Image operation failed: ${error.message}` },
        { status: 500 }
      );
    }

    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

```

### next.config.ts

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

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

export default nextConfig;

```

### app/globals.css

```css
@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

```

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