Skip to content

Add configurable pre-chat form for collecting user info before conversation #17

@anurag629

Description

@anurag629

Summary

Add a configurable pre-chat form that appears before the conversation starts. This lets widget owners collect user information (name, email, topic, etc.) upfront — a table-stakes feature for support and sales use cases. Unlike the AI-triggered LeadCaptureTool, the pre-chat form guarantees contact info is captured for every conversation.

Motivation

Currently, contact info collection depends on the AI deciding to call capture_lead mid-conversation. This is unreliable — users may leave before providing info, or the AI may not prompt for it. Most commercial chat widgets (Intercom, Drift, Crisp, etc.) offer a pre-chat form because:

  1. Every conversation becomes actionable — support teams can follow up
  2. Better routing — a "topic" field can inform the system prompt
  3. Reduced friction — users don't get asked for email mid-conversation
  4. Analytics — know who's chatting, even if the conversation is short

Proposed UX

Flow

User clicks FAB
  → Pre-chat form appears (instead of chat panel)
  → User fills in fields and submits
  → Form data sent to server with first message context
  → Chat panel opens with conversation ready
  → (Optional) Welcome message references user by name

Visual Design

The pre-chat form should render inside the existing panel (same dimensions, same shadow DOM, same theme). It replaces the messages area before the first message:

┌─────────────────────────┐
│  Header (bot name/icon) │
├─────────────────────────┤
│                         │
│   Welcome text          │
│                         │
│   [Name field]          │
│   [Email field]         │
│   [Topic dropdown]      │  ← configurable fields
│   [Message textarea]    │
│                         │
│   [Start Chat button]   │
│                         │
├─────────────────────────┤
│  Footer                 │
└─────────────────────────┘

After submission, the form slides out and the normal chat view appears.

Implementation Guide

Phase 1: Widget Config

File: packages/widget/src/widget.ts

Add preChatForm to WidgetConfig:

export interface PreChatFormConfig {
  enabled: boolean;
  title?: string;          // e.g., "Before we start..."
  subtitle?: string;       // e.g., "Tell us a bit about yourself"
  fields: PreChatField[];
  submitLabel?: string;    // Default: "Start Chat"
}

export interface PreChatField {
  name: string;            // field key, e.g., "email"
  type: 'text' | 'email' | 'select' | 'textarea';
  label: string;           // display label
  placeholder?: string;
  required?: boolean;      // default: false
  options?: string[];      // for type: 'select'
}

export interface WidgetConfig {
  // ... existing fields ...
  preChatForm?: PreChatFormConfig;
  onPreChatSubmit?: (data: Record<string, string>) => void;
}

Usage example:

ChatCops.init({
  apiUrl: '/chat',
  preChatForm: {
    enabled: true,
    title: 'Welcome!',
    subtitle: 'Please tell us about yourself so we can help you better.',
    fields: [
      { name: 'name', type: 'text', label: 'Your Name', required: true },
      { name: 'email', type: 'email', label: 'Email', required: true },
      { name: 'topic', type: 'select', label: 'How can we help?', options: ['Sales', 'Support', 'General'] },
      { name: 'message', type: 'textarea', label: 'Your question', placeholder: 'Describe what you need...' },
    ],
    submitLabel: 'Start Chatting',
  },
  onPreChatSubmit: (data) => console.log('Pre-chat data:', data),
});

Phase 2: Pre-Chat Form DOM Component

New file: packages/widget/src/dom/prechat-form.ts

Create a new DOM component following the existing pattern (see fab.ts, panel.ts, input.ts for conventions):

export interface PreChatFormOptions {
  title?: string;
  subtitle?: string;
  fields: PreChatField[];
  submitLabel: string;
  onSubmit: (data: Record<string, string>) => void;
}

export class PreChatForm {
  private container: HTMLDivElement;

  constructor(parent: HTMLElement, options: PreChatFormOptions) {
    // Build form inside parent element
    // Use cc-prefixed classes for styling (e.g., cc-prechat, cc-prechat-field)
    // Validate required fields on submit
    // Call onSubmit with collected data
  }

  destroy(): void {
    this.container.remove();
  }
}

Key implementation details:

  • Render inside the panel's message area slot (between header and footer)
  • Use CSS classes prefixed with cc-prechat- to avoid conflicts
  • Client-side validation: required fields, email format
  • Focus first field on show
  • Enter key on last field (or the only non-textarea field) submits
  • Disabled state on submit button until required fields are filled
  • Show validation errors inline below each field

Phase 3: Widget Lifecycle Changes

File: packages/widget/src/widget.ts

