Skip to main content

DHTMLX Kanban. Kanban with AI Chatbot assistant example

This demo integrates a DHTMLX Kanban board with an AI chatbot that can manipulate the board through natural language commands. Users can ask the chatbot to add, move, or delete cards. The chatbot interprets the intent, maps it to Kanban API calls, and executes them.

Live example

const columns = [
    { "label": "Backlog", "id": "backlog" },
    { "label": "In progress", "id": "inprogress" },
    { "label": "Testing", "id": "testing" },
    { "label": "Done", "id": "done" }
];

const rows = [
    { "label": "Feature", "id": "feature" },
    { "label": "Task", "id": "task" }
];

const cards = [
    { id: 1, label: "Integration with Angular/React", column: "backlog", type: "feature" },
    { id: 2, label: "Archive the cards/boards", column: "backlog", type: "task" },
    { id: 3, label: "Searching and filtering", column: "inprogress", type: "feature" },
    { id: 4, label: "Set the tasks priorities", column: "inprogress", type: "task" },
    { id: 5, label: "Custom icons", column: "testing", type: "feature" },
    { id: 6, label: "Integration with Gantt", column: "inprogress", type: "task" },
    { id: 7, label: "Drag and drop", column: "testing", type: "feature" },
    { id: 8, label: "Adding images", column: "inprogress", type: "task" },
    { id: 9, label: "Create cards and lists from the UI and from code", column: "backlog", type: "feature" },
    { id: 10, label: "Draw swimlanes", column: "testing", type: "task" },
    { id: 11, label: "Progress bar", column: "done", type: "feature" },
];


