# Project export: AngelShot

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: Natural conversation, safer situation.
- Devpost: https://devpost.com/software/angelshot
- GitHub: https://github.com/mikieyx/SafeTalk
- Demo: https://angelshot.co/
- Result: winner (Vapi: Show us your Voice AI)
- Team: 4 GitHub contributor(s) — Mauricio Curiel (50 commits), Steven Le (46 commits), NotMyPersonalAccount (43 commits), mikieyx (10 commits)

## Devpost submission (written by the team)

### Inspiration

Whether it's at a college party, a public outing, or during a simple stroll downtown, individuals often find themselves in uncomfortable, unwanted situations due to unwanted attention. In these moments, a discreet way to seek help is crucial. In the bartending world, this concept is known as an "angel shot"—a code word or drink order that discreetly signals to staff that a customer needs assistance. Phone calls are a powerful tool in these uncomfortable situations, as they not only deter unwanted attention by creating an external conversation but also offer a lifeline to contact emergency services or trusted individuals. Thus, they server as angel shots outside of a bartending context. But what if no one is available to answer the call? How can individuals ensure they'll have someone to talk to when they need help the most?

### What it does

AngelShot simulates a realistic phone call a variety of user-customizable AI-based assistants. Users will pre-define assistants that they can request a call from whenever they're placed in an unwanted situation. These assistants can take on roles; for instance, user's can create an assistant meant to be "an uncle that they haven't seen in awhile". Additionally, assistants can be given a conversation starter. This could range from topics like sports, gardening, etc... anything that the user will feel comfortable talking about in an uncomfortable situation. When an individual is in an uncomfortable situation, they can request a call from any of their created assistants to start a normal conversation. However, with each response, the assistant provides the user two discreet, context-based code words. These code words trigger pre-configured safety actions of two levels. For example, in a gardening-themed conversation, the assistant may provide the words "monstera" and "weeding". If the user says the first keyword "monstera" in their response, the assistant will know to share the user’s live conversation with emergency contacts. If the user says the second keyword "weeding" in their response, the assistant will know to forward you to emergency services instantly.

### How we built it

We deployed a Next.js application on Vercel, written in Typescript and styled using Tailwind + Shadcn and a variety of frontend libraries. For authentication, we used Clerk to allow users to quickly signup using their phone numbers. As a means of handling phone communication, we utilized VAPI's API to efficiently create customizable AI assistants. VAPI streamlined the integration of voice communication in our application, allowing us to simulate realistic phone calls with AI-based assistants. For speech-to-text functionality, we used a Deepgram's Nova 2 Phonecall Model, specifically tailored for low-bit phone calls. This ensures accurate transcription, even if users are calling from remote areas or in noisy environments, guaranteeing that conversations and safety triggers are captured correctly. Then, to simulate natural and context-aware dialogue, we used OpenAI's GPT-4 model. Using VAPI's API, we passed a system prompt to ensure the AI assistant can generate relevant conversations, generate two context-specific code words, and react appropriately via function calls if any of the code words are spoken. Lastly, for text-to-speech conversion, we chose ElevenLabs' models to create high-quality, natural-sounding voices for the AI assistants, enhancing the realism and comfort of the simulated calls.

### Challenges we ran into

Our entire team didn't have WiFi for essentially half the event, so we spent the first half of the event ideating. The last half of the event was when our application truly came to life. Another issue that we ran into was correctly prompt engineering the virtual assistant. Once we found the right prompts, it was smooth sailing.

### Accomplishments we're proud of

We're proud of developing a discreet safety tool that could potentially save lives. Integrating customizable AI assistants and creating a reliable emergency response system were key milestones that we were able to accomplish.

### What's next

We plan to enhance AngelShot with more customization options, additional safety features such as sharing location and real time stress level analysis. We also aim to improve accessibility, perhaps making the application into a mobile app using technologies like React Native.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

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

## Codebase structure (from repository index)

### Files (49 of 49)

