# Project export: Journova

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: Journaling and meditation are two of the most important habits. Journova helps you thoughtfully reflect on your journal notes as well as go through customized guided meditations.
- Devpost: https://devpost.com/software/humifier-meditate-and-journal
- GitHub: https://github.com/matviikotolyk/hume
- Demo: https://hume-meditation.vercel.app/
- Team: 1 GitHub contributor(s) — matviiko (9 commits)

## Devpost submission (written by the team)

### Inspiration

I have been journalling and meditating on and off throughout the recent few years and have always found benefit in having these habits as part of my daily routine. I would typically slip off because I wouldn't have an "accountability buddy" and I would forget to keep doing these on my own. I thought that guided meditations would be a great use case for Hume AI's EVI model. While there are still some limitations in order to fully replicate the experience of meditation, the model is surprisingly good at acting as a meditation guide.

## README (from the GitHub repository)

# Hume Personal Therapist

### Chat about your journal entries or complete a guided meditation session. Learn more about yourself and your emotions.

### Inspiration

I have been journalling and meditating on and off throughout the recent few years and have always found benefit in having these habits as part of my daily routine. I would typically slip off because I wouldn't have an "accountability buddy" and I would forget to keep doing these on my own. I thought that guided meditations would be a great use case for Hume AI's EVI model. While there are still some limitations in order to fully replicate the experience of meditation, the model is surprisingly good at acting as a meditation guide. I also found EVI to be a great way to analyze my journal entries and gain insights into my emotional state.

### What it does

The users have two main options: go through the guided meditation session or chat about their journal entries with the EVI model. When starting a conversation, the custom EVI leads the interaction by asking the user how they feel and if they want to do a guided meditation session. The assistant then follows up with a few questions to customize the meditation sessions and guides the user through the proceess.
Additionally, the user can upload a journal entry and the app will analyze the emotional state of the entry and provide insights into the user's emotional state. They can select it to be part of the context for the EVI model which then allows the model to refer to the content of the entry. The user can ask for suggestions and tips regarding wellness and lifestyle and the model will use the web search tool to return responses. Furthermore, by clicking Web Search, the user initiates a Google search query based on the response generated by the assistant. This reduces the friction of having to search for information on their own and allows the user to get more information about their emotional state.

### How we built it

I started by creating a Next.js app with create-t3-app. I then added the Voice SDK and the Supabase auth and database to the app. I also added the Groq SDK to the app to use llama on groq to analyze the content of the uploaded pdf file. I used the pdf.js library to parse the pdf file and extract the text. Next, I added a landing page that allows the user to upload a journal entry and analyze it. Ensuring that uploading the PDF file works took the longest and required using a few technologies at the same time.

### Challenges we ran into

- The main challenge I ran into is making the EVI model sounds similar to an actual human meditation guide. I had to tweak the prompts and the system prompt to make it sound more like a guided meditation. I was going to integrate the resumeAssistant and pauseAssistant functions from the Voice SDK to make it more interactive, but haven't implemented it yet as the wait would sometimes be too long and make the user disengaged from the normal interaction. Overall, the model is very realistic and it's going to be about further tweaks to improve the UX.

### Accomplishments that we're proud of

- PDF parser with llama on groq analyzing the content of the pdf and then passing it to the EVI model as a system prompt. This combines different technologies to create a feature that is both powerful and easy to use.
- Search results using Google Search API based on the query generated by llama on groq from the assistant's inputs.
- User friendly UI that makes it fairly easy for the user to start interacting with the app.
- Llama on Groq integration to analyze the content of the pdf and pass it to the EVI model as a system prompt.

### What we learned

- I used supabase auth for the first time; learned to use Hume's EVI API; used Google Search API to get search results; used Twilio Sendgrid to send signup emails; learned how to use Groq SDK to use llama to analyze the contents of a pdf file; learned how to parse a PDF using the pdf.js library.

### What's next for Journova

First things first, I will double verify the auth is working end-to-end. I will also improve how the search results are displayed, making it more Perplexity-like to improve usability and value. Next, I will make the app more multimodal exploring other types of things users can chat about, like their moods, their goals, and their dreams. Additionally, I will add an option to chat via keyboard input since some users may not be super comfortable talking about their feelings. Moreover, it will enhance the app's accessibility and usability for users with disabilities. Finally, I will work on creating a simple mobile app that has the same functionality but a very simple UI to interact with EVI.

