# Project export: skog

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: Leveraging empathic voice AI for panic attack support
- Devpost: https://devpost.com/software/skog
- GitHub: https://github.com/jasukej/calhacks
- Demo: http://calhacks-jasukej-kezs-projects.vercel.app/
- Team: 3 GitHub contributor(s) — Kezia (27 commits), Owen Cooke (3 commits), Allan Tan (3 commits)

## Devpost submission (written by the team)

### Overview

Video Demo: Watch here

### Inspiration

💡 Panic attacks and chronic anxiety affect millions of people every year, often catching them in moments when help is out of reach. Skog is designed to fill that gap, providing on-demand support to those who need it most. Whether it’s guiding users through calming exercises, contacting an emergency contact, or helping them uncover patterns in their anxiety triggers, Skog empowers individuals with immediate relief and long-term tools for managing their mental health. Skog isn’t just an app—it’s a lifeline for moments when you feel most alone.

### What it does

🧠 Skog offers real-time, AI-driven support during panic attacks and anxiety episodes. It combines voice-assisted grounding techniques, emotional analysis, and automated emergency contact alerts to give users the tools they need to regain control. Key features include: Breathe: A soothing, voice-guided breathing exercise enhanced with visual cues, using Cartesia API for calming narration. 3 Senses: A grounding exercise where users are guided to focus on their environment—what they see, hear, and feel—facilitated by real-time feedback from a voice assistant powered by VAPI. Talk with Skog: An empathetic AI agent provides emotional support, with responses adapted through real-time analysis of the user’s emotional state via Hume EVI. Anxiety Trigger Detection: Skog identifies recurring anxiety triggers, offering users personalized coping strategies and insights for long-term management. Community Forum: A supportive space where users can share their stories, connect, and learn from each other’s experiences.

### How we built it

🛠️ Frontend: Next.js and React for creating a smooth and intuitive user interface. TailwindCSS and Shadcn for sleek, adaptable styling. Framer Motion to ensure an engaging, visually-pleasing experience. Backend: Firebase for secure data management and user authentication. Twilio for sending SMS notifications to emergency contacts during critical moments. Cartesia API for real-time voice narration that guides users through calming exercises. VAPI for interactive, real-time feedback during grounding exercises. Hume EVI to detect user emotions and personalize responses based on their current emotional state. OpenAI and Google Search API to analyze anxiety triggers and recommend reliable coping strategies.

### Challenges we ran into

