# Project export: Kali

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: TreeHacks 2026
- Tagline: Speak your plans out loud. Kali recognizes and organizes everything, no planning, no remembering.
- Devpost: https://devpost.com/software/kali-0as6cv
- GitHub: https://github.com/ShahdAbouhussein/TreeHacks-2026
- Demo: https://tree-hacks-2026-psi.vercel.app/
- Video: https://www.youtube.com/embed/CxuV147HULk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — ShahdAbouhussein (42 commits), Carmah Hawwari (11 commits), Claude Opus 4.6 (6 commits)

## Devpost submission (written by the team)

### Inspiration

We both have ADHD. And we both live the same frustrating cycle: someone mentions lunch plans while we're walking to class. We agree. We mean to add it to our calendar. We forget. We miss lunch. Or we're mid-conversation and realize we need to move a meeting. But stopping to open our laptop and navigate through calendar interfaces feels like too much. So we don't. And we double-book ourselves. We were tired of living like this. Tired of being the person frantically typing on their phone while walking, head down, just to capture a fleeting thought before it vanishes. To-do list apps demand we sit down, categorize, and organize when our brains don't work that way. Productivity tools require more executive function than we have to give. We didn't want another to-do list, another complex calendar interface, or have to try harder to remember things. We wanted something that worked the way our brains actually work: spontaneous, conversational, and immediate. Something that meets our standards for design and simplicity, not just another cluttered app that adds to the mental load. So we built Kali. A calendar that listens when we remember things in passing, captures them instantly without making us stop, and organizes our lives without requiring us to be organized people. Because the best productivity system for ADHD isn't one that forces us to change, it's one that adapts to how we already think.

### What it does

Kali is a voice-first productivity assistant designed for people with ADHD. Instead of navigating complex calendar interfaces, users simply speak their intent. A user can say "Meeting with Sarah tomorrow at 3pm" and Kali automatically creates the event. By converting natural language into structured calendar operations, Kali eliminates the cognitive friction and executive function overload of manual scheduling. Planning becomes conversational, fast, and intuitive.

### How we built it

We built Kali using a modern full-stack architecture designed for reliability and real-time interaction: Frontend: React, Vite, TypeScript, Tailwind Authentication and Database: Firebase Auth and Firestore Backend: Express with TypeScript Speech-to-text: Open-source Whisper running locally AI Parsing: OpenAI API with structured JSON schema enforcement The system pipeline works as follows: Audio is recorded directly in the browser using the MediaRecorder API. The audio file is sent to our Express backend and transcribed using Whisper. The transcript is sent to the OpenAI API, which extracts structured intent using a strict JSON schema. The structured response defines the action type and parameters for creating events, along with title, date, and time. The backend uses this deterministic output to safely perform calendar mutations. We used OpenAI's structured response format with JSON schema validation to ensure machine-readable outputs. This guarantees that natural language is converted into predictable backend operations, making the system reliable enough to automate real calendar actions. Example structured output:

### Challenges we ran into

One of our biggest challenges was production deployment architecture. Whisper requires Python and ffmpeg, which cannot run inside standard serverless environments. We had to separate frontend and backend deployment concerns while maintaining secure HTTPS communication for microphone access. We also encountered: Strict TypeScript enforcement during production builds Environment variable injection issues during Vite build time OAuth domain authorization configuration in Firebase CORS handling between Vercel and a locally tunneled backend Audio format conversion from WebM to WAV for Whisper compatibility Ensuring consistent structured JSON output from the OpenAI API Most of the complexity was not in the model itself, but in integrating multiple systems into a stable, production-ready pipeline.

### Accomplishments we're proud of

We're a two-person team. One of us has never done a hackathon before in their life. The other has done exactly one. Only one of is a coder and the other is a designer. And yet, in under 24 hours, we built and deployed a fully functional AI-powered voice assistant with real speech recognition, natural language processing, and live calendar integration. We're incredibly proud that we: Went from zero to a working, deployed product that people can actually use Integrated new AI technologies that many experienced developers struggle with Built something genuinely meaningful for people with ADHD, not just another CRUD app Made it work as a tiny team of two Solved real production deployment challenges that would normally require a full engineering team Created a polished, thoughtful user experience despite massive time constraints Most teams at hackathons have more experience, more developers, and more resources. We had none of those advantages. But we had a clear vision, relentless execution, and refused to compromise on building something that actually matters with quality. For a first-time hackathon participant and a designer-developer duo with barely any hackathon experience between us, we didn't just participate, we shipped a complete AI product that works in production. We're proud we proved that ambition and determination can beat experience and resources.

