← Back to DevBytes

Building an AI Receptionist: Business-in-a-Box Guide

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

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:

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 {

— Ad —

Google AdSense will appear here after approval

← Back to all articles