```
.eslintrc.json
.gitignore
actions/assistant.ts
actions/emergency_contact.ts
app/api/(dbServerActions)/mongoActions.ts
app/api/callContact/[contactid]/route.ts
app/api/endCall/route.ts
app/api/notifyContact/[user_phone_number]/route.ts
app/api/vapiAgentUtils.ts
app/call/[id]/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
components.json
components/AddAssistantForm.tsx
components/AddContactForm.tsx
components/Assistants.tsx
components/AssistantsList.tsx
components/Calls.tsx
components/CallsList.tsx
components/Contacts.tsx
components/ContactsList.tsx
components/OngoingCalls.tsx
components/pdfGenerator.tsx
components/ui/alert-dialog.tsx
components/ui/button.tsx
components/ui/command.tsx
components/ui/dialog.tsx
components/ui/form.tsx
components/ui/input.tsx
components/ui/label.tsx
components/ui/phone-input.tsx
components/ui/popover.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/table.tsx
components/ui/toast.tsx
components/ui/toaster.tsx
hooks/use-toast.ts
lib/prisma.ts
lib/utils.ts
middleware.ts
next.config.mjs
package.json
postcss.config.mjs
prisma/schema.prisma
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @clerk/nextjs@^5.7.5, @hookform/resolvers@^3.9.0, @prisma/client@5.21.1, @radix-ui/react-alert-dialog@^1.1.2, @radix-ui/react-dialog@^1.1.2, @radix-ui/react-icons@^1.3.0, @radix-ui/react-label@^2.1.0, @radix-ui/react-popover@^1.1.2, @radix-ui/react-scroll-area@^1.2.0, @radix-ui/react-select@^2.1.2, @radix-ui/react-slot@^1.1.0, @radix-ui/react-toast@^1.2.2, @types/node@^20, @types/react@^18, @types/react-dom@^18, class-variance-authority@^0.7.0, clsx@^2.1.1, cmdk@1.0.0, eslint@^8, eslint-config-next@14.2.15, jspdf@^2.5.2, lucide-react@^0.453.0, next@14.2.15, postcss@^8, prisma@^5.21.1, react@^18, react-dom@^18, react-hook-form@^7.53.1, react-phone-number-input@^3.4.9, tailwind-merge@^2.5.4, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5, zod@^3.23.8

### Recent commits (newest first)

- remove dayjs
- Merge branch 'main' of github.com:mikieyx/SafeTalk
- use local time
- testing in prod lets go
- fixed notified fields, changed them to datetimes
- links again oops
- pdf formatting
- update prompting and say nine one one
- removed ngrok links
- finalize style
- add ability to create pdf
- add calls section to home page and link to call page
- add call
- Merge pull request #28 from mikieyx/transcript_stuff
- formatting + added the other fields
- add summary and transcript to calls
- cleanup content field
- changed inital prompt so AI sounds more natural
- Merge pull request #27 from mikieyx/fix_prod_again
- removed all websocket stuff

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

### package.json

```
{
  "name": "safetalk",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "prisma generate && next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@clerk/nextjs": "^5.7.5",
    "@hookform/resolvers": "^3.9.0",
    "@prisma/client": "5.21.1",
    "@radix-ui/react-alert-dialog": "^1.1.2",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-popover": "^1.1.2",
    "@radix-ui/react-scroll-area": "^1.2.0",
    "@radix-ui/react-select": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "cmdk": "1.0.0",
    "jspdf": "^2.5.2",
    "lucide-react": "^0.453.0",
    "next": "14.2.15",
    "react": "^18",
    "react-dom": "^18",
    "react-hook-form": "^7.53.1",
    "react-phone-number-input": "^3.4.9",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15",
    "postcss": "^8",
    "prisma": "^5.21.1",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### app/page.tsx

```typescript
import Assistants from "@/components/Assistants";
import Calls from "@/components/Calls";
import Contacts from "@/components/Contacts";
import prisma from "@/lib/prisma";
import { currentUser } from "@clerk/nextjs/server";

export default async function Home() {
  const user = await currentUser();

  if (!user) {
    return null;
  }

  const phoneNumber = user.primaryPhoneNumber!.phoneNumber;
  let dbUser = await prisma.user.findUnique({
    where: {
      phone_number: phoneNumber,
    },
    include: {
      emergency_sending: true,
      assistants: true,
    },
  });
  if (!dbUser) {
    dbUser = await prisma.user.create({
      data: {
        phone_number: phoneNumber,
        firstName: user.firstName!,
        lastName: user.lastName!,
      },
      include: {
        emergency_sending: true,
        assistants: true,
      },
    });
  }
  const calls = await prisma.call.findMany({
    where: {
      user_phone_number: phoneNumber,
    },
  });

  return (
    <div className="space-y-8 my-8">
      <Contacts contacts={dbUser.emergency_sending} />
      <Assistants assistants={dbUser.assistants} />
      <Calls calls={calls} />
    </div>
  );
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import Image from "next/image";
import localFont from "next/font/local";
import "./globals.css";
import {
  ClerkProvider,
  RedirectToSignIn,
  SignedIn,
  SignedOut,
  UserButton,
} from "@clerk/nextjs";
import { Toaster } from "@/components/ui/toaster";
import OngoingCalls from "@/components/OngoingCalls";
import { Suspense } from "react";
import Link from "next/link";

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

export const metadata: Metadata = {
  title: "AngelShot",
  description: "Natural conversation, safer situation.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gradient-to-r from-yellow-100 to-pink-200`}
        >
          <SignedOut>
            <RedirectToSignIn />
          </SignedOut>
          <SignedIn>
            <div className="flex justify-between items-center px-4 md:px-32 h-20 shadow-md">
              <Link href="/">
                <Image
                  className="w-64 h-auto transition-transform duration-300 transform hover:scale-110"
                  src="/AngelShotText.png"
                  alt="AngelShot"
                  width={200}
                  height={100}
                />
              </Link>
              <UserButton />
            </div>
            <div>
              <Suspense>
                <OngoingCalls />
              </Suspense>
            </div>
          </SignedIn>
          {children}
          <Toaster />
        </body>
      </html>
    </ClerkProvider>
  );
}