### What we learned

We learned that building AI-powered applications is primarily an infrastructure challenge. The model is only one component of a much larger system. Key lessons included: Environment variables in Vite are injected at build time and must exist during production builds. Reliable LLM automation requires strict schema validation rather than free-form text generation. Audio handling on the web requires careful format conversion and HTTPS enforcement. Separating parsing logic from execution logic increases safety and scalability. Deployment stability is critical for AI-driven user experiences. We also learned how deeply user experience design impacts accessibility. Reducing interaction friction can be just as powerful as adding new features.

### What's next

We plan to expand Kali into a fully voice-driven calendar control system. Next steps include: Voice-based event deletion such as "Cancel my 3pm meeting tomorrow" Voice-based rescheduling such as "Move my 2pm meeting to Friday at 11am" Automatic syncing directly with Google Calendar instead of requiring manual uploads Persistent Whisper services to reduce latency Conflict detection and intelligent scheduling suggestions Context-aware task prioritization and grouping Our long-term goal is to build a conversational executive function layer that sits on top of existing productivity tools and makes them feel adaptive, supportive, and effortless.

## README (from the GitHub repository)

# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs['recommended-typescript'],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])
```


## Detected evidence (automated analysis)

Indexed codebase: 47 recognized source files, 403 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Firebase (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (61 of 61)

```
.gitignore
.vercelignore
chrono-variables.json
components.json
design-system.json
eslint.config.js
index.html
package.json
profound.json
README.md
server/.nvmrc
server/firebaseAdmin.ts
server/index.ts
server/package.json
server/transcribe.py
server/tsconfig.json
src/App.css
src/App.tsx
src/components/DesktopSidebar.tsx
src/components/DesktopWeekCalendar.tsx
src/components/FloatingAssistant.tsx
src/components/InlineDatePicker.tsx
src/components/ui/button.tsx
src/components/ui/scroll-area.tsx
src/components/ui/separator.tsx
src/components/ui/tooltip.tsx
src/home/App.css
src/home/App.tsx
src/home/features/calendar/CalendarGrid.tsx
src/home/features/calendar/CalendarPage.tsx
src/home/features/calendar/DayCell.tsx
src/home/features/calendar/ViewDropdown.tsx
src/home/features/home/components/AddItemModal.tsx
src/home/features/home/components/BottomNav.tsx
src/home/features/home/components/EventItem.tsx
src/home/features/home/components/EventList.tsx
src/home/features/home/components/Header.tsx
src/home/features/home/components/TaskCard.tsx
src/home/features/home/components/TasksSection.tsx
src/home/features/home/components/WeekStrip.tsx
src/home/features/home/HomePage.tsx
src/home/features/tasks/ChatBar.tsx
src/home/features/tasks/TaskListItem.tsx
src/home/features/tasks/TasksPage.tsx
src/home/main.tsx
src/home/pages/CalendarPage.tsx
src/index.css
src/lib/firebase.ts
src/lib/icsParser.ts
src/lib/importEvents.ts
src/lib/useEvents.ts
src/lib/useTasks.ts
src/lib/utils.ts
src/main.tsx
src/pages/AssistantPage.tsx
src/pages/Chat.tsx
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vercel.json
vite.config.ts
```

### Dependencies

- package.json: @base-ui/react@^1.2.0, @eslint/js@^9.39.1, @fontsource-variable/geist@^5.2.8, @tailwindcss/vite@^4.1.18, @types/node@^24.10.1, @types/react@^19.2.7, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, firebase@^12.9.0, framer-motion@^12.34.0, globals@^16.5.0, ical@^0.8.0, lucide-react@^0.577.0, react@^19.2.0, react-dom@^19.2.0, react-use-measure@^2.1.7, shadcn@^4.0.3, tailwind-merge@^3.5.0, tailwindcss@^4.1.18, tw-animate-css@^1.4.0, typescript@~5.9.3, typescript-eslint@^8.48.0, vite@^7.3.1
- server/package.json: @types/cors@^2.8.19, @types/express@^5.0.6, @types/multer@^2.0.0, @types/node@^25.2.3, axios@^1.13.5, cors@^2.8.6, dotenv@^17.3.1, express@^5.2.1, firebase-admin@^13.6.1, multer@^2.0.2, openai@^6.22.0, pdf-parse@^2.4.5, pdf2json@^4.0.2, ts-node@^10.9.2, tsx@^4.21.0, typescript@^5.9.3

### Recent commits (newest first)

- feat: replace left border with double outline on chat item cards
- feat: profile popover, airy chatbar, sidebar shade tweak
- feat: shadow token system, floating assistant, UI polish
- chore: remove push-variables.mjs containing Figma token
- feat: bug fixes, UI polish, drag-to-create calendar events, assistant API wiring
- feat: initial commit
- Merge pull request #15 from ShahdAbouhussein/carmah
- Merge branch 'main' into carmah
- UI polish: new logo on home, Flourish category, smoother toggle animation, larger drag handle
- Merge pull request #14 from ShahdAbouhussein/carmah
- fixing unused variables error
- UI updates: event delete button, smoother waveform, matching card styles
- Add event editing, custom time picker, ICS recurring events, calendar UI improvements
- Add event editing, custom time picker, ICS recurring events, calendar UI improvements
- Merge pull request #13 from ShahdAbouhussein/shahd_dev_branch
- understanding weekdays
- understanding weekdays
- fixing date +1 issue
- fixing date +1 issue
- Merge branch 'main' into shahd_dev_branch

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

### package.json

```
{
  "name": "treehacks-2026",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@base-ui/react": "^1.2.0",
    "@fontsource-variable/geist": "^5.2.8",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "firebase": "^12.9.0",
    "framer-motion": "^12.34.0",
    "ical": "^0.8.0",
    "lucide-react": "^0.577.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-use-measure": "^2.1.7",
    "shadcn": "^4.0.3",
    "tailwind-merge": "^3.5.0",
    "tw-animate-css": "^1.4.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@tailwindcss/vite": "^4.1.18",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.7",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "tailwindcss": "^4.1.18",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.48.0",
    "vite": "^7.3.1"
  }
}

