# Project export: LearnLocal

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 2026
- Tagline: Transform local food into local learning — AI-powered, community-grounded lessons that help families discover, understand, and use locally available produce.
- Devpost: https://devpost.com/software/learnlocal-uqj5p4
- GitHub: https://github.com/leblas/LearnLocal
- Team: 1 GitHub contributor(s) — Luis (2 commits)

## Devpost submission (written by the team)

### Inspiration

LearnLocal was inspired by experiences working with local food and community organizations. As part of leadership roles with a nonprofit community farm and youth-led food initiatives, we witnessed a recurring challenge in local food systems: food availability does not automatically translate into food access or food use. Even in agricultural regions with abundant local produce, many low-income and farmworker communities continue to experience barriers to healthy food access. Cost perception plays a role—processed foods are often seen as cheaper, easier, or more familiar than fresh produce. At the same time, organizations working to improve food access face another challenge: education and outreach. Many families receive produce but may not know: What it is How to prepare it Why it matters nutritionally When it is in season How it connects to local sustainability and agriculture Creating educational materials and outreach programs is expensive, time intensive, and difficult to scale. We asked: What if AI could help local organizations turn food into learning? That question became LearnLocal.

### What it does

LearnLocal is an AI-powered educational platform that transforms local food information into engaging and accessible learning experiences. Instead of generating generic content, LearnLocal combines AI with trusted local context to create lessons that are grounded in community knowledge. Users can: Explore local foods and their stories Learn about seasonality and sustainability Discover preparation ideas and usage guidance Engage with community-focused educational content Receive personalized lessons that remain persistent across sessions The goal is not only to teach about food—but to help strengthen connections between people, their communities, and local agriculture.

### How we built it

LearnLocal was built as a full-stack web application focused on grounded AI generation. Our system includes: Next.js (App Router) for the web application Tailwind CSS for UI development Claude API for educational lesson generation Static grounding datasets using structured JSON files Redis memory layer to persist educational context To improve quality, we implemented a grounding pipeline: User selects or explores a topic Structured food and community datasets are retrieved Local context is injected into prompts AI generates educational content Results are stored for continuity and personalization This architecture helps reduce hallucination while keeping lessons locally relevant.

### Challenges we ran into

One of the biggest challenges was balancing AI flexibility with educational trustworthiness. AI can generate content quickly, but educational systems require accuracy and meaningful context. We faced challenges including: Preventing generic AI responses Structuring local food data Designing memory without over-personalization Creating lessons that remain engaging while grounded Another challenge was maintaining simplicity while supporting future expansion into community and nonprofit use cases.

### Accomplishments we're proud of

We are proud that LearnLocal goes beyond a standard AI chatbot. Key accomplishments include: Built a grounded AI lesson generation pipeline Integrated persistent memory for improved continuity Connected educational content with community values Designed for accessibility and scalability Created a concept that supports nonprofits rather than replacing them Most importantly, we built something rooted in a real community problem.

### What we learned

This project taught us that effective AI systems are not built by adding more generation—they are built by adding better context. We learned: Grounding improves educational quality Community knowledge matters as much as technical architecture Simplicity often creates stronger user experiences AI works best when augmenting human efforts We also gained experience designing full-stack AI systems that combine memory, structured data, and user interaction.

### What's next

This project is only the beginning. Next steps include: Expanding the local food knowledge base Supporting multilingual educational content Adding recipes and meal guidance Introducing nonprofit and educator dashboards Measuring learning outcomes Connecting with local food hubs and community organizations Long term, we envision LearnLocal becoming a platform that helps communities transform food access into food empowerment.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 116 recognized source files, 361 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 154)

