# Project export: Funlingo

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: Cal Hacks 11.0
- Tagline: Funlingo taps into the power of voice technology to transform language learning into an interactive and immersive adventure.
- Devpost: https://devpost.com/software/funlingo
- GitHub: https://github.com/andouu/calhacks
- Demo: https://funlingo.courses/
- Video: https://www.youtube.com/embed/GKcLLK4TQSs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Andrew Zhou (16 commits)

## Devpost submission (written by the team)

### Inspiration

Our innovative platform combines the power of conversational AI with the fun of interactive games, transforming traditional language learning into an engaging and playful experience. With a vibrant, child-friendly interface and exciting games like 'Fruit Slash' for vocabulary building and a math challenge that lets kids solve basic arithmetic problems in Spanish, we make learning feel like play. The magic lies in our use of voice technology.

### What it does

Kids respond to challenges by speaking, and our integration with Gladia’s speech-to-text engine ensures that their responses are instantly processed and validated. This conversation-based approach allows young learners to practice speaking Spanish in a natural and fun way, making language learning more interactive than ever. We believe that the best learning happens when you're having fun, and we’re changing the way kids learn languages—one conversation at a time.

### How we built it

• Design & Prototyping: Used Figma to create a fun and interactive user interface, focusing on a kid-friendly design. • Frontend Development: Built the frontend using React and Next.js, implementing responsive components and game interfaces. • Speech-to-Text Integration: Integrated Gladia for live audio processing, converting speech into text in real time. • Text Processing Logic: Implemented simpdjson to handle JSON queries and manage data from the speech-to-text responses. • Game Functionality: Developed game UIs that utilize the processed text to validate responses and provide feedback within interactive games.

### Challenges we ran into

• Limited Time and Expertise: Couldn't fine-tune voice models to the desired accuracy due to time constraints and lack of expertise. • Latency Issues: Experienced delays with API calls between the speech-to-text service and the conversational agent, affecting response times. • Poor Internet Connectivity: Encountered frequent connectivity issues throughout the hackathon, costing valuable development time. • Limited Workspace: Faced difficulties collaborating effectively due to cramped working conditions and lack of sufficient space.

### Accomplishments we're proud of

• Team Collaboration: Successfully worked together despite various struggles, demonstrating strong teamwork and resilience. • Rapid Prototyping: Quickly tested different voice software to validate the idea and identify a feasible solution. • UI Design and User Experience: Created a friendly, appealing, and fun interface that makes learning enjoyable for kids. • Clean and Interactive Interface: Developed a clean, user-friendly interface that enhances the learning experience through engaging interactions.

### What we learned

• Real time streaming ain't easy, if you want low latency! We promise awesome latency streaming!

### What's next

• Character-Based Conversation Agents: Train voice models to incorporate popular TV and movie characters, allowing kids to learn from their favorite personalities and making the experience more engaging. • Personalized Pronunciation Tutor: Develop a conversation agent that listens to kids’ pronunciation and provides real-time feedback, acting as a personalized tutor to help them improve their speaking skills. • Research-Backed Educational Content: Collaborate with educators and researchers to design games that are not only fun but also scientifically proven to enhance language learning outcomes.

## README (from the GitHub repository)

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: 13 recognized source files, 40 KB.
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (25 of 25)

```
.eslintrc.json
.gitignore
bun.lockb
next.config.mjs
package.json
README.md
src/app/Components/UnstyledLink.module.scss
src/app/Components/UnstyledLink.tsx
src/app/Context/Microphone.tsx
src/app/globals.scss
src/app/Hooks/useMicrophone.tsx
src/app/layout.tsx
src/app/page.module.scss
src/app/page.tsx
src/app/play/countdown/Game.tsx
src/app/play/countdown/page.module.scss
src/app/play/countdown/page.tsx
src/app/play/fruitSlash/Game.tsx
src/app/play/fruitSlash/page.module.scss
src/app/play/fruitSlash/page.tsx
src/app/play/layout.tsx
src/app/play/page.module.scss
src/app/play/page.tsx
src/app/Util/Random.ts
tsconfig.json
```

### Dependencies

