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