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:
- Every conversation becomes actionable — support teams can follow up
- Better routing — a "topic" field can inform the system prompt
- Reduced friction — users don't get asked for email mid-conversation
- 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:
- Add a
preChatCompleted flag (persisted to sessionStorage so it's per-tab, survives page navigation)
- 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
- 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
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_leadmid-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:Proposed UX
Flow
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:
After submission, the form slides out and the normal chat view appears.
Implementation Guide
Phase 1: Widget Config
File:
packages/widget/src/widget.tsAdd
preChatFormtoWidgetConfig:Usage example:
Phase 2: Pre-Chat Form DOM Component
New file:
packages/widget/src/dom/prechat-form.tsCreate a new DOM component following the existing pattern (see
fab.ts,panel.ts,input.tsfor conventions):Key implementation details:
cc-prechat-to avoid conflictsPhase 3: Widget Lifecycle Changes
File:
packages/widget/src/widget.tsModify the
Widgetclass to manage pre-chat state:preChatCompletedflag (persisted tosessionStorageso it's per-tab, survives page navigation)init()(line 91-172):preChatForm.enabledand!preChatCompletedand no saved conversation history, show the form instead of messagespreChatCompleted = truein sessionStoragemessagefield was provided, auto-send it as the first user messagenamefield was provided, personalize the welcome message (e.g., "Hi {name}! How can I help?")onPreChatSubmitcallback and emitpreChatSubmiteventStorage key suggestion:
chatcops-prechat-{sessionId}Phase 4: Send Pre-Chat Data to Server
File:
packages/widget/src/api/types.tsExtend
WidgetChatRequestto include pre-chat data:File:
packages/server/src/handler.tsWhen
req.userDatais present, append it to the system prompt so the AI has context:File:
packages/server/src/config.tsAdd
userDatatochatRequestSchema:Phase 5: Styling
File:
packages/widget/src/styles/widget.cssAdd styles for the pre-chat form. Follow the existing dark theme and CSS custom properties:
Phase 6: i18n
File:
packages/widget/src/i18n.tsAdd new locale strings to
WidgetLocaleStrings:Files to Modify
packages/widget/src/widget.tspreChatFormconfig, form lifecycle,preChatCompletedstatepackages/widget/src/dom/prechat-form.tspackages/widget/src/styles/widget.csspackages/widget/src/i18n.tspackages/widget/src/api/types.tsuserDatatoWidgetChatRequestpackages/widget/src/storage.tspackages/server/src/handler.tsuserDatainto system promptpackages/server/src/config.tsuserDatato Zod schemaTests to Add
PreChatFormrenders correct fields from configWidgetshows form whenpreChatForm.enabledand no historyWidgetskips form whenpreChatCompletedis true in sessionStorageWidgetChatRequest.userDatauserDatainto system promptmessagefield auto-sends as first user message after form submitAcceptance Criteria
text,email,select, andtextareafield typesuserDatais sent to the server and available in the system promptonPreChatSubmitcallback fires with form datapreChatFormis not configured (backward compatible)@media (max-width: 768px)breakpoint)