const board = new kanban.Kanban("#kanban", {
    cards,
    columnKey: "column",
    columns,
    columnShape: {
        menu: {
            show: true,
            items: ({ column, columnIndex, columns, readonly }) => {
                if (columnIndex <= 0) {
                    return [
                        { id: "add-card", icon: "wxi-plus", text: "Add new card" },
                        { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                        { id: "move-column:right", icon: "wxi-arrow-right", text: "Move right" },
                        { id: "delete-column", icon: "wxi-delete", text: "Delete" }
                    ]
                }
                if (columnIndex >= columns.length - 1) {
                    return [
                        { id: "add-card", icon: "wxi-plus", text: "Add new card" },
                        { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                        { id: "move-column:left", icon: "wxi-arrow-left", text: "Move left" },
                        { id: "delete-column", icon: "wxi-delete", text: "Delete" }
                    ]
                }
                return [
                    { id: "add-card", icon: "wxi-plus", text: "Add new card" },
                    { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                    { id: "move-column:right", icon: "wxi-arrow-right", text: "Move right" },
                    { id: "move-column:left", icon: "wxi-arrow-left", text: "Move left" },
                    { id: "delete-column", icon: "wxi-delete", text: "Delete" }
                ]
            }
        }
    },
    rowKey: "type",
    rows,
    rowShape: {
        menu: {
            show: true,
            items: ({ row, rowIndex, rows, readonly }) => {
                if (rowIndex <= 0) {
                    return [
                        { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                        { id: "move-row:down", icon: "wxi-arrow-down", text: "Move down" },
                        { id: "delete-row", icon: "wxi-delete", text: "Delete" }
                    ]
                }
                if (rowIndex >= rows.length - 1) {
                    return [
                        { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                        { id: "move-row:up", icon: "wxi-arrow-up", text: "Move up" },
                        { id: "delete-row", icon: "wxi-delete", text: "Delete" }
                    ]
                }
                return [
                    { id: "set-edit", icon: "wxi-edit", text: "Rename" },
                    { id: "move-row:up", icon: "wxi-arrow-up", text: "Move up" },
                    { id: "move-row:down", icon: "wxi-arrow-down", text: "Move down" },
                    { id: "delete-row", icon: "wxi-delete", text: "Delete" }
                ]
            }
        }
    },
})
const toolbar = new kanban.Toolbar("#toolbar", {
    api: board.api,
    items: [
        "search",
        "spacer",
        "redo",
        "undo",
        "sort",
        "addColumn",
        "addRow"
    ]
})

// Provide your OpenAI API key
const api_key = ""

const schema = {
    "name": "intention",
    "strict": true,
    "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": ["response", "actions"],
        "properties": {
            "response": { "type": "string" },
            "actions": {
                "type": "array",
                "minItems": 1,
                "items": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": ["action", "arguments"],
                    "properties": {
                        "action": {
                            "type": "string",
                            "enum": ["addCard()", "deleteCard()", "moveCard()", "<|NO_ACTION_REQUIRED|>"]
                        },
                        "arguments": {
                            "anyOf": [
                                {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": ["label", "column", "type"],
                                    "properties": {
                                        "label": { "type": "string" },
                                        "column": { "type": "string" },
                                        "type": { "type": "string" },
                                    }
                                },
                                {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": ["id"],
                                    "properties": {
                                        "id": { "type": ["string", "number"] }
                                    }
                                },
                                {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": ["id", "column", "type"],
                                    "properties": {
                                        "id": { "type": ["string", "number"] },
                                        "column": { "type": "string" },
                                        "type": { "type": "string" },
                                    }
                                },
                                { "type": "null" }
                            ]
                        }
                    }
                }
            }
        }
    }
}

const chat = new chatbot.ChatBot("#agent", {
    sidebar: false,
    focus: false,
    format: "markdown",
    render: "flow",
    agents: [{
        id: 1,
        name: "Assistant",
        response: async (data) => {
            try {
                if (!api_key) {
                    return {
                        content: "Please, provide API key."
                    }
                }

                const system_prompt = [{
                    role: "system",
                    content: `You are a DHTMLX Kanban  Assistant.`
                }]
                const context_prompt = [{role: "system", content: `User's board state: ${JSON.stringify(board.api.getState())}`}]
                let messages = data.map(item => {
                    return {
                        role: item.role === "agent" ? "assistant" : "user",
                        content: item.content
                    }
                })
                let completion_request = {
                    model: "gpt-4.1-nano",
                    messages: system_prompt.concat(context_prompt).concat(messages),
                    response_format: {
                        type: "json_schema",
                        json_schema: schema
                    }
                }

                let completion_response = await fetch("https://api.openai.com/v1/chat/completions", {
                    method: "POST",
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${api_key}`
                    },
                    body: JSON.stringify(completion_request)
                });

                if (completion_response.status != 200) {
                    return {
                        content: `Request failed: ${completion_response.error}`
                    }
                }
                let completion = await completion_response.json()
                let payload = JSON.parse(completion.choices[0].message.content)

                for (const action of payload.actions) {
                    console.log(action)
                    if (action.action === "addCard()") {
                        board.addCard({
                            columnId: action.arguments.column,
                            rowId: action.arguments.type,
                            card: { label: action.arguments.label }
                        })
                    }
                    if (action.action === "deleteCard()") {
                        board.deleteCard({
                            id: action.arguments.id
                        });                        
                    }
                    if (action.action === "moveCard()") {
                        board.moveCard({
                            id: action.arguments.id,
                            columnId: action.arguments.column,
                            rowId: action.arguments.type
                        });
                    }
                    if (action.action === "<|NO_ACTION_REQUIRED|>") {
                        continue
                    }
                }
                return { content: `${payload.response}` };
            } catch (error) {
                return {
                    content: `Request failed: ${error}`
                }
            }
        }
    }]
});


const agentButton = document.getElementById('agent-button');
const backdrop = document.getElementById('modal-backdrop');
const modal = document.getElementById('modal');

function openAgentPopup() {
    backdrop.style.display = 'flex';
    backdrop.setAttribute('aria-hidden', 'false');
}

function closeAgentPopup() {
    backdrop.style.display = 'none';
    backdrop.setAttribute('aria-hidden', 'true');
}

agentButton.addEventListener('click', openAgentPopup);
backdrop.addEventListener('click', (e) => {
    if (e.target === backdrop) closeAgentPopup();
});
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && backdrop.style.display === 'flex') {
        closeAgentPopup();
    }
});
<!doctype html>

<html lang="ru">
    <head>
        <title>Agentic Chatbot Demo</title>
        <script src="./dist/chatbot/chatbot.js"></script>
        <script src="./dist/kanban/kanban.js"></script>
        <link href="./dist/chatbot/chatbot.css" rel="stylesheet">
        <link href="./dist/kanban/kanban.css" rel="stylesheet">
        <script src="https://snippet.dhtmlx.com/codebase/data/kanban/01/dataset.js"></script>
        <script src="./script.js" defer></script>

<style>
html, body {
                height: 100%;
                width: 100%;
                margin: 0;
            }

            #kanban {
                height: calc(100% - 56px);
                width: 100%;
            }

            :root {
                --agent-size: 32px;
                --agent-bg-1: #070708;
                --agent-bg-2: #18181A;
                --agent-accent: #8B6F5E; 
                --agent-foreground: #FFF9F7;
                --agent-danger: #6A3F2B; 
                --agent-border: #1B1B1D; 
                --agent-radius: 12px;
                --agent-shadow: 0 8px 22px rgba(10,10,12,0.72), 0 6px 20px rgba(139,111,94,0.12), 0 2px 6px rgba(139,111,94,0.56) inset;
                --agent-shadow-hover: 0 22px 48px rgba(10,10,12,0.75), 0 18px 42px rgba(139,111,94,0.20), 0 4px 12px rgba(139,111,94,0.58) inset;
                --agent-font-weight: 600;
                --agent-font-size: 14px;
                --scroll-thumb: rgba(139,111,94,0.26);
                --scroll-thumb-hover: rgba(139,111,94,0.36);
                --scroll-track: rgba(255,255,255,0.02);
                --scroll-corner: transparent;
                --scroll-width: 10px;
            }

            #agent-button {
                --size: var(--agent-size);
                display: inline-flex;
                align-items: center;
                gap: 10px;
                padding: 8px 12px;
                height: var(--size);
                width: var(--size);
                background: linear-gradient(135deg, var(--agent-bg-1) 0%, var(--agent-bg-2) 100%);
                color: var(--agent-foreground);
                border: none;
                border-radius: var(--agent-radius);
                box-shadow: var(--agent-shadow);
                cursor: pointer;
                font-weight: var(--agent-font-weight);
                font-size: var(--agent-font-size);
                transition: transform .12s ease, box-shadow .12s ease;
                justify-content: center;
                position: fixed;
                right: 48px;
                bottom: 48px;
                z-index: 100;
            }

                #agent-button:focus {
                    outline: 2px solid rgba(139,111,94,0.22);
                    outline-offset: 2px;
                }

                #agent-button:hover {
                    transform: translateY(-3px);
                    box-shadow: var(--agent-shadow-hover);
                }

            .wx-material-theme .wx-kanban,
            .wx-material-theme .wx-editor,
            .wx-material-theme .wx-toolbar,
            .wx-material-theme .wx-portal,
            .wx-material-theme {
                --wx-theme-name: graphite-flame-muted;
                --wx-color-primary: var(--agent-accent);
                --wx-color-accent: var(--agent-foreground);
                --wx-color-danger: var(--agent-danger);
                --wx-color-secondary: #FFF9F7;
                --wx-color-secondary-hover: #DFD9D7;
                --wx-color-secondary-font: rgba(0, 0, 0, .7);
                --wx-color-secondary-font-hover: rgba(0, 0, 0, .7);
                --wx-background: #070708;
                --wx-background-alt: #0C0C0E;
                --wx-kanban-background: var(--wx-background-alt);
                --wx-kanban-content-background: #0A0A0C;
                --wx-color-font: var(--agent-foreground);
                --wx-color-font-alt: #B7B9BB;
                --wx-border: 1px solid var(--agent-border);
                --wx-input-border-focus: 1px solid rgba(139,111,94,0.22);
                --wx-kanban-box-border: var(--wx-border);
                --wx-kanban-card-border: var(--wx-border);
                --wx-kanban-card-border-radius: var(--agent-radius);
                --wx-kanban-header-border-radius: var(--agent-radius);
                --wx-kanban-shadow: 0 8px 22px rgba(8,8,10,0.72);
                --wx-kanban-user-icon-size: 36px;
                --wx-kanban-toolbar-height: 56px;
                --wx-progress-height: 4px;
                --wx-kanban-collapsed-padding: calc(var(--wx-padding) - 4px);
                --wx-kanban-collapsed-margin: 0;
                --wx-kanban-collapsed-background: linear-gradient(180deg, rgba(24,24,26,0.20), rgba(24,24,26,0.08));
                --wx-kanban-collapsed-background-hover: linear-gradient(180deg, rgba(139,111,94,0.00), rgba(139,111,94,0.05));
                --wx-chat-button-toggle-sidebar-background: var(--wx-background);
                --wx-chat-icon-color: var(--wx-color-primary);
                --wx-chat-msg-background: linear-gradient(90deg, rgba(139,111,94,0.12), rgba(106,63,43,0.06));
                --wx-chat-textarea-color: var(--wx-color-font-alt);
                --wx-chat-msg-flow-background-hover: var(--agent-bg-2)
            }

            .card-highlight {
                background: linear-gradient(90deg, rgba(139,111,94,0.08), rgba(106,63,43,0.03));
                border-left: 4px solid var(--agent-accent);
            }

            .tag-fire {
                background: rgba(139,111,94,0.14);
                color: #FFF6F2;
                border-radius: 8px;
                padding: 2px 8px;
                font-weight: 700;
            }

            .badge-danger {
                background: var(--agent-danger);
                color: #fff;
                border-radius: 8px;
                padding: 2px 6px;
                font-weight: 700;
            }

            .wx-styled-scroll {
                --scrollbar-width: var(--scroll-width);
                -webkit-overflow-scrolling: touch;
                scrollbar-width: thin;
                scrollbar-color: var(--scroll-thumb) var(--scroll-track);
            }

                .wx-styled-scroll::-webkit-scrollbar {
                    width: var(--scrollbar-width);
                    height: var(--scrollbar-width);
                    background: transparent;
                }

                .wx-styled-scroll::-webkit-scrollbar-track {
                    background: var(--scroll-track);
                    border-radius: calc(var(--scrollbar-width) * 2);
                }

                .wx-styled-scroll::-webkit-scrollbar-thumb {
                    background: linear-gradient(180deg, var(--scroll-thumb), rgba(139,111,94,0.18));
                    border-radius: calc(var(--scrollbar-width) * 2);
                    border: 2px solid transparent;
                    background-clip: padding-box;
                    box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
                }

                .wx-styled-scroll:hover::-webkit-scrollbar-thumb {
                    background: linear-gradient(180deg, var(--scroll-thumb-hover), rgba(139,111,94,0.26));
                }

                .wx-styled-scroll::-webkit-scrollbar-corner {
                    background: var(--scroll-corner);
                }

                .wx-styled-scroll:focus-within::-webkit-scrollbar-thumb,
                .wx-styled-scroll:active::-webkit-scrollbar-thumb {
                    background: linear-gradient(180deg, var(--scroll-thumb-hover), rgba(139,111,94,0.30));
                    box-shadow: 0 2px 8px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.02);
                }

                .wx-styled-scroll.horizontal {
                    overflow-x: auto;
                    overflow-y: hidden;
                }

            .wx-styled-scroll {
                scrollbar-width: thin;
                scrollbar-color: var(--scroll-thumb) var(--scroll-track);
            }

                .wx-styled-scroll.has-scrollbar::after {
                    content: "";
                    position: absolute;
                    right: 0;
                    top: 0;
                    width: calc(var(--scrollbar-width));
                    height: 100%;
                    pointer-events: none;
                    background: linear-gradient(90deg, rgba(0,0,0,0.0), rgba(0,0,0,0.25));
                    border-top-right-radius: 6px;
                    border-bottom-right-radius: 6px;
                }

                .wx-styled-scroll[style], .wx-styled-scroll {
                    position: relative;
                    overflow: auto;
                }
</style>

The code uses DHTMLX ChatBot widget connected to OpenAI's API. A JSON schema defines the structured output format with three possible actions: addCard(), deleteCard(), and moveCard(). The chatbot's system prompt includes the current board state (via board.api.getState()), giving the LLM context to understand which cards exist and where they are. The response handler parses the structured output and maps each action to the corresponding Kanban API method. Column and row menus are customized with position-aware items (showing "Move left"/"Move right" based on column position).

Solution overview

  1. Initialize Kanban and ChatBot widgets
  2. Define a JSON schema for structured LLM output (response text + action array)
  3. In the chatbot's response handler, send the board state as context to the LLM
  4. Parse the structured response and execute each action via Kanban API methods
  5. Display the LLM's text response in the chat

Key points

  • Board state as context: board.api.getState() is passed to the LLM on every message, so it always knows the current cards and columns
  • Structured output: The JSON schema enforces that the LLM returns valid action objects. No free-text parsing needed
  • API key required: The demo needs an OpenAI API key set in the api_key variable
  • Custom dark theme: The demo applies a custom "graphite-flame-muted" theme using CSS variables on both the Kanban and ChatBot widgets

Additional resources