Demo site: https://chat-component.opensource.mieweb.org/
Video demo: https://youtube.com/shorts/oBetyZPDVvg
https://pm.mieweb.com/issues/144868
A React chat component with Tailwind CSS styling and Zustand state management, designed to be embeddable in both Bootstrap and Tailwind environments without style conflicts.
- Self-Contained Bundle: Includes React 19 - no external dependencies needed
- React-based: Modern React component architecture
- Tailwind CSS: Styled with Tailwind CSS with
tw-prefix for encapsulation - Zustand State Management: Efficient in-memory state management
- Bootstrap Compatible: No style conflicts when embedded in Bootstrap pages
- Message Callbacks: Bubble up new messages to parent components
- State Export/Import: Save and restore conversation state
- Responsive Design: Mobile-friendly with collapsible sidebar
- Multi-conversation Support: Manage multiple conversation threads
- Rich Message Types: Support for messages, lab results, imaging reports, and events
- Read-Only Mode: Display conversations without interactive controls for viewing-only scenarios
src/chat-component-embed.jsx- Entry point for building the embeddable bundle. This file defines what gets exported when the component is built intodist/chat-component.umd.js. It bundles React 19, ReactDOM, the ChatComponent, and the Zustand store together for self-contained distribution.
- Embedding Guide - Complete guide for embedding the component in HTML pages
- API Reference - Component props and store methods
- Examples - Usage examples for different frameworks
npm install @mieweb/chat-componentThis repository is set up to publish from publish.yml using npm trusted publishing with GitHub Actions OIDC.
To enable trusted publishing for @mieweb/chat-component on npm:
- In npm package settings, add a GitHub Actions trusted publisher for:
- Organization or user:
mieweb - Repository:
chat-component - Workflow filename:
publish.yml - Allowed actions:
npm publish
- Publish by pushing a version tag such as
v1.0.1. - After the first successful OIDC publish, switch npm package publishing access to require two-factor authentication and disallow tokens, then revoke any old automation tokens.
Notes:
- The workflow must stay at
.github/workflows/publish.yml, and the filename must exactly match what you configure on npm. - Provenance is generated automatically when publishing with trusted publishing from a public GitHub repository to a public npm package.
- If you later add a GitHub environment to the publish job, the same environment name must also be configured in the npm trusted publisher settings.
import ChatComponent from '@mieweb/chat-component';
function App() {
const handleMessageSent = (data) => {
console.log('New message:', data);
// Handle the new message (e.g., send to server)
};
return (
<ChatComponent
onMessageSent={handleMessageSent}
height="500px"
maxWidth="1100px"
/>
);
}For embedding in plain HTML pages without a build system, see the Embedding Guide for complete instructions including:
- Loading dependencies
- Initializing the component
- Loading conversations from your API
- Receiving messages from WebSocket/polling
- Sending messages to your backend
import ChatComponent from '@mieweb/chat-component';
function BootstrapPage() {
return (
<div className="container">
<div className="card">
<div className="card-body">
<h5 className="card-title">Chat Interface</h5>
<ChatComponent
height="500px"
maxWidth="100%"
/>
</div>
</div>
</div>
);
}import ChatComponent from '@mieweb/chat-component';
function TailwindPage() {
return (
<div className="container mx-auto p-4">
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold mb-4">Chat Interface</h2>
<ChatComponent
height="500px"
maxWidth="100%"
/>
</div>
</div>
);
}Display a conversation without any interactive controls (no sidebar, compose area, or action buttons):
import ChatComponent from '@mieweb/chat-component';
function ReadOnlyConversation() {
const conversation = {
id: 735,
title: 'Patient Consultation',
open: true,
unread: false,
lastActivity: '2025-12-10 14:30',
thread: [
{
type: 'message',
role: 'external',
senderId: 100,
sender_name: 'Jane Doe',
channel: 'portal',
time: '2025-12-10 14:10',
text: 'I have a question about my test results.'
},
{
type: 'message',
role: 'internal',
senderId: 200,
sender_name: 'Dr. Smith',
channel: 'portal',
time: '2025-12-10 14:20',
text: 'Your test results look great!'
}
]
};
return (
<ChatComponent
readOnly={true}
conversation={conversation}
currentUserId={100}
height="400px"
/>
);
}Customize how reference links (documents, prescriptions, appointments) are generated:
import ChatComponent from '@mieweb/chat-component';
function CustomLinksExample() {
const buildLink = (refType, refId, item) => {
// Build URLs based on your application's routing structure
switch(refType) {
case 'doc':
return `/patient/documents/${refId}`;
case 'rx':
return `/patient/prescriptions/${refId}`;
case 'appt':
return `/patient/appointments/${refId}`;
default:
return `#${refType}/${refId}`;
}
};
return (
<ChatComponent
linkBuilder={buildLink}
height="500px"
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
initialData |
Object |
null |
Initial conversation data to load |
onMessageSent |
Function |
null |
Callback when a new message is sent |
className |
String |
'' |
Additional CSS classes for the root element |
height |
String |
'500px' |
Height of the chat component |
maxWidth |
String |
'1100px' |
Maximum width of the chat component |
currentUserId |
Number |
null |
Integer ID of the current user viewing the component |
readOnly |
Boolean |
false |
Enable read-only mode for displaying conversations without interactive controls |
conversation |
Object |
null |
Conversation object to display in read-only mode |
initialActiveConversationId |
number |
null |
ID of the conversation to show on mount |
onConversationOpened |
Function |
null |
Callback when a conversation is selected. Receives { conversationId, conversation } |
onConversationCreated |
Function |
null |
Callback when a new conversation is created. Receives { conversationId, conversation } |
onConversationClosed |
Function |
null |
Callback when the Close button is clicked. Receives { conversationId, conversation } |
hideNewButton |
boolean |
false |
Hide the "New Conversation" button in the conversation list |
newConversationLabel |
string |
'' |
Label text for the New Conversation button. When empty, the button shows + |
hideToggleButton |
boolean |
false |
Hide the sidebar toggle button in the top bar |
hideStatusToggle |
boolean |
false |
Hide the conversation status toggle button in the top bar |
hideConversationStatus |
boolean |
false |
Hide the conversation status badge (Open/Closed label) in the top bar |
showCloseButton |
boolean |
false |
Show a Close button instead of the Toggle Status button in the top bar |
disableClosedConversations |
boolean |
false |
When true, disables the compose area when a conversation's status is 'closed' |
hideDeliveryMethod |
boolean |
false |
Hide the delivery method dropdown in the compose area (useful for patient/external user views) |
linkBuilder |
Function |
null |
Custom function to build reference links. Receives (refType, refId, item) and returns a URL string |
onNewConversation |
Function |
null |
Custom handler for New Conversation button. Receives helper functions { openDialog, createConversation } |
The linkBuilder prop allows you to customize how links are generated for attached documents, prescriptions, appointments, and other references in messages, as well as for conversations in the conversation list. If not provided, the default format #${refType}/${refId} is used for references, and no link button appears for conversations.
// Example: Custom link builder
linkBuilder={(refType, refId, item) => {
switch(refType) {
case 'doc':
return `/documents/${refId}`;
case 'rx':
return `/prescriptions/${refId}`;
case 'appt':
return `/appointments/${refId}`;
case 'conversation':
return `/conversations/${refId}`;
default:
return `#${refType}/${refId}`;
}
}}The function receives:
refType: Type of reference (e.g., 'doc', 'rx', 'appt', 'conversation')refId: Unique identifier for the reference or conversationitem: Complete item object containing all data (for messages: title, text, time, channel, etc.; for conversations: the entire conversation object)
When linkBuilder is provided and returns a URL for the 'conversation' refType, a right arrow button will appear on each conversation in the conversation list, allowing users to navigate to the conversation's detail page.
The onMessageSent callback receives an object with:
{
conversationId: 735,
message: {
type: 'message',
role: 'physician',
channel: 'portal',
time: '2025-10-30 14:25',
text: 'Message text'
}
}Called when a user sends a message.
onMessageSent={({ conversationId, message }) => {
console.log('Message sent:', message);
console.log('In conversation:', conversationId);
// message includes: id, text, timestamp, senderId, channel
}}Called when a user selects/opens a conversation.
onConversationOpened={({ conversationId, conversation }) => {
console.log('Opened conversation:', conversationId);
console.log('Conversation data:', conversation);
}}Called when a new conversation is created.
onConversationCreated={({ conversationId, conversation }) => {
console.log('Created conversation:', conversationId);
console.log('Conversation data:', conversation);
}}Called when the Close button is clicked (only when showCloseButton is true).
onConversationClosed={({ conversationId, conversation }) => {
console.log('Close button clicked for conversation:', conversationId);
console.log('Conversation data:', conversation);
// Handle conversation close action (e.g., navigate away, hide component)
}}Custom handler for the New Conversation button. Receives helper functions to control behavior:
onNewConversation={({ openDialog, createConversation }) => {
// Example 1: Navigate to a URL
window.location.href = '/create-conversation';
// Example 2: Open the default dialog
openDialog();
// Example 3: Create a conversation programmatically
const newConv = createConversation('Quick Chat', true);
// Example 4: Create without triggering onConversationCreated
createConversation('Silent Create', false);
}}Helper Functions:
openDialog()- Opens the default new conversation dialogcreateConversation(title, triggerCallback)- Programmatically creates a conversationtitle(string, optional): Conversation title, defaults to 'New Conversation'triggerCallback(boolean, default: true): Whether to triggeronConversationCreatedcallback- Returns the created conversation object
Common Use Cases:
// Navigate to external page
onNewConversation={() => {
window.location.href = '/conversations/new';
}}
// Custom logic then open dialog
onNewConversation={({ openDialog }) => {
console.log('User creating new conversation');
trackAnalytics('new_conversation_clicked');
openDialog();
}}
// Programmatically create with custom title
onNewConversation={({ createConversation }) => {
createConversation('Patient Inquiry - ' + new Date().toLocaleDateString());
}}The component uses Zustand for state management. You can access the store to programmatically control the component and react to changes.
import { useChatStore } from '@mieweb/chat-component';
function MyComponent() {
const exportState = useChatStore(state => state.exportState);
const loadConversations = useChatStore(state => state.loadConversations);
const addMessage = useChatStore(state => state.addMessage);
// Load conversations from your API
const loadData = async () => {
const response = await fetch('/api/conversations');
const data = await response.json();
loadConversations(data);
};
// Inject an incoming message (e.g., from WebSocket)
const handleIncomingMessage = (message) => {
addMessage({
text: message.text,
channel: message.channel || 'auto',
});
};
return (
<div>
<button onClick={loadData}>Load Data</button>
{/* ... */}
</div>
);
}getActiveConversation(): Get the currently active conversationsetActiveConversation(id): Set the active conversation by IDaddMessage(message): Add a message to the active conversationupdateConversation(id, updates): Update a conversationtoggleConversationStatus(id): Toggle conversation open/closedmarkAsUnread(id): Mark a conversation as unreadcreateConversation(title): Create a new conversationsetSearchQuery(query): Set the search querytoggleSidebar(): Toggle the sidebar (mobile)loadConversations(data): Load conversation dataexportState(): Export current state
For detailed examples of receiving and sending messages, see EMBEDDING.md.
{
id: 735,
title: 'General Question',
reference_id: 'CASE-2025-001', // Optional: external reference identifier
open: true,
unread: false,
lastActivity: '2025-10-29 09:30',
thread: [
// Message items (see below)
]
}{
type: 'message',
role: 'external' | 'internal' | 'system',
senderId: 100, // Integer user/patient ID (null for system messages)
channel: 'portal' | 'sms' | 'voicemail' | 'auto',
time: '2025-10-29 08:12',
text: 'Message text'
}{
type: 'ref',
refType: 'doc' | 'rx' | 'appt', // Document, Prescription, Appointment, etc.
refId: 1001, // Reference to external document/record ID
title: 'CBC Result', // Optional: Display title for the reference
role: 'internal',
senderId: 200,
channel: 'auto',
time: '2025-10-29 08:30',
text: 'CBC Result: WBC elevated (12.3), mild neutrophilia. Reviewed by Dr. Smith: Consistent with mild infection, recommend follow-up.'
}Reference Types:
doc- Documents (lab results, imaging reports, clinical notes)rx- Prescriptionsappt- Appointments- Additional types can be added as needed
{
type: 'message',
role: 'system',
senderId: null,
channel: 'auto',
time: '2025-10-29 08:00',
text: 'New conversation initialized.'
}System messages are center-aligned in a yellow text box and are used for automated messages like conversation creation notifications. They use role: 'system' instead of 'external' or 'internal'.
import ChatComponent, { useChatStore } from '@mieweb/chat-component';
function App() {
const loadConversations = useChatStore(state => state.loadConversations);
const handleLoadData = () => {
const customData = {
conversations: [
{
id: 'custom1',
title: 'Custom Conversation',
open: true,
unread: false,
lastActivity: '2025-10-30 14:00',
thread: [
{
type: 'message',
role: 'patient',
channel: 'portal',
time: '2025-10-30 14:00',
text: 'Hello!'
}
]
}
],
activeConversationId: 'custom1'
};
loadConversations(customData);
};
return (
<div>
<button onClick={handleLoadData}>Load Custom Data</button>
<ChatComponent />
</div>
);
}The component uses Tailwind CSS with the tw- prefix to avoid conflicts with Bootstrap or other CSS frameworks. Additionally:
- Tailwind's
preflight(base/reset styles) is disabled - Custom CSS variables are scoped to
.chat-component-root - All component styles are prefixed and isolated
This ensures the component can be safely embedded in any environment without causing style conflicts.
npm install
npm run devThen visit:
- Tailwind demo: http://localhost:5173/demo-tailwind.html
- Bootstrap demo: http://localhost:5173/demo-bootstrap.html
npm run buildThis creates the production build in the dist/ directory.
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Mobile browsers (iOS Safari, Chrome Mobile)
ISC
Contributions are welcome! Please submit issues and pull requests to the GitHub repository.