```

### app/api/endCall/route.ts

```typescript
import { NextRequest } from "next/server";
import { endCall } from "../(dbServerActions)/mongoActions";
import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {
  return req.json()
    .then(async ({ message }) => {
      console.log("Received POST request:", message);

      const callId = message.call?.id || "";
      const summary = message.analysis?.summary || "";
      const transcript = message.transcript || "";
      const recordingUrl = message.recording_url || "";

      if (message.endedReason === "assistant-forwarded-call") {
        console.log("Updating call to be ongoing=false and authorities_notified=now");
        await prisma.call.updateMany({
          where: {
            user_phone_number: message.call.user_phone_number,
            ongoing: true,
          },
          data: {
            ongoing: false,
            authorities_notified: new Date()
        }})
      }

      return endCall(callId, summary, transcript, recordingUrl);
    })
    .then(() => {
      return new Response("Call ended successfully", { status: 200 });
    })
    .catch((error) => {
      console.error("Error processing request:", error);
      return new Response("Error processing request", { status: 500 });
    });
}

```

### app/call/[id]/page.tsx

```typescript
import prisma from "@/lib/prisma";
import { currentUser } from "@clerk/nextjs/server";
import { notFound } from "next/navigation";
import GeneratePDF from "@/components/pdfGenerator";

