# Project export: Companion AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Feeling unsafe walking/riding alone? Our app puts a context-aware, proactive AI companion in your hand to provide reassurance and notify emergency contacts if anything goes wrong.
- Devpost: https://devpost.com/software/companionai-8t3fk1
- GitHub: https://github.com/uvadhar11/ai-companion-app
- Demo: https://www.figma.com/slides/YloESfH6jAv7DRbomKLaY4/CompanionAI-Pitch-Deck?node-id=1-44&t=2wyVi0ERQatlSWPg-0
- Video: https://www.youtube.com/embed/DGR3XkWAHnc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Umang Vadhar (33 commits), Nicholas (12 commits), Vy Nguyen (11 commits), Andrew Bustos (3 commits)

## Devpost submission (written by the team)

### Inspiration

Imagine a loved one: your daughter, sister, or friend, walking home alone at night. The streetlights are too far apart, the silent atmosphere feels tense, and they grip their phone tightly like it’s the only thing keeping them safe. This fear isn't just a feeling; it's a daily reality. More than 50% of women in the United States are afraid of walking alone at night, according to Gallup's 2023 annual crime poll. We conducted a survey with women around us: our mom, friends, teachers, colleagues, ... about their experience taking an Uber ride or walking home alone. In those situations, they usually let someone else where know where they are at or talk on the phone. But ... what if you don't have someone to talk to? We’ve all seen those Tik-Toks of fake calls people play on speaker to feel safer just something to break the silence and let others know they’re not alone. So, we were so inspired to build CompanionAI who can talk to you on every of your ride or walk alone. It is companion that doesn't just wait for the worst to happen, but actively works to prevent it by providing a sense of presence and safety.

### What it does

Companion AI is a proactive AI safety companion that provides a real-time, reassuring voice presence for anyone feeling unsafe on a solo journey. Here are the features our app support: Voice companion: We created four unique personas that users can talk to, each with their own personality and style of conversation. 1) Mom: Soft-spoken, warm & calming. She checks in on you and is here for you. 2) Dad: Supportive & protective. He makes sure you feel safe and never alone on the road. 3) Sophie: Chill & talkative. She’ll distract you with fun topics and honest girl talk. 4) Nick: Relaxed, funny, keeps it light. He’ll talk about anything to help you feel more at ease. Voice companion: We created four unique personas that users can talk to, each with their own personality and style of conversation. 1) Mom: Soft-spoken, warm & calming. She checks in on you and is here for you. 2) Dad: Supportive & protective. He makes sure you feel safe and never alone on the road. 3) Sophie: Chill & talkative. She’ll distract you with fun topics and honest girl talk. 4) Nick: Relaxed, funny, keeps it light. He’ll talk about anything to help you feel more at ease. Two-way conversation: Our companion can hold natural, two-way conversation to directly counters our users' feeling of being a vulnerable target, making their solo journeys feel safer. The companion is fine-tuned to ask follow-up questions instead of only responding. Two-way conversation: Our companion can hold natural, two-way conversation to directly counters our users' feeling of being a vulnerable target, making their solo journeys feel safer. The companion is fine-tuned to ask follow-up questions instead of only responding. Context-aware support: Our companion is fine-tuned to ask questions such as "Are you walking or driving?", "Is the street dark?", ... to understand your situation and provide relevant. It is also refined to provide common-sense safety advice, such as "Let's have you take one earbud out to stay aware of your surroundings." Context-aware support: Our companion is fine-tuned to ask questions such as "Are you walking or driving?", "Is the street dark?", ... to understand your situation and provide relevant. It is also refined to provide common-sense safety advice, such as "Let's have you take one earbud out to stay aware of your surroundings." Customization: Users can provide personal details to help the AI companion have more natural conversations. Users can specify a "Safe Word/Phrase" to be immediately transferred to their emergency contact by the AI. Each companion can have a different emergency contact assigned for flexibility. Customization: Users can provide personal details to help the AI companion have more natural conversations. Users can specify a "Safe Word/Phrase" to be immediately transferred to their emergency contact by the AI. Each companion can have a different emergency contact assigned for flexibility. Live location: The app shows the live location and route from current location and final destination, for both driving and walking mode, with specific steps the user takes. Live location: The app shows the live location and route from current location and final destination, for both driving and walking mode, with specific steps the user takes.