🤔 Building a seamless user experience without a dedicated designer or Figma mockups. Integrating real-time WebSockets for communication was more complex than anticipated, leading to delays. Spent too much time dilly-dallying in SF (but who doesn't?)

### Accomplishments we're proud of

💪 Successfully delivering a fully functional app that offers real-time support during panic attacks. Seamlessly integrating multiple APIs to create a cohesive, responsive user experience.

### What we learned

🌟 Project Scoping: We gained valuable insights into defining achievable goals within short timeframes. API Integration: We mastered connecting WebSockets and handling complex emotion-detection APIs. Debugging in Next.js: Thanks to our mentor, we can now say goodbye to endless NextJS console logs.

### What's next

for Skog 🚀 Refined Narration: We plan to enhance the app's voice interaction to make it more personalized and compassionate. Professional Contact Integration: Skog complements therapy but isn’t a replacement. Our next step is integrating trusted professional contacts for more structured long-term care. Long-Term Tracking: We’ll expand Skog’s ability to track anxiety patterns over time, offering deeper insights for better mental health management.

## README (from the GitHub repository)

# **skog**

Automating panic attack first aid and trigger diagnosis with empathetic voice AI.

## 🚀 Demo

[Watch the demo](https://www.loom.com/share/c11ca1d0aef6483d9194c843a62fa4a2?sid=7c2e6dae-385d-4c59-b965-578395c6f3f6)

## 📸 Project Screenshots

<img src="https://d112y698adiu2z.cloudfront.net/photos/production/software_photos/003/093/360/datas/original.png" alt="project-screenshot" width="600">

<img src="https://d112y698adiu2z.cloudfront.net/photos/production/software_photos/003/090/942/datas/original.png" alt="project-screenshot" width="600">

<img src="https://d112y698adiu2z.cloudfront.net/photos/production/software_photos/003/091/651/datas/original.png" alt="project-screenshot" width="600">

## 🧐 Features

skog provides both **immediate support** for users experiencing panic attacks and a **comprehensive tool** to help mental health professionals diagnose and track anxiety triggers over time. Key features include:

- **Voice-Assisted Grounding Exercises**: Skog automates scientifically-backed de-escalation techniques such as breathing exercises and the 3-3-3 method, guiding users step-by-step through calming practices designed to alleviate panic attack symptoms quickly.

- **Panic Attack Trigger Detection**: Using advanced speech processing, Skog analyzes a user's verbal cues to identify potential anxiety triggers. Based on this real-time analysis, it offers personalized coping mechanisms tailored to the user’s specific situation, helping to reduce the frequency and intensity of future episodes.

- **Emergency Contact Alerts**: In critical moments, Skog automatically alerts a designated emergency contact via SMS when it detects signs of a severe panic attack, ensuring that users are not alone during high-stress situations.

- **Community Forum**: Skog provides a supportive online space where users can share their experiences, connect with others who face similar challenges, and validate their feelings. This sense of community helps users feel heard, reducing isolation and stigma associated with anxiety.

## 🛠️ Installation Steps

1. Clone the repository and install dependencies:

   ```bash
   npm install
   ```

2. Set up your API keys:

   ```bash
   echo /.env
   ```

3. Run the app in a development environment:

   ```bash
   npm run dev
   ```

## 💻 Built With

Technologies used in the project:

- **Next.js**
- **VAPI**
- **Cartesia**
- **OpenAI API**
- **Twilio**
- **Firebase**


## Detected evidence (automated analysis)

Indexed codebase: 43 recognized source files, 86 KB.
- CSS (language) — detected in the code
- Firebase (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (51 of 51)

```
.eslintrc.json
.gitignore
.vscode/launch.json
app/api/run-triggers-pipeline/route.ts
app/api/send-sms/route.ts
app/api/validate-input/route.ts
app/auth/page.tsx
app/breathe/page.tsx
app/contact/page.tsx
app/forum/page.tsx
app/globals.css
app/layout.tsx
app/menu/page.tsx
app/page.tsx
app/senses/page.tsx
app/share/page.tsx
app/summary/page.tsx
app/talk/page.tsx
components.json
components/chat/Chat.tsx
components/chat/Controls.tsx
components/chat/Expressions.tsx
components/chat/Messages.tsx
components/chat/MicFFT.tsx
components/chat/StartCall.tsx
components/HomeButton.tsx
components/Input.tsx
components/MenuButton.tsx
components/Post.tsx
components/Progress.tsx
components/ui/button.tsx
components/ui/checkbox.tsx
components/ui/input.tsx
components/ui/switch.tsx
components/ui/toggle.tsx
hooks/sendSMStoContact.ts
hooks/storeMessages.ts
lib/constants/expressionColors.ts
lib/constants/sensesAssistant.ts
lib/firebase.ts
lib/hume.ts
lib/twilio.ts
lib/utils.ts
lib/utils/gemini.ts
lib/vapi.sdk.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @cartesia/cartesia-js@^1.1.0, @google-cloud/storage@^7.13.0, @humeai/voice@^0.1.10, @humeai/voice-react@^0.1.16, @opentelemetry/api@^1.9.0, @phosphor-icons/react@^2.1.7, @radix-ui/react-checkbox@^1.1.2, @radix-ui/react-icons@^1.3.0, @radix-ui/react-slot@^1.1.0, @radix-ui/react-switch@^1.1.1, @radix-ui/react-toggle@^1.1.0, @types/node@^20, @types/react@^18, @types/react-dom@^18, @types/react-virtualized@^9.21.30, @vapi-ai/web@^2.1.4, axios@^1.7.7, class-variance-authority@^0.7.0, clsx@^2.1.1, compromise@^14.14.2, eslint@^8, eslint-config-next@14.2.15, firebase@^10.14.1, firebase-admin@^12.6.0, fluent-ffmpeg@^2.1.3, framer-motion@^11.11.9, hume@^0.9.1, lucide-react@^0.453.0, ml-kmeans@^6.0.0, next@14.2.15, openai@^4.68.1, phosphor-react@^1.4.1, postcss@^8, react@^18, react-dom@^18, react-hook-form@^7.53.1, react-loading-indicators@^1.0.0, react-spinners@^0.14.1, react-toastify@^10.0.6, react-virtualized@^9.22.5, remeda@^2.15.2, tailwind-merge@^2.5.4, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, twilio@^5.3.4, typescript@^5

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- Update README.md
- final commit
- fix vapi
- idk
- final styling + summarize flow
- minor tts config
- Merge pull request #10 from jasukej/allan-prompt-fix
- transcription is not a field
- Merge pull request #9 from jasukej/allan-prompt-fix
- removed logs
- prompt
- Merge pull request #8 from jasukej/kez
- mini change
- fixed merge conflicts
- fix: switch added
- contact alert toggle
- animations and styling

## 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": {
    "@cartesia/cartesia-js": "^1.1.0",
    "@google-cloud/storage": "^7.13.0",
    "@humeai/voice": "^0.1.10",
    "@humeai/voice-react": "^0.1.16",
    "@opentelemetry/api": "^1.9.0",
    "@phosphor-icons/react": "^2.1.7",
    "@radix-ui/react-checkbox": "^1.1.2",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.1",
    "@radix-ui/react-toggle": "^1.1.0",
    "@vapi-ai/web": "^2.1.4",
    "axios": "^1.7.7",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "compromise": "^14.14.2",
    "firebase": "^10.14.1",
    "firebase-admin": "^12.6.0",
    "fluent-ffmpeg": "^2.1.3",
    "framer-motion": "^11.11.9",
    "hume": "^0.9.1",
    "lucide-react": "^0.453.0",
    "ml-kmeans": "^6.0.0",
    "next": "14.2.15",
    "openai": "^4.68.1",
    "phosphor-react": "^1.4.1",
    "react": "^18",
    "react-dom": "^18",
    "react-hook-form": "^7.53.1",
    "react-loading-indicators": "^1.0.0",
    "react-spinners": "^0.14.1",
    "react-toastify": "^10.0.6",
    "react-virtualized": "^9.22.5",
    "remeda": "^2.15.2",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "twilio": "^5.3.4"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "@types/react-virtualized": "^9.21.30",
    "eslint": "^8",
    "eslint-config-next": "14.2.15",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import Link from "next/link";
import Image from "next/image";
import { Suspense } from "react";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

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 min-h-screen bg-gradient-to-r from-teal-100 to-blue-200`}
      >
      <header className="absolute top-4 left-4 z-10 flex flex-col space-y-4 items-center pl-1">
    {/* Skoggy Link */}
    <div className="relative flex items-center group">
      <Link href="/">
        <Image
          src="/images/skoggy.png"
          alt="Skoggy Logo"
          width={40}
          height={40}
          className="rounded-full peer hover:scale-110 transition-all duration-300"
        />
      </Link>
      <span className="absolute font-mono left-[3rem] opacity-0 scale-x-0 group-hover:scale-x-100 group-hover:opacity-100 origin-left transition-all duration-300 ml-2 text-teal-700 font-semibold">
        Home
      </span>
    </div>

    {/* Contact Link */}
    <div className="relative flex items-center group">
      <Link href="/contact">
        <Image
          src="/images/contact.png"
          alt="Contact"
          width={40}
          height={40}
          className="rounded-full peer-hover:translate-x-4 hover:scale-110 transition-all duration-300"
        />
    </Link>
    <span className="absolute font-mono left-[3rem] opacity-0 scale-x-0 group-hover:scale-x-100 group-hover:opacity-100 origin-left transition-all duration-300 ml-2 text-teal-700 font-semibold">
      Contact
    </span>
  </div>
</header>
  <Suspense fallback={<div>Loading...</div>}>
    {children}
  </Suspense>
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
"use client";

import { Button } from "@/components/ui/button";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { ArrowRight } from "lucide-react";
import { useState } from "react";
import { sendSMStoContact } from "@/hooks/sendSMStoContact";
export default function Home() {
  const router = useRouter();

  const handleButtonClick = async () => {
    router.push("/breathe");
    sendSMStoContact();
  };

  return (
    <div className="relative">
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-teal-100 to-blue-200">
      <button 
        className="relative w-64 h-64 cursor-pointer"
        onClick={handleButtonClick}>
        {/* Pulsing blurred background */}
        <div 
          className="absolute inset-0 bg-white rounded-full filter blur-xl animate-pulse"
          style={{
            animation: 'pulse 3s infinite, move 10s infinite',
          }}
        ></div>
        
        {/* Button text */}
        <div className="absolute inset-0 flex items-center justify-center">
          <span className="text-xl font-serif cursor-pointer font-bold text-teal-600 z-10 hover:scale-105 transition-all duration-300">
            skog
          </span>
        </div>
      </button>
    </div>

    <style jsx>{`
      @keyframes pulse {
        0%, 100% { transform: scale(1); opacity: 0.6; }
        50% { transform: scale(1.2); opacity: 1; }
      }
      @keyframes move {
        0%, 100% { transform: translate(0, 0); }
        25% { transform: translate(8px, 8px); }
        50% { transform: translate(0, 16px); }
        75% { transform: translate(-8px, 8px); }
      }
    `}</style>

    <div className="flex absolute bottom-0 justify-center w-full">
      <div className="font-mono pb-8 text-teal-800 text-sm font-semibold">tap to start</div>
    </div>
    <Button
      onClick={() => router.push('/share')}
      className="border-teal-950 cursor-pointer absolute bottom-8 right-8 border-[0.5px] px-4 py-2 flex items-center space-x-2 bg-transparent hover:bg-teal-700 text-teal-700 hover:text-white transition-all duration-300 ease-in-out transform hover:scale-105"
    >
      <span>i want to share</span>
      <ArrowRight className="w-4 h-4" />
    </Button>
  </div>
  );
}

```

### app/talk/page.tsx

```typescript
import MenuButton from '@/components/MenuButton';
import { getHumeAccessToken } from '@/lib/hume'
import dynamic from "next/dynamic";

const Chat = dynamic(() => import("@/components/chat/Chat"), {
  ssr: false,
});

async function TalkPage() {
  const accessToken = await getHumeAccessToken();

  if (!accessToken) {
    return <div>Error: Unable to fetch Hume access token</div>;
  }

  return (
    <div className={"grow flex flex-col"}>
      <Chat accessToken={accessToken} />
    </div>
  )
}

export default TalkPage

```

### app/forum/page.tsx

```typescript
"use client";

import { useState, useEffect } from 'react';
import { collection, getDocs } from 'firebase/firestore';
import { db } from '@/lib/firebase'; 
import { Button } from "@/components/ui/button";
import { useRouter } from 'next/navigation';
import { PlusCircle } from 'lucide-react';
import Post from '@/components/Post';
interface Post {
  id: string;
  content: string;
  createdAt: Date;
}

export default function ForumPage() {
  const [posts, setPosts] = useState<Post[]>([]);
  const router = useRouter();

  useEffect(() => {
    const fetchPosts = async () => {
      const postsCollection = collection(db, 'posts');
      const postSnapshot = await getDocs(postsCollection);
      const postList = postSnapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data(),
        createdAt: doc.data().createdAt
      })) as Post[];
      setPosts(postList.sort((a, b) => (new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())));
    };

    fetchPosts();
  }, []);

  return (
    <div className="min-h-screen bg-gradient-to-r from-cyan-100 to-blue-200 px-4 sm:px-6 lg:px-8 py-8">
      <div className="max-w-3xl mx-auto">
        <h1 className="text-3xl font-bold font-mono text-teal-800 mb-6">Community Forum</h1>
        <div className="space-y-6">
          {posts.map((post, index) => (
            <Post key={index} index={index} content={post.content} createdAt={post.createdAt} />
          ))}
        </div>
        <div className="mt-8 flex justify-end">
          <Button
            onClick={() => router.push('/share')}
            className="bg-teal-600 hover:bg-teal-700 text-white font-mono rounded-full font-bold py-2 px-6 transition duration-300 ease-in-out transform hover:scale-105 flex items-center"
          >
            <PlusCircle strokeWidth={1.2} className="w-4 h-4 mr-2" />
            <span>New Post</span>
          </Button>
        </div>
      </div>
    </div>
  );
}