- package.json: @types/node@^20, @types/react@^18, @types/react-dom@^18, @types/recordrtc@^5.6.14, eslint@^8, eslint-config-next@14.2.15, framer-motion@^11.11.9, next@14.2.15, react@^18, react-dom@^18, react-icons@^5.3.0, recordrtc@^5.6.2, sass@^1.80.3, scss@^0.2.4, typescript@^5

### Recent commits (newest first)

- Add: Update favicon
- Fix: Build issues
- Add: Fruit slash
- Add: Voice to fruit slash
- Add: Better pattern matching
- Update: Voice?
- Add: Microphone control
- Tmp: Patch build errors
- Add: Layout changes
- Add: Play again
- Add: Basic screen for fruit slash
- Add: More stuff to countdown
- Add: buttons
- Add: Menu
- Bootstrap
- Initial commit from Create Next App

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

### package.json

```
{
  "name": "calhacks",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/recordrtc": "^5.6.14",
    "framer-motion": "^11.11.9",
    "next": "14.2.15",
    "react": "^18",
    "react-dom": "^18",
    "react-icons": "^5.3.0",
    "recordrtc": "^5.6.2",
    "sass": "^1.80.3",
    "scss": "^0.2.4"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Pangolin } from "next/font/google";
import "./globals.scss";
import Link from "next/link";

const pangolin = Pangolin({
  subsets: ["latin"],
  weight: ["400"],
  variable: "--font-pangolin",
});

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${pangolin.className} ${pangolin.variable}`}>
        <div className="header">
          <Link href="/">
            <img className="logo" src="/logo.png" />
          </Link>
        </div>
        {children}
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
"use client";
import Link from "next/link";
import styles from "./page.module.scss";
import { motion } from "framer-motion";

