# Project export: VibeCheck

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 12.0
- Tagline: Ace your interviews using AI interviewers and realtime feedback
- Devpost: https://devpost.com/software/recruitme
- GitHub: https://github.com/8wali8/Calhacks2
- Video: https://www.youtube.com/embed/pa3y25rzbjw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — 8wali8 (1 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# Tavus Interview Demo

**100% client-side Tavus integration for hackathon demos.** No server code, no API routes, fully modular for embedding in Creao.

## ⚠️ Security Warning

This demo uses **NEXT_PUBLIC_*** environment variables, which means your Tavus API key is **exposed in the browser**. This is acceptable for hackathon demos but **must not be used in production**. In production, always proxy Tavus API calls through your server to protect credentials.

## Quick Start

### 1. Install dependencies

```bash
npm install
# or
pnpm install
```

### 2. Set up environment variables

Copy the example file and add your Tavus API key:

```bash
cp .env.local.example .env.local
```

Edit `.env.local`:

```env
# Required: Get your API key from https://platform.tavus.io
NEXT_PUBLIC_TAVUS_API_KEY=your_actual_api_key_here

# Optional: Reuse an existing persona instead of creating a new one each run
# NEXT_PUBLIC_TAVUS_PERSONA_ID=p1234567890

# Optional: Specify a replica ID
# NEXT_PUBLIC_TAVUS_REPLICA_ID=r1234567890
```

### 3. Run the dev server

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

