Create a custom visualizer
In this tutorial, you will build a custom visualizer that renders content inside DIAL Chat using the ChatVisualizerConnector library. By the end, you will have a React application that receives data from DIAL Chat and displays it in an iframe.
Prerequisites
- Node.js 24+ and npm
- A running DIAL Chat instance
- Familiarity with React and TypeScript
Goal
You will create a visualizer that:
- Connects to DIAL Chat via the ChatVisualizerConnector library
- Receives custom content from a model or application response
- Renders that content inside the Chat UI
Step 1: Configure DIAL Chat
Set the following environment variables in your DIAL Chat deployment. See DIAL Chat configuration for the full reference.
ALLOWED_IFRAME_SOURCES — a space-separated list of allowed iframe origins. For development, you can use *. For production, specify the exact origin where your visualizer is hosted.
ALLOWED_IFRAME_SOURCES=http://localhost:8000
CUSTOM_VISUALIZERS — a JSON array defining each visualizer:
CUSTOM_VISUALIZERS=[
{
"title": "CUSTOM_VISUALIZER",
"description": "Custom visualizer to render images",
"icon": "data:image/svg+xml;base64,<BASE64_ENCODED_SVG>",
"contentType": "image/png,image/jpg",
"url": "http://localhost:8000"
}
]
| Field | Description |
|---|---|
title | Unique identifier for the visualizer. Must match appName in your code. |
description | Short description shown in the Chat UI. |
icon | Base64-encoded SVG icon. |
contentType | Comma-separated MIME types this visualizer handles. |
url | URL where the visualizer is hosted. |
ALLOW_VISUALIZER_SEND_MESSAGES — optional flag. Set to true to allow the visualizer to send messages on behalf of the user (enables custom buttons and input fields in the iframe).
Step 2: Scaffold the project
Create a new React project and install the connector library:
mkdir my-visualizer && cd my-visualizer
npm init -y
npm i react react-dom @epam/ai-dial-chat-visualizer-connector
npm i -D typescript @types/react @types/react-dom
Project structure
After completing all steps, your project will look like this:
my-visualizer/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.tsx
│ └── Module.tsx
└── public/
└── index.html
Step 3: Understand the data contract
The model or application must send data as JSON with a layout object containing width and height properties. These interfaces are exported by the connector library:
interface CustomVisualizerDataLayout {
width: number;
height: number;
}
interface CustomVisualizerData {
layout: CustomVisualizerDataLayout;
}
The AttachmentData interface wraps the payload received from DIAL Chat:
interface AttachmentData {
mimeType: string;
visualizerData: CustomVisualizerData;
}
Step 4: Create my-visualizer/src/Module.tsx
This is the main visualizer component. It connects to DIAL Chat, receives data, and renders it.
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import {
AttachmentData,
ChatVisualizerConnector,
CustomVisualizerDataLayout,
} from '@epam/ai-dial-chat-visualizer-connector';
interface YourVisualizerLayout extends CustomVisualizerDataLayout {
// Add any extra layout properties your visualizer needs
}
export const Module: FC = () => {
const [data, setData] = useState<AttachmentData>();
const chatVisualizerConnector = useRef<ChatVisualizerConnector | null>(null);
const dialHost = '<YOUR_DIAL_CHAT_HOST>';
const appName = 'CUSTOM_VISUALIZER';
useEffect(() => {
if (!chatVisualizerConnector.current && dialHost && appName) {
chatVisualizerConnector.current = new ChatVisualizerConnector(
dialHost,
appName,
setData,
);
return () => {
chatVisualizerConnector.current?.destroy();
chatVisualizerConnector.current = null;
};
}
}, [appName, dialHost]);
useEffect(() => {
if (appName && dialHost) {
chatVisualizerConnector.current?.sendReady();
chatVisualizerConnector.current?.sendReadyToInteract();
}
}, [dialHost, appName]);
const typedVisualizerData = useMemo(() => {
return (
data?.visualizerData &&
(data.visualizerData as unknown as {
dataToRender: string;
layout: YourVisualizerLayout;
})
);
}, [data?.visualizerData]);
return (
<div>
{!!typedVisualizerData?.dataToRender && (
<div>{typedVisualizerData.dataToRender}</div>
)}
</div>
);
};
Key points:
dialHost— the URL of your DIAL Chat instance. Replace<YOUR_DIAL_CHAT_HOST>with the actual host.appName— must match thetitlevalue in theCUSTOM_VISUALIZERSenvironment variable.sendReady()— hides the loader in the iframe.sendReadyToInteract()— tells DIAL Chat the visualizer is ready to receive data.
Step 5: Create my-visualizer/src/index.tsx
This is the application entry point that mounts the visualizer component.
import { createRoot } from 'react-dom/client';
import { Module } from './Module';
const root = createRoot(document.getElementById('root')!);
root.render(<Module />);
Verification
- Start your visualizer application (for example, on
http://localhost:8000). - Restart DIAL Chat with the environment variables from Step 1.
- Send a message to a model or application configured to return
custom_contentwith a MIME type matching your visualizer'scontentType. - The response renders inside an iframe in the Chat conversation.
What you learned
- How to configure DIAL Chat to load custom visualizers via
ALLOWED_IFRAME_SOURCESandCUSTOM_VISUALIZERS - How to use the
ChatVisualizerConnectorlibrary to receive and render data from DIAL Chat - The data contract between DIAL Chat and a visualizer (
AttachmentData,CustomVisualizerDataLayout)
Further reading
- Custom content in Chat — full reference for the
custom_contentAPI, including the attachment types your visualizer can handle - Data visualization — overview of all visualization options in DIAL Chat
- DIAL Chat configuration — complete list of environment variables for Chat
- Security and embedding — Content Security Policy and iframe origins for Chat