export default function Home() {
  return (
    <div className={styles.wrapper}>
      <div className={styles.content}>
        <motion.span
          className={styles.big}
          initial={{ filter: "blur(5px)", scale: 2, opacity: 0 }}
          animate={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
          transition={{
            delay: 0.75,
            type: "spring",
            stiffness: 80,
            damping: 12.5,
          }}
        >
          Talking has never been so fun.
        </motion.span>
        <motion.span
          className={styles.small}
          initial={{ filter: "blur(5px)", scale: 2, opacity: 0 }}
          animate={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
          transition={{
            delay: 0.9,
            type: "spring",
            stiffness: 80,
            damping: 12.5,
          }}
        >
          Play vocal games that effectively teach you new languages, without
          being
          <br />
          boring, boring, boring.
        </motion.span>
        <Link href="/play">
          <motion.button
            className={`${styles.action} outlined`}
            initial={{ filter: "blur(5px)", scale: 0, opacity: 0 }}
            animate={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
            transition={{
              delay: 1.25,
              type: "spring",
              stiffness: 80,
              damping: 12.5,
            }}
          >
            Play Now!
          </motion.button>
        </Link>
      </div>
      <motion.div
        className={styles.ballWrapper}
        initial={{ y: "120vh", rotate: 90 }}
        animate={{
          y: "70vh",
          rotate: 0,
        }}
        transition={{ type: "spring", stiffness: 80, damping: 10 }}
      >
        <div className={styles.ball}>
          <img className={styles.face} src="/face.svg" />
        </div>
      </motion.div>
    </div>
  );
}

```

### src/app/play/layout.tsx

```typescript
import dynamic from "next/dynamic";
import { ReactNode } from "react";
const MicrophoneProvider = dynamic(() => import("../Context/Microphone"), {
  ssr: false,
});

export default function PlayLayout({ children }: { children: ReactNode }) {
  return <MicrophoneProvider>{children}</MicrophoneProvider>;
}

```

### src/app/play/page.tsx

```typescript
"use client";
import { motion } from "framer-motion";
import styles from "./page.module.scss";
import { useState } from "react";
import { UnstyledLink } from "../Components/UnstyledLink";

export default function Play() {
  const [difficulty, setDifficulty] = useState<
    "beginner" | "intermediate" | "expert"
  >();
  const [hovered, setHovered] = useState<string | null>(null);

  return (
    <div className={styles.wrapper}>
      <div className={styles.language}>🇪🇸 Spanish</div>
      <div className={styles.content}>
        <span className={styles.big}>Choose your game mode!</span>
        <div className={styles.difficultySelect}>
          <button
            className={styles.difficulty}
            style={{
              boxShadow:
                difficulty === "beginner"
                  ? "0 0 20px 0 var(--green)"
                  : undefined,
            }}
            onClick={() => setDifficulty("beginner")}
          >
            <div
              className={styles.face}
              style={{
                boxShadow: "0 0 2.5px 5px var(--green)",
                backgroundColor: "var(--green)",
              }}
            >
              <img src="/face.svg" />
            </div>
            <span>Beginner</span>
          </button>
          <button
            className={styles.difficulty}
            style={{
              boxShadow:
                difficulty === "intermediate"
                  ? "0 0 20px 0 var(--yellow)"
                  : undefined,
            }}
            onClick={() => setDifficulty("intermediate")}
          >
            <div
              className={styles.face}
              style={{
                boxShadow: "0 0 2.5px 5px var(--yellow)",
                backgroundColor: "var(--yellow)",
              }}
            >
              <img src="/face_small_neutral.svg" />
            </div>
            <span>Intermediate</span>
          </button>
          <button
            className={styles.difficulty}
            style={{
              boxShadow:
                difficulty === "expert" ? "0 0 20px 0 var(--red)" : undefined,
            }}
            onClick={() => setDifficulty("expert")}
          >
            <div
              className={styles.face}
              style={{
                boxShadow: "0 0 2.5px 5px var(--red)",
                backgroundColor: "var(--red)",
              }}
            >
              <img src="/face_small_frown.svg" />
            </div>
            <span>Expert</span>
          </button>
        </div>
        {difficulty && (
          <motion.div
            className={styles.gameSelect}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
          >
            <UnstyledLink href="/play/countdown">
              <motion.div
                className={styles.game}
                initial={{ scale: 0, opacity: 0 }}
                animate={{ scale: 1, opacity: 1 }}
                transition={{ type: "spring" }}
                onHoverStart={() => setHovered("countdown")}
                onHoverEnd={() => setHovered(null)}
              >
                <span className={styles.icon}>💣</span>
                <span className={styles.title}>Countdown</span>
                <span className={styles.description}>
                  Learn the basics of numbers by solving math equations as fast
                  as possible.
                </span>
              </motion.div>
            </UnstyledLink>
            <UnstyledLink href="/play/fruitSlash">
              <motion.div
                className={styles.game}
                initial={{ scale: 0, opacity: 0 }}
                animate={{ scale: 1, opacity: 1 }}
                transition={{ delay: 0.25, type: "spring" }}
                onHoverStart={() => setHovered("fruitSlash")}
                onHoverEnd={() => setHovered(null)}
              >
                <span className={styles.icon}>🍊</span>
                <span className={styles.title}>Fruit Slash</span>
                <span className={styles.description}>
                  Learn about fruits from a classic slashing game where fruits
                  are your mortal enemy.
                </span>
              </motion.div>
            </UnstyledLink>
          </motion.div>
        )}
      </div>
      <motion.div
        className={styles.ballWrapper}
        initial={{ y: "120vh", rotate: 90 }}
        animate={{
          y: "70vh",
          rotate:
            hovered === "countdown" ? -10 : hovered === "fruitSlash" ? 10 : 0,
        }}
        transition={{ type: "spring", stiffness: 80, damping: 10 }}
      >
        <div className={styles.ball}>
          <img className={styles.face} src="/face.svg" />
        </div>
      </motion.div>
    </div>
  );
}

```

### src/app/play/countdown/page.tsx

```typescript
"use client";
import { useState } from "react";
import styles from "./page.module.scss";
import { AnimatePresence, motion } from "framer-motion";
import dynamic from "next/dynamic";
const Game = dynamic(() => import("./Game"), { ssr: false });

const Help = ({ startGame }: { startGame: () => void }) => {
  return (
    <motion.div className={styles.wrapper} exit={{ opacity: 0 }}>
      <motion.span
        className={styles.title}
        initial={{ scale: 0, opacity: 0, filter: "blur(5px)" }}
        animate={{ scale: 1, opacity: 1, filter: "blur(0px)" }}
        transition={{
          duration: 0.15,
          type: "spring",
          stiffness: 120,
          damping: 15,
        }}
      >
        How To Play
      </motion.span>
      <div className={styles.cards}>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.35 }}
        >
          <img src="/equation.svg" />
          <p>
            In this game mode, you will be given a series of math problems.
            You&apos;ll answer by speaking.
          </p>
        </motion.div>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.5 }}
        >
          <img src="/hearts.svg" />
          <p>You have 3 lives, which you lose by not answering in time.</p>
        </motion.div>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.65 }}
        >
          <img src="/difficulty.png" />
          <p>
            The game gets harder as you progress. Get as high of a score as you
            can!
          </p>
        </motion.div>
      </div>
      <motion.button
        className={styles.play}
        initial={{ scale: 0, opacity: 0 }}
        animate={{ scale: 1, opacity: 1 }}
        transition={{ delay: 1 }}
        onClick={startGame}
      >
        Let&apos;s Play!
      </motion.button>
    </motion.div>
  );
};