```

### app/share/page.tsx

```typescript
"use client";

import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from "@/components/ui/button";
import { ArrowRight, House } from 'lucide-react';
import { getAuth } from 'firebase/auth';
import { db } from '@/lib/firebase';
import { addDoc, collection, getFirestore } from 'firebase/firestore';
import HomeButton from '@/components/HomeButton';

function SharePage() {
  const [post, setPost] = useState('');
  const router = useRouter();
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      // Assuming you have Firebase initialized and auth set up
      const auth = getAuth();
      const user = auth.currentUser;

      if (!user) {
        console.error('User not authenticated');
        return;
      }

      const db = getFirestore();
      const postsCollection = collection(db, 'posts');
      
      await addDoc(postsCollection, {
        content: post,
        userId: user.uid,
        createdAt: Date.now()
      });

      console.log('Post saved successfully');
      setPost('');
    } catch (error) {
      console.error('Error saving post:', error);
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-r from-cyan-100 to-blue-200 px-[10rem] flex flex-col items-center justify-center">
      <div className="w-full max-w-2xl bg-white p-8 rounded-lg shadow-md">
        <h1 className="text-2xl font-bold font-mono text-teal-800 mb-6">How are you feeling today?</h1>
        <form onSubmit={handleSubmit} className="space-y-6">
          <textarea
            value={post}
            onChange={(e) => setPost(e.target.value)}
            className="w-full h-40 p-4 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-teal-500"
            placeholder="Share your thoughts..."
          />
          <Button
            type="submit"
            className="bg-teal-600 hover:bg-teal-700 justify-end font-mono text-white rounded-full font-bold py-2 px-6 transition duration-300 ease-in-out transform"
          >
            POST
          </Button>
        </form>
      </div>
      <div className="flex justify-center space-x-10">
      <HomeButton />

        <Button
        onClick={() => router.push('/forum')}
        className="mt-8 mr-4 border-teal-950 cursor-pointer border-[0.5px] px-4 py-2 flex items-center space-x-2 bg-teal-700 hover:bg-teal-800 text-white transition-all duration-300 ease-in-out transform hover:scale-105"
      >
        <span className="flex items-center">
          Forum
          <ArrowRight strokeWidth={1.2} className="w-4 h-4 ml-1 hover:translate-x-[1px]" />
        </span>
      </Button>
      </div>
    </div>
  );
}