export default async function CallPage({
  params: { id },
}: {
  params: { id: string };
}) {
  const user = await currentUser();

  const call = await prisma.call.findUnique({
    where: {
      id,
    },
    include: {
      user: {
        include: {
          emergency_sending: true,
        },
      },
    },
  });

  if (
    !call ||
    (call.user_phone_number !== user!.primaryPhoneNumber!.phoneNumber &&
      !call.user.emergency_sending.some(
        (contact) =>
          contact.receiver_phone_number ===
          user!.primaryPhoneNumber!.phoneNumber
      ))
  ) {
    return notFound();
  }

  // Reformatting the transcript for better readability
  const formattedTranscript = call.transcript
    ? call.transcript.split("\n").map((line, index) => (
        <div key={index} className="whitespace-pre-wrap">
          {line}
        </div>
      ))
    : "No transcript available.";

  return (
    <div className="mx-4 md:mx-32 my-8 p-4 md:p-8 space-y-4 border rounded-xl bg-background">
      <div>
        <h1 className="font-bold text-lg md:text-2xl">Call Information</h1>
      </div>
      <div className="flex flex-col gap-3">
        <span>
          <span className="font-bold">Person: </span>
          {call.user_phone_number}
        </span>
        <span>
          <span className="font-bold">Start Time: </span>
          {call.start_time.toLocaleString()}
        </span>
        <span>
          <span className="font-bold">End Time: </span>
          {call.end_time ? call.end_time.toLocaleString() : "In Progress"}
        </span>
        <span>
          <span className="font-bold">Authorities Notified: </span>
          {call.authorities_notified
            ? call.authorities_notified.toLocaleString()
            : "N/A"}
        </span>
        <span>
          <span className="font-bold">Contacts Notified: </span>
          {call.contacts_notified
            ? call.contacts_notified.toLocaleString()
            : "N/A"}
        </span>
        <div className="flex space-x-2"></div>
        <span>
          <span className="font-bold">Summary:</span>
          <div className="mt-2 border-2 p-3">{call.summary}</div>
        </span>
        <span>
          <span className="font-bold">Transcript:</span>
          <div className="mt-2 border-2 p-3">{formattedTranscript}</div>
        </span>
        <span>
          <span className="font-bold">Recording:</span>
          <div className="mt-2 border-2 p-3">
            {call.recording_url ? (
              <audio controls src={call.recording_url} />
            ) : (
              "No recording available."
            )}
          </div>
        </span>
        <span>
          <div>
            <GeneratePDF call={call} />
          </div>
        </span>
      </div>
    </div>
  );
}

```

### app/api/callContact/[contactid]/route.ts

```typescript
import { type NextRequest } from "next/server";
import { getContactCallOptions } from "../../(dbServerActions)/mongoActions";
import { CallOptions, defaultOptions } from "../../vapiAgentUtils";
import { mergeJSON } from "@/lib/utils";
import prisma from "@/lib/prisma";

export async function POST(
  request: NextRequest,
  { params }: { params: { contactid: string } }
) {
  const userContactDefaultOptions = await getContactCallOptions(
    params.contactid
  );
  if (userContactDefaultOptions === null) {
    return Response.json(
      {
        error: "Assistant not found",
      },
      { status: 404 }
    );
  }

  const callParams: CallOptions = mergeJSON<CallOptions>(
    defaultOptions,
    userContactDefaultOptions
  );

  try {
    const response = await fetch("https://api.vapi.ai/call", {
      method: "POST",
      headers: {
        Authorization: String(process.env.VAPI_API_KEY),
        "Content-Type": "application/json",
      },
      body: JSON.stringify(callParams),
    });
    const data = await response.json();

    if (data.error) {
      return Response.json(
        {
          error: "Call failed. If you're in an emergency please call 911",
        },
        { status: 500 }
      );
    }

    await prisma.call.create({
      data: {
        id: data.id as string,
        user_phone_number: data.customer.number as string,
        start_time: data.createdAt as string,
      },
    });

    return Response.json({
      message: "Expect a call within the next 5 seconds",
    });
  } catch {
    return Response.json(
      {
        error: "Call failed. If you're in an emergency please call 911",
      },
      { status: 500 }
    );
  }
}

