voice-retell
Browser voice calling via Retell AI — WebRTC voice mode toggle in chat, web call token API, transcript bridging to chat messages, and mute/unmute controls. Use this skill when the user says "add voice", "voice calling", "setup retell", "add voice mode", or "setup voice-retell".
What this skill does
# Voice Retell
Browser-based voice calling powered by Retell AI. Adds a "Switch to voice" toggle inside the chat UI that connects via WebRTC, streams real-time audio to/from a Retell voice agent, and bridges call transcripts back into the same chat session for unified history.
## Prerequisites
- Next.js app with `src/` directory and App Router
- `ai-chat` skill installed (chat UI at `src/components/ai/chat.tsx`, sessions API)
- `auth` skill installed (`withAuth` at `@/lib/auth-guard`)
- `env-config` skill installed (`src/env.ts`)
- shadcn/ui initialized
## Installation
```bash
bun add retell-client-js-sdk
```
## Environment Variables
Add to `.env.local`:
```env
# Retell AI
RETELL_API_KEY=your-retell-api-key-here
RETELL_AGENT_ID=your-retell-agent-id-here
```
### Update `src/env.ts`
Add to the `server` object:
```typescript
server: {
// ... existing variables
RETELL_API_KEY: z.string(),
RETELL_AGENT_ID: z.string(),
},
```
Add to the `runtimeEnv` object:
```typescript
runtimeEnv: {
// ... existing variables
RETELL_API_KEY: process.env.RETELL_API_KEY,
RETELL_AGENT_ID: process.env.RETELL_AGENT_ID,
},
```
## What Gets Created
```
src/
├── app/
│ └── api/
│ └── ai/
│ └── voice/
│ └── route.ts # POST — create web call, return access token
├── lib/
│ └── voice/
│ └── retell.ts # Server-side Retell API helper
└── components/
└── ai/
├── voice-toggle.tsx # Mic button that starts/stops voice mode
└── voice-overlay.tsx # Active call overlay with status + mute
```
## What Gets Modified
```
src/
└── components/
└── ai/
└── chat.tsx # Add VoiceToggle to input area
```
## Setup Steps
### Step 1: Create `src/lib/voice/retell.ts`
```typescript
type CreateWebCallResponse = {
call_type: "web_call";
access_token: string;
call_id: string;
call_status: string;
};
type CreateWebCallOptions = {
agentId: string;
metadata?: Record<string, string>;
dynamicVariables?: Record<string, string>;
};
/**
* Create a Retell web call and return the access token.
* Must be called server-side — uses RETELL_API_KEY.
*/
export async function createWebCall(
options: CreateWebCallOptions
): Promise<CreateWebCallResponse> {
const apiKey = process.env.RETELL_API_KEY;
if (!apiKey) throw new Error("RETELL_API_KEY is not set");
const response = await fetch("https://api.retellai.com/v2/create-web-call", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
agent_id: options.agentId,
...(options.metadata && { metadata: options.metadata }),
...(options.dynamicVariables && {
retell_llm_dynamic_variables: options.dynamicVariables,
}),
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Retell API error ${response.status}: ${errorText}`);
}
return response.json() as Promise<CreateWebCallResponse>;
}
```
### Step 2: Create `src/app/api/ai/voice/route.ts`
```typescript
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { createWebCall } from "@/lib/voice/retell";
import { db } from "@/db";
import { chatSession, chatMessage } from "@/db/schema/chat";
import { eq, and } from "drizzle-orm";
type VoiceCallBody = {
sessionId?: string;
};
/** POST /api/ai/voice — create a Retell web call token */
export const POST = withAuth(async (request, { user }) => {
const body: VoiceCallBody = await request.json();
const agentId = process.env.RETELL_AGENT_ID;
if (!agentId) {
return NextResponse.json(
{ error: "RETELL_AGENT_ID is not configured" },
{ status: 500 }
);
}
// Resolve or create a chat session for transcript bridging
let activeSessionId = body.sessionId;
if (activeSessionId) {
const existing = await db
.select({ id: chatSession.id })
.from(chatSession)
.where(
and(
eq(chatSession.id, activeSessionId),
eq(chatSession.userId, user.id)
)
)
.limit(1);
if (existing.length === 0) {
return NextResponse.json(
{ error: "Session not found" },
{ status: 404 }
);
}
} else {
const [created] = await db
.insert(chatSession)
.values({ userId: user.id, title: "Voice Call" })
.returning({ id: chatSession.id });
activeSessionId = created.id;
}
try {
const call = await createWebCall({
agentId,
metadata: {
userId: user.id,
sessionId: activeSessionId,
},
});
return NextResponse.json({
accessToken: call.access_token,
callId: call.call_id,
sessionId: activeSessionId,
});
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Failed to create call",
},
{ status: 500 }
);
}
});
```
### Step 3: Create `src/components/ai/voice-overlay.tsx`
```tsx
"use client";
import { useCallback } from "react";
import { Microphone, MicrophoneSlash, Phone, X } from "@phosphor-icons/react";
type VoiceOverlayProps = {
isConnected: boolean;
isAgentTalking: boolean;
isMuted: boolean;
onMuteToggle: () => void;
onEndCall: () => void;
transcript: string | null;
};
export function VoiceOverlay({
isConnected,
isAgentTalking,
isMuted,
onMuteToggle,
onEndCall,
transcript,
}: VoiceOverlayProps) {
if (!isConnected) return null;
return (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-background/95 backdrop-blur-sm">
{/* Pulsing indicator */}
<div className="relative mb-8">
<div
className={`h-24 w-24 rounded-full ${
isAgentTalking
? "animate-pulse bg-primary/20"
: "bg-muted"
} flex items-center justify-center`}
>
<Phone
className={`h-10 w-10 ${
isAgentTalking ? "text-primary" : "text-muted-foreground"
}`}
/>
</div>
{isAgentTalking && (
<div className="absolute inset-0 animate-ping rounded-full bg-primary/10" />
)}
</div>
{/* Status */}
<p className="mb-2 text-sm font-medium">
{isAgentTalking ? "Agent is speaking..." : "Listening..."}
</p>
{/* Live transcript */}
{transcript && (
<p className="mb-8 max-w-md px-4 text-center text-sm text-muted-foreground">
{transcript}
</p>
)}
{/* Controls */}
<div className="flex gap-4">
<button
type="button"
onClick={onMuteToggle}
className={`flex h-14 w-14 items-center justify-center rounded-full transition-colors ${
isMuted
? "bg-destructive/10 text-destructive"
: "bg-muted hover:bg-muted/80"
}`}
title={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<MicrophoneSlash className="h-6 w-6" />
) : (
<Microphone className="h-6 w-6" />
)}
</button>
<button
type="button"
onClick={onEndCall}
className="flex h-14 w-14 items-center justify-center rounded-full bg-destructive text-destructive-foreground transition-colors hover:bg-destructive/90"
title="End call"
>
<X className="h-6 w-6" />
</button>
</div>
</div>
);
}
```
### Step 4: Create `src/components/ai/voice-toggle.tsx`
```tsx
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { RetellWebClient } from "retell-client-js-sdk";
import { Microphone } from "@phosphor-icons/react";
import { VoiceOverlay } from "./voice-overlay";
type TranscriptUpdate = {
tRelated in Image & Video
watch
IncludedWatch a video (URL or local path). Downloads with yt-dlp, extracts auto-scaled frames with ffmpeg, pulls the transcript from captions (or Whisper API fallback), and hands the result to Claude so it can answer questions about what's in the video.
physical-ai-defect-image-generation
IncludedUse when the user wants to orchestrate defect image generation, run associated setup, or handle outputs on OSMO. The Day 0 path handles cold-start with USD-to-ROI, image-edit augmentation, and AnomalyGen to create initial PCBA datasets. The Day 1 path performs inference and labeling on real images. This skill helps with first-time asset setup, creation of finetuning checkpoints, and configuring deployment. Trigger keywords: defect image generation, dig workflow, dig pipeline, defect image detection workflow, aoi pipeline, aoi anomalygen, usd2roi anomalygen, day 0 pcba, day 1 pcba, day 1 real-photo alignment, day 1 manual roi, metal surface anomaly, glass defect, anomalygen finetune, setup_pcb, setup_metal, setup_glass, setup_pretrained, dig setup, dig datasets, dig pretrained checkpoint, dig image-edit endpoint.
accelint-react-best-practices
IncludedReact performance optimization and best practices. ALWAYS use this skill when working with any React code - writing components, hooks, JSX; refactoring; optimizing re-renders, memoization, state management; reviewing for performance; fixing hydration mismatches; debugging infinite re-renders, stale closures, input focus loss, animations restarting; preventing remounting; implementing transitions, lazy initialization, effect dependencies. Even simple React tasks benefit from these patterns. Covers React 19+ (useEffectEvent, Activity, ref props). Triggers - useEffect, useState, useMemo, useCallback, memo, inline components, nested components, components inside components, re-render, performance, hydration, SSR, Next.js, useDeferredValue, combined hooks.
elevenlabs-agents
IncludedBuild conversational AI voice agents with ElevenLabs Platform using React, JavaScript, React Native, or Swift SDKs. Configure agents, tools (client/server/MCP), RAG knowledge bases, multi-voice, and Scribe real-time STT. Use when: building voice chat interfaces, implementing AI phone agents with Twilio, configuring agent workflows or tools, adding RAG knowledge bases, testing with CLI "agents as code", or troubleshooting deprecated @11labs packages, Android audio cutoff, CSP violations, dynamic variables, or WebRTC config. Keywords: ElevenLabs Agents, ElevenLabs voice agents, AI voice agents, conversational AI, @elevenlabs/react, @elevenlabs/client, @elevenlabs/react-native, @elevenlabs/elevenlabs-js, @elevenlabs/agents-cli, elevenlabs SDK, voice AI, TTS, text-to-speech, ASR, speech recognition, turn-taking model, WebRTC voice, WebSocket voice, ElevenLabs conversation, agent system prompt, agent tools, agent knowledge base, RAG voice agents, multi-voice agents, pronunciation dictionary, voice speed control, elevenlabs scribe, @11labs deprecated, Android audio cutoff, CSP violation elevenlabs, dynamic variables elevenlabs, case-sensitive tool names, webhook authentication
humanizer
IncludedHumanize AI-generated text by detecting and removing patterns typical of LLM output. Rewrites text to sound natural, specific, and human. Uses 28 pattern detectors, 560+ AI vocabulary terms across 3 tiers, and statistical analysis (burstiness, type-token ratio, readability) for comprehensive detection. Use when asked to humanize text, de-AI writing, make content sound more natural/human, review writing for AI patterns, score text for AI detection, or improve AI-generated drafts. Covers content, language, style, communication, and filler categories.
generating-mermaid-diagrams
IncludedSalesforce architecture diagrams using Mermaid with ASCII fallback. Use this skill when generating text-based diagrams for Salesforce architecture, OAuth flows, ERDs, integration sequences, or Agentforce structure. TRIGGER when: user says "diagram", "visualize", "ERD", or asks for sequence diagrams, flowcharts, class diagrams, or architecture visualizations in Mermaid. DO NOT TRIGGER when: user wants PNG/SVG image output (use generating-visual-diagrams), or asks about non-Salesforce systems.