export default SharePage;

```

### app/menu/page.tsx

```typescript
'use client'

import { useState, useEffect, useRef } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { motion, AnimatePresence } from 'framer-motion'
import { ArrowRight } from 'lucide-react'

const pages = ['breathe', '3 senses', 'talk with skog']

export default function MenuPage() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const containerRef = useRef<HTMLDivElement>(null)

  const [currentPage, setCurrentPage] = useState(() => {
    const from = searchParams.get('from')
    return from ? pages.indexOf(from) : 1 
  })

  const [isScrolling, setIsScrolling] = useState(false) // To prevent multiple scroll triggers

  useEffect(() => {
    const handleWheel = (e: WheelEvent) => {
      e.preventDefault()

      if (isScrolling) return 

      if (e.deltaY > 0 && currentPage < pages.length - 1) {
        // Scroll down: move to the next menu item
        setCurrentPage(prev => prev + 1)
      } else if (e.deltaY < 0 && currentPage > 0) {
        // Scroll up: move to the previous menu item
        setCurrentPage(prev => prev - 1)
      }

      setIsScrolling(true)

      // reenable scrolling after a short delay so animation completes
      setTimeout(() => {
        setIsScrolling(false)
      }, 700) 
    }

    const container = containerRef.current
    if (container) {
      container.addEventListener('wheel', handleWheel, { passive: false })
    }

    return () => {
      if (container) {
        container.removeEventListener('wheel', handleWheel)
      }
    }
  }, [currentPage, isScrolling])

  const handleClick = (page: string) => {
    switch (page) {
      case 'breathe':
        router.push('/breathe')
        break
      case '3 senses':
        router.push('/senses')
        break
      case 'talk with skog':
        router.push('/talk')
        break
    }
  }

  return (
    <div className="relative">
      <div ref={containerRef} className="h-screen flex items-center justify-center bg-gradient-to-r from-teal-100 to-blue-200 overflow-hidden">
        <div className="relative h-full w-full flex flex-col items-center justify-center">
          {pages.map((page, index) => (
            <motion.button
              key={page}
              className="px-6 py-3 text-2xl font-mono font-semibold text-teal-800 rounded-lg bg-opacity-50 hover:bg-opacity-70 transition-all duration-300 flex items-center"
              animate={{
                y: `calc(${(index - currentPage) * 80}% + ${(index - currentPage) * 10}px)`,
                scale: index === currentPage ? 1.3 : 1,
                opacity: index === currentPage ? 1 : 0.4,
              }}
              transition={{
                y: { type: 'spring', stiffness: 400, mass: 0.4, damping: 15 },
                scale: { duration: 0.20 },
                opacity: { duration: 0.20 }
              }}
              whileHover={{ scale: index === currentPage ? 1.3 : 1.05 }}
              whileTap={{ scale: 0.95 }}
              onClick={() => handleClick(page)}
            >
              {page}
              <AnimatePresence>
                {index === currentPage && (
                  <motion.div
                    initial={{ opacity: 0, x: -10 }}
                    animate={{ opacity: 1, x: 0 }}
                    exit={{ opacity: 0, x: -10 }}
                    transition={{ duration: 0.2 }}
                    className="ml-2"
                  >
                    <ArrowRight className="w-6 h-6" />
                  </motion.div>
                )}
              </AnimatePresence>
            </motion.button>
          ))}
        </div>
      </div>
      <div className="absolute bottom-0 font-mono left-0 w-full text-center text-md text-teal-800 pb-8 font-bold">MENU</div>
    </div>
  )
}