export default function Countdown() {
  const [showHelp, setShowHelp] = useState<boolean>(true);

  return (
    <AnimatePresence mode="wait">
      {showHelp ? (
        <Help key="help" startGame={() => setShowHelp(false)} />
      ) : (
        <Game key="game" />
      )}
    </AnimatePresence>
  );
}

```

### src/app/play/fruitSlash/page.tsx

```typescript
"use client";
import { AnimatePresence, motion } from "framer-motion";
import styles from "./page.module.scss";
import { useState } from "react";
import dynamic from "next/dynamic";
const Game = dynamic(() => import("./Game"), { ssr: false });

interface HelpProps {
  startGame: () => void;
}

const Help = ({ startGame }: HelpProps) => {
  return (
    <motion.div className={styles.wrapper} exit={{ opacity: 0 }}>
      <motion.span
        className={styles.title}
        initial={{ scale: 0, opacity: 0, filter: "blur(5px)" }}
        animate={{ scale: 1, opacity: 1, filter: "blur(0px)" }}
        transition={{
          duration: 0.15,
          type: "spring",
          stiffness: 120,
          damping: 15,
        }}
      >
        How To Play
      </motion.span>
      <div className={styles.cards}>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.35 }}
        >
          <img src="/fruits.png" />
          <p>
            Objects will fall from the top. You must say their names before they
            reach the bottom.
          </p>
        </motion.div>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.5 }}
        >
          <img src="/hearts.svg" />
          <p>You have 3 lives, which you lose by not answering in time.</p>
        </motion.div>
        <motion.div
          className={styles.card}
          initial={{ y: 30, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          transition={{ delay: 0.65 }}
        >
          <img src="/difficulty.png" />
          <p>
            The game gets harder as you progress. Get as high of a score as you
            can!
          </p>
        </motion.div>
      </div>
      <motion.button
        className={styles.play}
        initial={{ scale: 0, opacity: 0 }}
        animate={{ scale: 1, opacity: 1 }}
        transition={{ delay: 1 }}
        onClick={startGame}
      >
        Let&apos;s Play!
      </motion.button>
    </motion.div>
  );
};

export default function FruitSlash() {
  const [showHelp, setShowHelp] = useState<boolean>(true);

  return (
    <AnimatePresence>
      {showHelp ? (
        <Help key="help" startGame={() => setShowHelp(false)} />
      ) : (
        <Game key="game" />
      )}
    </AnimatePresence>
  );
}

```

### src/app/Util/Random.ts

```typescript
export class Random {
  static getRandomInt(min: number, max: number) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}

```

### src/app/Components/UnstyledLink.tsx

```typescript
import Link from "next/link";
import { ComponentProps } from "react";
import styles from "./UnstyledLink.module.scss";

export const UnstyledLink = (props: ComponentProps<typeof Link>) => (
  <Link {...props} className={styles.wrapper} />
);

```

### src/app/Hooks/useMicrophone.tsx

```typescript
"use client";
import { useContext } from "react";
import { MicrophoneContext } from "../Context/Microphone";

export const useMicrophone = () => {
  const value = useContext(MicrophoneContext);
  if (!value) {
    throw new Error("useMicrophone must be used within a MicrophoneProvider");
  }
  return value;
};

```

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