### How we built it

We built CompanionAI in 24 hours with a focus on creating a responsive and empathetic voice experience. Voice AI Platform: We chose Vapi as the backbone of our project for a real-time conversational agent that could respond with minimal delay, which is essential for a safety application. Voice AI Platform: We chose Vapi as the backbone of our project for a real-time conversational agent that could respond with minimal delay, which is essential for a safety application. AI & Personas: We used GPT-4.0 through Vapi to engineer system prompts to ensure the different personas were supportive, relatable, and context-aware. AI & Personas: We used GPT-4.0 through Vapi to engineer system prompts to ensure the different personas were supportive, relatable, and context-aware. Frontend: The application was built using React Native with Expo and TypeScript for a cross-platform development. Frontend: The application was built using React Native with Expo and TypeScript for a cross-platform development.

### Challenges we ran into

Tuning the AI persona: This is our biggest challenge throughout the project. Initially, our agent was too passive. It would give one-word reassurances but wouldn't engage. We had to iteratively redesign our prompts to transform it from a simple chatbot into an active listener that could gently probe for context. We learned that the difference between a bot and a companion lies in its ability to ask the right, simple question at the right time. Tuning the AI persona: This is our biggest challenge throughout the project. Initially, our agent was too passive. It would give one-word reassurances but wouldn't engage. We had to iteratively redesign our prompts to transform it from a simple chatbot into an active listener that could gently probe for context. We learned that the difference between a bot and a companion lies in its ability to ask the right, simple question at the right time. Ensuring the conversation felt immediate: When someone’s feeling anxious or unsafe, even a small delay can make a big difference. We aimed to fine-tune our prompts and use Vapi to create a flowing and natural interaction. Ensuring the conversation felt immediate: When someone’s feeling anxious or unsafe, even a small delay can make a big difference. We aimed to fine-tune our prompts and use Vapi to create a flowing and natural interaction. Technical difficulties: We encountered many bugs along the way, such as npm package incompatibility, map and route rendering, ... But we were able to resolve most of them thanks to our amazing team! Technical difficulties: We encountered many bugs along the way, such as npm package incompatibility, map and route rendering, ... But we were able to resolve most of them thanks to our amazing team!

### Accomplishments we're proud of

We are proud of creating an AI that is able to hold conversations tuned to safety and feels comforting. Being able to build a context-aware companion that moves beyond generic replies ("You've got this") to specific advice ("Is the street you are walking quiet or busy?") was a huge accomplishment for our team. It's the feature that makes us believe Companion AI a practical safety tool.

### What we learned

We learned from others around us that the fear of walking home or riding an Uber alone at night is something they deal with often, and it has not gone away. We were incredibly happy to learn that there are features and products out there dedicating to this cause, and that we could contribute our own ideas to this important space. This hackathon taught us that voice AI is a profoundly empathetic medium. The tone, pacing, and wording of an AI's response can have a real emotional impact on the listern. We also learned that the art of prompt engineering is crucial in tuning a friendly and relatable companion.

### What's next

The journey is just beginning. Our next steps are focused on making Companion AI an even more robust safety utility: Emergency contact integration: Automatically sending alerts with location data to pre-selected contacts when a safety phrase is detected. Proactive mapping: Integrating with mapping APIs to suggest safer routes based on lighting, time of day, and user-reported data. Expanding personas: Adding more voice and persona options to allow users to choose the companion that makes them feel most comfortable. See our business pitch/validity here: https://youtu.be/25wx4AbB6Wg

## README (from the GitHub repository)

# Welcome to your Expo app 👋

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).

## Get started

1. Install dependencies

   ```bash
   npm install
   ```

2. Start the app

   ```bash
   npx expo start
   ```

In the output, you'll find options to open the app in a

- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## Get a fresh project

When you're ready, run:

```bash
npm run reset-project
```

This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.

## Learn more

To learn more about developing your project with Expo, look at the following resources:

- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.

## Join the community

Join our community of developers creating universal apps.

- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.


## Detected evidence (automated analysis)

Indexed codebase: 41 recognized source files, 156 KB.
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (46 of 46)