```

### app/summary/page.tsx

```typescript
'use client'

import React, { useEffect, useState } from 'react';
import { db } from '@/lib/firebase';
import Progress from '@/components/Progress';
import { collection, query, limit, getDocs, orderBy } from 'firebase/firestore';
import { ClipLoader } from 'react-spinners';
import { CircleHelp, ExternalLink } from 'lucide-react';
import { motion } from 'framer-motion';
import HomeButton from '@/components/HomeButton';

interface SearchResult {
  trigger: string;
  summary: string;
  link: string;
}

export default function SummaryPage() {
  const [latestResult, setLatestResult] = useState<SearchResult[]>([]); 
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchLatestUserLog = async () => {
      try {
        const userLogsRef = collection(db, 'userLogs');
        const q = query(userLogsRef, orderBy('createdAt', 'desc'), limit(1));
        const querySnapshot = await getDocs(q);

        console.log(querySnapshot);
        console.log(querySnapshot.docs[0].data());

        if (!querySnapshot.empty) {
          const latestLog = querySnapshot.docs[0].data();
          if (latestLog.searchResults && latestLog.searchResults.length > 0) {
            setLatestResult(latestLog.searchResults);
          } else {
            setLatestResult([]);
          }

          console.log(latestResult);
        }
      } catch (error) {
        console.error("Error fetching user logs: ", error);
        setLatestResult([]); 
      }
      setLoading(false);
    };

    fetchLatestUserLog();
  }, []);

  return (
    <div className="relative h-screen flex flex-col justify-center items-center">
      <div className="flex flex-col px-[10rem]">
        <h1 className="text-2xl font-bold font-mono text-teal-800">Possible anxiety triggers</h1>
        <span className="flex items-center gap-x-2">
          <CircleHelp strokeWidth={1.25} size={18} />
          <p className="text-sm text-gray-700">These are the triggers that we found in your messages.</p>
        </span>
        <div className="flex gap-x-6 max-w-[54rem] items-start mt-8">
          {loading ? (
            <ClipLoader
              color="#0f766e"
              loading={loading}
              size={150}
              aria-label="Loading Spinner"
              data-testid="loader"
              className="mx-auto"
            />
          ) : (
            latestResult.length > 0 ? (
              latestResult.map((result, index) => (
              <motion.div
                key={index}
                initial={{ opacity: 0, y: 50 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.5, delay: index * 0.1 }}
                className="flex flex-col gap-y-2 max-w-[18rem]"
              >
                <h2 className="text-lg font-bold font-mono text-teal-800">{result.trigger}</h2>
                <p className="text-sm text-gray-700">{result.summary}</p>
              
                <div className="flex items-center hover:underline transition-all duration-300 text-sm gap-x-4 px-2 py-1 border-black border-[1px] w-fit rounded-full">
                  <a href={result.link} target="_blank" rel="noopener noreferrer" className="flex text-sm items-center gap-x-2">
                    <span>Learn more</span><ExternalLink className="hover:scale-105 transition-all duration-300" size={16} strokeWidth={1.5} />
                  </a>
                </div>
              </motion.div>
              ))
            ) : (
              <p>No search results available.</p>
            )
          )}
        </div>
      </div>
      <div className="absolute bottom-0 justify-center mb-8">
        <Progress currentStep={4} />
      </div>
      <div className="flex justify-end space-x-10">
        <HomeButton />
      </div>
    </div>
  );
}

