UNPKG

@agentman/chat-widget

Version:

Agentman Chat Widget for easy integration with web applications

961 lines (780 loc) 26.2 kB
# 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)*