```
.gitignore
.vscode/settings.json
app.json
app/_layout.tsx
app/(tabs)/_layout.tsx
app/(tabs)/companionScreen.tsx
app/(tabs)/historyScreen.tsx
app/(tabs)/index.tsx
app/(tabs)/profileScreen.tsx
app/+not-found.tsx
app/customize/[id].tsx
app/directions/index.tsx
app/modal.tsx
components/Collapsible.tsx
components/ExternalLink.tsx
components/HapticTab.tsx
components/HelloWave.tsx
components/other-pages/explore.tsx
components/other-pages/WelcomeScreen.tsx
components/ParallaxScrollView.tsx
components/PlaceAutocomplete.tsx
components/ThemedText.tsx
components/ThemedView.tsx
components/ui/IconSymbol.ios.tsx
components/ui/IconSymbol.tsx
components/ui/TabBarBackground.ios.tsx
components/ui/TabBarBackground.tsx
constants/Colors.ts
eslint.config.js
hooks/useColorScheme.ts
hooks/useColorScheme.web.ts
hooks/useFrameworkReady.ts
hooks/useStorage.ts
hooks/useThemeColor.ts
package.json
pkgVoiceModels/conversation_contexts.py
pkgVoiceModels/main.py
pkgVoiceModels/make_call.py
pkgVoiceModels/server.py
pkgVoiceModels/voice_setup.py
README.md
scripts/reset-project.js
services/GoogleMapsService.ts
services/StorageService.ts
tsconfig.json
types/storage.ts
```

### Dependencies

- package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.1.0, @react-native-async-storage/async-storage@^2.1.0, @react-native-community/cli@latest, @react-navigation/bottom-tabs@^7.3.10, @react-navigation/elements@^2.3.8, @react-navigation/native@^7.1.6, @types/react@~19.0.10, @types/react-native@^0.72.8, eslint@^9.25.0, eslint-config-expo@~9.2.0, expo@~53.0.12, expo-blur@~14.1.5, expo-constants@~17.1.6, expo-contacts@~14.2.5, expo-font@~13.3.1, expo-haptics@~14.1.4, expo-image@~2.3.0, expo-linking@~7.1.5, expo-location@~18.1.5, expo-router@~5.1.0, expo-splash-screen@~0.30.9, expo-status-bar@~2.2.3, expo-symbols@~0.4.5, expo-system-ui@~5.0.9, expo-web-browser@~14.2.0, react@19.0.0, react-dom@19.0.0, react-native@0.79.4, react-native-gesture-handler@~2.24.0, react-native-maps@1.20.1, react-native-reanimated@~3.17.4, react-native-safe-area-context@5.4.0, react-native-screens@~4.11.1, react-native-web@~0.20.0, react-native-webview@13.13.5, typescript@~5.8.3

### Recent commits (newest first)

- correct endpoint
- fix outgoing call
- use umangs token
- fix: move Vapi client initialization to the main block
- feat: implement outbound call functionality
- Finalized commit
- Resolve conflict
- convo context for mom
- conversation contexts
- Merge branch 'main' of https://github.com/uvadhar11/ai-companion-app
- Add backend API to update context
- Add automatic route calculation when destination is selected
- resolve merge with saving stuff
- add save functionality for database stuff
- Add Auto-Save to Profile Basic Information
- Implement Persistent Data Storage System
- resolve conflict with profile screen
- better profile settings with contact selection, and some other changes
- Updated package-lock.json
- Updated package-lock.json

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

### package.json

```
{
  "name": "my-app",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.1.0",
    "@react-native-async-storage/async-storage": "^2.2.0",
    "@react-navigation/bottom-tabs": "^7.3.10",
    "@react-navigation/elements": "^2.3.8",
    "@react-navigation/native": "^7.1.6",
    "@react-native-async-storage/async-storage": "^2.1.0",
    "expo": "~53.0.12",
    "expo-blur": "~14.1.5",
    "expo-constants": "~17.1.6",
    "expo-contacts": "~14.2.5",
    "expo-font": "~13.3.1",
    "expo-haptics": "~14.1.4",
    "expo-image": "~2.3.0",
    "expo-linking": "~7.1.5",
    "expo-location": "~18.1.5",
    "expo-router": "~5.1.0",
    "expo-splash-screen": "~0.30.9",
    "expo-status-bar": "~2.2.3",
    "expo-symbols": "~0.4.5",
    "expo-system-ui": "~5.0.9",
    "expo-web-browser": "~14.2.0",
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-native": "0.79.4",
    "react-native-gesture-handler": "~2.24.0",
    "react-native-maps": "1.20.1",
    "react-native-reanimated": "~3.17.4",
    "react-native-safe-area-context": "5.4.0",
    "react-native-screens": "~4.11.1",
    "react-native-web": "~0.20.0",
    "react-native-webview": "13.13.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@react-native-community/cli": "latest",
    "@types/react": "~19.0.10",
    "@types/react-native": "^0.72.8",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~9.2.0",
    "typescript": "~5.8.3"
  },
  "private": true
}

```