```

### app/auth/page.tsx

```typescript
'use client';

import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { 
  signInWithEmailAndPassword, 
  createUserWithEmailAndPassword, 
  onAuthStateChanged,
  signInWithPopup,
  GoogleAuthProvider
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

export default function AuthPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [isSignUp, setIsSignUp] = useState(false);

  const handleEmailAuth = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      if (isSignUp) {
        await createUserWithEmailAndPassword(auth, email, password);
      } else {
        await signInWithEmailAndPassword(auth, email, password);
      }
      router.push('/contact'); // redirect after successful login/signup
    } catch (error) {
      setError(isSignUp ? 'Failed to sign up. Please try again.' : 'Invalid credentials. Please try again.');
    }
  };

  const handleGoogleSignIn = async () => {
    const provider = new GoogleAuthProvider();
    try {
      await signInWithPopup(auth, provider);
      router.push('/contact');
    } catch (error) {
      setError('Failed to sign in with Google. Please try again.');
    }
  };

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        router.push('/contact');
      }
    });
    return () => unsubscribe();
  }, [router]);

  return (
    <div className="flex flex-col gap-y-4 md:flex-row md:gap-x-[6rem] items-center justify-center h-screen">
      <div className="flex flex-col text-teal-800">
        <span className="text-xl font-regular">{isSignUp ? 'Sign up for' : 'Sign in to'}</span>
        <span className="text-6xl font-bold font-serif">skog</span>
      </div>
      <div className="bg-white w-[400px] backdrop-blur-sm bg-opacity-30 outline outline-neutral-300 p-8 rounded-lg shadow-md">
        <form onSubmit={handleEmailAuth} className="space-y-4">
          <div className="mb-4">
            <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
              Email
            </label>
            <input
              id="email"
              type="email"
              placeholder="Enter your email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="w-full p-2 border text-sm border-gray-300 rounded-md"
              required
            />
          </div>
          <div className="mb-4">
            <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
              Password
            </label>
            <input
              id="password"
              type="password"
              placeholder="Enter your password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="w-full p-2 border text-sm border-gray-300 rounded-md"
              required
            />
          </div>
          <button
            type="submit"
            className="w-full bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded"
          >
            {isSignUp ? 'Sign Up' : 'Sign In'}
          </button>
          {error && <p className="text-red-600 text-sm">{error}</p>}
        </form>
        <div className="flex items-center my-4">
          <div className="flex-grow border-t border-gray-300"></div>
          <span className="px-3 text-gray-500 text-sm">or</span>
          <div className="flex-grow border-t border-gray-300"></div>
        </div>
        <div className="mt-4">
        <button
            onClick={handleGoogleSignIn}
            className="w-full bg-white border border-gray-300 text-gray-700 rounded-md font-regular py-2 px-4 flex items-center justify-center"
          >
            <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" alt="Google logo" className="w-4 h-4 mr-2" />
            Sign in with Google
          </button>
        </div>
        <p className="mt-4 text-sm text-center">
          {isSignUp ? 'Already have an account?' : "Don't have an account?"}
          <button
            onClick={() => setIsSignUp(!isSignUp)}
            className="ml-1 text-teal-600 hover:underline"
          >
            {isSignUp ? 'Sign In' : 'Sign Up'}
          </button>
        </p>
      </div>
    </div>
  );
}