### Built With

Next.js, Supabase, Groq, React, Radix UI, TypeScript, Tailwind CSS, Twilio Sendgrid, Google Search API, and Google Search Engine ID


## Detected evidence (automated analysis)

Indexed codebase: 23 recognized source files, 48 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (30 of 30)

```
.env.example
.eslintrc.cjs
.gitignore
.vscode/settings.json
middleware.ts
next.config.js
package.json
postcss.config.cjs
prettier.config.js
README.md
src/app/_components/Auth.tsx
src/app/_components/ChatHistory.tsx
src/app/_components/Controls.tsx
src/app/_components/FileManager.tsx
src/app/_components/FileUpload.tsx
src/app/_components/LandingPage.tsx
src/app/_components/Messages.tsx
src/app/_components/SearchResults.tsx
src/app/_components/WelcomeModal.tsx
src/app/_hooks/useChatHistory.tsx
src/app/_lib/polyfills.ts
src/app/_lib/supabase.ts
src/app/auth/page.tsx
src/app/layout.tsx
src/app/page.tsx
src/app/providers.tsx
src/env.js
src/styles/globals.css
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @ai-sdk/groq@^0.0.1, @humeai/voice@^0.1.10, @humeai/voice-react@^0.1.16, @next/font@^14.2.15, @radix-ui/react-icons@^1.3.0, @radix-ui/react-slot@^1.1.0, @radix-ui/react-toast@^1.2.2, @radix-ui/themes@^3.1.4, @shadcn/ui@^0.0.4, @supabase/auth-helpers-nextjs@^0.10.0, @supabase/auth-helpers-react@^0.5.0, @supabase/supabase-js@^2.45.6, @t3-oss/env-nextjs@^0.10.1, @types/eslint@^8.56.10, @types/formidable@^3.4.5, @types/node@^20.14.10, @types/pdf-parse@^1.1.4, @types/react@^18.3.3, @types/react-dom@^18.3.0, @typescript-eslint/eslint-plugin@^8.1.0, @typescript-eslint/parser@^8.1.0, axios@^1.7.7, buffer@^6.0.3, class-variance-authority@^0.7.0, clsx@^2.1.1, crypto-browserify@^3.12.0, eslint@^8.57.0, eslint-config-next@^14.2.4, formidable@^3.5.2, geist@^1.3.0, groq-sdk@^0.7.0, lucide-react@^0.453.0, next@^14.2.4, pdf-parse@^1.1.1, pdfjs-dist@^4.7.76, postcss@^8.4.39, prettier@^3.3.2, prettier-plugin-tailwindcss@^0.6.5, radix-ui@^1.0.1, react@^18.3.1, react-dom@^18.3.1, react-dropzone@^14.2.10, react-pdf@^9.1.1, react-pdftotext@^1.3.3, stream-browserify@^3.0.0, string-replace-loader@^3.1.0, tailwind-merge@^2.5.4, tailwindcss@^3.4.3, tailwindcss-animate@^1.0.7, typescript@^5.5.3, zod@^3.23.3

### Recent commits (newest first)

- ismobile var
- padding on desktop
- mobile responsiveness
- welcome modal fixed + mobile responsiveness
- polyfills
- remove parse pdf api
- remove engines from package json
- bump node version
- eslint errors fix
- welcome modal
- update meta title
- update readme for calhacks
- type errors
- smpt provider setup for emails
- updated readme
- supabase setup for files and auth
- search and file upload
- first build
- top 3 emotion badges in message cards
- get rid of voice page for now

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

### package.json

```
{
  "name": "hume",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "node -v && next build",
    "dev": "next dev",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@ai-sdk/groq": "^0.0.1",
    "@humeai/voice": "^0.1.10",
    "@humeai/voice-react": "^0.1.16",
    "@next/font": "^14.2.15",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.2",
    "@radix-ui/themes": "^3.1.4",
    "@supabase/auth-helpers-nextjs": "^0.10.0",
    "@supabase/auth-helpers-react": "^0.5.0",
    "@supabase/supabase-js": "^2.45.6",
    "@t3-oss/env-nextjs": "^0.10.1",
    "axios": "^1.7.7",
    "buffer": "^6.0.3",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "crypto-browserify": "^3.12.0",
    "formidable": "^3.5.2",
    "geist": "^1.3.0",
    "groq-sdk": "^0.7.0",
    "lucide-react": "^0.453.0",
    "next": "^14.2.4",
    "pdf-parse": "^1.1.1",
    "pdfjs-dist": "^4.7.76",
    "radix-ui": "^1.0.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-dropzone": "^14.2.10",
    "react-pdf": "^9.1.1",
    "react-pdftotext": "^1.3.3",
    "stream-browserify": "^3.0.0",
    "string-replace-loader": "^3.1.0",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.3"
  },
  "devDependencies": {
    "@shadcn/ui": "^0.0.4",
    "@types/eslint": "^8.56.10",
    "@types/formidable": "^3.4.5",
    "@types/node": "^20.14.10",
    "@types/pdf-parse": "^1.1.4",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^8.1.0",
    "@typescript-eslint/parser": "^8.1.0",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.4",
    "postcss": "^8.4.39",
    "prettier": "^3.3.2",
    "prettier-plugin-tailwindcss": "^0.6.5",
    "tailwindcss": "^3.4.3",
    "typescript": "^5.5.3"
  },
  "ct3aMetadata": {
    "initVersion": "7.37.0"
  },
  "packageManager": "npm@10.8.2"
}