### app/_layout.tsx

```typescript
import {
  DarkTheme,
  DefaultTheme,
  ThemeProvider,
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';

import { useColorScheme } from '@/hooks/useColorScheme'
import { useFrameworkReady } from '@/hooks/useFrameworkReady';

export default function RootLayout() {
  useFrameworkReady();
  const colorScheme = useColorScheme();
  const [loaded] = useFonts({
    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
  });

  if (!loaded) {
    // Async font loading only occurs in development.
    return null;
  }

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name='(tabs)' options={{ headerShown: false }} />
        <Stack.Screen name='+not-found' />
        <Stack.Screen
          name='modal'
          options={{
            presentation: 'modal',
          }}
        />
      </Stack>
      <StatusBar style='dark' />
    </ThemeProvider>
  );
}
```

### pkgVoiceModels/server.py

```python
from flask import Flask, request
from conversation_contexts import PERSONAS
from make_call import make_outbound_call
from vapi import Vapi
import voice_setup
import os

app = Flask(__name__)

ADDITIONAL_CONTEXT = {}

PERMENANT_ID = os.getenv("PERMANENT_ID")
VAPI_API_KEY = os.getenv("VAPI_TOKEN")
UMANG_PHONE_ID = os.getenv("UMANG_PHONE_ID")
UMANG_API_KEY = os.getenv("UMANG_VAPI_TOKEN")


@app.route('/update_context', methods=['POST'])
def update_context():
    print("Endpoint /update_context was hit")
    data = request.json
    context = data.get('context')
    voice = data.get('voice', 'female_friend')

    if context:
        print("Received context:", context)
        if voice in PERSONAS:
            ADDITIONAL_CONTEXT[voice] = context
            full_context = f"{PERSONAS[voice]}\n* Additional user context:\n- {context}"
            PERSONAS[voice] = full_context
            print("Updated persona context:\n", full_context)
        else:
            print("Voice not found in PERSONAS:", voice)

        return {'message': 'Context received'}, 200
    else:
        return {'error': 'No context provided'}, 400


@app.route('/outbound_call', methods=['POST'])
def outbound_call():
    print("Endpoint /outbound_call was hit")
    data = request.json
    number = data.get('number')

    if not number:
        return {'error': 'No phone number provided'}, 400
    print("Received phone number:", number)
    try:
        vapi_assistant_id = voice_setup.get_existing_agent_id(
            vapi_client, PERMENANT_ID)

        call = make_outbound_call(
            vapi_client, vapi_assistant_id, number, UMANG_PHONE_ID)
        return {'message': 'Call initiated', 'call_id': call.id}, 200
    except Exception as e:
        print("Error making outbound call:", e)
        return {'error': str(e)}, 500


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000, debug=True)
    vapi_client = Vapi(token=UMANG_API_KEY)

```

### pkgVoiceModels/main.py