```

### server/package.json

```
{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "tsx index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.13.5",
    "cors": "^2.8.6",
    "dotenv": "^17.3.1",
    "express": "^5.2.1",
    "firebase-admin": "^13.6.1",
    "multer": "^2.0.2",
    "openai": "^6.22.0",
    "pdf-parse": "^2.4.5",
    "pdf2json": "^4.0.2"
  },
  "devDependencies": {
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/multer": "^2.0.0",
    "@types/node": "^25.2.3",
    "ts-node": "^10.9.2",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3"
  }
}

```

### src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### src/home/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '../index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### src/home/App.tsx

```typescript
import { useState, useMemo, useCallback, useEffect } from "react";
import { HomePage } from "./features/home/HomePage";
import { AddItemModal } from "./features/home/components/AddItemModal";
import type { CalendarEvent } from "../lib/useEvents";
import { useTasks } from "../lib/useTasks";

const DAY_LABELS = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"];

const TIME_SLOTS = Array.from({ length: 24 }, (_, i) => ({
  hour: i,
  label: i === 0 ? "12 AM" : i < 12 ? `${i} AM` : i === 12 ? "12 PM" : `${i - 12} PM`,
}));

function formatTime(date: Date): string {
  const h = date.getHours();
  const m = date.getMinutes();
  const suffix = h >= 12 ? "PM" : "AM";
  const hour12 = h % 12 || 12;
  return m === 0 ? `${hour12} ${suffix}` : `${hour12}:${m.toString().padStart(2, "0")} ${suffix}`;
}