```

### src/app/page.tsx

```typescript
import LandingPage from "./_components/LandingPage";
import { fetchAccessToken } from "hume";

export default async function Page() {
  const accessToken = await fetchAccessToken({
    apiKey: String(process.env.NEXT_PUBLIC_HUME_API_KEY),
    secretKey: String(process.env.NEXT_PUBLIC_HUME_SECRET_KEY),
  });

  if (!accessToken) {
    throw new Error("Failed to fetch access token");
  }

  return <LandingPage accessToken={accessToken} />;
}

```

### src/app/layout.tsx

```typescript
import "./_lib/polyfills";
import "~/styles/globals.css";
import "@radix-ui/themes/styles.css";
import { Theme } from "@radix-ui/themes";
import Providers from "./providers";

import { DM_Sans } from "next/font/google";
import { type Metadata } from "next";

const dm_sans = DM_Sans({
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Journova",
  description: "A therapist that uses AI to help you",
  icons: [{ rel: "icon", url: "/favicon_sun.ico" }],
};

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

```

### src/app/auth/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import type { User } from "@supabase/auth-helpers-nextjs";
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Box, Flex, Text, TextField, Button, Heading } from "@radix-ui/themes";
import * as Toast from "@radix-ui/react-toast";

export default function AuthPage() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [toastOpen, setToastOpen] = useState(false);
  const [toastMessage, setToastMessage] = useState("");
  const [toastType, setToastType] = useState<"success" | "error">("success");
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  const router = useRouter();
  const supabase = createClientComponentClient();

  useEffect(() => {
    const getUser = async () => {
      const {
        data: { user },
      } = await supabase.auth.getUser();
      setUser(user);
      setLoading(false);
    };
    getUser().catch((error) => {
      console.error("Error fetching user: ", error);
    });

    const handleMouseMove = (e: MouseEvent) => {
      setMousePosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, [supabase.auth]);

  const showToast = (message: string, type: "success" | "error") => {
    setToastMessage(message);
    setToastType(type);
    setToastOpen(true);
  };

  const handleSignUp = async () => {
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${window.location.origin}/auth`,
      },
    });
    if (error) {
      showToast(error.message, "error");
    } else if (data.user?.identities?.length === 0) {
      showToast(
        "This email is already registered. Please sign in instead.",
        "error",
      );
    } else {
      showToast("Check your email for the confirmation link!", "success");
    }
  };

  const handleSignIn = async () => {
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    if (error) {
      showToast(error.message, "error");
    } else {
      showToast("Signed in successfully!", "success");
      router.push("/"); // Redirect to landing page after successful sign in
    }
  };

  const handleSignOut = async () => {
    await supabase.auth.signOut();
    setUser(null);
    router.refresh();
    showToast("Signed out successfully!", "success");
  };

  if (loading) return <Text>Loading...</Text>;

  return (
    <div className="relative min-h-screen bg-gradient-to-t from-[#FED8B1] to-[#FCCAC4] py-16">
      <div
        className="pointer-events-none fixed inset-0"
        style={{
          background: `radial-gradient(circle ${100}px at ${mousePosition.x}px ${mousePosition.y}px, rgba(255, 255, 255, 1), transparent 80%) z-0`,
        }}
      />
      <Box className="z-10 mx-auto max-w-md">
        <Heading size="8" className="mb-8 text-center text-[#353535]">
          Mental Health Coach
        </Heading>
        <Box className="rounded-md border border-gray-300 bg-[#F5F5F5] p-6">
          {user ? (
            <Flex direction="column" gap="3">
              <Text>Welcome, {user.email}!</Text>
              <Button onClick={handleSignOut}>Sign Out</Button>
              <Link className="w-full" href="/">
                <Button className="hover:cursor-pointer" color="orange">
                  Go To Home Page
                </Button>
              </Link>
            </Flex>
          ) : (
            <Flex direction="column" gap="3">
              <TextField.Root
                type="email"
                placeholder="Email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
              />
              <TextField.Root
                type="password"
                placeholder="Password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
              <Button onClick={handleSignIn} className="hover:cursor-pointer">
                Sign In
              </Button>
              <Button
                onClick={handleSignUp}
                className="hover:cursor-pointer"
                variant="soft"
              >
                Sign Up
              </Button>
            </Flex>
          )}
        </Box>
      </Box>
      <Toast.Provider swipeDirection="right">
        <Toast.Root
          className="data-[state=closed]:animate-hide data-[state=open]:animate-slideIn data-[swipe=end]:animate-swipeOut grid grid-cols-[auto_max-content] items-center gap-x-[15px] rounded-md bg-white p-[15px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] [grid-template-areas:_'title_action'_'description_action'] data-[swipe=cancel]:translate-x-0 data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:transition-[transform_200ms_ease-out]"
          open={toastOpen}
          onOpenChange={setToastOpen}
          duration={3000}
        >
          <Toast.Title
            className={`mb-[5px] text-[15px] font-medium ${toastType === "success" ? "text-green-600" : "text-red-600"} [grid-area:_title]`}
          >
            {toastType === "success" ? "Success" : "Error"}
          </Toast.Title>
          <Toast.Description className="m-0 text-[13px] leading-[1.3] text-slate-600 [grid-area:_description]">
            {toastMessage}
          </Toast.Description>
        </Toast.Root>
        <Toast.Viewport className="fixed bottom-0 right-0 z-[2147483647] m-0 flex w-[390px] max-w-[100vw] list-none flex-col gap-2.5 p-[var(--viewport-padding)] outline-none [--viewpo
[truncated — 64 more characters]
```

### prettier.config.js

```javascript
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
  plugins: ["prettier-plugin-tailwindcss"],
};

