# Project export: jask

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: Interview practice readily available for everyone. Just ask.
- Devpost: https://devpost.com/software/jask
- GitHub: https://github.com/Neamal/jask
- Video: https://www.youtube.com/embed/O4VfpbZj8eE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Junhyung Yoon (2 commits), Subhash Prasad (2 commits), Akul Singh (1 commits)

## Devpost submission (written by the team)

### Inspiration

Preparing for technical interviews is a high-stress experience. While several AI practice platforms exist, we found they often feel robotic, lack conversational depth, or fail to provide the immediate, specific feedback that actually helps a developer improve. We were inspired to build jask to bridge that gap, creating a more realistic, responsive, and truly helpful practice partner that simulates the pressure and feedback of a live interview.

### What it does

jask is an AI-powered technical interviewer that provides live, interactive feedback. A user joins a real-time video and audio session, just like a real remote interview. The AI-interviewer presents a coding challenge and as the user types their solution into the shared editor, jask analyzes their code live. It provides instant feedback on correctness, logic, efficiency, and style, allowing the user to correct their mistakes and explain their thought process in real-time.

### How we built it

LiveKit: We used LiveKit's open-source platform to handle all real-time WebRTC communication. We also used Anthropic's Claude API for real-time code analysis and error checking. Every 10s when the user pauses for at least 10s, our backend sends a request to the anthropic agent to analyze the user's current code and gauge where the user may be stuck/confused. This agent sends the code analysis to the livekit agent which will then ask the user if they need help and give them assistance/feedback based on the analysis.

### Challenges we ran into

Passing the user's keystrokes from the frontend editor, sending that code to our Node.js backend for analysis by the AI, and then streaming the AI's feedback back to the user via LiveKit's data channels—all in a split second—required careful state management. Overcoming the initial latency to make the feedback feel truly instantaneous was a major breakthrough.

### Accomplishments we're proud of

Our biggest accomplishment is the end-to-end feedback loop. Integrating the Claude API calls with the seamless flow of conversation was the toughest part, as we needed to find the best triggers for generating this analysis in a way that is both descriptive and performant.

### What we learned

This project was a deep dive into the architecture of real-time applications. We didn't just learn "how to use LiveKit"; we learned the core principles of WebRTC, how to effectively manage real-time data channels, and the complexities of building stateful, low-latency, and interactive web applications from the ground up.

### What's next

Expand Question Types: Move beyond algorithm questions to include System Design, database, and behavioral interview modules. User Progress Dashboard: Implement user accounts and a dashboard to save interview history, track progress on specific topics, and identify areas for improvement. Advanced Feedback: Integrate feedback on non-technical aspects, such as communication clarity, speech patterns, and filler words, to provide holistic interview coaching.

## README (from the GitHub repository)

# Mock Interview App

A simple mock interview application that uses LiveKit for real-time voice conversations during technical interviews.

## Features

- **Landing Page**: Submit a LeetCode-style coding question
- **Live Interview**: Real-time voice conversation with AI interviewer
- **Code Editor**: Write your solution while discussing your approach
- **Simple Setup**: Minimal dependencies, powered by LiveKit

## Setup

### 1. Install Web App Dependencies

```bash
npm install
```

### 2. Configure LiveKit

