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 position

  • currentAndBefore: Returns conversation history plus current children

  • before: 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-chat

List all recruitment chats for the specified domain.

Response: Array of recruitment chat objects with metadata

POST /api/{domainId}/recruitment-chat

Create a new recruitment chat with optional duplication from existing chat.

Body: Chat configuration object Features: Supports duplicating existing chats via duplicateFrom parameter

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:

label

Type: String Description: Unique identifier within domain Validation: Must be unique across all chats in the domain

title

Type: String Description: Display title for chat widget Usage: Shown in chat header and CMS interface

introduction

Type: String Description: Welcome message text Usage: Displayed before chat conversation begins

chatBot

Type: ChatBot Object Description: Complete conversation tree structure Features: Full tree replacement with embedded chat resolution

botAvatar

Type: 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 updated timestamp

  • Original inserted timestamp preserved

  • Full 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:

  1. Reference Resolution: The node contains an EmbeddedRecruitmentChatId that references another chat

  2. Tree Replacement: During initialization, the embedded node is replaced with the actual content from the referenced chat

  3. Recursive Processing: If the embedded chat itself contains embedded references, they are resolved recursively (with depth limits)

  4. 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

  • Nothing represents an unlinked embedded chat placeholder

  • Just id references a specific recruitment chat to embed

Processing Flow

  1. Discovery: embeddedRecruitmentChatIds scans tree for embedded references

  2. Resolution: replaceEmbeddedChatBot replaces placeholder nodes with actual chat content

  3. ID Updates: updateNewIds assigns fresh IDs to prevent conflicts

  4. Depth 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 EmbeddedRecruitmentChatId type prevents direct cyclic references in Haskell types

  • Runtime 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

  1. 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"
    
  2. Path Integration: Tree path (e.g., [0, 2, 1]) combined with content hash

  3. UUID Creation: Deterministic UUID generated from hash using custom algorithm

  4. 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
  1. Page Usage Check: Scans all pages for pageRecruitmentChat field references

  2. Embedding Check: Examines all chat trees for EmbeddedChatBot nodes containing the target ID

  3. Validation Errors: Returns specific error messages (RecruitmentChatIsUsedByPage or RecruitmentChatIsUsedByOtherChat)

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

  1. Server: Render component aggregates chat data and available embedded chats

  2. Client: Elm app initializes with embedded chat resolution

  3. Resolution: replaceEmbeddedChatBot replaces placeholder nodes with actual content

  4. ID Assignment: New UUIDs generated for all embedded content

  5. Navigation: Tree.Zipper manages conversation state and progression

  6. Persistence: Stable IDs enable consistent localStorage state management