Modify the Widget class to manage pre-chat state:

  1. Add a preChatCompleted flag (persisted to sessionStorage so it's per-tab, survives page navigation)
  2. In init() (line 91-172):
    • If preChatForm.enabled and !preChatCompleted and no saved conversation history, show the form instead of messages
    • If form already completed or history exists, show normal chat view
  3. On form submit:
    • Store form data on the widget instance
    • Set preChatCompleted = true in sessionStorage
    • Remove form, show messages view
    • If a message field was provided, auto-send it as the first user message
    • If a name field was provided, personalize the welcome message (e.g., "Hi {name}! How can I help?")
    • Fire onPreChatSubmit callback and emit preChatSubmit event

Storage key suggestion: chatcops-prechat-{sessionId}

Phase 4: Send Pre-Chat Data to Server

File: packages/widget/src/api/types.ts

Extend WidgetChatRequest to include pre-chat data:

export interface WidgetChatRequest {
  conversationId: string;
  message: string;
  pageContext?: { url: string; title: string; /* ... */ };
  locale?: string;
  userData?: Record<string, string>;  // NEW — pre-chat form data
}

File: packages/server/src/handler.ts

When req.userData is present, append it to the system prompt so the AI has context:

if (req.userData) {
  const userInfo = Object.entries(req.userData)
    .map(([key, val]) => `${key}: ${val}`)
    .join(', ');
  systemPrompt += `\n\nUser information: ${userInfo}`;
}

File: packages/server/src/config.ts

Add userData to chatRequestSchema:

userData: z.record(z.string().max(500)).optional(),

Phase 5: Styling

File: packages/widget/src/styles/widget.css

Add styles for the pre-chat form. Follow the existing dark theme and CSS custom properties:

/* Pre-chat form */
.cc-prechat {
  flex: 1;
  overflow-y: auto;
  padding: 24px 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.cc-prechat-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--cc-text);
}

.cc-prechat-subtitle {
  font-size: 13px;
  color: var(--cc-text-secondary);
  margin-top: -8px;
}

.cc-prechat-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cc-prechat-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--cc-text);
}

.cc-prechat-label .cc-required {
  color: #ef4444;
  margin-left: 2px;
}

.cc-prechat-input,
.cc-prechat-select,
.cc-prechat-textarea {
  background: var(--cc-bg-input);
  border: 1px solid var(--cc-border);
  border-radius: 8px;
  padding: 8px 12px;
  color: var(--cc-text);
  font-family: var(--cc-font);
  font-size: 14px;
  outline: none;
  transition: border-color 0.15s ease;
}

.cc-prechat-input:focus,
.cc-prechat-select:focus,
.cc-prechat-textarea:focus {
  border-color: var(--cc-accent);
}

.cc-prechat-error {
  font-size: 12px;
  color: #ef4444;
}

.cc-prechat-submit {
  background: var(--cc-accent);
  color: var(--cc-text);
  border: none;
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease;
  margin-top: 8px;
}

.cc-prechat-submit:hover {
  background: var(--cc-accent-hover);
}

.cc-prechat-submit:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Phase 6: i18n

File: packages/widget/src/i18n.ts

Add new locale strings to WidgetLocaleStrings:

export interface WidgetLocaleStrings {
  // ... existing ...
  preChatTitle: string;
  preChatSubtitle: string;
  preChatSubmit: string;
  preChatRequired: string;      // "This field is required"
  preChatInvalidEmail: string;  // "Please enter a valid email"
}

Files to Modify

File Change
packages/widget/src/widget.ts Add preChatForm config, form lifecycle, preChatCompleted state
packages/widget/src/dom/prechat-form.ts New file — pre-chat form DOM component
packages/widget/src/styles/widget.css Pre-chat form styles
packages/widget/src/i18n.ts Add pre-chat locale strings
packages/widget/src/api/types.ts Add userData to WidgetChatRequest
packages/widget/src/storage.ts Persist pre-chat completion state
packages/server/src/handler.ts Inject userData into system prompt
packages/server/src/config.ts Add userData to Zod schema

Tests to Add

  • Unit: PreChatForm renders correct fields from config
  • Unit: Required field validation prevents submission
  • Unit: Email field validates format
  • Unit: Widget shows form when preChatForm.enabled and no history
  • Unit: Widget skips form when preChatCompleted is true in sessionStorage
  • Unit: Form data is included in WidgetChatRequest.userData
  • Unit: Server injects userData into system prompt
  • Unit: message field auto-sends as first user message after form submit

Acceptance Criteria

  • Pre-chat form renders inside the existing panel (same theme, shadow DOM isolated)
  • Supports text, email, select, and textarea field types
  • Required field validation with inline error messages
  • Email format validation
  • Form completion persisted per session — refreshing a page doesn't re-show the form
  • After submission, chat view appears with optional auto-sent first message
  • userData is sent to the server and available in the system prompt
  • onPreChatSubmit callback fires with form data
  • Form is skipped when preChatForm is not configured (backward compatible)
  • Form is skipped when conversation history already exists
  • Works in both popup and inline modes
  • Mobile responsive (follows existing @media (max-width: 768px) breakpoint)
  • All existing tests pass, new tests cover form component and widget lifecycle

Metadata

Metadata

Assignees

Labels

featureNew feature or capabilityin-progressSomeone is working on the issuepriority: highHigh priority itemserverAffects @chatcops/server packagewidgetAffects @chatcops/widget package

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions