# Project export: FamilyShield

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 2026
- Tagline: FamilyShield turns suspicious calls, voicemails, and messages into a personalized stop-and-verify plan, using a private Safety Circle to help families avoid scams before they happen.
- Devpost: https://devpost.com/software/familyshield
- GitHub: https://github.com/IceCreamUnicorn/FamilyShield
- Video: https://www.youtube.com/embed/PC0TL1Serzk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — IceCreamUnicorn (9 commits)

## Devpost submission (written by the team)

### Inspiration

Scams do not work because people are stupid. They work because scammers create panic, urgency, secrecy, and confusion at exactly the wrong moment. A person may know something feels wrong, but still be pressured into calling a fake number, sharing a verification code, buying gift cards, or sending money before they have time to verify. I wanted to build something that helps families slow down before the damage happens. FamilyShield was inspired by the idea that scam defense should not just be detection. It should be a calm, private safety checkpoint that helps people stop, verify, and contact someone they trust before acting.

### What it does

FamilyShield turns suspicious calls, voicemails, and messages into a personalized safety report. Users can paste a suspicious message or upload an audio file. FamilyShield transcribes audio, analyzes the content for scam signals, and generates a clear report explaining: the likely scam type red flags and manipulation tactics suspicious requested actions evidence from the message or transcript safe verification steps what not to do next a copyable warning to send to a trusted person FamilyShield also includes two local personalization features: Profile and Safety Circle. The Profile describes who the app is protecting, such as “Me,” “Parent / grandparent,” or “Family member,” along with an optional name and preferred verification style. The Safety Circle stores trusted contacts, relationships, optional phone numbers, and an optional family verification phrase for emergency impersonation scams. This helps users check with a real person before responding to scam pressure. Both features are stored only in the browser using localStorage and are not sent to Anthropic. The AI analysis happens first, and the local profile information is only used afterward to personalize the Safety Protocol and Safety Circle Check. How I built it FamilyShield was built with a Next.js frontend and API routes for backend logic. The interface uses Tailwind CSS and shadcn/ui components to create a calm, readable safety-focused experience. I used Deepgram to transcribe uploaded voicemails or recorded calls into text. Then I used Anthropic Claude to analyze the transcript or pasted message and return a structured scam-safety report. I used Redis as a scam playbook layer. The playbook stores common scam patterns such as gift card requests, verification code theft, family emergency impersonation, government impersonation, remote access scams, urgency pressure, secrecy pressure, and unverified callback requests. Instead of treating the playbook as the entire universe of scams, FamilyShield first analyzes the message broadly, then uses Redis to enrich matched scam patterns with safer next steps. The final report combines: Deepgram transcription Anthropic scam analysis Redis scam playbook matches local Profile and Safety Circle personalization a clear Safety Protocol for the user’s next action Challenges I ran into One major challenge was avoiding a generic “AI says this is a scam” experience. Early versions felt too much like a simple AI wrapper. I realized the product needed to focus less on classification and more on what happens after detection. That led us to build the Safety Protocol and Safety Circle Check. These sections turn the report into a practical next-step workflow: do not call the provided number, do not share codes, do not send money, contact someone trusted, and verify through an independent channel. Another challenge was catching setup-stage scams. Some scams do not immediately ask for money or passwords. For example, a fake credit card interest-rate reduction voicemail may only ask the victim to call an unverified number. The actual scam happens later. I updated FamilyShield to detect these suspicious setup paths, not just obvious payment requests. I also had to think carefully about privacy. Since scam victims may be older adults or families dealing with sensitive situations, I avoided storing raw audio, full transcripts, or personal account details in the personalization layer. Accomplishments that I're proud of I are proud that FamilyShield became more than a scam detector. It became a stop-and-verify workflow. The Safety Circle feature directly attacks one of the most dangerous scam tactics: isolation. If a caller says “do not tell your daughter” or pressures the user to act alone, FamilyShield highlights that behavior and encourages the user to contact someone trusted before making a decision. I are also proud of the end-to-end flow: users can upload audio, receive a transcript, get a structured scam analysis, see matched scam playbook patterns, and receive personalized next steps based on their local Safety Circle. Most importantly, the app is designed to be calm. Instead of overwhelming the user with technical explanations, FamilyShield gives plain-English guidance at the moment when slowing down matters most. What I learned I learned that scams are not just an information problem. In many cases, a calm person can identify a scam. The real danger is that scammers manipulate emotion, urgency, and isolation so the victim acts before verifying. I also learned that AI products become stronger when the model is only one part of a broader workflow. Claude helps understand messy human language, but FamilyShield adds structure around that analysis: scam playbook matching, safety protocols, trusted-contact checks, and local personalization. Finally, I learned that privacy-first design matters. For a tool like this, personalization should help the user without requiring invasive tracking or unnecessary storage.

### What's next

Next, I would like to add optional saved incident history so families can notice repeated scam patterns over time without storing raw transcripts or audio, and also expand to Website scams like purposely typoed domains trying to fake, and turn it into a browser extension. I also want to improve the Safety Circle experience with one-tap SMS sharing, better accessibility for older adults, multilingual scam reports, and more robust detection for long-form scams such as romance scams, fake job scams, and investment scams. Long term, FamilyShield could become a family scam-safety companion: a private place where suspicious messages, voicemails, and calls are turned into clear verification steps before anyone sends money, shares codes, or acts under pressure.

## README (from the GitHub repository)

# FamilyShield

AI scam-call safety assistant that turns suspicious voicemails, calls, or messages into a plain-English risk explanation, red flags, and safe verification steps.

## Problem
Scam calls exploit panic, urgency, and isolation. Older adults and families often need a simple way to slow down, understand the manipulation, and verify safely before sending money or personal information.

## What it does
- Transcribes suspicious audio
- Detects scam patterns and manipulation tactics
- Highlights red flags
- Generates safe next steps
- Creates a family-friendly explanation
- Stores/retrieves similar scam patterns
- Monitors app/API failures

## Tech stack
- Frontend:
- Backend:
- Deepgram:
- Anthropic:
- Redis:
- Sentry:

## Demo flow
1. Upload or record suspicious voicemail.
2. App transcribes the audio.
3. App identifies scam risk and red flags.
4. App recommends verification steps.
5. User can share a simple warning with a trusted family member.

## Safety note
This tool does not make final legal, financial, or security decisions. It helps users slow down, identify warning signs, and verify through trusted channels.


## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 158 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Redis (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

## Codebase structure (from repository index)

### Files (53 of 53)

```
.env.example
.gitignore
.vs/FamilyShield/FileContentIndex/d2d6050d-e3e2-491a-a1e4-3cdfd71cb1d6.vsidx
.vs/ProjectSettings.json
.vs/slnx.sqlite
app/analyze/page.tsx
app/api/analyze/route.ts
app/api/redis-health/route.ts
app/api/redis/playbook/route.ts
app/api/redis/seed-playbook/route.ts
app/api/transcribe/route.ts
app/circle/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
app/privacy/page.tsx
app/profile/page.tsx
app/report/page.tsx
components/LoadingPanel.tsx
components/Navbar.tsx
components/RedFlagCard.tsx
components/RiskReport.tsx
components/SafeSteps.tsx
components/SafetyCircleCheck.tsx
components/SafetyCircleForm.tsx
components/SafetyProfileForm.tsx
components/SafetyProtocol.tsx
components/TrustedContactMessage.tsx
components/ui/alert.tsx
components/ui/badge.tsx
components/ui/button.tsx
components/ui/card.tsx
components/ui/separator.tsx
components/ui/skeleton.tsx
components/ui/textarea.tsx
eslint.config.mjs
lib/anthropic.ts
lib/redis.ts
lib/safetyCircle.ts
lib/safetyProfile.ts
lib/sampleReports.ts
lib/scamPlaybook.ts
lib/scamPrompt.ts
lib/types.ts
lib/utils.ts
LICENSE
next-env.d.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.105.0, @deepgram/sdk@^5.4.0, @radix-ui/react-slot@^1.3.0, @types/node@20.14.10, @types/react@18.3.3, @types/react-dom@18.3.0, autoprefixer@10.4.19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9.39.4, eslint-config-next@^16.2.9, lucide-react@^1.21.0, next@^16.2.9, postcss@^8.5.15, react@18.3.1, react-dom@18.3.1, redis@^6.0.0, tailwind-merge@^3.6.0, tailwindcss@3.4.4, tailwindcss-animate@^1.0.7, typescript@5.5.3

### Recent commits (newest first)

- Revamp UI & add safety pages and audio upload
- Add motion utilities and animated loading UI
- Add analyze/report UI, Redis playbook, transcribe
- Structured RiskReport fields and scenario selector
- Add Deepgram transcription API and UI
- Add Anthropic analysis API and UI integration
- Add FamilyShield Next.js app scaffold
- Update README with project overview and features
- Initial commit

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

### package.json

```
{
  "name": "familyshield",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "lint": "eslint ."
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@deepgram/sdk": "^5.4.0",
    "@radix-ui/react-slot": "^1.3.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^1.21.0",
    "next": "^16.2.9",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "redis": "^6.0.0",
    "tailwind-merge": "^3.6.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "20.14.10",
    "@types/react": "18.3.3",
    "@types/react-dom": "18.3.0",
    "autoprefixer": "10.4.19",
    "eslint": "^9.39.4",
    "eslint-config-next": "^16.2.9",
    "postcss": "^8.5.15",
    "tailwindcss": "3.4.4",
    "typescript": "5.5.3"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Navbar } from "@/components/Navbar";
