Recruitment Chat & Chatbot System
The recruitment chat system provides interactive chatbot functionality for guiding users through job search and application processes. The system supports two main deployment modes and uses a tree-structured conversation flow.
Architecture Overview
The recruitment chat system consists of several key components:
Backend (Haskell/Yesod)
- FloHam.Cms.Model.RecruitmentChat: Core data model
- FloHam.Cms.Model.RecruitmentChat.ChatBot: Tree-structured conversation configuration
- Handler.Api.RecruitmentChat: CRUD API for individual chats
- Handler.Api.RecruitmentChatCollection: Collection management API
- Render.Component.RecruitmentChat: Server-side rendering component
Frontend (Elm)
- RecruitmentChat.elm: Main widget container and visibility management
- View.ChatBot.elm: Core conversation engine and node rendering
- Data.ChatBot.elm: Tree navigation and data structures
Deployment Modes
The system supports two distinct deployment modes controlled by the currentNodesOnly flag:
- Embedded Component Mode (
currentNodesOnly = True) Used when the chat is embedded as a “KeuzeCompass” component within page content:
Shows only the current conversation level (immediate children/options)
No conversation history display
No progressive disclosure delays
Compact presentation suitable for inline page content
Back button available in job application forms for navigation
- Live Chat Widget Mode (
currentNodesOnly = False) Used for the live chat widget in the bottom-right corner of pages:
Shows full conversation history leading to current position
Progressive disclosure with timed delays based on content length
Chat bubble interface with show/hide functionality
Introduction message with auto-hide after 10 seconds
Mobile-responsive with body scroll control
Conversation Flow Structure
Conversations are modeled as tree structures where each node represents content or interaction:
Node Types
Text: Display message to user
Option: Clickable button that advances conversation
Link: External or internal link with configurable target (_blank or _self)
VacancyOverview: Filtered job listings with search and location functionality
SalaryCalculator: Interactive salary calculation with age/hours inputs
Image/Video: Media content display
EmbeddedChatBot: Reference to another recruitment chat (enables reusable components)
Root: Starting point of conversation tree
Tree Navigation
The system uses Elm’s Tree.Zipper for efficient navigation:
current: Returns immediate children of current positioncurrentAndBefore: Returns conversation history plus current childrenbefore: Returns nodes that came before current position
Example Flow:
Root
├── Text "How can we help you today?"
├── Option "Find a Job"
│ ├── Text "Great! Let me show you available positions"
│ └── VacancyOverview [filters: location, department]
└── Option "Salary Information"
├── SalaryCalculator (age + hours input)
└── Text "Based on your input: €{calculated_salary} per month"
Dynamic Content Integration
Job Search Integration - VacancyOverview nodes fetch live job data via API - Location autocomplete with TomTom integration - Configurable filters (department, location, contract type) - Pagination with “view more” functionality - Direct application flow with dynamic form loading
Form Integration - Job application forms loaded dynamically via FormV2Schema - Form fields configured server-side based on domain settings - Privacy policy integration - Validation and submission handling
State Persistence - Conversation state stored in localStorage with unique keys per chat - Structure hash validation for detecting chat configuration changes - User inputs (salary calculator values) preserved across sessions - Application form state maintained during navigation
API Endpoints
Collection Endpoints
GET /api/{domainId}/recruitment-chatList all recruitment chats for the specified domain.
Response: Array of recruitment chat objects with metadata
POST /api/{domainId}/recruitment-chatCreate a new recruitment chat with optional duplication from existing chat.
Body: Chat configuration object Features: Supports duplicating existing chats via
duplicateFromparameter
Individual Chat Endpoints
GET /api/{domainId}/recruitment-chat/{id}Retrieve complete configuration for a specific recruitment chat.
Response: Full chat object including conversation tree structure
PATCH /api/{domainId}/recruitment-chat/{id}Update recruitment chat configuration (supports partial updates).
Body: Object containing fields to update Features: Automatic timestamp tracking, field validation
DELETE /api/{domainId}/recruitment-chat/{id}Delete recruitment chat after comprehensive validation.
Validation: Checks page usage and embedded chat references Response: 202 Accepted on successful deletion
Field Updates
The PATCH endpoint supports partial updates for these fields:
labelType: String Description: Unique identifier within domain Validation: Must be unique across all chats in the domain
titleType: String Description: Display title for chat widget Usage: Shown in chat header and CMS interface
introductionType: String Description: Welcome message text Usage: Displayed before chat conversation begins
chatBotType: ChatBot Object Description: Complete conversation tree structure Features: Full tree replacement with embedded chat resolution
botAvatarType: Optional Image Description: Avatar image for bot representation Format: ImageSource object (upload ID or URL)
Validation Rules
- Domain Isolation
All operations scoped to the provided domain
Cross-domain references are prohibited
- Label Uniqueness
Labels must be unique within each domain
Duplicate label creation/updates will be rejected
- Deletion Protection
Chats cannot be deleted if referenced by any pages
Chats cannot be deleted if embedded in other chats
Comprehensive usage validation before deletion
- Automatic Tracking
All updates include automatic
updatedtimestampOriginal
insertedtimestamp preservedFull audit trail maintained
Server-Side Rendering
The Render.Component.RecruitmentChat module prepares data for Elm initialization:
Data Aggregation - Domain context and language settings - Available recruitment chats for embedding - Form schema for job applications - Upload URL mappings for media content - Local storage keys for state persistence
Configuration Generation The renderer creates a JavaScript configuration object containing:
{
recruitmentChat: {...}, // Main chat configuration
recruitmentChats: [...], // Available embedded chats
localStorageState: {...}, // Restored conversation state
showRecruitmentChat: boolean, // Initial visibility
showChat: number, // Auto-show delay in seconds
domainId: string, // Current domain
langCode: string, // Language preference
schemaFields: [...], // Dynamic form configuration
currentNodesOnly: boolean, // Deployment mode flag
uploads: {...} // Media URL mappings
}
Embedded Chat System
The recruitment chat system supports embedding one chat within another via EmbeddedChatBot nodes. This enables creating reusable conversation components that can be shared across multiple chats.
How Embedding Works
When a conversation tree contains an EmbeddedChatBot node:
Reference Resolution: The node contains an
EmbeddedRecruitmentChatIdthat references another chatTree Replacement: During initialization, the embedded node is replaced with the actual content from the referenced chat
Recursive Processing: If the embedded chat itself contains embedded references, they are resolved recursively (with depth limits)
ID Regeneration: All embedded nodes receive new unique IDs to prevent conflicts
Embedded Chat ID System
newtype EmbeddedRecruitmentChatId = EmbeddedRecruitmentChatId Int64
-- In Node definition:
EmbeddedChatBot (Maybe EmbeddedRecruitmentChatId)
Uses separate ID type to prevent cyclic dependencies
Nothingrepresents an unlinked embedded chat placeholderJust idreferences a specific recruitment chat to embed
Processing Flow
Discovery:
embeddedRecruitmentChatIdsscans tree for embedded referencesResolution:
replaceEmbeddedChatBotreplaces placeholder nodes with actual chat contentID Updates:
updateNewIdsassigns fresh IDs to prevent conflictsDepth Limiting: Recursive depth is limited to prevent infinite embedding loops
Example Structure:
Main Chat Tree:
├── Text "Welcome! What brings you here?"
├── Option "Job Search"
│ └── EmbeddedChatBot (Just recruitmentChatId_123)
└── Option "Company Info"
└── Text "About our company..."
After Embedding Resolution:
├── Text "Welcome! What brings you here?"
├── Option "Job Search"
│ ├── Text "What type of position interests you?" (from chat 123)
│ ├── Option "Engineering" (from chat 123)
│ └── Option "Marketing" (from chat 123)
└── Option "Company Info"
└── Text "About our company..."
Cyclic Dependency Prevention
Separate
EmbeddedRecruitmentChatIdtype prevents direct cyclic references in Haskell typesRuntime depth limits prevent infinite recursion during tree processing
Validation prevents embedding a chat within itself (direct or indirect)
Node ID Generation System
The system uses a sophisticated ID generation approach that has evolved to support both legacy and modern requirements:
ID Types
type NodeId
= NodeId Int -- Legacy sequential IDs
| UuidNodeId Uuid -- Modern deterministic UUIDs
| NodeIdUnset -- Placeholder for new nodes
Legacy Sequential IDs
- Simple integer-based IDs assigned sequentially
- Used for backward compatibility with existing conversations
- Generated by updateNewIds function during tree processing
Modern UUID System
- Deterministic UUIDs based on tree path and content hash
- Generated by generateUuidId function using node position and content
- Ensures same content at same position always gets same ID
- Provides better stability when tree structure changes
ID Generation Process
Content Hashing: Each node type generates a unique content hash:
contentHash = case node of ChatBot.Text t -> t ChatBot.Option t -> t ChatBot.Link settings -> settings.url ++ settings.title ChatBot.VacancyOverview _ -> "vacancy_overview" ChatBot.SalaryCalculator _ -> "salary_calc" ChatBot.EmbeddedChatBot _ -> "embedded_chat"
Path Integration: Tree path (e.g.,
[0, 2, 1]) combined with content hashUUID Creation: Deterministic UUID generated from hash using custom algorithm
Collision Handling: Fallback to sequential IDs if UUID generation fails
Mixed ID Support
- Frontend handles both legacy and UUID-based IDs transparently
- Custom equality function nodeIdEquals enables mixed comparisons
- Gradual migration from sequential to UUID-based system
State Persistence - Conversation state stored using stable UUIDs when available - Hash validation detects when chat structure changes significantly - Automatic state reset when incompatible structure changes detected
Progressive Disclosure
In live chat mode, nodes appear with calculated delays to create natural conversation pacing:
Delay Calculation - Text nodes: 32ms per character, clamped between 1250-2000ms - Option buttons: 16ms per character, clamped between 750-2000ms - Interactive elements: Fixed 600ms delay - Root node: No delay (conversation starter)
Queue Management - Nodes are queued for display in tree traversal order - Only one node marked as “in_progress” at a time - Queue bypassed in embedded component mode for immediate display
Security & Validation
Domain Isolation - All operations scoped to provided domain - Cross-domain access prevented at API level
Referential Integrity
- Deletion blocked if chat used by pages (PageRecruitmentChat field)
- Deletion blocked if chat embedded in other chats (EmbeddedChatBot nodes)
- EmbeddedChatBot nodes validate target existence during rendering
Embedded Chat Validation
The system performs comprehensive validation before allowing chat deletion:
-- Check for page usage
isUsedByPage :: RecruitmentChatId -> DB Bool
-- Check for embedding in other chats
isUsedByOtherChatBot :: RecruitmentChatId -> DB Bool
Page Usage Check: Scans all pages for
pageRecruitmentChatfield referencesEmbedding Check: Examines all chat trees for
EmbeddedChatBotnodes containing the target IDValidation Errors: Returns specific error messages (
RecruitmentChatIsUsedByPageorRecruitmentChatIsUsedByOtherChat)
Input Validation - Label uniqueness enforced within domains - Required fields validated - Malformed tree structures rejected
Authentication - Session tokens passed for API requests - Form submissions include CSRF protection - File uploads require appropriate permissions
Troubleshooting
Common Issues
Chat not displaying
- Check showRecruitmentChat flag in page configuration
- Verify chat exists and belongs to correct domain
- Check browser console for JavaScript errors
Conversation state lost
- localStorage may be cleared or corrupt
- Chat structure hash mismatch triggers reset
- Check browser localStorage for chat_state_ keys
Forms not loading - Verify FormV2Schema configuration - Check vacancy template has formIdV2 set - Ensure form exists and is accessible
Media not displaying - Check upload URL mappings in configuration - Verify image/video files exist and are accessible - Check ImageSource configuration in chat nodes
Embedded chats not loading
- Verify the referenced EmbeddedRecruitmentChatId exists
- Check that embedded chat belongs to the same domain
- Ensure no circular references (chat A embeds chat B which embeds chat A)
- Monitor for recursive depth limits being reached
- Check browser console for embeddedRecruitmentChatIds errors
Chat deletion failing
- Check if chat is used by any pages (pageRecruitmentChat field)
- Scan other chats for EmbeddedChatBot nodes referencing this chat
- Review validation error messages for specific usage details
- Use API to get comprehensive usage report before deletion
Performance Considerations - Large conversation trees may impact initial load time - VacancyOverview nodes trigger API calls on display - Consider limiting embedded chat depth to prevent cycles - Monitor localStorage usage for conversation state - Embedded chat resolution happens during initialization, not runtime - ID regeneration for embedded nodes may cause temporary inconsistencies
Implementation Details
Key Functions and Modules
Backend (Haskell):
- FloHam.Cms.Model.RecruitmentChat.ChatBot.hs: Core node definitions and embedding types
- Handler.Api.RecruitmentChat.hs: CRUD operations and validation logic
- Render.Component.RecruitmentChat.hs: Server-side rendering and data preparation
Frontend (Elm):
- Data.ChatBot.elm: Tree navigation, ID generation, and embedded chat resolution
- View.ChatBot.elm: Node rendering and conversation management
- RecruitmentChat.elm: Widget container and initialization
Critical Processing Functions
-- Embedded chat resolution
fromChatBotWithEmbedded : ChatBots -> ChatBot -> Tree NodeWithId
replaceEmbeddedChatBot : Int -> ChatBots -> Tree NodeWithId -> Tree NodeWithId
embeddedRecruitmentChatIds : Tree NodeWithId -> List (NodeId, RecruitmentChatId)
-- ID management
generateUuidId : List Int -> ChatBot.Node -> NodeId
updateNewIds : Tree NodeWithId -> Tree NodeWithId
nodeIdEquals : NodeId -> NodeId -> Bool
Data Flow Summary
Server: Render component aggregates chat data and available embedded chats
Client: Elm app initializes with embedded chat resolution
Resolution:
replaceEmbeddedChatBotreplaces placeholder nodes with actual contentID Assignment: New UUIDs generated for all embedded content
Navigation: Tree.Zipper manages conversation state and progression
Persistence: Stable IDs enable consistent localStorage state management