```

### app/breathe/page.tsx

```typescript
'use client'

import React, { useEffect, useState } from 'react'
import { motion } from 'framer-motion';
import Progress from '@/components/Progress';
import MenuButton from '@/components/MenuButton';
import { useTTS } from '@cartesia/cartesia-js/react';

// Backend: call Cartesia API for every cycle
function BreathePage() {
    const [breathePhase, setBreathePhase] = useState<string>('inhale');
    const [sliderProgress, setSliderProgress] = useState(0);
    const [isStarted, setIsStarted] = useState(false);

    // Initialize TTS
    const tts = useTTS({
        apiKey: process.env['NEXT_PUBLIC_CARTESIA_API_KEY'] || '',
        sampleRate: 44100,
    })

    // Helper for playing breathing phase
    const handlePlay = async (text: string) => {
        // Begin buffering the audio.
        const response = await tts.buffer({
            model_id: "sonic-english",
            voice: {
                mode: "id",
                id: "21b81c14-f85b-436d-aff5-43f2e788ecf8",
                "__experimental_controls": {
                    "speed": "slowest",
                    "emotion": [
                        "positivity:high",
                        "curiosity:high",
                        "sadness:low"
                    ]
                }
            },
            transcript: text,
        });

        // Immediately play the audio
        await tts.play();
    }

    // Start breathing cycle
    useEffect(() => {
        if (!isStarted) return;

        let isMounted = true;
        let animationFrameId: number;

        const breathingCycle = async () => {
            if (!isMounted) return;

            // Inhale phase
            setBreathePhase('inhale');
            handlePlay('breathe in');
            setSliderProgress(0); // Reset progress for inhale
            const inhaleStartTime = Date.now();
            const animateInhale = () => {
                const elapsed = Date.now() - inhaleStartTime;
                const progress = Math.min(elapsed / 4000, 1);
                setSliderProgress(progress * 100);
                if (progress < 1 && isMounted) {
                    animationFrameId = requestAnimationFrame(animateInhale);
                }
            };
            animateInhale();
            await new Promise(resolve => setTimeout(resolve, 4000));

            if (!isMounted) return;
            // Hold phase
            setBreathePhase('hold');
            handlePlay('hold your breath');
            await new Promise(resolve => setTimeout(resolve, 7000));

            if (!isMounted) return;
            // Exhale phase
            setBreathePhase('exhale');
            handlePlay('breathe out');
            setSliderProgress(100); // Start exhale from 100% progress
            const exhaleStartTime = Date.now();
            const animateExhale = () => {
                const elapsed = Date.now() - exhaleStartTime;
                const progress = Math.max(1 - elapsed / 8000, 0);
                setSliderProgress(progress * 100);
                if (progress > 0 && isMounted) {
                    animationFrameId = requestAnimationFrame(animateExhale);
                }
            };
            animateExhale();
            await new Promise(resolve => setTimeout(resolve, 8000));
        };

        const startBreathing = async () => {
            while (isMounted) {
                await breathingCycle();
            }
        };

        startBreathing();

        return () => {
            isMounted = false;
            cancelAnimationFrame(animationFrameId);
        };
    }, [isStarted]);

    const handleStart = () => {
        setIsStarted(true);
    };

    return (
        <div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-teal-100 to-blue-200">
            {!isStarted ? (
                <button
                    onClick={handleStart}
                    className="px-6 py-3 text-white shadow-sm border-[1px] border-teal-800 font-mono bg-teal-500 rounded-full hover:bg-teal-600 transition-colors"
                >
                    START BREATHING EXERCISE
                </button>
            ) : (
                <>
                    <div className="relative w-64 h-64">
                        <motion.div
                            className="absolute inset-0 bg-white rounded-full filter blur-md"
                            animate={{
                                scale: breathePhase === 'inhale' ? 1.5 : breathePhase === 'exhale' ? 0.8 : 1.5,
                            }}
                            transition={{
                                duration: breathePhase === 'inhale' ? 4 : breathePhase === 'exhale' ? 8 : 0,
                                ease: "easeInOut"
                            }}
                        />
                    </div>
                    <p className="mt-24 text-2xl font-semibold font-mono text-teal-600">
                        {breathePhase === 'inhale' ? 'breathe in' : breathePhase === 'exhale' ? 'breathe out' : 'hold'}
                    </p>
                    <div className="w-64 h-1 bg-teal-100 rounded-full mt-4">
                        <div 
                            className="h-full bg-teal-500 rounded-full transition-all duration-100 ease-linear"
                            style={{ width: `${sliderProgress}%` }}
                        ></div>
                    </div>
                </>
            )}
            <div className="w-full absolute flex bottom-0 justify-center">
                <Progress currentStep={1} />
                <MenuButton from="breathe" />
            </div>
        </div>
    )
}

export default BreathePage;


```

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