import "./globals.css";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
  display: "swap",
});

export const metadata: Metadata = {
  title: "FamilyShield — Scam Safety Assistant",
  description:
    "Turn suspicious calls and messages into a calm safety report. FamilyShield helps families identify scam red flags, manipulation tactics, and safer next steps.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={inter.variable}>
      <body className="font-sans">
        <Navbar />
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import Link from "next/link";
import { Shield, Mic, Brain, Database, ShieldCheck, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

const features = [
  {
    title: "Voice layer",
    body: "Upload a voicemail or recorded call and turn it into readable text with Deepgram.",
    icon: Mic,
  },
  {
    title: "Reasoning layer",
    body: "Get a calm AI risk explanation with red flags, evidence, and safer next steps.",
    icon: Brain,
  },
  {
    title: "Memory layer",
    body: "Match messages against a Redis-backed scam playbook of known tactics.",
    icon: Database,
  },
];

function ReportPreviewCard() {
  return (
    <div className="glass-card rounded-2xl p-6 shadow-glow">
      <div className="flex items-center justify-between">
        <p className="text-[10px] font-bold uppercase tracking-[0.2em] text-sage-300">
          FamilyShield safety report
        </p>
        <Badge variant="high">High risk</Badge>
      </div>
      <h3 className="mt-3 text-lg font-black text-cream">
        Family Emergency Imposter Scam
      </h3>
      <p className="mt-2 text-sm leading-6 text-cream/75">
        High-risk scam indicators detected. The message uses urgency, secrecy, and an unusual payment
        request to pressure the recipient…
      </p>
      <div className="mt-4 space-y-2">
        <div className="flex items-center gap-2.5">
          <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-sage-500/20 text-[10px] font-bold text-sage-300">
            1
          </span>
          <span className="text-xs text-cream/75">
            Pause and do not send money or codes
          </span>
        </div>
        <div className="flex items-center gap-2.5">
          <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-sage-500/20 text-[10px] font-bold text-sage-300">
            2
          </span>
          <span className="text-xs text-cream/75">
            Call the family member using a saved number
          </span>
        </div>
        <div className="flex items-center gap-2.5">
          <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-sage-500/20 text-[10px] font-bold text-sage-300">
            3
          </span>
          <span className="text-xs text-cream/75">
            Contact another trusted relative or caregiver
          </span>
        </div>
      </div>
      <div className="mt-4 flex flex-wrap gap-2">
        <Badge variant="muted">Anthropic analysis</Badge>
        <Badge variant="default">3 safety patterns</Badge>
      </div>
    </div>
  );
}

export default function Home() {
  return (
    <main className="min-h-screen">
      {/* ── Hero ── */}
      <section className="hero-gradient">
        <div className="mx-auto grid w-full max-w-7xl gap-12 px-5 py-16 sm:px-8 lg:grid-cols-[1fr_0.75fr] lg:items-center lg:px-10 lg:py-24">
          <div className="animate-fade-in-up flex flex-col">
            <p className="text-xs font-bold uppercase tracking-[0.24em] text-sage-300">
              Slow down before you respond
            </p>
            <h1 className="mt-5 max-w-2xl text-4xl font-black leading-[1.1] text-cream sm:text-5xl lg:text-[3.4rem]">
              Turn suspicious calls and messages into a{" "}
              <span className="text-gradient">calm safety report.</span>
            </h1>
            <p className="mt-6 max-w-xl text-base leading-7 text-cream/75">
              FamilyShield helps families identify scam red flags, manipulation
              tactics, and safer next steps — before anyone sends money, codes,
              or personal information.
            </p>
            <div className="mt-8 flex flex-col gap-3 sm:flex-row">
              <Button asChild size="lg">
                <Link href="/analyze">
                  Analyze a message
                  <ArrowRight className="ml-2 h-4 w-4" />
                </Link>
              </Button>
              <Button variant="outline" size="lg" asChild>
                <Link href="/analyze">Try demo scenarios</Link>
              </Button>
            </div>
          </div>

          <aside className="animate-fade-in-up" style={{ animationDelay: "120ms" }}>
            <ReportPreviewCard />
          </aside>
        </div>
      </section>

      {/* ── Feature Cards ── */}
      <section className="section-gradient">
        <div className="mx-auto w-full max-w-7xl px-5 py-16 sm:px-8 lg:px-10">
          <p className="text-center text-xs font-bold uppercase tracking-[0.24em] text-sage-300">
            Built for careful verification
          </p>
          <h2 className="mt-3 text-center text-2xl font-black text-cream sm:text-3xl">
            Three layers of protection
          </h2>
          <div className="mt-10 grid gap-5 sm:grid-cols-3">
            {features.map((feature, index) => {
              const Icon = feature.icon;
              return (
                <article
                  key={feature.title}
                  className="glass-card motion-card animate-fade-in-up rounded-2xl p-6"
                  style={{ animationDelay: `${index * 80}ms` }}
                >
                  <div className="flex h-11 w-11 items-center justify-center rounded-xl bg-sage-500/15">
                    <Icon className="h-5 w-5 text-sage-300" strokeWidth={2} />
                  </div>
                  <h3 className="mt-4 text-base font-black text-cream">
                    {feature.title}
                  </h3>
                  <p className="mt-2 text-sm leading-6 text-cream/75">
                    {feature.body}
                  </p>
                </article>
              );
            })}
          </div>
        </div>
      </section>

      {/* ── Privacy Callout ── */}
      <section className="section-gradient border-t border-white/[0.04]">
        <div className="mx-auto w-full max-w-7x
[truncated — 1226 more characters]
```

### app/circle/page.tsx

```typescript
import { SafetyCircleForm } from "@/components/SafetyCircleForm";

export default function CirclePage() {
  return (
    <main className="min-h-screen section-gradient">
      <section className="mx-auto w-full max-w-5xl px-5 py-10 sm:px-8 lg:px-10">
        <SafetyCircleForm />
      </section>
    </main>
  );
}

```

### app/profile/page.tsx

```typescript
import { SafetyProfileForm } from "@/components/SafetyProfileForm";

export default function ProfilePage() {
  return (
    <main className="min-h-screen section-gradient">
      <section className="mx-auto w-full max-w-4xl px-5 py-10 sm:px-8 lg:px-10">
        <SafetyProfileForm />
      </section>
    </main>
  );
}

```

### app/privacy/page.tsx

```typescript
const privacyNotes = [
  "FamilyShield is user-initiated, not always-on monitoring.",
  "It does not listen to calls in real time.",
  "During the demo, audio or text you submit may be sent to APIs for transcription and analysis.",
  "FamilyShield is not legal advice, financial advice, or law enforcement advice.",
  "Users should verify through official channels and trusted contacts before acting.",
];

export default function PrivacyPage() {
  return (
    <main className="min-h-screen section-gradient">
      <section className="mx-auto w-full max-w-4xl px-5 py-10 sm:px-8 lg:px-10">
        <div className="animate-fade-in-up premium-surface rounded-2xl p-6 shadow-card sm:p-8">
          <p className="text-xs font-bold uppercase tracking-[0.18em] text-sage-700">
            Privacy and safety
          </p>
          <h1 className="mt-2 text-3xl font-black text-slate-950">Privacy-first by design</h1>
          <p className="mt-4 text-base leading-7 text-slate-700">
            FamilyShield is built to help families slow down and verify suspicious messages. It is
            not a call monitor, automatic blocker, legal advisor, financial advisor, or law
            enforcement tool.
          </p>
          <div className="mt-6 grid gap-3">
            {privacyNotes.map((note) => (
              <article
                key={note}
                className="motion-card rounded-lg border border-slate-200 bg-slate-50 p-4"
              >
                <p className="text-sm leading-6 text-slate-700">{note}</p>
              </article>
            ))}
          </div>
        </div>
      </section>
    </main>
  );
}

```

### app/report/page.tsx

```typescript
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { ReportSkeleton } from "@/components/LoadingPanel";
import { RiskReport } from "@/components/RiskReport";
import {
  normalizeSafetyCircle,
  SAFETY_CIRCLE_STORAGE_KEY,
} from "@/lib/safetyCircle";
import {
  normalizeSafetyProfile,
  SAFETY_PROFILE_STORAGE_KEY,
} from "@/lib/safetyProfile";
import type { RiskReport as RiskReportType, SafetyCircle, SafetyProfile } from "@/lib/types";

export default function ReportPage() {
  const [report, setReport] = useState<RiskReportType | null>(null);
  const [safetyProfile, setSafetyProfile] = useState<SafetyProfile | null>(null);
  const [safetyCircle, setSafetyCircle] = useState<SafetyCircle | null>(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [copyStatus, setCopyStatus] = useState("");

  useEffect(() => {
    const timeoutId = window.setTimeout(() => {
      const savedReport = localStorage.getItem("familyshield:lastReport");
      const savedProfile = localStorage.getItem(SAFETY_PROFILE_STORAGE_KEY);
      const savedCircle = localStorage.getItem(SAFETY_CIRCLE_STORAGE_KEY);

      if (savedReport) {
        try {
          setReport(JSON.parse(savedReport) as RiskReportType);
        } catch {
          localStorage.removeItem("familyshield:lastReport");
        }
      }

      if (savedProfile) {
        try {
          setSafetyProfile(normalizeSafetyProfile(JSON.parse(savedProfile)));
        } catch {
          localStorage.removeItem(SAFETY_PROFILE_STORAGE_KEY);
        }
      }

      if (savedCircle) {
        try {
          setSafetyCircle(normalizeSafetyCircle(JSON.parse(savedCircle)));
        } catch {
          localStorage.removeItem(SAFETY_CIRCLE_STORAGE_KEY);
        }
      }

      setIsLoaded(true);
    }, 0);

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

  function handleClear() {
    localStorage.removeItem("familyshield:lastReport");
    setReport(null);
    setCopyStatus("");
  }

  async function handleCopySummary() {
    if (!report) {
      return;
    }

    const summary = [
      `FamilyShield report: ${report.scamType}`,
      `Risk: ${report.riskLevel} (${report.confidence}% confidence)`,
      report.summary,
      `Recommended response: ${report.recommendedResponse}`,
    ].join("\n\n");

    try {
      await navigator.clipboard.writeText(summary);
      setCopyStatus("Copied");
    } catch {
      setCopyStatus("Copy unavailable");
    }
  }

  return (
    <main className="min-h-screen section-gradient">
      <section className="mx-auto w-full max-w-7xl px-5 py-8 sm:px-8 lg:px-10">
        <div className="animate-fade-in-up mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <p className="text-xs font-bold uppercase tracking-[0.18em] text-sage-300">
              Safety report
            </p>
            <h1 className="mt-2 text-3xl font-black text-cream">Your FamilyShield report</h1>
          </div>
          {report ? (
            <div className="flex flex-col gap-3 sm:flex-row">
              <Link
                href="/analyze"
                className="focus-ring motion-card inline-flex items-center justify-center rounded-lg bg-cream px-5 py-3 text-sm font-bold text-navy shadow-lg shadow-black/15 transition hover:bg-ivory"
              >
                Analyze another
              </Link>
              <button
                type="button"
                onClick={handleCopySummary}
                className="focus-ring motion-card inline-flex items-center justify-center rounded-lg border border-cream/25 bg-white/10 px-5 py-3 text-sm font-bold text-cream transition hover:bg-white/15"
              >
                {copyStatus || "Copy report summary"}
              </button>
              <button
                type="button"
                onClick={handleClear}
                className="focus-ring motion-card inline-flex items-center justify-center rounded-lg border border-cream/25 bg-white/10 px-5 py-3 text-sm font-bold text-cream transition hover:bg-white/15"
              >
                Clear report
              </button>
            </div>
          ) : null}
        </div>

        {!isLoaded ? <ReportSkeleton /> : null}

        {isLoaded && report ? (
          <RiskReport
            report={report}
            safetyProfile={safetyProfile}
            safetyCircle={safetyCircle}
          />
        ) : null}

        {isLoaded && !report ? (
          <div className="animate-fade-in-up rounded-lg border border-white/80 bg-white/85 p-8 text-center shadow-soft backdrop-blur">
            <div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl border border-teal-100 bg-teal-50 text-sm font-black text-teal-700 shadow-sm">
              FS
            </div>
            <h2 className="mt-4 text-2xl font-black text-slate-950">No report yet</h2>
            <p className="mx-auto mt-3 max-w-xl text-sm leading-6 text-slate-600">
              Analyze a suspicious message first, then your latest safety report will appear here.
            </p>
            <Link
              href="/analyze"
              className="focus-ring motion-card mt-6 inline-flex items-center justify-center rounded-lg bg-shield-ink px-5 py-3 text-sm font-bold text-white shadow-lg shadow-slate-900/15 transition hover:bg-slate-800"
            >
              Analyze a suspicious message
            </Link>
          </div>
        ) : null}
      </section>
    </main>
  );
}

```

### app/analyze/page.tsx

```typescript
"use client";

import { useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Upload, FileAudio, Shield, Sparkles, X } from "lucide-react";
import { AnalysisLoadingScreen, TranscriptionLoadingScreen } from "@/components/LoadingPanel";
import { sampleScamScenarios, sampleVoicemailTranscript } from "@/lib/sampleReports";
import type { RiskReport as RiskReportType } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";

type DiarizedSegment = {
  speaker: number;
  role: "likely_scammer" | "likely_scammee" | "unknown";
  label: string;
  text: string;
  start?: number;
};

const flowSteps = [
  "Paste a message or upload audio.",
  "Transcribe the voicemail if needed.",
  "Analyze scam risk and tactics.",
  "Verify safely before acting.",
];

export default function AnalyzePage() {
  const router = useRouter();
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [message, setMessage] = useState("");
  const [hasAnalyzed, setHasAnalyzed] = useState(false);
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [error, setError] = useState("");
  const [audioFile, setAudioFile] = useState<File | null>(null);
  const [transcript, setTranscript] = useState("");
  const [diarizedSegments, setDiarizedSegments] = useState<DiarizedSegment[]>([]);
  const [isTranscribing, setIsTranscribing] = useState(false);
  const [isDraggingAudio, setIsDraggingAudio] = useState(false);
  const [transcriptionError, setTranscriptionError] = useState("");
  const [loadedSampleTitle, setLoadedSampleTitle] = useState("");
  const [selectedScenarioTitle, setSelectedScenarioTitle] = useState("");

  const characterCount = useMemo(() => message.trim().length, [message]);
  const selectedScenario = sampleScamScenarios.find(
    (scenario) => scenario.title === selectedScenarioTitle,
  );

  function resetAnalysisState() {
    setHasAnalyzed(false);
    setError("");
  }

  function handleSample() {
    setMessage(sampleVoicemailTranscript);
    setTranscript("");
    setDiarizedSegments([]);
    resetAnalysisState();
    setLoadedSampleTitle("Sample voicemail");
    setSelectedScenarioTitle("");
  }

  function handleScenarioSample(title: string) {
    const scenario = sampleScamScenarios.find((sample) => sample.title === title);
    setSelectedScenarioTitle(title);

    if (!scenario) {
      setLoadedSampleTitle("");
      return;
    }

    setMessage(scenario.text);
    setTranscript("");
    setDiarizedSegments([]);
    setTranscriptionError("");
    resetAnalysisState();
    setLoadedSampleTitle(scenario.title);
  }

  function handleAudioFile(nextFile: File | null) {
    setAudioFile(nextFile);
    setTranscript("");
    setDiarizedSegments([]);
    setTranscriptionError("");
  }

  async function analyzeText(textToAnalyze: string) {
    setHasAnalyzed(true);
    setError("");

    if (!textToAnalyze.trim()) {
      return;
    }

    setIsAnalyzing(true);

    try {
      const response = await fetch("/api/analyze", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text: textToAnalyze }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || "Analysis failed. Please try again.");
      }

      localStorage.setItem("familyshield:lastReport", JSON.stringify(data as RiskReportType));
      router.push("/report");
    } catch (caughtError) {
      setError(
        caughtError instanceof Error
          ? caughtError.message
          : "Analysis failed. Please try again.",
      );
    } finally {
      setIsAnalyzing(false);
    }
  }

  async function handleTranscribe() {
    setTranscriptionError("");
    setTranscript("");
    setDiarizedSegments([]);

    if (!audioFile) {
      setTranscriptionError("Choose an audio file before transcribing.");
      return;
    }

    setIsTranscribing(true);

    try {
      const formData = new FormData();
      formData.append("audio", audioFile);

      const response = await fetch("/api/transcribe", {
        method: "POST",
        body: formData,
      });
      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || "Transcription failed. Please try another file.");
      }

      const nextTranscript = String(data.diarizedTranscript || data.transcript || "").trim();
      const nextSegments = Array.isArray(data.diarizedSegments)
        ? (data.diarizedSegments as DiarizedSegment[])
        : [];

      setTranscript(nextTranscript);
      setDiarizedSegments(nextSegments);
      setMessage(nextTranscript);
      setLoadedSampleTitle("");
      setSelectedScenarioTitle("");
      resetAnalysisState();
    } catch (caughtError) {
      setTranscriptionError(
        caughtError instanceof Error
          ? caughtError.message
          : "Transcription failed. Please try another file.",
      );
    } finally {
      setIsTranscribing(false);
    }
  }

  return (
    <main className="min-h-screen section-gradient">
      <section className="mx-auto grid w-full max-w-7xl gap-6 px-5 py-8 sm:px-8 lg:grid-cols-[1fr_19rem] lg:px-10">
        {/* ── Main Input Card ── */}
        <div className="animate-fade-in-up space-y-5">
          <div className="premium-surface rounded-2xl p-6 shadow-card sm:p-7">
            {/* Header */}
            <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
              <div>
                <p className="text-[10px] font-bold uppercase tracking-[0.22em] text-sage-600">
                  Analyze
                </p>
                <h1 className="mt-1.5 text-2xl font-black text-slate-900 sm:text-3xl">
                  Analyze a suspicious message
                </h1>
        
[truncated — 12979 more characters]
```

### app/api/redis-health/route.ts

```typescript
import { NextResponse } from "next/server";
import { getRedisClient } from "@/lib/redis";

export const runtime = "nodejs";

export async function GET() {
  try {
    const redis = await getRedisClient();
    await redis.set("familyshield:health", "connected");
    const value = await redis.get("familyshield:health");

    if (value !== "connected") {
      return NextResponse.json(
        { ok: false, error: "Redis health check returned an unexpected value." },
        { status: 502 },
      );
    }

    return NextResponse.json({ ok: true, value });
  } catch (error) {
    const message =
      error instanceof Error && error.message === "REDIS_URL is not configured"
        ? "REDIS_URL is not configured."
        : "Redis health check failed.";

    return NextResponse.json({ ok: false, error: message }, { status: 500 });
  }
}

```

### app/api/transcribe/route.ts

```typescript
import { Buffer } from "node:buffer";
import { DeepgramClient } from "@deepgram/sdk";
import { NextResponse } from "next/server";

export const runtime = "nodejs";

type SpeakerRole = "likely_scammer" | "likely_scammee" | "unknown";

type DiarizedSegment = {
  speaker: number;
  role: SpeakerRole;
  label: string;
  text: string;
  start?: number;
  end?: number;
};

export async function POST(request: Request) {
  const apiKey = process.env.DEEPGRAM_API_KEY;

  if (!apiKey) {
    return NextResponse.json({ error: "Deepgram API key is not configured." }, { status: 500 });
  }

  let formData: FormData;

  try {
    formData = await request.formData();
  } catch {
    return NextResponse.json({ error: "Invalid multipart form data." }, { status: 400 });
  }

  const audio = formData.get("audio");

  if (!(audio instanceof File)) {
    return NextResponse.json({ error: "Audio file is required." }, { status: 400 });
  }

  if (audio.size === 0) {
    return NextResponse.json({ error: "Audio file is empty." }, { status: 400 });
  }

  try {
    const audioBuffer = Buffer.from(await audio.arrayBuffer());
    const deepgram = new DeepgramClient({ apiKey });
    const response = await deepgram.listen.v1.media.transcribeFile(
      {
        data: audioBuffer,
        filename: audio.name,
        contentType: audio.type || "application/octet-stream",
        contentLength: audio.size,
      },
      {
        model: "nova-3",
        smart_format: true,
        punctuate: true,
        diarize: true,
        utterances: true,
      },
    );

    if (!("results" in response)) {
      return NextResponse.json(
        { error: "Deepgram accepted the audio but did not return a transcript yet." },
        { status: 502 },
      );
    }

    const transcript =
      response.results.channels?.[0]?.alternatives?.[0]?.transcript?.trim() || "";

    if (!transcript) {
      return NextResponse.json(
        { error: "Deepgram returned an empty transcript for this file." },
        { status: 422 },
      );
    }

    const diarizedSegments = buildDiarizedSegments(response.results.utterances ?? []);
    const diarizedTranscript = diarizedSegments.length
      ? diarizedSegments.map((segment) => `${segment.label}: ${segment.text}`).join("\n")
      : transcript;

    return NextResponse.json({ transcript, diarizedTranscript, diarizedSegments });
  } catch {
    return NextResponse.json(
      { error: "Transcription failed. Check the audio file and Deepgram API key." },
      { status: 502 },
    );
  }
}

function buildDiarizedSegments(
  utterances: Array<{
    speaker?: number;
    transcript?: string;
    start?: number;
    end?: number;
  }>,
) {
  const baseSegments = utterances
    .filter((utterance) => utterance.transcript?.trim())
    .map((utterance) => ({
      speaker: utterance.speaker ?? 0,
      text: utterance.transcript?.trim() ?? "",
      start: utterance.start,
      end: utterance.end,
    }));

  if (baseSegments.length === 0) {
    return [];
  }

  const rolesBySpeaker = inferSpeakerRoles(baseSegments);

  return baseSegments.map((segment) => {
    const role = rolesBySpeaker.get(segment.speaker) ?? "unknown";
    return {
      ...segment,
      role,
      label: formatSpeakerLabel(segment.speaker, role),
    };
  });
}

function inferSpeakerRoles(segments: Array<{ speaker: number; text: string }>) {
  const scores = new Map<number, number>();

  for (const segment of segments) {
    scores.set(segment.speaker, (scores.get(segment.speaker) ?? 0) + scoreScammerLanguage(segment.text));
  }

  const ranked = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
  const roles = new Map<number, SpeakerRole>();
  const likelyScammer = ranked[0];

  if (likelyScammer && likelyScammer[1] >= 2) {
    roles.set(likelyScammer[0], "likely_scammer");
  }

  for (const [speaker] of ranked) {
    if (!roles.has(speaker)) {
      roles.set(speaker, likelyScammer && likelyScammer[1] >= 2 ? "likely_scammee" : "unknown");
    }
  }

  return roles;
}

function scoreScammerLanguage(text: string) {
  const normalized = text.toLowerCase();
  const scammerSignals = [
    "gift card",
    "read me the numbers",
    "verification code",
    "six-digit code",
    "wire transfer",
    "crypto",
    "remote access",
    "install",
    "don't tell",
    "do not tell",
    "urgent",
    "right now",
    "immediately",
    "arrest",
    "warrant",
    "account may be frozen",
    "selected",
    "guaranteed",
  ];
  const targetSignals = [
    "let me call",
    "i need to verify",
    "i will verify",
    "i don't know",
    "who is this",
    "can i call you back",
    "official number",
  ];

  const scammerScore = scammerSignals.reduce(
    (score, signal) => score + (normalized.includes(signal) ? 1 : 0),
    0,
  );
  const targetScore = targetSignals.reduce(
    (score, signal) => score + (normalized.includes(signal) ? 1 : 0),
    0,
  );

  return scammerScore - targetScore;
}

function formatSpeakerLabel(speaker: number, role: SpeakerRole) {
  if (role === "likely_scammer") {
    return `Speaker ${speaker} · likely scammer/caller`;
  }

  if (role === "likely_scammee") {
    return `Speaker ${speaker} · likely target/recipient`;
  }

  return `Speaker ${speaker} · role unclear`;
}

```

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