export default config;

```

### next.config.js

```javascript
/**
 * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
 * for Docker builds.
 */
await import("./src/env.js");

/** @type {import("next").NextConfig} */
const config = {
  reactStrictMode: true,
};

export default config;

```

### tailwind.config.ts

```typescript
import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";

export default {
  content: ["./src/**/*.tsx"],
  theme: {
    extend: {
      fontFamily: {
        sans: ["var(--font-sans)", ...fontFamily.sans],
      },
    },
  },
  plugins: [],
} satisfies Config;

```

### middleware.ts

```typescript
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });

  const {
    data: { session },
  } = await supabase.auth.getSession();

  if (!session && req.nextUrl.pathname !== "/login") {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  return res;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

```

### src/env.js

```javascript
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  /**
   * Specify your server-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars.
   */
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]),
  },

  /**
   * Specify your client-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars. To expose them to the client, prefix them with
   * `NEXT_PUBLIC_`.
   */
  client: {
    // NEXT_PUBLIC_CLIENTVAR: z.string(),
  },

  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
    // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
  },
  /**
   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
   * useful for Docker builds.
   */
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
  /**
   * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
   * `SOME_VAR=''` will throw an error.
   */
  emptyStringAsUndefined: true,
});

```

### src/styles/globals.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;


```

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