```
.agents/skills/iris-development/.cursor-plugin/plugin.json
.agents/skills/iris-development/references/ltm-bulk-create.md
.agents/skills/iris-development/references/ltm-organize.md
.agents/skills/iris-development/references/ltm-search.md
.agents/skills/iris-development/references/promotion-overview.md
.agents/skills/iris-development/references/session-add-event.md
.agents/skills/iris-development/references/session-retrieval.md
.agents/skills/iris-development/references/session-when-to-use.md
.agents/skills/iris-development/references/setup-auth-token.md
.agents/skills/iris-development/references/setup-cloud-service.md
.agents/skills/iris-development/SKILL.md
.agents/skills/redis-clustering/.cursor-plugin/plugin.json
.agents/skills/redis-clustering/references/hash-tags.md
.agents/skills/redis-clustering/references/read-replicas.md
.agents/skills/redis-clustering/SKILL.md
.agents/skills/redis-connections/.cursor-plugin/plugin.json
.agents/skills/redis-connections/references/blocking.md
.agents/skills/redis-connections/references/client-cache.md
.agents/skills/redis-connections/references/pipelining.md
.agents/skills/redis-connections/references/pooling.md
.agents/skills/redis-connections/references/timeouts.md
.agents/skills/redis-connections/SKILL.md
.agents/skills/redis-core/.cursor-plugin/plugin.json
.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.json
.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.md
.agents/skills/redis-core/evals/core/baselines/baseline.json
.agents/skills/redis-core/evals/core/baselines/model-matrix.json
.agents/skills/redis-core/evals/core/baselines/README.md
.agents/skills/redis-core/evals/core/evals.json
.agents/skills/redis-core/evals/core/model-matrix.json
.agents/skills/redis-core/references/choose-data-structure.md
.agents/skills/redis-core/references/key-naming.md
.agents/skills/redis-core/SKILL.md
.agents/skills/redis-observability/.cursor-plugin/plugin.json
.agents/skills/redis-observability/references/commands.md
.agents/skills/redis-observability/references/metrics.md
.agents/skills/redis-observability/SKILL.md
.agents/skills/redis-query-engine/.cursor-plugin/plugin.json
.agents/skills/redis-query-engine/references/dialect.md
.agents/skills/redis-query-engine/references/field-types.md
.agents/skills/redis-query-engine/references/index-creation.md
.agents/skills/redis-query-engine/references/index-management.md
.agents/skills/redis-query-engine/references/query-optimization.md
.agents/skills/redis-query-engine/references/skip-initial-scan.md
.agents/skills/redis-query-engine/SKILL.md
.agents/skills/redis-security/.cursor-plugin/plugin.json
.agents/skills/redis-security/references/acls.md
.agents/skills/redis-security/references/auth.md
.agents/skills/redis-security/references/network.md
.agents/skills/redis-security/SKILL.md
.agents/skills/redis-semantic-cache/.cursor-plugin/plugin.json
.agents/skills/redis-semantic-cache/references/best-practices.md
.agents/skills/redis-semantic-cache/references/langcache-usage.md
.agents/skills/redis-semantic-cache/SKILL.md
.agents/skills/redis-vector-search/.cursor-plugin/plugin.json
.agents/skills/redis-vector-search/references/algorithm-choice.md
.agents/skills/redis-vector-search/references/hybrid-search.md
.agents/skills/redis-vector-search/references/index-creation.md
.agents/skills/redis-vector-search/references/rag-pattern.md
.agents/skills/redis-vector-search/SKILL.md
learn-local/.agents/skills/iris-development/.cursor-plugin/plugin.json
learn-local/.agents/skills/iris-development/references/ltm-bulk-create.md
learn-local/.agents/skills/iris-development/references/ltm-organize.md
learn-local/.agents/skills/iris-development/references/ltm-search.md
learn-local/.agents/skills/iris-development/references/promotion-overview.md
learn-local/.agents/skills/iris-development/references/session-add-event.md
learn-local/.agents/skills/iris-development/references/session-retrieval.md
learn-local/.agents/skills/iris-development/references/session-when-to-use.md
learn-local/.agents/skills/iris-development/references/setup-auth-token.md
learn-local/.agents/skills/iris-development/references/setup-cloud-service.md
learn-local/.agents/skills/iris-development/SKILL.md
learn-local/.agents/skills/redis-clustering/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-clustering/references/hash-tags.md
learn-local/.agents/skills/redis-clustering/references/read-replicas.md
learn-local/.agents/skills/redis-clustering/SKILL.md
learn-local/.agents/skills/redis-connections/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-connections/references/blocking.md
learn-local/.agents/skills/redis-connections/references/client-cache.md
learn-local/.agents/skills/redis-connections/references/pipelining.md
learn-local/.agents/skills/redis-connections/references/pooling.md
learn-local/.agents/skills/redis-connections/references/timeouts.md
learn-local/.agents/skills/redis-connections/SKILL.md
learn-local/.agents/skills/redis-core/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.json
learn-local/.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.md
learn-local/.agents/skills/redis-core/evals/core/baselines/baseline.json
learn-local/.agents/skills/redis-core/evals/core/baselines/model-matrix.json
learn-local/.agents/skills/redis-core/evals/core/baselines/README.md
learn-local/.agents/skills/redis-core/evals/core/evals.json
learn-local/.agents/skills/redis-core/evals/core/model-matrix.json
learn-local/.agents/skills/redis-core/references/choose-data-structure.md
learn-local/.agents/skills/redis-core/references/key-naming.md
learn-local/.agents/skills/redis-core/SKILL.md
learn-local/.agents/skills/redis-observability/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-observability/references/commands.md
learn-local/.agents/skills/redis-observability/references/metrics.md
learn-local/.agents/skills/redis-observability/SKILL.md
learn-local/.agents/skills/redis-query-engine/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-query-engine/references/dialect.md
learn-local/.agents/skills/redis-query-engine/references/field-types.md
learn-local/.agents/skills/redis-query-engine/references/index-creation.md
learn-local/.agents/skills/redis-query-engine/references/index-management.md
learn-local/.agents/skills/redis-query-engine/references/query-optimization.md
learn-local/.agents/skills/redis-query-engine/references/skip-initial-scan.md
learn-local/.agents/skills/redis-query-engine/SKILL.md
learn-local/.agents/skills/redis-security/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-security/references/acls.md
learn-local/.agents/skills/redis-security/references/auth.md
learn-local/.agents/skills/redis-security/references/network.md
learn-local/.agents/skills/redis-security/SKILL.md
learn-local/.agents/skills/redis-semantic-cache/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-semantic-cache/references/best-practices.md
learn-local/.agents/skills/redis-semantic-cache/references/langcache-usage.md
learn-local/.agents/skills/redis-semantic-cache/SKILL.md
learn-local/.agents/skills/redis-vector-search/.cursor-plugin/plugin.json
learn-local/.agents/skills/redis-vector-search/references/algorithm-choice.md
learn-local/.agents/skills/redis-vector-search/references/hybrid-search.md
learn-local/.agents/skills/redis-vector-search/references/index-creation.md
learn-local/.agents/skills/redis-vector-search/references/rag-pattern.md
learn-local/.agents/skills/redis-vector-search/SKILL.md
[34 more files omitted for size]
```

