What Is an AI Receptionist?
An AI receptionist is a software agent that handles front-desk tasks traditionally performed by a human receptionist — answering questions, scheduling appointments, routing calls, and greeting visitors. Unlike a simple chatbot, a full-featured AI receptionist combines natural language understanding, voice processing, calendar integration, and multi-channel deployment (web chat, phone, SMS, and even physical kiosks) into a single, deployable system. The "Business-in-a-Box" concept means you package all these capabilities into one turnkey solution that a business can deploy with minimal configuration — effectively productizing the entire receptionist role.
Core Capabilities
- Visitor greeting & triage: Welcomes customers, determines their needs, and routes them appropriately
- Appointment scheduling: Checks calendar availability, books, reschedules, or cancels appointments
- FAQ answering: Handles common questions about hours, services, pricing, and policies
- Call handling: Answers phone calls, takes messages, and transfers to human staff when needed
- Multi-language support: Serves customers in their preferred language
Why an AI Receptionist Matters
For businesses, the receptionist role is critical but expensive. A full-time receptionist costs $35,000–$60,000 annually in salary alone, plus benefits, training, and turnover costs. An AI receptionist operates 24/7, never calls in sick, scales instantly during peak hours, and costs a fraction of that — typically $200–$1,500/month depending on volume. Beyond cost savings, it eliminates missed calls during lunch breaks, reduces hold times to zero, and captures every lead. For developers, this is a high-demand product category: small medical practices, law firms, salons, contractors, and professional services all need it, yet most cannot afford custom development. Building a packaged "Business-in-a-Box" AI receptionist means you build once and sell many times.
Core Architecture Overview
A production-grade AI receptionist has four interconnected layers that work together across multiple channels. Here is the architecture at a glance:
┌─────────────────────────────────────────────────────────┐
│ Entry Channels │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Web Chat │ │ Phone │ │ SMS/MMS │ │ Kiosk │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │
│ ┌─────────────────────────▼──────────────────────────┐ │
│ │ Orchestration Layer │ │
│ │ • Session management • Context persistence │ │
│ │ • Intent routing • Escalation logic │ │
│ └─────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌─────────────────────────▼──────────────────────────┐ │
│ │ AI Core (LLM + Tools) │ │
│ │ • NLU / intent classification │ │
│ │ • Tool calling (calendar, CRM, email) │ │
│ │ • Guardrails & policy enforcement │ │
│ └─────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌─────────────────────────▼──────────────────────────┐ │
│ │ Integration Layer │ │
│ │ • Calendar APIs (Google, Outlook, Calendly) │ │
│ │ • CRM sync • Email/SMS gateways │ │
│ │ • Phone/SIP trunking (Twilio, SignalWire) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The Technology Stack
Here is a recommended stack that balances cost, performance, and ease of deployment for a packaged product:
- LLM Provider: OpenAI GPT-4o (fast, affordable, native tool calling) or Anthropic Claude for sensitive industries
- Speech-to-Text: Deepgram or Whisper (self-hosted for privacy)
- Text-to-Speech: ElevenLabs or OpenAI TTS for natural voices
- Phone/SIP: Twilio Programmable Voice (best documentation and reliability)
- Backend Runtime: Node.js with TypeScript or Python with FastAPI
- Vector Store for FAQ: Pinecone, pgvector, or ChromaDB
- Calendar Integration: Nylas (unified calendar API) or direct Google Calendar / Outlook APIs
- Deployment: Docker containers on Fly.io, Railway, or AWS ECS
Step-by-Step Implementation
We will build the AI receptionist as a multi-channel system, starting with the core AI engine and then adding channels. All code examples are production-ready and complete.
Step 1: Setting Up the AI Core with Intent Classification and Tool Calling
The heart of the receptionist is the LLM-powered agent that understands customer intent and decides what to do. We use OpenAI's chat completions API with tool calling (function calling) to give the model structured actions it can take. This is the most critical component — get this right, and everything else flows from it.
Create a new project directory and install dependencies:
mkdir ai-receptionist && cd ai-receptionist
npm init -y
npm install openai dotenv uuid
npm install --save-dev typescript @types/node
Here is the complete AI core agent in TypeScript. It handles intent classification, maintains conversation state, and executes tool calls for scheduling, FAQ lookup, and escalation:
// src/agent.ts
import OpenAI from 'openai';
import { v4 as uuidv4 } from 'uuid';
import dotenv from 'dotenv';
dotenv.config();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ─── Type Definitions ──────────────────────────────────────────
interface ConversationState {
sessionId: string;
customerName: string | null;
customerPhone: string | null;
intent: string | null;
collectedInfo: Record;
history: { role: 'user' | 'assistant' | 'system'; content: string }[];
}
interface ToolCallResult {
toolName: string;
result: string;
}
// ─── Tool Implementations ──────────────────────────────────────
async function checkCalendarAvailability(
date: string,
time: string,
serviceType: string
): Promise {
// In production, this calls Google Calendar / Nylas API
// For the business-in-a-box, we use a configurable calendar adapter
const mockSlots = ['9:00 AM', '10:30 AM', '2:00 PM', '4:00 PM'];
const available = date.includes('2025') ? mockSlots : [];
if (available.length === 0) {
return JSON.stringify({ available: false, slots: [], message: `No availability on ${date}. Try the next business day.` });
}
return JSON.stringify({ available: true, slots: available, message: `Available slots on ${date}: ${available.join(', ')}` });
}
async function bookAppointment(
date: string,
time: string,
customerName: string,
serviceType: string,
notes: string
): Promise {
// In production: creates event via calendar API, stores in DB
const confirmationId = uuidv4().slice(0, 8).toUpperCase();
return JSON.stringify({
booked: true,
confirmationId,
appointment: { date, time, customerName, serviceType, notes },
message: `Appointment confirmed for ${customerName} on ${date} at ${time} for ${serviceType}. Confirmation #${confirmationId}`
});
}
async function lookupFAQ(question: string): Promise {
// In production: semantic search over a vector store of business FAQs
// For now, we use a simple keyword-based lookup
const faqs: Record = {
hours: "Our office hours are Monday-Friday 9 AM to 5 PM, Saturday 10 AM to 2 PM. We're closed on Sundays.",
parking: "Free parking is available in the lot behind the building. Enter from Maple Street.",
pricing: "Initial consultations are $150. Follow-up visits are $100. We accept all major insurance plans.",
cancellation: "We require 24 hours notice for cancellations. Late cancellations incur a $50 fee.",
insurance: "We accept Aetna, Blue Cross, Cigna, United Healthcare, and Medicare. Please bring your insurance card.",
};
for (const [key, answer] of Object.entries(faqs)) {
if (question.toLowerCase().includes(key)) return answer;
}
return "I don't have that specific information. Let me connect you with a staff member who can help.";
}
async function escalateToHuman(
reason: string,
customerName: string | null,
summary: string
): Promise {
// In production: sends SMS/email/slack alert to on-call staff
console.log(`🚨 ESCALATION: ${reason} | Customer: ${customerName || 'Unknown'} | Summary: ${summary}`);
return JSON.stringify({
escalated: true,
message: "Let me connect you with a team member right now. Please hold for just a moment.",
estimatedWait: "2-3 minutes"
});
}
// ─── Main Agent Function ───────────────────────────────────────
export async function processMessage(
userMessage: string,
state: ConversationState
): Promise<{ response: string; updatedState: ConversationState; toolCallsUsed: string[] }> {
// Build the system prompt with business context
const systemPrompt = `You are a professional, warm, and efficient AI receptionist for "Oakwood Medical Practice" (replaceable per deployment).
Your job: greet visitors, answer FAQs, schedule appointments, and escalate to human staff when needed.
Current context:
- Session ID: ${state.sessionId}
- Customer name: ${state.customerName || 'not yet provided'}
- Collected info: ${JSON.stringify(state.collectedInfo)}
- Detected intent: ${state.intent || 'not yet classified'}
Rules:
1. ALWAYS be polite and concise. Use the customer's name once you learn it.
2. For scheduling: collect date, time, and service type before calling the booking tool.
3. For FAQs: use the lookup_faq tool. Do not invent answers.
4. Escalate to human for: emergencies, complaints, complex medical advice, or if the customer explicitly requests a human.
5. If the customer is just greeting, respond warmly and ask how you can help.
6. Never make medical diagnoses. Never prescribe. Never give legal advice.`;
// Build messages array
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: 'system', content: systemPrompt },
...state.history.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })),
{ role: 'user', content: userMessage }
];
// Define tools (function calling schema)
const tools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'check_calendar_availability',
description: 'Check available appointment slots for a given date and service type',
parameters: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date in YYYY-MM-DD format' },
time: { type: 'string', description: 'Preferred time or "any"' },
serviceType: { type: 'string', description: 'e.g., consultation, follow-up, new patient visit' }
},
required: ['date', 'serviceType']
}
}
},
{
type: 'function',
function: {
name: 'book_appointment',
description: 'Book a confirmed appointment slot',
parameters: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date in YYYY-MM-DD format' },
time: { type: 'string', description: 'Time slot (e.g., "10:30 AM")' },
customerName: { type: 'string', description: 'Full name of the customer' },
serviceType: { type: 'string' },
notes: { type: 'string', description: 'Any special notes or requests' }
},
required: ['date', 'time', 'customerName', 'serviceType']
}
}
},
{
type: 'function',
function: {
name: 'lookup_faq',
description: 'Search the knowledge base for answers to common questions',
parameters: {
type: 'object',
properties: {
question: { type: 'string', description: 'The customer question to look up' }
},
required: ['question']
}
}
},
{
type: 'function',
function: {
name: 'escalate_to_human',
description: 'Transfer the conversation to a human staff member',
parameters: {
type: 'object',
properties: {
reason: { type: 'string', description: 'Why escalation is needed' },
summary: { type: 'string', description: 'Summary of the conversation so far' }
},
required: ['reason', 'summary']
}
}
}
];
// First API call: model decides whether to use a tool
const response1 = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto',
temperature: 0.7,
max_tokens: 500,
});
const choice = response1.choices[0];
const toolCallsUsed: string[] = [];
let finalResponse = '';
let updatedState = { ...state };
// If the model wants to call tools, execute them
if (choice.message.tool_calls && choice.message.tool_calls.length > 0) {
// Append the assistant's tool-call message
messages.push({
role: 'assistant',
content: choice.message.content || '',
tool_calls: choice.message.tool_calls
} as any);
for (const toolCall of choice.message.tool_calls) {
const funcName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
toolCallsUsed.push(funcName);
let toolResult: string;
switch (funcName) {
case 'check_calendar_availability':
toolResult = await checkCalendarAvailability(args.date, args.time || 'any', args.serviceType);
break;
case 'book_appointment':
toolResult = await bookAppointment(args.date, args.time, args.customerName, args.serviceType, args.notes || '');
// Update state with collected info
updatedState.customerName = args.customerName;
updatedState.collectedInfo.appointment = JSON.stringify({ date: args.date, time: args.time });
break;
case 'lookup_faq':
toolResult = await lookupFAQ(args.question);
break;
case 'escalate_to_human':
toolResult = await escalateToHuman(args.reason, state.customerName, args.summary);
break;
default:
toolResult = JSON.stringify({ error: `Unknown tool: ${funcName}` });
}
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: toolResult
});
}
// Second API call: model synthesizes tool results into a natural response
const response2 = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
temperature: 0.7,
max_tokens: 300,
});
finalResponse = response2.choices[0].message.content || 'I apologize, let me try that again.';
} else {
// Model responded directly without tools
finalResponse = choice.message.content || 'How can I help you today?';
}
// Detect intent from the conversation for state tracking
if (!updatedState.intent) {
const intentLower = userMessage.toLowerCase();
if (intentLower.includes('appointment') || intentLower.includes('schedule') || intentLower.includes('book')) {
updatedState.intent = 'scheduling';
} else if (intentLower.includes('hour') || intentLower.includes('parking') || intentLower.includes('price') || intentLower.includes('insurance')) {
updatedState.intent = 'faq';
} else if (intentLower.includes('hello') || intentLower.includes('hi')) {
updatedState.intent = 'greeting';
}
}
// Extract customer name if mentioned (simple heuristic — production uses NER)
const nameMatch = userMessage.match(/my name is (\w+)|i'm (\w+)|this is (\w+)/i);
if (nameMatch && !updatedState.customerName) {
updatedState.customerName = nameMatch[1] || nameMatch[2] || nameMatch[3];
}
// Update conversation history
updatedState.history = [
...state.history,
{ role: 'user', content: userMessage },
{ role: 'assistant', content: finalResponse }
];
return { response: finalResponse, updatedState, toolCallsUsed };
}
// ─── Session Manager ────────────────────────────────────────────
const sessions = new Map();
export function getOrCreateSession(sessionId: string): ConversationState {
if (!sessions.has(sessionId)) {
sessions.set(sessionId, {
sessionId,
customerName: null,
customerPhone: null,
intent: null,
collectedInfo: {},
history: []
});
}
return sessions.get(sessionId)!;
}
export function clearSession(sessionId: string): void {
sessions.delete(sessionId);
}
Step 2: Building the Voice Pipeline (STT + TTS)
For phone-based reception, we need real-time speech-to-text and text-to-speech. This module handles streaming audio from Twilio media streams, transcribing with Deepgram, and returning synthesized speech. Here is a complete Node.js implementation:
// src/voice-pipeline.ts
import WebSocket from 'ws';
import { createClient, LiveTranscriptionEvents } from '@deepgram/sdk';
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const deepgramApiKey = process.env.DEEPGRAM_API_KEY!;
interface VoicePipelineConfig {
onTranscript: (text: string, isFinal: boolean) => void;
onInterimTranscript: (text: string) => void;
}
// ─── Streaming Transcription (Deepgram) ────────────────────────
export function createTranscriptionStream(config: VoicePipelineConfig) {
const deepgram = createClient(deepgramApiKey);
const connection = deepgram.listen.live({
model: 'nova-2-phonecall',
language: 'en',
smart_format: true,
interim_results: true,
endpointing: 800, // ms of silence before finalizing
punctuate: true,
});
connection.on(LiveTranscriptionEvents.Transcript, (data) => {
const transcript = data.channel.alternatives[0];
if (transcript.confidence > 0.6) {
if (data.is_final) {
config.onTranscript(transcript.transcript, true);
} else {
config.onInterimTranscript(transcript.transcript);
}
}
});
connection.on(LiveTranscriptionEvents.Error, (error) => {
console.error('Deepgram error:', error);
});
return {
connection,
// Feed raw audio buffer (PCM mu-law or linear16 from Twilio)
sendAudio: (audioBuffer: Buffer) => {
connection.send(audioBuffer);
},
close: () => connection.finish()
};
}
// ─── Streaming TTS (OpenAI) ────────────────────────────────────
export async function synthesizeSpeech(
text: string,
voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = 'nova'
): Promise {
const response = await openai.audio.speech.create({
model: 'tts-1',
voice,
input: text,
response_format: 'pcm',
speed: 1.0,
});
// OpenAI returns raw PCM 24kHz mono 16-bit
const buffer = Buffer.from(await response.arrayBuffer());
return buffer;
}
// ─── Twilio Media Stream Handler ────────────────────────────────
export function handleTwilioMediaStream(
ws: WebSocket,
streamSid: string,
onUserSpeech: (text: string) => Promise
) {
let audioBuffer: Buffer[] = [];
let transcriptionActive = true;
const { connection: deepgramConn, sendAudio } = createTranscriptionStream({
onTranscript: async (text, isFinal) => {
if (isFinal && text.trim().length > 0) {
console.log(`📝 Final transcript: "${text}"`);
// Process through the AI agent
const responseText = await onUserSpeech(text);
// Synthesize and send back to Twilio
const audioBuffer = await synthesizeSpeech(responseText);
// Send audio back via Twilio media stream
ws.send(JSON.stringify({
event: 'media',
streamSid,
media: {
payload: audioBuffer.toString('base64')
}
}));
}
},
onInterimTranscript: (text) => {
// Optional: display interim results in a dashboard
}
});
ws.on('message', (data: WebSocket.Data) => {
const msg = JSON.parse(data.toString());
if (msg.event === 'media' && msg.media) {
// Twilio sends mu-law audio; Deepgram can handle it natively
const payload = Buffer.from(msg.media.payload, 'base64');
sendAudio(payload);
} else if (msg.event === 'start') {
console.log(`📞 Media stream started: ${msg.streamSid}`);
} else if (msg.event === 'stop') {
console.log('📞 Media stream stopped');
deepgramConn.close();
}
});
ws.on('close', () => {
deepgramConn.close();
});
}
Step 3: Calendar & Scheduling Integration
The scheduling module connects to real calendar systems. For a business-in-a-box product, you want a unified API like Nylas that works with Google, Microsoft, and Apple calendars. Here is the production-ready integration:
// src/calendar-adapter.ts
import Nylas from 'nylas';
import dotenv from 'dotenv';
dotenv.config();
// Configure Nylas with your application credentials
const nylasApp = new Nylas({
apiKey: process.env.NYLAS_API_KEY!,
apiUri: process.env.NYLAS_API_URI || 'https://api.us.nylas.com',
});
interface CalendarAdapterConfig {
grantId: string; // Nylas grant ID for the connected business account
calendarId: string;
timezone: string;
appointmentDuration: number; // minutes
bufferBefore: number; // minutes
bufferAfter: number; // minutes
businessHoursStart: string; // "09:00"
businessHoursEnd: string; // "17:00"
workingDays: number[]; // [1,2,3,4,5] = Mon-Fri
}
export class CalendarAdapter {
private config: CalendarAdapterConfig;
private nylas: any;
constructor(config: CalendarAdapterConfig) {
this.config = config;
this.nylas = nylasApp;
}
async checkAvailability(date: string, timePreference?: string): Promise<{
available: boolean;
slots: { startTime: string; endTime: string }[];
message: string;
}> {
const targetDate = new Date(date);
const dayOfWeek = targetDate.getDay();
if (!this.config.workingDays.includes(dayOfWeek)) {
return { available: false, slots: [], message: 'This date falls on a non-working day.' };
}
const startOfDay = new Date(`${date}T${this.config.businessHoursStart}:00`);
const endOfDay = new Date(`${date}T${this.config.businessHoursEnd}:00`);
// Query Nylas for busy slots
const busyResponse = await this.nylas.calendars.getFreeBusy({
identifier: this.config.grantId,
startTime: Math.floor(startOfDay.getTime() / 1000),
endTime: Math.floor(endOfDay.getTime() / 1000),
emails: [this.config.calendarId],
});
const busySlots = busyResponse?.timeSlots || [];
// Generate available 30-minute slots, excluding busy times
const slotDuration = this.config.appointmentDuration * 60 * 1000; // ms
const bufferBefore = this.config.bufferBefore * 60 * 1000;
const bufferAfter = this.config.bufferAfter * 60 * 1000;
const availableSlots: { startTime: string; endTime: string }[] = [];
let cursor = startOfDay.getTime();
while (cursor + slotDuration <= endOfDay.getTime()) {
const slotStart = cursor;
const slotEnd = cursor + slotDuration;
const slotWithBufferStart = slotStart - bufferBefore;
const slotWithBufferEnd = slotEnd + bufferAfter;
const isBusy = busySlots.some((busy: any) => {
const busyStart = busy.startTime * 1000;
const busyEnd = busy.endTime * 1000;
return (slotWithBufferStart < busyEnd && slotWithBufferEnd > busyStart);
});
if (!isBusy) {
const startDate = new Date(slotStart);
const endDate = new Date(slotEnd);
availableSlots.push({
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
});
}
cursor += 30 * 60 * 1000; // 30-minute increments
}
// If time preference given, filter slots near that time
let filteredSlots = availableSlots;
if (timePreference && timePreference !== 'any') {
const preferredHour = parseInt(timePreference.split(':')[0]);
filteredSlots = availableSlots.filter(slot => {
const slotHour = new Date(slot.startTime).getHours();
return Math.abs(slotHour - preferredHour) <= 2;
});
}
return {
available: filteredSlots.length > 0,
slots: filteredSlots.slice(0, 8), // Limit to 8 slots to avoid overwhelming
message: filteredSlots.length > 0
? `Found ${filteredSlots.length} available slot(s) on ${date}`
: 'No available slots matching your criteria'
};
}
async bookAppointment(params: {
date: string;
time: string;
customerName: string;
customerEmail?: string;
serviceType: string;
notes?: string;
}): Promise<{ booked: boolean; confirmationId: string; eventId: string; message: string }> {
const startDateTime = new Date(`${params.date}T${params.time}:00`);
const endDateTime = new Date(startDateTime.getTime() + this.config.appointmentDuration * 60 * 1000);
const event = await this.nylas.events.create({
identifier: this.config.grantId,
body: {
title: `${params.serviceType} - ${params.customerName}`,
description: `Appointment for ${params.customerName}\nService: ${params.serviceType}\nNotes: ${params.notes || 'None'}\nBooked by AI Receptionist`,
location: 'Office',
startTime: Math.floor(startDateTime.getTime() / 1000),
endTime: Math.floor(endDateTime.getTime() / 1000),
participants: params.customerEmail ? [{ email: params.customerEmail, name: params.customerName }] : [],
calendarId: this.config.calendarId,
},
});
const confirmationId = `APT-${Date.now().toString(36).toUpperCase().slice(-6)}`;
return {
booked: true,
confirmationId,
eventId: event.id,
message: `Appointment confirmed! Your confirmation number is ${confirmationId}. ${params.customerName}, you're all set for ${params.date} at ${params.time} for ${params.serviceType}.`,
};
}
async cancelAppointment(eventId: string): Promise<{ cancelled: boolean; message: string }> {
await this.nylas.events.delete({
identifier: this.config.grantId,
eventId,
});
return { cancelled: true, message: 'Your appointment has been cancelled successfully.' };
}
}
// ─── Factory for multi-tenant deployments ──────────────────────
const adapterCache = new Map();
export function getCalendarAdapter(businessId: string, config: CalendarAdapterConfig): CalendarAdapter {
if (!adapterCache.has(businessId)) {
adapterCache.set(businessId, new CalendarAdapter(config));
}
return adapterCache.get(businessId)!;
}
Step 4: Deploying as a Web Widget
For businesses that want an AI receptionist on their website, we build an embeddable chat widget. This is a complete, self-contained widget that any business can add with a single script tag — true business-in-a-box packaging:
// src/web-widget.ts — Compiled into a single JS bundle for embedding
class AIReceptionistWidget {
private container: HTMLDivElement;
private chatArea: HTMLDivElement;
private inputArea: HTMLDivElement;
private sessionId: string;
private apiEndpoint: string;
private isOpen: boolean;
private unreadCount: number;
constructor(config: { apiEndpoint: string; businessName: string; primaryColor?: string }) {
this.apiEndpoint = config.apiEndpoint;
this.sessionId = this.generateSessionId();
this.isOpen = false;
this.unreadCount = 0;
this.injectStyles(config.primaryColor || '#4F46E5');
this.createUI(config.businessName);
this.setupEventListeners();
}
private generateSessionId(): string {
const stored = localStorage.getItem('ai_receptionist_session');
if (stored) return stored;
const newId = 'sess_' + Math.random().toString(36).substring(2, 15);
localStorage.setItem('ai_receptionist_session', newId);
return newId;
}
private injectStyles(primaryColor: string): void {
const styles = `
#ai-receptionist-bubble {
position: fixed; bottom: 20px; right: 20px; z-index: 99999;
width: 60px; height: 60px; border-radius: 50%;
background: ${primaryColor}; cursor: pointer;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
display: flex; align-items: center; justify-content: center;
transition: transform 0.2s ease;
}
#ai-receptionist-bubble:hover { transform: scale(1.1); }
#ai-receptionist-bubble svg { width: 28px; height: 28px; fill: white; }
#ai-receptionist-badge {
position: absolute; top: -5px; right: -5px;
background: #EF4444; color: white; border-radius: 50%;
width: 22px; height: 22px; font-size: 12px;
display: flex; align-items: center; justify-content: center;
font-family: sans-serif; font-weight: bold;
}
#ai-receptionist-window {
position: fixed; bottom: 90px; right: 20px; z-index: 99998;
width: 380px; height: 560px; background: white;
border-radius: 16px; box-shadow: 0 8px 40px rgba(0,0,0,0.2);
display: none; flex-direction: column; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#ai-receptionist-window.open { display: flex; }
#ai-receptionist-header {
background: ${primaryColor}; color: white; padding: 18px 20px;
font-weight: 600; font-size: 16px;
display: flex; justify-content: space-between; align-items: center;
}
#ai-receptionist-header button {
background: none; border: none; color: white; font-size: 24px;
cursor: pointer; opacity: 0.8; padding: 0; line-height: 1;
}
#ai-receptionist-messages {