function getWeekDays(selectedDate: Date) {
  const dayOfWeek = selectedDate.getDay();
  const startOfWeek = new Date(selectedDate);
  startOfWeek.setDate(selectedDate.getDate() - dayOfWeek);

  const today = new Date();
  const todayDate = today.getDate();
  const todayMonth = today.getMonth();
  const todayYear = today.getFullYear();

  return Array.from({ length: 7 }, (_, i) => {
    const d = new Date(startOfWeek);
    d.setDate(startOfWeek.getDate() + i);
    return {
      date: d.getDate(),
      month: d.getMonth(),
      year: d.getFullYear(),
      label: DAY_LABELS[i],
      isSelected:
        d.getDate() === selectedDate.getDate() &&
        d.getMonth() === selectedDate.getMonth() &&
        d.getFullYear() === selectedDate.getFullYear(),
      isToday:
        d.getDate() === todayDate &&
        d.getMonth() === todayMonth &&
        d.getFullYear() === todayYear,
    };
  });
}

interface AppProps {
  onSeeAllTasks?: () => void;
  onNavPress?: (id: string) => void;
  events?: CalendarEvent[];
  userId?: string;
  userName?: string;
  desktopAddTrigger?: number;
  onAssistantOpen?: () => void;
}

function App({ onSeeAllTasks, onNavPress, events = [], userId, userName, desktopAddTrigger, onAssistantOpen }: AppProps) {
  const [selectedDate, setSelectedDate] = useState(() => new Date());
  const [weekDirection, setWeekDirection] = useState(0);
  const [showAddModal, setShowAddModal] = useState(false);
  const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null);
  const [eventAnchor, setEventAnchor] = useState<{ top: number; left: number } | null>(null);
  const [dragCreateInfo, setDragCreateInfo] = useState<{ date: string; startTime: string; endTime: string } | null>(null);
  const [scrollKey, setScrollKey] = useState(0);
  const [showSummary, setShowSummary] = useState(false);
  const [summary, setSummary] = useState("");
  const [summaryLoading, setSummaryLoading] = useState(false);
  const { tasks: firestoreTasks } = useTasks(userId);

  // Open add modal when desktop sidebar triggers it
  useEffect(() => {
    if (desktopAddTrigger && desktopAddTrigger > 0) {
      setShowAddModal(true);
    }
  }, [desktopAddTrigger]);

  const days = useMemo(() => getWeekDays(selectedDate), [selectedDate]);

  const handleDayPress = useCallback((date: number, month: number, year: number) => {
    setSelectedDate(new Date(year, month, date));
    setScrollKey((k) => k + 1);
  }, []);

  const handleNextWeek = useCallback(() => {
    setWeekDirection(1);
    setSelectedDate((prev) => {
      const next = new Date(prev);
      next.setDate(prev.getDate() + 7);
      return next;
    });
  }, []);

  const handleEventPress = useCallback((eventId: string, clickEvent?: React.MouseEvent) => {
    console.log("🔥 Event pressed:", eventId);
    const found = events.find((e) => e.id === eventId);
    console.log("🔥 Found event:", found, "userId:", userId);
    if (found) {
      setEditingEvent(found);
      if (clickEvent) {
        const rect = (clickEvent.currentTarget as HTMLElement).getBoundingClientRect();
        setEventAnchor({ top: rect.top, left: rect.left });
      } else {
        setEventAnchor(null);
      }
    }
  }, [events]);

  const handleDragCreate = useCallback((date: string, startTime: string, endTime: string, anchorPosition: { top: number; left: number }) => {
    console.log("[drag] handleDragCreate called:", date, startTime, endTime);
    setDragCreateInfo({ date, startTime, endTime });
    setEventAnchor(anchorPosition);
    setEditingEvent(null);
    setShowAddModal(true);
  }, []);

  const handlePrevWeek = useCallback(() => {
    setWeekDirection(-1);
    setSelectedDate((prev) => {
      const next = new Date(prev);
      next.setDate(prev.getDate() - 7);
      return next;
    });
  }, []);

  const handleAiPress = useCallback(() => {
    onAssistantOpen?.();
  }, [onAssistantOpen]);

  const selectedEvents = useMemo(() => {
    const dayStart = new Date(selectedDate);
    dayStart.setHours(0, 0, 0, 0);
    const dayEnd = new Date(selectedDate);
    dayEnd.setHours(23, 59, 59, 999);

    return events
      .filter((e) => e.start <= dayEnd && e.end >= dayStart)
      .map((e) => {
        if (e.allDay) {
          return {
            id: e.id,
            title: e.title,
            timeRange: "All day",
            startHour: 0,
            durationHours: 1,
          };
        }
        // Clamp start/end to the visible day
        const visibleStart = e.start < dayStart ? dayStart : e.start;
        const visibleEnd = e.end > dayEnd ? dayEnd : e.end;
        const startHour = visibleStart.getHours() + visibleStart.getMinutes() / 60;
        const endHour = visibleEnd > dayEnd
          ? 24
          : visibleEnd.getHours() + visibleEnd.getMinutes() / 60;

        return {
          id: e.id,
          title: e.title,
          timeRange: `${formatTime(e.start)} - ${formatTime(e.end)}`,
          startHour,
          durationHours: Math.max(endHour - startHour, 0.5),
        };
      });
  }, [events, selectedDate]);

  // All events for the visible week, keyed by day-of-month
  const weekEventsByDay = useMemo(() => {
    const map: Record<string, ty
[truncated — 3363 more characters]
```

### src/App.tsx

```typescript
import { useEffect, useState, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { auth, db } from "./lib/firebase";
import {
  onAuthStateChanged,
  signOut,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
} from "firebase/auth";
import { doc, setDoc, collection, getDocs, writeBatch } from "firebase/firestore";
import Home from "./home/App";
import { BottomNav } from "./home/features/home/components/BottomNav";
import TasksPage from "./home/features/tasks/TasksPage";
import CalendarPage from "./home/features/calendar/CalendarPage";
import Chat from "./pages/Chat";
import AssistantPage from "./pages/AssistantPage";
import FloatingAssistant from "./components/FloatingAssistant";
import { parseIcsFile } from "./lib/icsParser";
import { importEventsToFirestore } from "./lib/importEvents";
import { useEvents } from "./lib/useEvents";
import { DesktopSidebar } from "./components/DesktopSidebar";

const KaliLogo = () => (
  <svg width="29" height="25" viewBox="0 0 29 25" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M21.6333 0C22.9681 0.104094 24.0386 0.83307 24.7934 1.91075C25.5685 3.00821 25.8575 4.36986 25.5931 5.68214C25.0461 8.54764 21.8167 10.5302 19.1928 11.2148C18.8591 11.3019 18.3249 11.3661 18.0594 11.541L18.1543 11.6973C20.7611 13.1106 24.5205 10.6652 26.9663 10.1784C27.315 10.1177 28.0975 10.1924 28.3406 10.5011C29.7512 12.2944 28.6402 15.3338 27.4194 16.7284C26.3351 17.9687 24.7913 18.7254 23.1345 18.8276C19.1256 19.1096 17.183 16.9782 14.4873 14.6596C15.7129 17.401 20.4082 21.2419 16.6316 23.7253C14.9389 24.8355 12.8714 25.2428 10.8767 24.8591C6.54646 22.8821 9.96539 16.6931 10.9677 13.5786C10.3636 13.9381 9.87413 14.2608 9.33474 14.7033C7.65519 16.1355 5.08867 19.6262 2.72963 17.4132C-3.14951 11.8983 1.42686 9.32598 7.41625 9.32556C7.87663 9.32556 9.50469 8.97875 9.29871 8.34703C8.6172 6.19115 7.30356 4.37923 8.69599 2.17462C10.4063 -0.533312 13.8269 1.62773 15.0735 3.62752C15.549 4.39038 15.577 6.47522 16.1568 6.87086C17.8846 6.56595 18.6554 0.803285 21.6333 0Z" fill="#6F8F7A"/>
  </svg>
);

const BackChevron = ({ onClick }: { onClick: () => void }) => (
  <button type="button" onClick={onClick} className="absolute left-lg top-[52px]">
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
      <path d="M15 6L9 12L15 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  </button>
);

const ArrowButton = ({ onClick, disabled }: { onClick: () => void; disabled?: boolean }) => (
  <button
    type="button"
    onClick={onClick}
    disabled={disabled}
    className="flex h-14 w-14 items-center justify-center rounded-full bg-accent text-white shadow-subtle transition-colors hover:bg-accent-dark disabled:opacity-40"
  >
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 12h14M12 5l7 7-7 7" />
    </svg>
  </button>
);

export default function App() {
  const [user, setUser] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [isLogin, setIsLogin] = useState(false);
  const [step, setStep] = useState(0);
  const [authError, setAuthError] = useState("");
  const [icsFile, setIcsFile] = useState<File | null>(null);
  const [importing, setImporting] = useState(false);
  const [importSuccess, setImportSuccess] = useState("");
  const [uploadedFileName, setUploadedFileName] = useState("");
  const [deleting, setDeleting] = useState(false);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [activeTab, setActiveTab] = useState("home");
  const [chatOpen, setChatOpen] = useState(false);
  const [assistantOpen, setAssistantOpen] = useState(false);
  const [settingsTab, setSettingsTab] = useState<"profile" | "calendars" | "tasks" | "appearance">("profile");
  // Track if add modal should open (triggered from desktop sidebar)
  const [desktopAddTrigger, setDesktopAddTrigger] = useState(0);

  const { events } = useEvents(user?.uid);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  const handleAuth = async () => {
    setAuthError("");
    try {
      if (isLogin) {
        await signInWithEmailAndPassword(auth, email, password);
        console.log("Signed in");
      } else {
        const userCredential = await createUserWithEmailAndPassword(
          auth,
          email,
          password
        );

        const newUser = userCredential.user;

        await setDoc(doc(db, "users", newUser.uid), {
          email: newUser.email,
          createdAt: new Date(),
        });

        console.log("User + Firestore doc created");
        setStep(3);
      }
    } catch (error: any) {
      console.error("Auth error:", error.code);
      setAuthError(
        error.code === "auth/email-already-in-use"
          ? "That email is already registered."
          : error.code === "auth/wrong-password" || error.code === "auth/invalid-credential"
          ? "Incorrect email or password."
          : error.code === "auth/weak-password"
          ? "Password must be at least 6 characters."
          : "Something went wrong. Please try again."
      );
    }
  };

  const handleIcsUpload = async () => {
    if (!icsFile || !user) return;
    setImporting(true);
    setAuthError("");
    setImportSuccess("");
    try {
      const content = await icsFile.text();
      const parsed = parseIcsFile(content);
      if (parsed.length === 0) {
        setAuthError("No events found in the file. Make sure it's a valid .ics file.");
        return;
      }
      await importEventsToFirestore(user.uid, parsed);
      setImportSuccess(`Successfully imported ${parsed.length} event${parsed.length > 1 ? "
[truncated — 29285 more characters]
```

### server/index.ts

```typescript
import express, { Request, Response, NextFunction } from "express";
import cors from "cors";
import dotenv from "dotenv";
import { adminAuth, adminDb, Timestamp } from "./firebaseAdmin";
import multer from "multer";
import { exec } from "child_process";
import fs from "fs";
import path from "path";
import OpenAI from "openai";
// pdf2json loaded via require in the handler to avoid ESM/CJS issues


dotenv.config();

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const app = express();

app.use(cors());
app.use(express.json());

/**
 * Health check route
 */
app.get("/api/health", (req: Request, res: Response) => {
  res.json({ status: "Server running" });
});

const upload = multer({ dest: "uploads/" });

app.post("/api/transcribe", upload.single("audio"), async (req: any, res) => {
  console.log("🔥 /api/transcribe hit");

  const audioPath = req.file?.path;

  if (!audioPath) {
    console.log("No file uploaded");
    return res.status(400).json({ error: "No audio uploaded" });
  }

  console.log("Audio saved at:", audioPath);

  // Rename with proper extension so OpenAI accepts it
  const ext = req.file?.originalname?.split(".").pop() || "webm";
  const properPath = audioPath + "." + ext;
  fs.renameSync(audioPath, properPath);

  try {
    const transcription = await openai.audio.transcriptions.create({
      file: fs.createReadStream(properPath),
      model: "whisper-1",
    });

    console.log("Transcript:", transcription.text);

    fs.unlinkSync(properPath);

    res.json({ transcript: transcription.text });
  } catch (error: any) {
    console.error("Whisper API error:", error.message || error);
    console.error("Full error:", JSON.stringify(error, null, 2));

    if (fs.existsSync(properPath)) fs.unlinkSync(properPath);

    res.status(500).json({ error: "Transcription failed", detail: error.message });
  }
});


app.post("/api/parse-transcript", async (req: Request, res: Response) => {
  const { transcript, userId, timeZone, clientDate } = req.body;

  if (!transcript || !userId) {
    return res.status(400).json({ error: "transcript and userId are required" });
  }

  const formatDateInTimeZone = (date: Date, tz?: string) => {
    try {
      const parts = new Intl.DateTimeFormat("en-CA", {
        timeZone: tz,
        year: "numeric",
        month: "2-digit",
        day: "2-digit",
      }).formatToParts(date);
      const y = parts.find((p) => p.type === "year")?.value;
      const m = parts.find((p) => p.type === "month")?.value;
      const d = parts.find((p) => p.type === "day")?.value;
      if (y && m && d) return `${y}-${m}-${d}`;
    } catch {
      // Fallback to server-local date if timezone is invalid
    }
    const local = new Date(date);
    const y = local.getFullYear();
    const m = String(local.getMonth() + 1).padStart(2, "0");
    const d = String(local.getDate()).padStart(2, "0");
    return `${y}-${m}-${d}`;
  };

  const tz = typeof timeZone === "string" && timeZone.trim() ? timeZone : undefined;
  const clientToday =
    typeof clientDate === "string" && /^\d{4}-\d{2}-\d{2}$/.test(clientDate)
      ? clientDate
      : undefined;
  const today = clientToday || formatDateInTimeZone(new Date(), tz);
  const weekdayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];

  const extractSingleWeekday = (text: string) => {
    const lower = text.toLowerCase();
    const found = new Set<number>();
    weekdayNames.forEach((name, idx) => {
      const re = new RegExp(`\\b${name}\\b`, "i");
      if (re.test(lower)) found.add(idx);
    });
    if (found.size !== 1) return null;
    return Array.from(found)[0];
  };

  const hasExplicitDate = (text: string) => {
    const lower = text.toLowerCase();
    if (/\b\d{4}-\d{2}-\d{2}\b/.test(text)) return true;
    if (/\b\d{1,2}\/\d{1,2}(\/\d{2,4})?\b/.test(text)) return true;
    if (/\b\d{1,2}(st|nd|rd|th)\b/.test(lower)) return true;
    if (/\b(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)\b/.test(lower)) {
      return true;
    }
    return false;
  };

  const toYmd = (d: Date) =>
    `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;

  const resolveWeekdayDate = (base: Date, weekday: number, isNext: boolean) => {
    const baseDay = base.getDay();
    let delta = (weekday - baseDay + 7) % 7;
    if (isNext && delta === 0) delta = 7;
    const result = new Date(base);
    result.setDate(base.getDate() + delta);
    return result;
  };

  try {
    // Fetch existing items so GPT can match deletion requests
    const existingTasks: any[] = [];
    const existingEvents: any[] = [];
    const tasksSnap = await adminDb.collection("users").doc(userId).collection("tasks").get();
    tasksSnap.forEach((d) => existingTasks.push({ id: d.id, ...d.data() }));
    const eventsSnap = await adminDb.collection("users").doc(userId).collection("events").get();
    eventsSnap.forEach((d) => existingEvents.push({ id: d.id, ...d.data() }));

    const completion = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      response_format: { type: "json_object" },
      messages: [
        {
          role: "system",
          content: `You are an assistant that extracts actionable tasks and calendar events from voice transcripts, and also detects requests to remove existing items.
Today's date is ${today}. Use this as the authoritative "today" when resolving relative dates.
Return JSON only with this schema:
{
  "tasks": [{ "title": string, "dueDate": "YYYY-MM-DD", "description": string, "category": "protect"|"progress"|"maintain"|"flourish" }],
  "events": [{ "title": string, "start": "ISO 8601", "end": "ISO 8601", "description": string }],
  "deletions": [{ "id": string, "type": "task"|"event", "title": string }]
}

Rules for ADDITIONS:
- "Meeting with X" or time-specific activities → event
- "Assignment due" 
[truncated — 36083 more characters]
```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    host: true,
    allowedHosts: true,
    proxy: {
      "/api": {
        target: "http://localhost:5001",
        changeOrigin: true,
      },
    },
  },
})

```

### eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
    <title>treehacks-2026</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

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