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