```

### app/api/notifyContact/[user_phone_number]/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { getEmergencyContacts } from "../../(dbServerActions)/mongoActions";
import { emergencyContactOptions } from "../../vapiAgentUtils";
import prisma from "@/lib/prisma";

export async function POST(
  request: NextRequest,
  { params }: { params: { user_phone_number: string } }
) {
  try {
    const requestBody = await request.json();
    console.log("Received POST request:", requestBody);

    const toolCalls = requestBody.message?.toolCalls;

    if (!toolCalls || toolCalls.length === 0) {
      return NextResponse.json(
        { error: "No tool calls found" },
        { status: 400 }
      );
    }

    const toolCall = toolCalls[0];
    const toolCallId = toolCall.id || "default_tool_call_id";
    const transcript = toolCall.transcript || "No transcript provided";

    const response = {
      results: [
        {
          toolCallId,
          result: `Processed transcript: ${transcript}`,
        },
      ],
    };

    const emergencyContacts = await getEmergencyContacts(
      params.user_phone_number
    );

    for (const emergencyContact of emergencyContacts) {
      const callParams = emergencyContactOptions(
        emergencyContact.sender.firstName,
        emergencyContact.receiver_phone_number
      );
      // text notify emergencyContact
      const status = await fetch("https://api.vapi.ai/call", {
        method: "POST",
        headers: {
          Authorization: String(process.env.VAPI_API_KEY),
          "Content-Type": "application/json",
        },
        body: JSON.stringify(callParams),
      });
      if (!status.ok) {
        console.error("Failed to notify emergency contact:", emergencyContact);
        continue;
      }
      console.log(
        "Notifying emergency contact:",
        emergencyContact.receiver_phone_number
      );
    }

    // get all ongoing calls for user with phone number
    await prisma.call.updateMany({
      where : {
        user_phone_number: params.user_phone_number,
      },
      data: {
        contacts_notified: new Date()
      }});

    // for each ongoing call set contacted_emergency_contacts to true

    console.log("Response to be sent:", response);
    return NextResponse.json(response, { status: 200 });
  } catch (error) {
    console.error("Error processing request:", error);
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    );
  }
}

```

### middleware.ts

```typescript
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
};

```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
    darkMode: ["class"],
    content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
  	extend: {
  		colors: {
  			background: 'hsl(var(--background))',
  			foreground: 'hsl(var(--foreground))',
  			card: {
  				DEFAULT: 'hsl(var(--card))',
  				foreground: 'hsl(var(--card-foreground))'
  			},
  			popover: {
  				DEFAULT: 'hsl(var(--popover))',
  				foreground: 'hsl(var(--popover-foreground))'
  			},
  			primary: {
  				DEFAULT: 'hsl(var(--primary))',
  				foreground: 'hsl(var(--primary-foreground))'
  			},
  			secondary: {
  				DEFAULT: 'hsl(var(--secondary))',
  				foreground: 'hsl(var(--secondary-foreground))'
  			},
  			muted: {
  				DEFAULT: 'hsl(var(--muted))',
  				foreground: 'hsl(var(--muted-foreground))'
  			},
  			accent: {
  				DEFAULT: 'hsl(var(--accent))',
  				foreground: 'hsl(var(--accent-foreground))'
  			},
  			destructive: {
  				DEFAULT: 'hsl(var(--destructive))',
  				foreground: 'hsl(var(--destructive-foreground))'
  			},
  			border: 'hsl(var(--border))',
  			input: 'hsl(var(--input))',
  			ring: 'hsl(var(--ring))',
  			chart: {
  				'1': 'hsl(var(--chart-1))',
  				'2': 'hsl(var(--chart-2))',
  				'3': 'hsl(var(--chart-3))',
  				'4': 'hsl(var(--chart-4))',
  				'5': 'hsl(var(--chart-5))'
  			}
  		},
  		borderRadius: {
  			lg: 'var(--radius)',
  			md: 'calc(var(--radius) - 2px)',
  			sm: 'calc(var(--radius) - 4px)'
  		}
  	}
  },
  plugins: [require("tailwindcss-animate")],
};
export default config;

```

### lib/prisma.ts

```typescript
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient()
}

declare const globalThis: {
  prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
```

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