1. Sign up for a free account at [LiveKit Cloud](https://cloud.livekit.io/)
2. Create a new project
3. Copy your API Key, API Secret, and WebSocket URL
4. Create a `.env.local` file:

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

5. Edit `.env.local` with your LiveKit credentials:

```env
LIVEKIT_API_KEY=your_api_key_here
LIVEKIT_API_SECRET=your_api_secret_here
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
```

### 3. Set Up the AI Agent (Python)

The AI interviewer runs as a separate Python service:

1. **Install Python dependencies** (requires Python 3.11+):
   ```bash
   cd agent
   pip install -e .
   ```

2. **Configure API keys** in `agent/.env.local`:
   ```env
   # Already has LiveKit credentials
   ASSEMBLYAI_API_KEY=your_assemblyai_key_here
   OPENAI_API_KEY=your_openai_key_here
   CARTESIA_API_KEY=your_cartesia_key_here
   ```

3. **Get API keys**:
   - [AssemblyAI](https://www.assemblyai.com/) - Speech-to-text
   - [OpenAI](https://platform.openai.com/) - GPT-4 mini for conversation
   - [Cartesia](https://cartesia.ai/) - Text-to-speech

### 4. Run the Application

**Terminal 1 - Web App:**
```bash
npm run dev
```

**Terminal 2 - AI Agent:**
```bash
cd agent
python agent.py dev
```

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

## How It Works

1. **Enter Question**: On the landing page, enter a coding question (e.g., "Write a function to find the longest palindromic substring")

2. **Start Interview**: Click "Start Interview" to enter the interview room

3. **Enable Microphone**: Allow microphone access when prompted

4. **Code & Talk**: Write your solution in the code editor while discussing your approach out loud

5. **AI Interaction**: The LiveKit room enables voice communication (you'll need to implement the AI agent separately using LiveKit's Agent Framework)

## Tech Stack

- **Next.js 15** - React framework with App Router
- **TypeScript** - Type safety
- **Tailwind CSS** - Styling
- **LiveKit** - Real-time voice communication
- **@livekit/components-react** - Pre-built LiveKit UI components

## Project Structure

```
jask/
├── app/
│   ├── api/
│   │   └── livekit-token/    # Token generation endpoint
│   ├── interview/             # Interview room page
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx              # Landing page
├── agent/
│   ├── agent.py              # LiveKit AI agent
│   ├── pyproject.toml        # Python dependencies
│   └── .env.local            # Agent API keys
├── .env.example
├── .env.local
├── package.json
└── README.md
```

## How the Agent Works

The AI interviewer (JASK) automatically joins interview rooms and:
- Greets the candidate and asks about their background
- Listens to the candidate's voice explanations
- Provides guidance and hints without giving away the solution
- Evaluates problem-solving skills and technical communication
- Uses AssemblyAI for speech recognition
- Uses OpenAI GPT-4 for intelligent responses
- Uses Cartesia for natural-sounding voice output

## License

MIT


## Detected evidence (automated analysis)

Indexed codebase: 10 recognized source files, 18 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (23 of 23)

```
.claude/settings.local.json
.env.example
.eslintrc.json
.gitignore
agent/agent.py
agent/jask_interview_agent.egg-info/dependency_links.txt
agent/jask_interview_agent.egg-info/PKG-INFO
agent/jask_interview_agent.egg-info/requires.txt
agent/jask_interview_agent.egg-info/SOURCES.txt
agent/jask_interview_agent.egg-info/top_level.txt
agent/pyproject.toml
agent/uv.lock
app/api/livekit-token/route.ts
app/globals.css
app/interview/page.tsx
app/layout.tsx
app/page.tsx
next.config.js
package.json
postcss.config.js
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- agent/pyproject.toml: livekit-agents[silero,turn-detector]@~=1.2, livekit-plugins-noise-cancellation@~=0.2, python-dotenv@>=1.1.1
- package.json: @livekit/components-react@^2.5.0, @types/node@^20.10.5, @types/react@^18.2.45, @types/react-dom@^18.2.18, autoprefixer@^10.4.16, eslint@^8.56.0, eslint-config-next@^15.0.0, livekit-client@^2.5.0, livekit-server-sdk@^2.6.0, next@^15.0.0, postcss@^8.4.32, react@^18.3.1, react-dom@^18.3.1, tailwindcss@^3.4.0, typescript@^5.3.3

### Recent commits (newest first)

- modify env config
- voice agent works
- initial website stuff
- first layout
- chore: add interview-helper prototype template

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

### package.json

```
{
  "name": "mock-interview-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "next": "^15.0.0",
    "@livekit/components-react": "^2.5.0",
    "livekit-client": "^2.5.0",
    "livekit-server-sdk": "^2.6.0"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.10.5",
    "@types/react": "^18.2.45",
    "@types/react-dom": "^18.2.18",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "tailwindcss": "^3.4.0",
    "eslint": "^8.56.0",
    "eslint-config-next": "^15.0.0"
  }
}

```

### agent/pyproject.toml

```
[project]
name = "jask-interview-agent"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "livekit-agents[silero,turn-detector]~=1.2",
    "livekit-plugins-noise-cancellation~=0.2",
    "python-dotenv>=1.1.1",
]

```

### app/layout.tsx

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

export const metadata: Metadata = {
  title: 'Mock Interview App',
  description: 'Practice technical interviews with AI',
}

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

```

### app/page.tsx

```typescript
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function Home() {
  const [question, setQuestion] = useState('')
  const router = useRouter()

  const handleStart = () => {
    if (question.trim()) {
      const encodedQuestion = encodeURIComponent(question)
      router.push(`/interview?question=${encodedQuestion}`)
    }
  }

  return (
    <main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
      <div className="max-w-2xl w-full bg-white rounded-2xl shadow-xl p-8">
        <h1 className="text-4xl font-bold text-gray-800 mb-2">
          Mock Interview Practice
        </h1>
        <p className="text-gray-600 mb-8">
          Practice your technical interview skills with AI-powered feedback
        </p>

        <div className="space-y-4">
          <div>
            <label htmlFor="question" className="block text-sm font-medium text-gray-700 mb-2">
              Enter your coding question
            </label>
            <textarea
              id="question"
              rows={6}
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none resize-none"
              placeholder="Example: Write a function that returns the nth Fibonacci number..."
              value={question}
              onChange={(e) => setQuestion(e.target.value)}
            />
          </div>

          <button
            onClick={handleStart}
            disabled={!question.trim()}
            className="w-full bg-indigo-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
          >
            Start Interview
          </button>
        </div>

        <div className="mt-8 p-4 bg-blue-50 rounded-lg">
          <h3 className="font-semibold text-gray-800 mb-2">How it works:</h3>
          <ul className="text-sm text-gray-600 space-y-1 list-disc list-inside">
            <li>Enter a LeetCode-style question</li>
            <li>Start the interview and enable your microphone</li>
            <li>Write your solution in the code editor</li>
            <li>Discuss your approach with the AI interviewer</li>
          </ul>
        </div>
      </div>
    </main>
  )
}

```

### app/interview/page.tsx

```typescript
'use client'

import { useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
import {
  LiveKitRoom,
  RoomAudioRenderer,
  useTracks,
  useLocalParticipant,
  useRoomContext,
} from '@livekit/components-react'
import { Track, RoomEvent, ConnectionState } from 'livekit-client'

function InterviewRoom({ question }: { question: string }) {
  const [code, setCode] = useState('')
  const [debugLogs, setDebugLogs] = useState<string[]>([])
  const tracks = useTracks([Track.Source.Microphone])
  const { localParticipant } = useLocalParticipant()
  const room = useRoomContext()

  useEffect(() => {
    const addLog = (msg: string) => {
      const timestamp = new Date().toLocaleTimeString()
      setDebugLogs(prev => [...prev, `[${timestamp}] ${msg}`].slice(-20))
    }

    addLog(`Room state: ${room.state}`)
    addLog(`Local participant: ${localParticipant.identity}`)
    addLog(`Microphone enabled: ${localParticipant.isMicrophoneEnabled}`)

    const handleConnectionStateChange = (state: ConnectionState) => {
      addLog(`Connection state changed: ${state}`)
    }

    const handleParticipantConnected = (participant: any) => {
      addLog(`Participant connected: ${participant.identity}`)
    }

    const handleParticipantDisconnected = (participant: any) => {
      addLog(`Participant disconnected: ${participant.identity}`)
    }

    const handleTrackSubscribed = (track: any, publication: any, participant: any) => {
      addLog(`Track subscribed from ${participant.identity}: ${track.kind}`)
    }

    const handleDataReceived = (payload: Uint8Array, participant: any) => {
      const text = new TextDecoder().decode(payload)
      addLog(`Data received from ${participant?.identity || 'unknown'}: ${text}`)
    }

    room.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChange)
    room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)
    room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
    room.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
    room.on(RoomEvent.DataReceived, handleDataReceived)

    return () => {
      room.off(RoomEvent.ConnectionStateChanged, handleConnectionStateChange)
      room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)
      room.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
      room.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
      room.off(RoomEvent.DataReceived, handleDataReceived)
    }
  }, [room, localParticipant])

  return (
    <div className="min-h-screen bg-gray-50 flex flex-col">
      <RoomAudioRenderer />

      {/* Header */}
      <div className="bg-white border-b border-gray-200 p-4">
        <div className="max-w-7xl mx-auto">
          <h1 className="text-2xl font-bold text-gray-800">Interview Session</h1>
          <p className="text-sm text-gray-600 mt-1">
            {localParticipant.isMicrophoneEnabled ? (
              <span className="text-green-600">🎤 Microphone Active</span>
            ) : (
              <span className="text-red-600">🎤 Microphone Off</span>
            )}
          </p>
        </div>
      </div>

      {/* Main Content */}
      <div className="flex-1 max-w-7xl w-full mx-auto p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Question Panel */}
        <div className="space-y-4">
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-lg font-semibold text-gray-800 mb-3">Question</h2>
            <p className="text-gray-700 whitespace-pre-wrap">{question}</p>
          </div>

          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-lg font-semibold text-gray-800 mb-3">Instructions</h2>
            <ul className="text-sm text-gray-600 space-y-2">
              <li>• Speak your thoughts out loud as you code</li>
              <li>• The AI interviewer can hear you and provide feedback</li>
              <li>• Write your solution in the code editor</li>
              <li>• Explain your approach and time/space complexity</li>
            </ul>
          </div>

          <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
            <p className="text-sm text-gray-700 mb-2">
              <strong>⚠️ Note:</strong> This app currently only establishes a LiveKit room.
              To have an AI interviewer that speaks to you, you need to set up a LiveKit Agent.
            </p>
          </div>

          {/* Debug Panel */}
          <div className="bg-gray-900 rounded-lg shadow overflow-hidden">
            <div className="bg-gray-800 px-4 py-2">
              <h3 className="text-sm font-semibold text-white">Debug Console</h3>
            </div>
            <div className="p-4 h-48 overflow-y-auto font-mono text-xs">
              {debugLogs.length === 0 ? (
                <p className="text-gray-500">Waiting for events...</p>
              ) : (
                debugLogs.map((log, i) => (
                  <div key={i} className="text-green-400 mb-1">{log}</div>
                ))
              )}
            </div>
          </div>
        </div>

        {/* Code Editor Panel */}
        <div className="bg-white rounded-lg shadow overflow-hidden flex flex-col">
          <div className="bg-gray-800 text-white px-4 py-2 flex items-center justify-between">
            <span className="text-sm font-medium">Code Editor</span>
            <span className="text-xs text-gray-400">JavaScript</span>
          </div>
          <textarea
            value={code}
            onChange={(e) => setCode(e.target.value)}
            className="flex-1 p-4 font-mono text-sm resize-none focus:outline-none bg-gray-900 text-gray-100"
            placeholder="// Write your solution here...

function solution() {
  // Your code
}
"
            spellCheck={false}
          />
          <div className="bg-gray-100 px-4 py-2 text-xs text-gray-600">
            Lines: {code.split('\n').length} | Characters: {
[truncated — 1945 more characters]
```

### app/api/livekit-token/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { AccessToken } from 'livekit-server-sdk'

export async function POST(request: NextRequest) {
  try {
    const { roomName, participantName } = await request.json()

    if (!roomName || !participantName) {
      return NextResponse.json(
        { error: 'Missing roomName or participantName' },
        { status: 400 }
      )
    }

    const apiKey = process.env.LIVEKIT_API_KEY
    const apiSecret = process.env.LIVEKIT_API_SECRET

    if (!apiKey || !apiSecret) {
      return NextResponse.json(
        { error: 'Server configuration error' },
        { status: 500 }
      )
    }

    const at = new AccessToken(apiKey, apiSecret, {
      identity: participantName,
    })

    at.addGrant({
      room: roomName,
      roomJoin: true,
      canPublish: true,
      canSubscribe: true,
    })

    const token = await at.toJwt()

    return NextResponse.json({ token })
  } catch (error) {
    console.error('Error generating token:', error)
    return NextResponse.json(
      { error: 'Failed to generate token' },
      { status: 500 }
    )
  }
}

```

### postcss.config.js

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

```

### next.config.js

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig

```

### tailwind.config.ts

```typescript
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
export default config

```

### app/globals.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

```

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