```python
import os
from dotenv import load_dotenv
from vapi import Vapi 
import voice_setup
import make_call
from conversation_contexts import PERSONAS

load_dotenv() 

VAPI_API_KEY = os.getenv("VAPI_TOKEN")
VAPI_PHONE_ID = os.getenv("PHONE_ID")
PERMENANT_ID = os.getenv("PERMANENT_ID")

UMANG_API_KEY = os.getenv("UMANG_VAPI_TOKEN")
UMANG_PHONE_ID = os.getenv("UMANG_PHONE_ID")
UMANG_PERMENANT_ID = os.getenv("UMANG_PERMENANT_ID")



all_voices = {"mom": "jennifer",
              "dad": "chris",
              "female_friend": "melissa",
              "male_friend": "elliot",
              } 


voice = "mom"   #pick voice here. Must be a dictionary key that exists above in 'all_voices'

#voice_agent = all_voices[voice]     
voice_agent = all_voices.get(voice)

conversation_context = voice_setup.change_convo_context(voice)
#print(conversation_context)

#persona = voice_agents["Dad"]
#convo_context = voice_setup.change_convo_context(persona)  #changes conversation context based on voice_agent.

safe_phrase = "Do you want to get pizza?"
phone_number = "+12095020198"
umang_phone_number = "+12799778354"

if __name__ == "__main__":
    #creates a vapi client object using vapi token.
    vapi_client = Vapi(token=UMANG_API_KEY)  

    #retrieves the entire vapi object in json format.
    #vapi_assistant_object = voice_setup.get_existing_agent_object(vapi_client, UMANG_PERMENANT_ID)

    #finds an existing vapi assistant from the vapi token in .env file.
    vapi_assistant_id = voice_setup.get_existing_agent_id(vapi_client, UMANG_PERMENANT_ID)

    #updates the vapi assistants conversation context, persona (voice), safe phrase, and emergency contact phone number.
    voice_assistant = voice_setup.update_agent(conversation_context, voice_agent, vapi_client, vapi_assistant_id, safe_phrase, umang_phone_number)

    #THE LINE BELOW IS WORKING CODE TO MAKE OUTGOING CALLS, WE HAVE 9 LEFT, USE SPARINGLY
    #make_call.make_outbound_call(vapi_client, vapi_assistant_id, umang_phone_number, UMANG_PHONE_ID)


```

### app/(tabs)/_layout.tsx

```typescript
import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Tabs } from "expo-router";
import React from "react";
import { Platform } from "react-native";

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: Platform.select({
          ios: {
            // Use a transparent background on iOS to show the blur effect
            position: "absolute",
          },
          default: {},
        }),
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="house.fill" color={color} />
          ),
        }}
      />

      <Tabs.Screen
        name="companionScreen"
        options={{
          title: "Call AI",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="phone.fill" color={color} />
          ),
        }}
      />

      <Tabs.Screen
        name="historyScreen"
        options={{
          title: "History",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="clock.fill" color={color} />
          ),
        }}
      />

      <Tabs.Screen
        name="profileScreen"
        options={{
          title: "Profile",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="person.fill" color={color} />
          ),
        }}
      />

    </Tabs>
  );
}

```

### app/directions/index.tsx