### Dependencies

- learn-local/package.json: @anthropic-ai/sdk@^0.105.0, @deepgram/sdk@^5.4.0, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.2.9, framer-motion@^12.40.0, lucide-react@^1.21.0, next@16.2.9, react@19.2.4, react-dom@19.2.4, redis@^6.0.0, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- Final Version for Demo
- Intgrate the claude api
- Initial commit

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

### learn-local/CLAUDE.md

```markdown
@AGENTS.md

```

### learn-local/AGENTS.md

```markdown
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

```

### learn-local/package.json

```
{
  "name": "learn-local",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@deepgram/sdk": "^5.4.0",
    "framer-motion": "^12.40.0",
    "lucide-react": "^1.21.0",
    "next": "16.2.9",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "redis": "^6.0.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.9",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### learn-local/src/app/layout.tsx

```typescript
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'LearnLocal – Discover Where Your Food Comes From',
  description:
    'Scan or select a food item and receive a personalized lesson about where it comes from, its environmental impact, and one local action you can take today.',
  keywords: 'local food, sustainability, farm to table, food education, community, kids learning',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
      </head>
      <body className="antialiased">
        {children}
      </body>
    </html>
  );
}

```

### learn-local/src/app/page.tsx

```typescript
'use client';

import { useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import NavBar from '@/components/NavBar';
import HomeVoiceInput from '@/components/HomeVoiceInput';
import {
  FOOD_ITEMS,
  LEARNING_STYLES,
  STYLE_LABEL,
  toApiLanguage,
  saveProfileToRedis,
  VOICE_TRANSCRIPT_KEY,
  type AgeMode,
  type LangMode,
  type LearningStyle,
} from '@/lib/constants';
import { ChevronRight, Sparkles, ArrowLeft } from 'lucide-react';

const ageModes: { id: AgeMode; emoji: string; color: string; border: string; activeBg: string }[] = [
  { id: 'kid',     emoji: '🧒', color: 'text-pink-600',   border: 'border-pink-200',   activeBg: 'linear-gradient(145deg,#fff0f5,#ffd6e7)' },
  { id: 'student', emoji: '🎒', color: 'text-violet-600', border: 'border-violet-200', activeBg: 'linear-gradient(145deg,#f5f0ff,#e4d4ff)' },
  { id: 'adult',   emoji: '🧑', color: 'text-teal-600',   border: 'border-teal-200',   activeBg: 'linear-gradient(145deg,#f0fffe,#ccfbf1)' },
];

const languages: { id: LangMode; flag: string; label: string; native: string }[] = [
  { id: 'en', flag: '🇺🇸', label: 'English', native: 'English' },
  { id: 'es', flag: '🇲🇽', label: 'Spanish', native: 'Español' },
];

const copy = {
  en: {
    step0Title: 'Who is learning today?',
    step0Sub: 'We personalize every lesson to your learner type.',
    step1Title: 'Choose your language',
    step1Sub: 'Your lesson and voice will match this language.',
    step2Title: 'How do you like to learn?',
    step2Sub: 'Pick a style — story, visual, or quick facts.',
    step3Title: 'What food did you find today?',
    step3Sub: 'Tap a food or hold the mic to ask.',
    startBtn: 'Start My Lesson',
    loading: 'Starting your lesson…',
    back: 'Back',
    badge: 'Discover Your Food\'s Story',
    heroTitle: ['Every bite tells', 'a local story.'],
    heroSub: 'Learn where food comes from, understand its impact, and connect with your community — one lesson at a time. 🌾',
  },
  es: {
    step0Title: '¿Quién aprende hoy?',
    step0Sub: 'Personalizamos cada lección según tu tipo de aprendiz.',
    step1Title: 'Elige tu idioma',
    step1Sub: 'Tu lección y voz coincidirán con este idioma.',
    step2Title: '¿Cómo te gusta aprender?',
    step2Sub: 'Elige un estilo — historia, visual o datos rápidos.',
    step3Title: '¿Qué alimento encontraste hoy?',
    step3Sub: 'Toca un alimento o mantén el micrófono para preguntar.',
    startBtn: 'Comenzar mi lección',
    loading: 'Iniciando tu lección…',
    back: 'Atrás',
    badge: 'Descubre la historia de tu alimento',
    heroTitle: ['Cada bocado cuenta', 'una historia local.'],
    heroSub: 'Aprende de dónde viene la comida, entiende su impacto y conéctate con tu comunidad — una lección a la vez. 🌾',
  },
};

const ageLabelMap: Record<LangMode, Record<AgeMode, string>> = {
  en: { kid: 'Kid', student: 'Student', adult: 'Adult' },
  es: { kid: 'Niño/a', student: 'Estudiante', adult: 'Adulto/a' },
};

function StepDots({ current, total }: { current: number; total: number }) {
  return (
    <div className="flex items-center justify-center gap-2 mb-6">
      {Array.from({ length: total }).map((_, i) => (
        <div
          key={i}
          style={{
            width: i === current ? 24 : 8,
            height: 8,
            borderRadius: 4,
            background: i === current
              ? 'linear-gradient(90deg,#f97316,#f43f5e)'
              : i < current ? '#fb923c' : '#e7e5e4',
            transition: 'all 0.3s ease',
          }}
        />
      ))}
    </div>
  );
}

export default function HomePage() {
  const router = useRouter();

  const [step, setStep]               = useState<0 | 1 | 2 | 3>(0);
  const [ageMode, setAgeMode]         = useState<AgeMode | null>(null);
  const [lang, setLang]               = useState<LangMode>('en');
  const [learningStyle, setLearningStyle] = useState<LearningStyle>('story');
  const [food, setFood]               = useState<string | null>(null);
  const [loading, setLoading]         = useState(false);
  const [voiceError, setVoiceError]   = useState('');

  const t = copy[lang];
  const apiLanguage = toApiLanguage(lang);

  const persistProfile = useCallback(async (style: LearningStyle) => {
    if (!ageMode) return;
    await saveProfileToRedis({
      learnerType:   ageMode,
      language:      apiLanguage,
      learningStyle: style,
    });
  }, [ageMode, apiLanguage]);

  function navigateToLesson(selectedFood: string, fromVoice = false, transcript?: string) {
    if (!ageMode) return;
    if (fromVoice && transcript) {
      sessionStorage.setItem(VOICE_TRANSCRIPT_KEY, transcript);
    }
    const params = new URLSearchParams({
      food: selectedFood,
      age: ageMode,
      lang,
      style: learningStyle,
      ...(fromVoice ? { fromVoice: '1' } : {}),
    });
    router.push(`/lesson?${params.toString()}`);
  }

  function handleLearn() {
    if (!food || !ageMode) return;
    setLoading(true);
    persistProfile(learningStyle);
    setTimeout(() => navigateToLesson(food), 600);
  }

  function handleVoiceFood(foodId: string, transcript: string) {
    if (!ageMode) return;
    setFood(foodId);
    setVoiceError('');
    setLoading(true);
    persistProfile(learningStyle);
    setTimeout(() => navigateToLesson(foodId, true, transcript), 600);
  }

  return (
    <div className="min-h-screen" style={{ background: 'linear-gradient(160deg,#fffbf5 0%,#fff7ed 50%,#fef3c7 100%)' }}>
      <NavBar />

      <section className="max-w-2xl mx-auto px-4 pt-10 pb-4 text-center">
        <div
          className="inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-semibold mb-4"
          style={{ background: '#fff3e0', border: '1px solid #fed7aa', color: '#c2410c' }}
        >
          <Sparkles size={14} />
          {t.badge}
        </div>
        <h1 className="text-4xl sm:text-5xl font-bold text-stone-900 leading-tight mb-3" style={{ fontFamily: 'Outfit, sans-serif' }}>
          {t.he
[truncated — 9743 more characters]
```

### learn-local/src/app/lesson/page.tsx

```typescript
'use client';

import { Suspense, useEffect, useState, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import NavBar from '@/components/NavBar';
import VoiceLesson, { type LessonResult } from '@/components/VoiceLesson';
import { VOICE_TRANSCRIPT_KEY, type LearningStyle } from '@/lib/constants';
import {
  ArrowLeft, RefreshCw, MapPin, Leaf, Users, Zap, Star,
  ChevronRight, BookOpen, Sparkles,
} from 'lucide-react';

// ── Types ────────────────────────────────────────────────────────────────────
type Status      = 'idle' | 'loading' | 'success' | 'error';
type SaveStatus  = 'idle' | 'saving'  | 'saved'   | 'failed';

// ── Static maps ───────────────────────────────────────────────────────────────
const foodMeta: Record<string, { emoji: string; label: string; esLabel: string; accent: string; lightBg: string }> = {
  strawberry: { emoji: '🍓', label: 'Strawberry', esLabel: 'Fresa',    accent: '#e11d48', lightBg: '#fff1f2' },
  apple:      { emoji: '🍎', label: 'Apple',      esLabel: 'Manzana',  accent: '#dc2626', lightBg: '#fef2f2' },
  tomato:     { emoji: '🍅', label: 'Tomato',     esLabel: 'Jitomate', accent: '#ea580c', lightBg: '#fff7ed' },
  lettuce:    { emoji: '🥬', label: 'Lettuce',    esLabel: 'Lechuga',  accent: '#16a34a', lightBg: '#f0fdf4' },
};

const ageMeta: Record<string, { emoji: string; en: string; es: string }> = {
  kid:     { emoji: '🧒', en: 'Kid',      es: 'Niño/a'     },
  student: { emoji: '🎒', en: 'Student',  es: 'Estudiante' },
  adult:   { emoji: '🧑', en: 'Adult',    es: 'Adulto/a'   },
};

const langMeta: Record<string, { flag: string; label: string }> = {
  en: { flag: '🇺🇸', label: 'English' },
  es: { flag: '🇲🇽', label: 'Español' },
};

const cards = [
  {
    key: 'foodStory' as const,
    icon: MapPin,
    color: '#f97316',
    bg: '#fff7ed',
    border: '#fed7aa',
    en: { title: 'Food Story',   sub: 'Where it comes from'           },
    es: { title: 'Historia',     sub: 'De dónde viene'                },
  },
  {
    key: 'learn' as const,
    icon: BookOpen,
    color: '#8b5cf6',
    bg: '#f5f3ff',
    border: '#ddd6fe',
    en: { title: 'Learn',        sub: 'Key concept'                   },
    es: { title: 'Aprende',      sub: 'Concepto clave'                },
  },
  {
    key: 'impact' as const,
    icon: Leaf,
    color: '#10b981',
    bg: '#ecfdf5',
    border: '#a7f3d0',
    en: { title: 'Impact',       sub: 'Environmental & community'     },
    es: { title: 'Impacto',      sub: 'Medio ambiente y comunidad'    },
  },
  {
    key: 'takeAction' as const,
    icon: Zap,
    color: '#f59e0b',
    bg: '#fffbeb',
    border: '#fde68a',
    en: { title: 'Take Action',  sub: 'Something you can do today'   },
    es: { title: 'Actúa',        sub: 'Algo que puedes hacer hoy'    },
  },
  {
    key: 'funFact' as const,
    icon: Star,
    color: '#ec4899',
    bg: '#fdf2f8',
    border: '#fbcfe8',
    en: { title: 'Fun Fact',     sub: 'Did you know?'                 },
    es: { title: 'Dato curioso', sub: '¿Sabías que…?'                 },
  },
];

// ── Loading skeleton ──────────────────────────────────────────────────────────
function LoadingSkeleton() {
  return (
    <div className="space-y-4 animate-pulse">
      {[...Array(5)].map((_, i) => (
        <div
          key={i}
          className="rounded-2xl p-5"
          style={{ background: '#f5f5f4', border: '1px solid #e7e5e4' }}
        >
          <div className="flex items-center gap-3 mb-3">
            <div className="w-9 h-9 rounded-xl bg-stone-200" />
            <div className="space-y-1.5">
              <div className="h-3.5 w-24 rounded bg-stone-200" />
              <div className="h-2.5 w-16 rounded bg-stone-100" />
            </div>
          </div>
          <div className="space-y-2">
            <div className="h-3 w-full rounded bg-stone-200" />
            <div className="h-3 w-5/6 rounded bg-stone-200" />
            <div className="h-3 w-4/6 rounded bg-stone-200" />
          </div>
        </div>
      ))}
    </div>
  );
}

// ── Lesson card ───────────────────────────────────────────────────────────────
function LessonCard({
  cardDef,
  text,
  lang,
  visible,
  delay,
}: {
  cardDef: typeof cards[number];
  text: string;
  lang: string;
  visible: boolean;
  delay: number;
}) {
  const { icon: Icon, color, bg, border } = cardDef;
  const lbl = lang === 'es' ? cardDef.es : cardDef.en;

  return (
    <div
      className="rounded-2xl p-5 transition-all duration-500"
      style={{
        background: bg,
        border: `1.5px solid ${border}`,
        opacity: visible ? 1 : 0,
        transform: visible ? 'translateY(0)' : 'translateY(16px)',
        transitionDelay: `${delay}ms`,
      }}
    >
      <div className="flex items-center gap-3 mb-3">
        <div
          className="w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0"
          style={{ background: `${color}18` }}
        >
          <Icon size={18} style={{ color }} />
        </div>
        <div>
          <p className="text-sm font-bold text-stone-800" style={{ fontFamily: 'Outfit, sans-serif' }}>
            {lbl.title}
          </p>
          <p className="text-xs text-stone-400">{lbl.sub}</p>
        </div>
      </div>
      <p className="text-sm text-stone-700 leading-relaxed">{text}</p>
    </div>
  );
}

// ── Main lesson content ───────────────────────────────────────────────────────
function LessonContent() {
  const searchParams = useSearchParams();
  const food        = searchParams.get('food')  || 'strawberry';
  const ageMode     = searchParams.get('age')   || 'student';
  const lang        = searchParams.get('lang')  || 'en';
  const style       = (searchParams.get('style') || 'story') as LearningStyle;
  const fromVoice   = searchParams.get('fromVoice') === '1';

  const [status,     setStatus]     = useState<Status>(fromVoice ? 'loading' : 'idle');
  const [lesson,     setLesson]     = useState<Lesso
[truncated — 12492 more characters]
```

### learn-local/src/app/api/redis-test/route.ts

```typescript
import { rawSet, rawGet, isRedisConnected } from '@/lib/redis';

export async function GET(): Promise<Response> {
  const connected = await isRedisConnected();
  if (!connected) {
    return Response.json({
      success: false,
      error: 'Redis is not connected. Check REDIS_HOST, REDIS_PORT, REDIS_USERNAME, and REDIS_PASSWORD in .env.local',
    });
  }

  try {
    await rawSet('foo', 'bar');
    const value = await rawGet('foo');

    return Response.json({
      success: true,
      value,
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : 'Unknown error';
    return Response.json({
      success: false,
      error: msg,
    });
  }
}

```

### learn-local/src/app/api/save-profile/route.ts

```typescript
import { saveProfile, type UserProfile } from '@/lib/redis';

export async function POST(request: Request): Promise<Response> {
  let body: Partial<UserProfile>;
  try {
    body = await request.json();
  } catch {
    return Response.json({ error: 'Invalid JSON' }, { status: 400 });
  }

  const { learnerType, language, learningStyle } = body;
  if (!learnerType || !language || !learningStyle) {
    return Response.json(
      { error: 'Missing required fields: learnerType, language, learningStyle' },
      { status: 400 }
    );
  }

  try {
    await saveProfile({ learnerType, language, learningStyle });
    return Response.json({ saved: true });
  } catch (err) {
    const msg = err instanceof Error ? err.message : 'Unknown error';
    console.error('[save-profile] Redis error:', msg);
    // Non-fatal — lesson generation continues even without Redis
    return Response.json({ saved: false, warning: msg });
  }
}

```

### learn-local/src/app/api/transcribe/route.ts

```typescript
import { transcribeAudio, type LearnerLanguage } from '@/lib/deepgram';

export async function POST(request: Request): Promise<Response> {
  const apiKey = process.env.DEEPGRAM_API_KEY;
  if (!apiKey || apiKey === 'your-deepgram-api-key-here') {
    return Response.json(
      { error: 'DEEPGRAM_API_KEY is not configured in .env.local' },
      { status: 500 },
    );
  }

  try {
    const formData = await request.formData();
    const audio = formData.get('audio');
    const language = (formData.get('language') as LearnerLanguage) || 'English';

    if (!(audio instanceof Blob) || audio.size === 0) {
      return Response.json({ error: 'No audio provided' }, { status: 400 });
    }

    const buffer = Buffer.from(await audio.arrayBuffer());
    const mimeType = audio.type || 'audio/webm';

    const { text } = await transcribeAudio(buffer, language, mimeType);

    return Response.json({ text });
  } catch (err) {
    const msg = err instanceof Error ? err.message : 'Transcription failed';
    console.error('[transcribe]', msg);
    return Response.json({ error: msg }, { status: 502 });
  }
}

```

### learn-local/src/app/api/speak/route.ts

```typescript
import { speakLesson, type LearnerLanguage, type LearnerType } from '@/lib/deepgram';

interface SpeakBody {
  text:         string;
  language:     LearnerLanguage;
  learnerType:  LearnerType;
}

export async function POST(request: Request): Promise<Response> {
  const apiKey = process.env.DEEPGRAM_API_KEY;
  if (!apiKey || apiKey === 'your-deepgram-api-key-here') {
    return Response.json(
      { error: 'DEEPGRAM_API_KEY is not configured in .env.local' },
      { status: 500 },
    );
  }

  let body: Partial<SpeakBody>;
  try {
    body = await request.json();
  } catch {
    return Response.json({ error: 'Invalid JSON' }, { status: 400 });
  }

  const { text, language, learnerType } = body;
  if (!text || !language || !learnerType) {
    return Response.json(
      { error: 'Missing required fields: text, language, learnerType' },
      { status: 400 },
    );
  }

  try {
    const audioBuffer = await speakLesson({ text, language, learnerType });

    return new Response(audioBuffer, {
      headers: {
        'Content-Type':  'audio/wav',
        'Cache-Control': 'no-store',
      },
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : 'Speech synthesis failed';
    console.error('[speak]', msg);
    return Response.json({ error: msg }, { status: 502 });
  }
}

```

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