Open [http://localhost:3000](http://localhost:3000) in your browser.

## How It Works

### Persona-First Flow

1. **Edit Persona & Context**: Use the left panel to customize the interviewer persona and interview context as JSON
2. **Create or Reuse Persona**:
   - If `NEXT_PUBLIC_TAVUS_PERSONA_ID` is set → reuse that persona
   - Otherwise → create a new persona from your JSON inputs
3. **Create Conversation**: The app calls Tavus to create a conversation with the persona
4. **Embed Interview**: The conversation URL is embedded in an iframe with camera/mic permissions
5. **Event Bridge**: Messages from the iframe are forwarded to the metrics panel

### Architecture

```
┌─────────────────┐
│  Browser Only   │  ← No server code
├─────────────────┤
│ TavusInterview  │  ← React component
│ (client-side)   │
├─────────────────┤
│ fetch() calls   │  ← Direct to Tavus API
│ ↓               │
│ Tavus API       │
│ ↓               │
│ Iframe embed    │  ← conversation_url
└─────────────────┘
```

## Components

### `<TavusInterview>` React Component

Main interview component with props:

```tsx
import { TavusInterview } from "@/components/TavusInterview";

<TavusInterview
  persona={{
    name: "Technical Recruiter",
    systemPrompt: "You are a friendly technical recruiter...",
    topics: ["experience", "skills"],
    tone: "friendly",
    followUpStyle: "balanced",
    questionStyle: "hybrid",
    maxQuestions: 5,
    maxFollowUpsPerQuestion: 2,
    attachContextFromInterview: true,
  }}
  context={{
    company: "Acme Corp",
    role: "Senior Engineer",
    seniority: "Senior",
    jdHighlights: ["5+ years backend", "API design"],
    extraContext: "Fast-paced startup",
  }}
  autoplay={true}
  onEvent={(message) => console.log(message)}
/>
```

### `<tavus-interview>` Web Component

For embedding in non-React apps or Creao:

```html
<script>
  // Listen for events
  window.addEventListener('tavus:event', (event) => {
    console.log('Tavus event:', event.detail);
  });
</script>

<tavus-interview
  persona='{"name":"Recruiter","systemPrompt":"You are a friendly recruiter..."}'
  context='{"company":"Acme","role":"Engineer"}'
  autoplay="true"
></tavus-interview>
```

**Note**: The web component is automatically registered when you import `useTavusWebComponent()` in your app.

### `<MetricsPanel>` Debug Component

Shows a scrolling log of all events:

```tsx
import { MetricsPanel } from "@/components/MetricsPanel";

const [events, setEvents] = useState<UIMessage[]>([]);

<MetricsPanel events={events} />
```

Debug hook in browser console:

```js
window.__pushMetric({
  type: "note",
  timestamp: Date.now(),
  text: "Test event"
});
```

## Data Contracts

### `PersonaInput`

Defines the interviewer's behavior:

```ts
interface PersonaInput {
  name: string;
  systemPrompt: string;
  topics?: string[];
  tone?: "neutral" | "friendly" | "direct" | "challenging";
  followUpStyle?: "balanced" | "deep-dive" | "rapid-fire" | "supportive";
  questionStyle?: "behavioral" | "technical" | "hybrid";
  maxQuestions?: number;
  maxFollowUpsPerQuestion?: number;
  attachContextFromInterview?: boolean;
}
```

### `InterviewContext`

Describes the role and company:

```ts
interface InterviewContext {
  company: string;
  role: string;
  seniority?: string;
  jdHighlights?: string[];
  extraContext?: string;
}
```

### `UIMessage`

Event structure for metrics:

```ts
interface UIMessage {
  type: "ready" | "connected" | "disconnected" | "error" | "note";
  timestamp: number;
  text?: string;
  payload?: unknown; // For opaque iframe messages
}
```

## Embedding in Creao

### Option 1: React Component

```tsx
import { TavusInterview } from "@/components/TavusInterview";

function MyCreaoPage() {
  return (
    <TavusInterview
      persona={myPersona}
      context={myContext}
      autoplay={true}
      onEvent={handleMetrics}
    />
  );
}
```

### Option 2: Web Component

```tsx
import { useTavusWebComponent } from "@/components/TavusInterviewWebComponent";

function MyCreaoPage() {
  useTavusWebComponent(); // Register once in your app

  return (
    <div>
      <tavus-interview
        persona={JSON.stringify(myPersona)}
        context={JSON.stringify(myContext)}
      />
    </div>
  );
}
```

## Tavus API Calls

All calls are made directly from the browser using `fetch()`:

### Base URL

```
https://tavusapi.com/v2
```

### Headers

```
x-api-key: NEXT_PUBLIC_TAVUS_API_KEY
content-type: application/json
```

### Create Persona

```
POST /personas
```

Payload built from `PersonaInput` + `InterviewContext`. Skipped if `NEXT_PUBLIC_TAVUS_PERSONA_ID` is set.

### Create Conversation

```
POST /conversations
```

Payload: `{ persona_id, replica_id? }`

Returns: `{ conversation_id, conversation_url }`

### Get Conversation (optional)

```
GET /conversations/{id}
```

For polling if needed.

## CORS & Network Issues

If the venue network blocks direct calls to Tavus:

1. Create a minimal proxy endpoint on your server
2. Update `TAVUS_BASE_URL` in [lib/tavus-client.ts](lib/tavus-client.ts) to point to your proxy
3. Move `NEXT_PUBLIC_TAVUS_API_KEY` to a server-side env var

## File Structure

```
tavus/
├── app/
│   ├── layout.tsx          # Root layout
│   ├── page.tsx            # Main demo page with editors
│   └── globals.css         # All styling (no Tailwind)
├── components/
│   ├── TavusInterview.tsx           # Main interview component
│   ├── MetricsPanel.tsx             # Event log display
│   └── TavusInterviewWebComponent.tsx  # Web component wrapper
├── lib/
│   └── tavus-client.ts     # Tavus API client (fetch only)
├── types/
│   └── index.ts            # TypeScript contracts
├── .env.local.example      # Template for env vars
└── README.md               # This file
```

## Development Checklist

- [x] Runs with `npm run dev` after setting `NEXT_PUBLIC_TAVUS_API_KEY`
- [x] Toggle `NEXT_PUBLIC_TAVUS_PERSONA_ID` to skip persona creation
- [x] Creating conversation returns `conversation_url` that loads in iframe
- [x] Event log shows `ready`, `connected`, and forwards iframe messages as `note`
- [x] No server files, no API routes, no Tailwind, no extra libs
- [x] Component props match data contracts exactly
- [x] Web component `<tavus-interview>` dispatches `tavus:event` with `UIMessage`

## License

MIT (Hackathon demo - use at your own risk)


## Detected evidence (automated analysis)

Indexed codebase: 40 recognized source files, 216 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Hugging Face (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (47 of 47)

```
.claude/settings.local.json
.env.local.example
.eslintrc.json
.gitignore
ANALYTICS_INTEGRATION.md
app/api/extract-job/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
ARCHITECTURE.md
components/AnalyticsPanel.tsx
components/index.ts
components/LiveBadges.tsx
components/MetricsPanel.tsx
components/TavusInterview.tsx
components/TavusInterviewWebComponent.tsx
COPY_TO_CREAO.md
CREAO_INTEGRATION.md
examples/web-component-example.html
FILE_INDEX.md
hooks/useMetrics.ts
JOB_EXTRACTION.md
lib/analytics/analyticsController.ts
lib/analytics/asr.ts
lib/analytics/config.ts
lib/analytics/goemotions.ts
lib/analytics/metricsBus.ts
lib/analytics/scoring.ts
lib/analytics/types.d.ts
lib/index.ts
lib/tavus-client.ts
next.config.js
package.json
postcss.config.js
PROJECT_SUMMARY.md
public/worklets/analyzer.worklet.js
QUICKSTART.md
README.md
scripts/setup.sh
STRUCTURE.txt
tailwind.config.js
TROUBLESHOOTING.md
tsconfig.json
types/index.ts
UI_GUIDE.md
UPDATES.md
workers/faceWorker.ts
```

### Dependencies

- package.json: @tavily/core@^0.5.12, @tensorflow-models/face-landmarks-detection@^1.0.2, @tensorflow/tfjs@^4.15.0, @types/node@^20, @types/react@^18, @types/react-dom@^18, @xenova/transformers@^2.17.2, autoprefixer@^10.4.17, next@14.2.18, openai@^4.77.0, postcss@^8.4.33, react@^18.3.1, react-dom@^18.3.1, tailwindcss@^3.4.1, typescript@^5, zod@^3.23.8

### Recent commits (newest first)

- everything works
- tavus agents

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

### QUICKSTART.md

```markdown
# Quick Start Guide

Get up and running in 60 seconds.

## 1. Setup (30 seconds)

```bash
# Clone or navigate to the project
cd tavus

# Run setup script
bash scripts/setup.sh

# Or manually:
cp .env.local.example .env.local
npm install
```

## 2. Configure (15 seconds)

Edit `.env.local`:

```env
NEXT_PUBLIC_TAVUS_API_KEY=your_key_from_tavus_platform
```

Get your key: https://platform.tavus.io

## 3. Run (15 seconds)

```bash
npm run dev
```

Open http://localhost:3000

## That's It!

You should see:
- ✅ Left panel with JSON editors
- ✅ Center panel with interview UI
- ✅ Bottom panel with event log

## First Test

1. Click "Start Interview" in the center panel
2. Watch the event log at the bottom
3. You should see:
   - `ready` event (persona + conversation created)
   - `connected` event (iframe loaded)
   - Tavus interview iframe with camera/mic permissions

## Common Issues

### "API key not set" warning

→ Edit `.env.local` and add your key, then restart:
```bash
# Stop the server (Ctrl+C)
npm run dev
```

### Port 3000 in use

→ Run on a different port:
```bash
npm run dev -- -p 3001
```

### "Module not found"

→ Install dependencies:
```bash
npm install
```

## Next Steps

- **Edit the persona**: Change the JSON in the left panel
- **Reuse a persona**: Set `NEXT_PUBLIC_TAVUS_PERSONA_ID` in `.env.local`
- **Enable autoplay**: Check the "Autoplay on mount" box
- **Test events**: Open browser console and type:
  ```js
  window.__pushMetric({ type: "note", timestamp: Date.now(), text: "test" })
  ```

## Full Documentation

- [README.md](README.md) - Complete setup and usage
- [CREAO_INTEGRATION.md](CREAO_INTEGRATION.md) - Embed in Creao
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Fix common issues
- [ARCHITECTURE.md](ARCHITECTURE.md) - How it all works

## One-Liner for Demos

```bash
cp .env.local.example .env.local && \
  echo "NEXT_PUBLIC_TAVUS_API_KEY=your_key" > .env.local && \
  npm install && \
  npm run dev
```

(Replace `your_key` with your actual Tavus API key)

```

### JOB_EXTRACTION.md

```markdown
# Dynamic Job Extraction Integration

The app now dynamically extracts job information from a URL and generates personalized interviews.

## What Changed

### 1. New API Route: `/api/extract-job`
**File**: `app/api/extract-job/route.ts`

Uses your exact script logic:
1. **Tavily** searches and summarizes the job posting
2. **OpenAI (gpt-4o-mini)** extracts structured data:
   - Company name
   - Role title
   - Job summary
   - 3 generated interview questions

### 2. Completely Redesigned Frontend
**File**: `app/page.tsx`

#### New User Flow:
```
1. Enter Job URL
   ↓
2. Click "Analyze Job"
   ↓
3. See extracted company, role, questions
   ↓
4. Click "Start Interview"
   ↓
5. Interview begins with AI recruiter
   ↓
6. Analytics track performance in real-time
```

#### Features:
- Pre-filled with Meta job URL as example
- Clean single-input interface
- Loading states during extraction
- Error handling with clear messages
- Shows extracted data before starting
- "Try Different Job" button to reset

### 3. Dynamic Persona & Context
The interview is now fully customized based on extracted data:

**Persona (AI Recruiter)**:
- Name: `{Company} Recruiter`
- System prompt includes:
  - Company name
  - Role title
  - Generated interview questions

**Interview Context**:
- Company from extraction
- Role from extraction
- Summary as job highlights
- Questions integrated into context

### 4. Environment Variables
**Added to `.env.local`**:
```env
TAVILY_API_KEY=tvly-dev-...
OPENAI_API_KEY=sk-svcacct-...
```

These are **server-side only** (not exposed to browser).

## How It Works

```
User enters job URL
    ↓
Frontend calls /api/extract-job
    ↓
Server fetches job data via Tavily
    ↓
Server extracts structure via OpenAI
    ↓
Returns: { company, role, summary, questions }
    ↓
Frontend builds persona + context dynamically
    ↓
Starts Tavus interview with custom data
    ↓
Analytics track performance
```

## API Response Format

```typescript
{
  company: "Meta",
  role: "Software Engineer, Infrastructure",
  summary: "Build large-scale infrastructure...",
  questions: [
    "Tell me about your experience with distributed systems",
    "How do you approach system design?",
    "What's your experience with large-scale infrastructure?"
  ]
}
```

## Example Usage

1. **Default Meta Job** (pre-filled):
   ```
   https://www.metacareers.com/jobs/1471056164046415
   ```

2. **Try any job URL**:
   - LinkedIn jobs
   - Indeed postings
   - Company career pages
   - Any public job posting

3. **Generated Interview**:
   - AI interviewer acts as recruiter from that company
   - Asks questions relevant to that specific role
   - Uses the 3 generated questions as guidance
   - Tracks your performance with analytics

## Files Modified

### New Files:
- `app/api/extract-job/route.ts` - API endpoint
- `JOB_EXTRACTION.md` - This documentation

### Modified Files:
- `app/page.tsx` - Completely refactored for job URL input
- `package.json` - Added `@tavily/core`, `op
[truncated — 1057 more characters]
```

### package.json

```
{
  "name": "tavus-interview-demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@tavily/core": "^0.5.12",
    "@tensorflow-models/face-landmarks-detection": "^1.0.2",
    "@tensorflow/tfjs": "^4.15.0",
    "@xenova/transformers": "^2.17.2",
    "next": "14.2.18",
    "openai": "^4.77.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.4.17",
    "postcss": "^8.4.33",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### lib/index.ts

```typescript
/**
 * Public exports for Tavus API utilities
 */

export {
  createObjectives,
  createPersona,
  createConversation,
  getConversation,
  buildSystemPrompt,
  buildConversationalContext,
  getReusablePersonaId,
  getReplicaId,
} from "./tavus-client";

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Tavus Interview Demo",
  description: "Client-side Tavus integration for hackathon",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### components/index.ts

```typescript
/**
 * Public exports for embedding in other projects
 */

export { TavusInterview } from "./TavusInterview";
export type { TavusInterviewProps } from "./TavusInterview";

export { MetricsPanel } from "./MetricsPanel";
export type { MetricsPanelProps } from "./MetricsPanel";

export {
  registerTavusWebComponent,
  useTavusWebComponent,
} from "./TavusInterviewWebComponent";

```

### types/index.ts

```typescript
/**
 * Data contracts for Tavus integration
 * These types define the public API surface for the interview system
 */

export interface InterviewContext {
  company: string;
  role: string;
  seniority?: string;
  jdHighlights?: string[];
  extraContext?: string;
}

export interface PersonaInput {
  name: string;
  systemPrompt: string; // main instructions
  topics?: string[];
  tone?: "neutral" | "friendly" | "direct" | "challenging";
  followUpStyle?: "balanced" | "deep-dive" | "rapid-fire" | "supportive";
  questionStyle?: "behavioral" | "technical" | "hybrid";
  maxQuestions?: number;
  maxFollowUpsPerQuestion?: number;
  attachContextFromInterview?: boolean; // if true, include InterviewContext in instructions
}

export interface InterviewSession {
  conversationId: string;
  conversationUrl: string;
}

export type UIMessageType = "ready" | "connected" | "disconnected" | "error" | "note";

export interface UIMessage {
  type: UIMessageType;
  timestamp: number;
  text?: string;
  payload?: unknown; // For opaque pass-through data
}

/**
 * Tavus API types - exact spec from docs.tavus.io
 * Do NOT add fields not in the official API documentation
 */

export interface TavusPersonaPayload {
  pipeline_mode: "full" | "echo"; // Required; use "full" for standard interviews
  system_prompt: string; // Required with pipeline_mode "full"
  persona_name?: string;
  context?: string; // Optional global context for persona
  default_replica_id?: string; // Optional; if set, conversations don't need replica_id
  objectives_id?: string; // Optional; objectives ID from POST /v2/objectives
  document_ids?: string[];
  document_tags?: string[];
  layers?: Record<string, unknown>; // Advanced; leave undefined unless needed
}

export interface TavusPersonaResponse {
  persona_id: string;
  persona_name: string;
  created_at: string;
  [key: string]: unknown; // Allow other fields, but don't depend on them
}

export interface TavusConversationPayload {
  persona_id: string; // Required
  replica_id?: string; // Required if persona has no default_replica_id
  audio_only?: boolean;
  callback_url?: string; // For webhooks; requires server (skip for browser-only)
  conversation_name?: string;
  conversational_context?: string; // Per-run context (e.g., job/company details)
  custom_greeting?: string;
  memory_stores?: string[];
  document_ids?: string[];
  document_tags?: string[];
  document_retrieval_strategy?: "speed" | "quality" | "balanced";
  test_mode?: boolean;
  properties?: Record<string, unknown>;
}

export interface TavusConversationResponse {
  conversation_id: string;
  conversation_url: string;
  conversation_name?: string;
  status?: string;
  replica_id?: string;
  persona_id?: string;
  created_at?: string;
  [key: string]: unknown; // Allow other fields, but don't depend on them
}

export interface TavusErrorResponse {
  error?: string;
  message?: string;
  [key: string]: unknown;
}

/**
 * Objectives API types - from docs.tavus.io/api-reference/objectives
 */

export interface ObjectiveData {
  objective_name: string; // Required; no spaces
  objective_prompt: string; // Required; defines what the objective should accomplish
  confirmation_mode?: "auto" | "manual"; // Optional; default "auto"
  output_variables?: string[]; // Optional; variables for extraction
  modality?: "verbal" | "visual"; // Optional; default "verbal"
  next_conditional_objectives?: Record<string, string>; // Optional; maps objective names to conditions
  next_required_objectives?: string[]; // Optional; auto-activated after completion
  callback_url?: string; // Optional; webhook for completion notifications
}

export interface TavusObjectivesPayload {
  data: ObjectiveData[]; // Array of objectives
}

export interface TavusObjectivesResponse {
  objectives_id: string;
  created_at?: string;
  [key: string]: unknown;
}

```

### app/page.tsx

```typescript
"use client";

/**
 * Main interview page - simplified with job URL input
 */

import { useState } from "react";
import { TavusInterview } from "@/components/TavusInterview";
import { AnalyticsPanel } from "@/components/AnalyticsPanel";
import { useTavusWebComponent } from "@/components/TavusInterviewWebComponent";
import {
  PersonaInput,
  InterviewContext,
  UIMessage,
  TavusObjectivesPayload,
} from "@/types";

interface ExtractedJobData {
  company: string;
  role: string;
  summary: string;
  questions: string[];
}

export default function HomePage() {
  // Register web component
  useTavusWebComponent();

  // Job extraction state
  const [jobUrl, setJobUrl] = useState("https://www.metacareers.com/jobs/1471056164046415");
  const [extracting, setExtracting] = useState(false);
  const [extractError, setExtractError] = useState<string | null>(null);
  const [jobData, setJobData] = useState<ExtractedJobData | null>(null);

  // Interview state
  const [isInterviewRunning, setIsInterviewRunning] = useState(false);
  const [interviewStarted, setInterviewStarted] = useState(false);
  const [interviewEnded, setInterviewEnded] = useState(false);
  const [finalSummary, setFinalSummary] = useState<any>(null);

  // Extract job data from URL
  const handleExtractJob = async () => {
    if (!jobUrl.trim()) {
      setExtractError("Please enter a job URL");
      return;
    }

    setExtracting(true);
    setExtractError(null);
    setJobData(null);

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

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || "Failed to extract job data");
      }

      const data: ExtractedJobData = await response.json();
      setJobData(data);
      console.log("[HomePage] Job data extracted:", data);
    } catch (err) {
      const message = err instanceof Error ? err.message : "Unknown error";
      setExtractError(message);
      console.error("[HomePage] Extract error:", err);
    } finally {
      setExtracting(false);
    }
  };

  // Build persona and context from extracted data
  const persona: PersonaInput | null = jobData
    ? {
        name: `${jobData.company} Recruiter`,
        systemPrompt: `You are a friendly recruiter at ${jobData.company} conducting an interview for the ${jobData.role} position. Ask thoughtful questions about the candidate's experience and skills relevant to this role. Be conversational and encouraging. Here are some key questions to cover: ${jobData.questions.join("; ")}`,
        topics: ["experience", "technical skills", "motivation", "culture fit"],
        tone: "friendly",
        followUpStyle: "balanced",
        questionStyle: "hybrid",
        maxQuestions: 5,
        maxFollowUpsPerQuestion: 2,
        attachContextFromInterview: true,
      }
    : null;

  const context: InterviewContext | null = jobData
    ? {
        company: jobData.company,
        role: jobData.role,
        seniority: "Mid-Senior",
        jdHighlights: [jobData.summary],
        extraContext: `Interview questions focus: ${jobData.questions.join(", ")}`,
      }
    : null;

  const objectives: TavusObjectivesPayload = {
    data: [
      {
        objective_name: "assess_technical_fit",
        objective_prompt:
          "Evaluate the candidate's technical expertise and how it aligns with the role requirements.",
        confirmation_mode: "auto",
      },
      {
        objective_name: "evaluate_motivation",
        objective_prompt:
          "Assess the candidate's motivation for the role and alignment with company values.",
        confirmation_mode: "auto",
      },
    ],
  };

  // Handle interview events
  const handleEvent = (message: UIMessage) => {
    console.log("[HomePage] Interview event:", message.type);

    if (message.type === "connected") {
      setIsInterviewRunning(true);
    } else if (message.type === "disconnected") {
      // Mark interview as stopped but don't auto-redirect
      setIsInterviewRunning(false);
    } else if (message.type === "error") {
      setIsInterviewRunning(false);
    }
  };

  // Manually end interview and go to finalization
  const handleEndInterview = () => {
    setIsInterviewRunning(false);
    setInterviewEnded(true);
  };

  // Handle analytics summary
  const handleAnalyticsSummary = (summary: any) => {
    setFinalSummary(summary);
  };

  // Start interview
  const handleStartInterview = () => {
    setInterviewStarted(true);
  };

  return (
    <div style={{
      minHeight: '100vh',
      display: 'flex',
      flexDirection: 'column',
      background: '#f9fafb',
    }}>
      <header style={{
        background: 'white',
        borderBottom: '1px solid #e5e7eb',
        padding: '16px 24px',
      }}>
        <h1 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>
          AI Interview Practice
        </h1>
      </header>

      <main style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
        {interviewEnded ? (
          // Finalization Screen
          <div style={{
            maxWidth: '800px',
            margin: '80px auto',
            padding: '0 24px',
            width: '100%',
          }}>
            <div style={{
              background: 'white',
              borderRadius: '12px',
              padding: '40px',
              boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
            }}>
              <div style={{ textAlign: 'center', marginBottom: '32px' }}>
                <h2 style={{
                  margin: '0 0 8px 0',
                  fontSize: '28px',
                  fontWeight: 600,
                  color: '#10b981',
                }}>
                  Interview Complete!
                </h2>
                <p style={{
                  margin: 0,
                  color: '#6b7280',
                  fontSize: '15px',
  
[truncated — 14352 more characters]
```

### app/api/extract-job/route.ts

```typescript
import { tavily } from "@tavily/core";
import OpenAI from "openai";
import { zodResponseFormat } from "openai/helpers/zod.mjs";
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const JobInfoSchema = z.object({
  company: z.string(),
  role: z.string(),
  summary: z.string(),
  questions: z.array(z.string()),
});

export async function POST(request: NextRequest) {
  try {
    const { jobUrl } = await request.json();

    if (!jobUrl) {
      return NextResponse.json(
        { error: "Job URL is required" },
        { status: 400 }
      );
    }

    // Get API keys from environment variables
    const tavilyApiKey = process.env.TAVILY_API_KEY;
    const openaiApiKey = process.env.OPENAI_API_KEY;

    if (!tavilyApiKey || !openaiApiKey) {
      return NextResponse.json(
        { error: "API keys not configured" },
        { status: 500 }
      );
    }

    console.log("[extract-job] Fetching job data from Tavily...");
    const tavilyClient = tavily({ apiKey: tavilyApiKey });
    const tavilyResponse = await tavilyClient.search(
      `Summarize this job posting: ${jobUrl}`,
      { includeAnswer: true }
    );

    console.log("[extract-job] Extracting structured data with OpenAI...");
    const openai = new OpenAI({
      apiKey: openaiApiKey,
    });

    const openaiResponse = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      max_tokens: 400,
      messages: [
        {
          role: "system",
          content:
            "You are an API that extracts structured job information from text. Respond ONLY in this exact JSON format: {\"company\": string, \"role\": string, \"summary\": string, \"questions\": string[]}",
        },
        {
          role: "user",
          content: `Extract the company, role, and job summary. Then, generate 3 interview questions that are typically asked in an interview for this role at this company: ${JSON.stringify(
            tavilyResponse
          )}`,
        },
      ],
      response_format: zodResponseFormat(JobInfoSchema, "job_info"),
    });

    const extractedData = JSON.parse(
      openaiResponse.choices[0].message.content || "{}"
    );

    console.log("[extract-job] Extraction complete:", extractedData);

    return NextResponse.json(extractedData);
  } catch (error) {
    console.error("[extract-job] Error:", error);
    return NextResponse.json(
      {
        error: "Failed to extract job information",
        details: error instanceof Error ? error.message : String(error),
      },
      { status: 500 }
    );
  }
}

```

### postcss.config.js

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

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