```typescript
import {
  View,
  Text,
  TouchableOpacity,
  ScrollView,
  StyleSheet,
  Dimensions,
} from "react-native";
import { useLocalSearchParams, router, Stack } from "expo-router";

const { height } = Dimensions.get("window");

export default function DirectionsScreen() {
  const { routeInfo: routeInfoString, routeMode } = useLocalSearchParams();
  const routeInfo = routeInfoString
    ? JSON.parse(routeInfoString as string)
    : null;

  const getManeuverIcon = (maneuver: string): string => {
    const icons: { [key: string]: string } = {
      "turn-left": "↰",
      "turn-right": "↱",
      "turn-slight-left": "↖",
      "turn-slight-right": "↗",
      "turn-sharp-left": "↺",
      "turn-sharp-right": "↻",
      "uturn-left": "↶",
      "uturn-right": "↷",
      merge: "⤴",
      "fork-left": "↖",
      "fork-right": "↗",
      ferry: "⛴",
      "roundabout-left": "↺",
      "roundabout-right": "↻",
      straight: "↑",
    };
    return icons[maneuver] || "↑";
  };

  if (!routeInfo) {
    return (
      <View style={styles.container}>
        <Text style={styles.headerTitle}>No directions found.</Text>
        <TouchableOpacity
          onPress={() => router.back()}
          style={styles.backButton}
        >
          <Text style={styles.backButtonText}>← Back</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <>
      <Stack.Screen
        options={{
          headerShown: false,
        }}
      />
      <View style={styles.container}>
        <View style={styles.header}>
          <TouchableOpacity
            onPress={() => router.back()}
            style={styles.backButton}
          >
            <Text style={styles.backButtonText}>← Back</Text>
          </TouchableOpacity>
          <Text style={styles.headerTitle}>Directions</Text>
          <View style={{ width: 40 }} />
        </View>
        <View style={styles.routeStats}>
          <Text style={styles.routeDistance}>{routeInfo.distance}</Text>
          <Text style={styles.routeDuration}>
            <Text>{routeInfo.duration} • </Text>
            <Text>{routeMode === "walking" ? "Walking" : "Driving"}</Text>
          </Text>
        </View>
        <ScrollView
          style={styles.directionsContainer}
          showsVerticalScrollIndicator={false}
        >
          {routeInfo.steps.map((step: any, index: number) => (
            <View key={index} style={styles.directionStep}>
              <View style={styles.stepIcon}>
                <Text style={styles.stepIconText}>
                  {getManeuverIcon(step.maneuver)}
                </Text>
              </View>
              <View style={styles.stepInfo}>
                <Text style={styles.stepInstruction}>{step.instruction}</Text>
                <Text style={styles.stepDetails}>
                  <Text>{step.distance} • </Text>
                  <Text>{step.duration}</Text>
                </Text>
              </View>
            </View>
          ))}
        </ScrollView>
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f8f9fa",
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    paddingHorizontal: 20,
    paddingVertical: 15,
    backgroundColor: "white",
    borderBottomWidth: 1,
    borderBottomColor: "#e5e7eb",
  },
  backButton: {
    padding: 5,
  },
  backButtonText: {
    paddingTop: 40,
    color: "#1e40af",
    fontSize: 16,
    fontWeight: "500",
  },
  headerTitle: {
    paddingTop: 40,
    fontSize: 18,
    alignSelf: "center",
    fontWeight: "bold",
    color: "#1f2937",
  },
  routeStats: {
    paddingHorizontal: 20,
    paddingVertical: 15,
    backgroundColor: "white",
    borderBottomWidth: 1,
    borderBottomColor: "#f3f4f6",
  },
  routeDistance: {
    fontSize: 18,
    fontWeight: "bold",
    color: "#1f2937",
  },
  routeDuration: {
    fontSize: 14,
    color: "#6b7280",
    marginTop: 2,
  },
  directionsContainer: {
    maxHeight: height * 0.7,
    paddingHorizontal: 20,
  },
  directionStep: {
    flexDirection: "row",
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: "#f3f4f6",
    alignItems: "flex-start",
  },
  stepIcon: {
    width: 32,
    height: 32,
    borderRadius: 16,
    backgroundColor: "#f3f4f6",
    justifyContent: "center",
    alignItems: "center",
    marginRight: 15,
    marginTop: 2,
  },
  stepIconText: {
    fontSize: 16,
    color: "#1e40af",
  },
  stepInfo: {
    flex: 1,
  },
  stepInstruction: {
    fontSize: 15,
    color: "#1f2937",
    fontWeight: "500",
    marginBottom: 4,
  },
  stepDetails: {
    fontSize: 13,
    color: "#6b7280",
  },
});
```

### app/(tabs)/index.tsx

