@agentman/chat-widget
Version:
Agentman Chat Widget for easy integration with web applications
961 lines (780 loc) • 26.2 kB
Markdown
# Stage 2: Config Property Consolidation Plan
## Overview
**Goal:** Restructure ChatConfig to eliminate property overlap, create clear semantic grouping, and prepare for variant-specific configurations in Stage 3.
**Status:** 🔴 Not Started
**Prerequisite:** ✅ Stage 1 Complete (Variant Renaming)
**Breaking Changes:** ⚠️ YES - This is a major refactoring with no backwards compatibility
## Current Problems
### 1. Theme Property Fragmentation
**Problem:** Theme-related properties are scattered across multiple locations:
```typescript
// Current scattered approach
interface ChatConfig {
theme?: Partial<ChatTheme>; // Main theme object
toggleStyle?: { // Duplicate of theme.toggle*
backgroundColor?: string;
textColor?: string;
iconColor?: string;
};
toggleText?: string; // String config mixed with colors
inputBarBrandText?: string; // String config at root level
logo?: string; // Asset at root level
headerLogo?: string; // Asset at root level
assets?: Partial<ChatAssets>; // Duplicate asset location
icons?: Partial<ChatIcons>; // Icon customization separate
}
interface ChatTheme {
// Core colors
textColor: string;
backgroundColor: string;
// Toggle colors (duplicated by toggleStyle)
toggleBackgroundColor: string;
toggleTextColor: string;
toggleIconColor: string;
// Input bar colors (but inputBarBrandText is in ChatConfig)
inputBarBrandBackground?: string;
inputBarBrandText?: string;
inputBarLogoBackground?: string;
// ... 6 more input bar properties
}
```
**Issues:**
- `toggleStyle` duplicates `theme.toggle*` properties
- `inputBarBrandText` in ChatConfig but related colors in ChatTheme
- Assets defined in both `logo/headerLogo` AND `assets.logo/headerLogo`
- No clear hierarchy or grouping
### 2. Layout Property Confusion
**Problem:** Positioning and sizing properties with unclear applicability:
```typescript
interface ChatConfig {
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // Only for corner variant
initialHeight?: string; // Only for corner/centered
initialWidth?: string; // Only for corner/centered
mobileHeight?: string; // All variants
mobileMaxHeight?: string; // All variants
initiallyOpen?: boolean; // Only for corner variant
}
```
**Issues:**
- No indication which properties apply to which variants
- Unclear if desktop/mobile settings conflict
- No validation that position is only used with corner variant
### 3. Behavior Property Sprawl
**Problem:** Functional toggles mixed with data configuration:
```typescript
interface ChatConfig {
enableAttachments?: boolean;
collectClientMetadata?: boolean;
collectIPAddress?: boolean;
initiallyOpen?: boolean;
persistence?: PersistenceConfig;
streaming?: { enabled?: boolean; ... };
debug?: boolean | DebugConfig;
markdownConfig?: { ... };
disclaimer?: { enabled: boolean; ... };
}
```
**Issues:**
- Feature flags scattered throughout config
- No clear "features" or "behavior" section
- Hard to see all toggles at a glance
### 4. Content vs Configuration Mixing
**Problem:** User-facing content mixed with technical configuration:
```typescript
interface ChatConfig {
title?: string; // Content
placeholder?: string; // Content
toggleText?: string; // Content
inputBarBrandText?: string; // Content
initialMessage?: string; // Content
messagePrompts?: { // Content
welcome_message?: string;
prompts?: [string?, string?, string?];
};
apiUrl: string; // Technical config
agentToken: string; // Technical config
containerId: string; // Technical config
}
```
**Issues:**
- Content strings scattered across root level
- No clear "content" vs "config" separation
- Hard to localize or manage content separately
## Proposed New Structure
### High-Level Organization
```typescript
interface ChatConfig {
// ══════════════════════════════════════════════════════════
// CORE CONFIGURATION (Required)
// ══════════════════════════════════════════════════════════
api: {
url: string;
token: string;
};
variant: ChatVariant;
containerId: string;
// ══════════════════════════════════════════════════════════
// THEMING & VISUAL DESIGN
// ══════════════════════════════════════════════════════════
theme: {
colors: {
text: string;
background: string;
button: string;
buttonText: string;
agentMessage: string;
userMessage: string;
};
toggle: {
background: string;
text: string;
icon: string;
content?: string; // Toggle button text (e.g., "Chat with us")
};
inputBar: {
brandText?: string; // "Ask AI"
brandBackground?: string;
brandTextColor?: string;
logoBackground?: string;
logoIcon?: string;
buttonBackground?: string;
buttonIcon?: string;
glowColor?: string;
};
};
assets: {
logo: string;
headerLogo: string;
icons?: {
close?: string;
send?: string;
minimize?: string;
maximize?: string;
expand?: string;
collapse?: string;
reduce?: string;
};
};
// ══════════════════════════════════════════════════════════
// LAYOUT & POSITIONING
// ══════════════════════════════════════════════════════════
layout: {
// Desktop settings
desktop?: {
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
width?: string;
height?: string;
initiallyOpen?: boolean;
};
// Mobile settings
mobile?: {
height?: string;
maxHeight?: string;
};
};
// ══════════════════════════════════════════════════════════
// CONTENT & MESSAGING
// ══════════════════════════════════════════════════════════
content: {
title?: string;
placeholder?: string;
initialMessage?: string;
welcome?: {
message?: string;
prompts?: [string?, string?, string?];
};
disclaimer?: {
enabled: boolean;
message: string;
linkText?: string;
linkUrl?: string;
};
};
// ══════════════════════════════════════════════════════════
// BEHAVIOR & FEATURES
// ══════════════════════════════════════════════════════════
features: {
attachments?: {
enabled: boolean;
maxFileSize?: number;
allowedTypes?: string[];
};
streaming?: {
enabled?: boolean;
preferStreaming?: boolean;
fallbackToNonStreaming?: boolean;
};
persistence?: {
enabled: boolean;
storageKey?: string;
maxConversations?: number;
maxMessagesPerConversation?: number;
ttlDays?: number;
};
metadata?: {
collectClientMetadata?: boolean;
collectIPAddress?: boolean;
custom?: Record<string, any>;
};
};
// ══════════════════════════════════════════════════════════
// CLOSED VIEW MODE (Corner variant only)
// ══════════════════════════════════════════════════════════
closedView?: {
mode: ClosedViewMode;
delay?: number; // Delay before showing (ms)
};
// ══════════════════════════════════════════════════════════
// ADVANCED CONFIGURATION
// ══════════════════════════════════════════════════════════
advanced?: {
debug?: {
enabled: boolean;
logLevel?: 'error' | 'warn' | 'info' | 'debug' | 'verbose';
timestamps?: boolean;
console?: boolean;
logger?: (level: string, message: string, data?: any) => void;
};
markdown?: {
cdnUrls?: string[];
timeout?: number;
options?: {
gfm?: boolean;
breaks?: boolean;
headerIds?: boolean;
mangle?: boolean;
pedantic?: boolean;
smartLists?: boolean;
smartypants?: boolean;
};
};
animations?: {
forceAnimations?: boolean; // Override prefers-reduced-motion
customDurations?: {
expand?: number;
collapse?: number;
transition?: number;
};
};
};
}
```
## Migration Guide
### Before (Current)
```typescript
const widget = new ChatWidget({
apiUrl: 'https://api.example.com',
agentToken: 'abc123',
variant: 'corner',
containerId: 'chat-container',
title: 'Support Chat',
placeholder: 'Type your message...',
initiallyOpen: false,
position: 'bottom-right',
initialHeight: '600px',
initialWidth: '400px',
mobileHeight: '80vh',
theme: {
textColor: '#000',
backgroundColor: '#fff',
buttonColor: '#4f46e5',
buttonTextColor: '#fff',
toggleBackgroundColor: '#1e1b4b',
toggleTextColor: '#fff',
toggleIconColor: '#fff',
},
toggleText: 'Chat with us',
inputBarBrandText: 'Ask AI',
logo: '/logo.svg',
headerLogo: '/header-logo.svg',
agentClosedView: 'welcome-card',
enableAttachments: true,
collectClientMetadata: true,
messagePrompts: {
welcome_message: 'How can we help?',
prompts: ['Pricing', 'Features', 'Support']
},
streaming: {
enabled: true,
preferStreaming: true
},
persistence: {
enabled: true,
storageKey: 'chat_history'
}
});
```
### After (Stage 2)
```typescript
const widget = new ChatWidget({
// Core config - clean and obvious
api: {
url: 'https://api.example.com',
token: 'abc123'
},
variant: 'corner',
containerId: 'chat-container',
// All layout settings in one place
layout: {
desktop: {
position: 'bottom-right',
width: '400px',
height: '600px',
initiallyOpen: false
},
mobile: {
height: '80vh'
}
},
// All theme settings consolidated
theme: {
colors: {
text: '#000',
background: '#fff',
button: '#4f46e5',
buttonText: '#fff',
agentMessage: '#1e293b',
userMessage: '#4f46e5'
},
toggle: {
background: '#1e1b4b',
text: '#fff',
icon: '#fff',
content: 'Chat with us'
},
inputBar: {
brandText: 'Ask AI'
}
},
// All assets together
assets: {
logo: '/logo.svg',
headerLogo: '/header-logo.svg'
},
// All content strings grouped
content: {
title: 'Support Chat',
placeholder: 'Type your message...',
welcome: {
message: 'How can we help?',
prompts: ['Pricing', 'Features', 'Support']
}
},
// Clear feature flags
features: {
attachments: {
enabled: true
},
streaming: {
enabled: true,
preferStreaming: true
},
persistence: {
enabled: true,
storageKey: 'chat_history'
},
metadata: {
collectClientMetadata: true
}
},
// Closed view (corner variant only)
closedView: {
mode: 'welcome-card'
}
});
```
## Benefits Analysis
### 1. Clarity Improvements
| Aspect | Before | After |
|--------|--------|-------|
| **API Config** | Mixed with other properties | Clear `api` namespace |
| **Layout** | 6 properties scattered | Organized by desktop/mobile |
| **Theme** | 3 locations (theme, toggleStyle, root) | Single `theme` namespace |
| **Assets** | 4 properties (logo, headerLogo, assets, icons) | Single `assets` namespace |
| **Features** | 8+ scattered boolean flags | Clear `features` object |
| **Content** | 5+ scattered string properties | Single `content` namespace |
### 2. Developer Experience
**Before:**
- "Where do I set the toggle button color?" → Could be `theme.toggleBackgroundColor` OR `toggleStyle.backgroundColor`
- "How do I configure mobile layout?" → Search for "mobile" across all properties
- "What features can I enable?" → Scan entire config for boolean flags
**After:**
- "Where do I set the toggle button color?" → `theme.toggle.background`
- "How do I configure mobile layout?" → `layout.mobile`
- "What features can I enable?" → Look in `features` object
### 3. Type Safety Improvements
```typescript
// Variant-aware type checking (Stage 3 prep)
type CornerConfig = ChatConfig & {
layout: {
desktop: {
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
initiallyOpen?: boolean;
};
};
closedView?: { mode: ClosedViewMode };
};
type InputBarOnlyConfig = ChatConfig & {
layout?: never; // Layout settings not applicable
closedView?: never; // No closed view mode
};
// TypeScript can now enforce variant-specific rules!
```
### 4. Configuration Size
```typescript
// Before: 35+ root-level properties
// After: 8 root-level namespaces
// Easier to navigate
// Clearer intent
// Better autocomplete
// Simpler documentation
```
## Implementation Plan
### Step 1: Create New Type Definitions (1 hour)
**File:** `assistantWidget/types/config-v2.ts`
```typescript
export interface ChatConfigV2 {
api: ApiConfig;
variant: ChatVariant;
containerId: string;
theme: ThemeConfig;
assets: AssetsConfig;
layout?: LayoutConfig;
content?: ContentConfig;
features?: FeaturesConfig;
closedView?: ClosedViewConfig;
advanced?: AdvancedConfig;
}
// Define all sub-interfaces with full JSDoc
```
**Deliverable:** Complete type definitions with documentation
### Step 2: Create Config Adapter (2 hours)
**File:** `assistantWidget/config/ConfigAdapter.ts`
```typescript
/**
* Adapts ChatConfigV2 to internal usage throughout the codebase
* No backwards compatibility needed - clean break
*/
export class ConfigAdapter {
constructor(private config: ChatConfigV2) {}
// Flattened accessors for internal use
getApiUrl(): string { return this.config.api.url; }
getApiToken(): string { return this.config.api.token; }
getTextColor(): string { return this.config.theme.colors.text; }
getBackgroundColor(): string { return this.config.theme.colors.background; }
getToggleBackground(): string { return this.config.theme.toggle.background; }
getToggleText(): string { return this.config.theme.toggle.content || 'Chat'; }
getDesktopPosition(): string | undefined {
return this.config.layout?.desktop?.position;
}
isAttachmentsEnabled(): boolean {
return this.config.features?.attachments?.enabled ?? true;
}
// ... all accessor methods
// Validation methods
validate(): void {
if (this.config.variant === 'corner' && !this.config.layout?.desktop?.position) {
throw new Error('Corner variant requires layout.desktop.position');
}
if (this.config.variant === 'input-bar-only' && this.config.closedView) {
throw new Error('input-bar-only variant does not support closedView');
}
}
}
```
**Deliverable:** Config adapter with all accessors and validation
### Step 3: Update ConfigManager (2 hours)
**File:** `assistantWidget/config/ConfigManager.ts`
```typescript
export class ConfigManager {
private adapter: ConfigAdapter;
constructor(config: ChatConfigV2) {
this.adapter = new ConfigAdapter(config);
this.adapter.validate();
}
// Update all methods to use adapter
getApiUrl(): string {
return this.adapter.getApiUrl();
}
// Apply defaults with new structure
private applyDefaults(config: Partial<ChatConfigV2>): ChatConfigV2 {
return {
api: config.api!,
variant: config.variant!,
containerId: config.containerId!,
theme: {
colors: {
text: config.theme?.colors?.text ?? '#0f172a',
background: config.theme?.colors?.background ?? '#ffffff',
button: config.theme?.colors?.button ?? '#4f46e5',
buttonText: config.theme?.colors?.buttonText ?? '#ffffff',
agentMessage: config.theme?.colors?.agentMessage ?? '#1e293b',
userMessage: config.theme?.colors?.userMessage ?? '#4f46e5',
},
toggle: {
background: config.theme?.toggle?.background ?? '#1e1b4b',
text: config.theme?.toggle?.text ?? '#ffffff',
icon: config.theme?.toggle?.icon ?? '#ffffff',
content: config.theme?.toggle?.content ?? 'Chat',
},
inputBar: config.theme?.inputBar ?? {}
},
assets: {
logo: config.assets?.logo ?? '',
headerLogo: config.assets?.headerLogo ?? '',
icons: config.assets?.icons ?? {}
},
layout: config.layout,
content: config.content,
features: config.features,
closedView: config.closedView,
advanced: config.advanced
};
}
}
```
**Deliverable:** ConfigManager using new structure with defaults
### Step 4: Update All Consumers (4 hours)
Update all files that read from config:
**StyleManager.ts**
```typescript
// Before
const textColor = this.config.theme?.textColor ?? '#0f172a';
// After
const textColor = this.configAdapter.getTextColor();
```
**ChatWidget.ts**
```typescript
// Before
this.apiService = new ApiService(this.config.apiUrl, this.config.agentToken);
// After
this.apiService = new ApiService(
this.configAdapter.getApiUrl(),
this.configAdapter.getApiToken()
);
```
**UIManager.ts**
```typescript
// Before
if (this.config.initiallyOpen) { ... }
// After
if (this.configAdapter.isInitiallyOpen()) { ... }
```
**Files to update (~30 files):**
- ChatWidget.ts
- ViewManager.ts
- UIManager.ts
- StyleManager.ts
- InputBarSheetController.ts
- All handlers/
- All components/
- All styles/
**Deliverable:** All files using ConfigAdapter
### Step 5: Update Documentation (1 hour)
- Update README.md with new config structure
- Update all test files with new config
- Update JSDoc examples
- Create migration guide
**Deliverable:** Complete documentation update
### Step 6: Update TypeScript Declarations (1 hour)
**File:** `index.d.ts`
```typescript
export interface ChatConfigV2 {
api: {
url: string;
token: string;
};
variant: 'corner' | 'centered' | 'input-bar-only' | 'input-bar-sheet';
containerId: string;
theme?: {
colors?: {
text?: string;
background?: string;
// ... all color properties
};
toggle?: {
background?: string;
text?: string;
icon?: string;
content?: string;
};
inputBar?: {
// ... all input bar properties
};
};
// ... rest of config
}
export class ChatWidget {
constructor(config: ChatConfigV2);
// ... rest of API
}
```
**Deliverable:** Updated TypeScript declarations
## Testing Strategy
### Unit Tests
```typescript
describe('ConfigAdapter', () => {
it('should access nested properties correctly', () => {
const config: ChatConfigV2 = {
api: { url: 'https://api.test', token: 'abc' },
variant: 'corner',
containerId: 'test',
theme: {
colors: { text: '#000' }
}
};
const adapter = new ConfigAdapter(config);
expect(adapter.getTextColor()).toBe('#000');
});
it('should validate variant-specific requirements', () => {
const config: ChatConfigV2 = {
api: { url: 'https://api.test', token: 'abc' },
variant: 'corner',
containerId: 'test'
// Missing layout.desktop.position
};
const adapter = new ConfigAdapter(config);
expect(() => adapter.validate()).toThrow('requires layout.desktop.position');
});
it('should apply defaults correctly', () => {
const config: Partial<ChatConfigV2> = {
api: { url: 'https://api.test', token: 'abc' },
variant: 'corner',
containerId: 'test'
};
const manager = new ConfigManager(config as ChatConfigV2);
expect(manager.getToggleBackground()).toBe('#1e1b4b'); // default
});
});
```
### Integration Tests
1. **All Variants:** Test each variant with new config structure
2. **All Closed Views:** Test each closed view mode
3. **Theme Variations:** Test all theme properties apply correctly
4. **Layout Options:** Test desktop/mobile layout configurations
5. **Feature Flags:** Test enabling/disabling each feature
### Manual Testing
- Update all demo files to use new config
- Test WordPress plugin integration
- Test CDN usage with new config
- Test all combinations of variant + closed view + theme
## Breaking Changes Summary
### Removed Properties
All root-level properties moved to namespaces. No direct equivalents.
### Renamed Properties
| Old | New |
|-----|-----|
| `apiUrl` | `api.url` |
| `agentToken` | `api.token` |
| `theme.textColor` | `theme.colors.text` |
| `theme.backgroundColor` | `theme.colors.background` |
| `theme.toggleBackgroundColor` | `theme.toggle.background` |
| `toggleText` | `theme.toggle.content` |
| `inputBarBrandText` | `theme.inputBar.brandText` |
| `logo` | `assets.logo` |
| `headerLogo` | `assets.headerLogo` |
| `icons` | `assets.icons` |
| `position` | `layout.desktop.position` |
| `initialHeight` | `layout.desktop.height` |
| `initialWidth` | `layout.desktop.width` |
| `initiallyOpen` | `layout.desktop.initiallyOpen` |
| `mobileHeight` | `layout.mobile.height` |
| `mobileMaxHeight` | `layout.mobile.maxHeight` |
| `title` | `content.title` |
| `placeholder` | `content.placeholder` |
| `initialMessage` | `content.initialMessage` |
| `messagePrompts` | `content.welcome` |
| `enableAttachments` | `features.attachments.enabled` |
| `streaming` | `features.streaming` |
| `persistence` | `features.persistence` |
| `collectClientMetadata` | `features.metadata.collectClientMetadata` |
| `collectIPAddress` | `features.metadata.collectIPAddress` |
| `clientMetadata` | `features.metadata.custom` |
| `agentClosedView` | `closedView.mode` |
| `debug` | `advanced.debug` |
| `markdownConfig` | `advanced.markdown` |
| `forceAnimations` | `advanced.animations.forceAnimations` |
### Removed Duplicate Properties
- `toggleStyle` → Use `theme.toggle` instead
- `assets.logo` (when `logo` also exists) → Use `assets.logo` only
- `messagePrompts.show` → Use `closedView.mode` instead
## Timeline
| Task | Duration | Dependencies |
|------|----------|--------------|
| Step 1: New Type Definitions | 1 hour | None |
| Step 2: Config Adapter | 2 hours | Step 1 |
| Step 3: Update ConfigManager | 2 hours | Step 2 |
| Step 4: Update All Consumers | 4 hours | Step 3 |
| Step 5: Update Documentation | 1 hour | Step 4 |
| Step 6: Update TypeScript Declarations | 1 hour | Step 4 |
| Testing & Bug Fixes | 2 hours | All steps |
**Total Estimated Time:** 13 hours (1.5-2 days)
## Success Metrics
### Code Quality
- ✅ Zero duplicate property definitions
- ✅ Clear semantic grouping (8 namespaces vs 35+ root properties)
- ✅ Type-safe configuration with variant-aware validation
- ✅ 100% test coverage for ConfigAdapter
### Developer Experience
- ✅ Autocomplete shows logical property groups
- ✅ Less than 3 seconds to find any config property in docs
- ✅ Clear error messages for invalid configurations
- ✅ Reduced config size by ~40% (grouped structure is more compact)
### Documentation
- ✅ Complete migration guide from v1 to v2
- ✅ Updated README with all new examples
- ✅ JSDoc on every config property
- ✅ Visual comparison showing before/after structure
## Risk Assessment
### Low Risk ✅
- Type system will catch most migration errors
- ConfigAdapter isolates changes to one layer
- Can test incrementally per component
### Medium Risk ⚠️
- Large refactor touching ~30 files
- Requires updating all demo/test files
- WordPress plugin needs update
### Mitigation Strategy
1. **Feature flag:** Add `useConfigV2` flag to gradually migrate
2. **Incremental rollout:** Update component by component
3. **Comprehensive tests:** Unit + integration tests before merging
4. **Documentation first:** Complete migration guide before implementation
## Next Steps After Stage 2
**Stage 3: Variant-Specific Config Types**
- Create discriminated unions for each variant
- Enforce variant-specific properties at TypeScript level
- Example: `CornerConfig` requires `layout.desktop.position`
**Stage 4: Config Builder API**
- Fluent API for constructing configs
- Example: `ChatConfig.corner().withToggle('Chat').withTheme(...)`
**Stage 5: Config Validation Library**
- Runtime validation with detailed error messages
- JSON Schema generation for documentation
- Config testing utilities
## Questions for Review
1. ✅ **Breaking changes acceptable?** - No backwards compatibility
2. ✅ **Namespace structure makes sense?** - 8 top-level namespaces
3. ✅ **Migration path clear?** - Direct property mapping table provided
4. ✅ **Timeline realistic?** - 13 hours / 1.5-2 days
5. ✅ **Testing strategy sufficient?** - Unit + integration + manual
*Document created: 2025-01-12*
*Status: 🟡 Awaiting Approval*
*Blocks: Stage 3 (Variant-Specific Config Types)*