```typescript
"use client";
import { useState, useEffect, useRef } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  SafeAreaView,
  Alert,
  ScrollView,
  ActivityIndicator,
  Dimensions,
  FlatList,
  Keyboard,
} from "react-native";
import MapView, { Marker, Polyline } from "react-native-maps";
import * as Location from "expo-location";
import { router } from "expo-router";

const { width, height } = Dimensions.get("window");

// You'll need to get this from Google Cloud Console
const GOOGLE_MAPS_API_KEY = process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY;

// interface MapScreenProps {
//   onNavigateBack: () => void;
// }

interface RouteStep {
  instruction: string;
  distance: string;
  duration: string;
  maneuver: string;
  startLocation: { lat: number; lng: number };
  endLocation: { lat: number; lng: number };
}

interface RouteInfo {
  distance: string;
  duration: string;
  steps: RouteStep[];
  coordinates: { latitude: number; longitude: number }[];
  bounds: {
    northeast: { lat: number; lng: number };
    southwest: { lat: number; lng: number };
  };
}

interface PlacePrediction {
  place_id: string;
  description: string;
  structured_formatting: {
    main_text: string;
    secondary_text: string;
  };
}

export default function MapScreen(/*{ onNavigateBack }: MapScreenProps*/) {
  const [currentLocation, setCurrentLocation] = useState<{
    latitude: number;
    longitude: number;
  } | null>(null);
  const [destination, setDestination] = useState("");
  const [destinationCoords, setDestinationCoords] = useState<{
    latitude: number;
    longitude: number;
  } | null>(null);
  const [routeMode, setRouteMode] = useState<"driving" | "walking">("driving");
  const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
  const [loading, setLoading] = useState(false);
  const [showDirections, setShowDirections] = useState(false);
  const [locationLoading, setLocationLoading] = useState(true);
  const [predictions, setPredictions] = useState<PlacePrediction[]>([]);
  const [showPredictions, setShowPredictions] = useState(false);
  const [predictionLoading, setPredictionLoading] = useState(false);
  const mapRef = useRef<MapView>(null);

  useEffect(() => {
    getCurrentLocation();
  }, []);

  // Debounced autocomplete search
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (destination.length > 2) {
        getPlacePredictions(destination);
      } else {
        setPredictions([]);
        setShowPredictions(false);
      }
    }, 300); // 300ms delay

    return () => clearTimeout(timeoutId);
  }, [destination]);

  // AUTO-CALCULATE ROUTE WHEN DESTINATION IS SET OR ROUTE MODE CHANGES
  useEffect(() => {
    if (currentLocation && destinationCoords && !loading) {
      calculateRoute();
    }
  }, [currentLocation, destinationCoords, routeMode]);

  const getCurrentLocation = async () => {
    try {
      setLocationLoading(true);
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== "granted") {
        Alert.alert(
          "Permission denied",
          "Location permission is required for navigation"
        );
        return;
      }

      const location = await Location.getCurrentPositionAsync({
        accuracy: Location.Accuracy.High,
      });

      setCurrentLocation({
        latitude: location.coords.latitude,
        longitude: location.coords.longitude,
      });
    } catch (error) {
      Alert.alert("Error", "Failed to get current location");
      console.error("Location error:", error);
    } finally {
      setLocationLoading(false);
    }
  };

  const getPlacePredictions = async (input: string) => {
    if (!GOOGLE_MAPS_API_KEY) {
      return;
    }

    try {
      setPredictionLoading(true);
      let url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(
        input
      )}&key=${GOOGLE_MAPS_API_KEY}`;

      // Add location bias if current location is available
      if (currentLocation) {
        url += `&location=${currentLocation.latitude},${currentLocation.longitude}&radius=50000`;
      }

      const response = await fetch(url);
      const data = await response.json();

      if (data.status === "OK") {
        setPredictions(data.predictions);
        setShowPredictions(true);
      } else {
        setPredictions([]);
        setShowPredictions(false);
      }
    } catch (error) {
      console.error("Places API error:", error);
      setPredictions([]);
      setShowPredictions(false);
    } finally {
      setPredictionLoading(false);
    }
  };

  const selectPrediction = async (prediction: PlacePrediction) => {
    setDestination(prediction.description);
    setShowPredictions(false);
    setPredictions([]);
    Keyboard.dismiss();

    // Get place details to get coordinates
    try {
      const coords = await getPlaceDetails(prediction.place_id);
      if (coords) {
        setDestinationCoords(coords);
        // Route will be automatically calculated by the useEffect above
      }
    } catch (error) {
      console.error("Error getting place details:", error);
    }
  };

  const getPlaceDetails = async (placeId: string) => {
    try {
      const response = await fetch(
        `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=geometry&key=${GOOGLE_MAPS_API_KEY}`
      );
      const data = await response.json();

      if (data.status === "OK" && data.result.geometry) {
        const location = data.result.geometry.location;
        return {
          latitude: location.lat,
          longitude: location.lng,
        };
      }
      return null;
    } catch (error) {
      console.error("Place details error:", error);
      return null;
    }
  };

  const geocodeDestination = async (address: string) => {
    try {
      const response = await fetch(
        `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
          ad
[truncated — 19788 more characters]
```

### eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### hooks/useColorScheme.ts

```typescript
export { useColorScheme } from 'react-native';

```

### hooks/useFrameworkReady.ts

```typescript
import { useEffect } from 'react';

declare global {
  interface Window {
    frameworkReady?: () => void;
  }
}

export function useFrameworkReady() {
  useEffect(() => {
    window.frameworkReady?.()
  })
}
```

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