Skip to content

MiiFlow/assistant-ui

Repository files navigation

@miiflow/assistant-ui

React components and hooks for building custom Miiflow chat interfaces. Install as an npm package for full control over layout, styling, and behavior.

Installation

npm install @miiflow/assistant-ui

Peer dependencies: react >= 18, react-dom >= 18

The styled components use TailwindCSS. If your project doesn't use Tailwind, import the pre-built CSS instead:

import "@miiflow/assistant-ui/styles.css";

If you're embedding inside an existing page and want to avoid Tailwind's preflight (CSS reset) affecting the host page:

import "@miiflow/assistant-ui/styles-no-preflight.css";

Quick Start

import { useMiiflowChat } from "@miiflow/assistant-ui/client";
import {
  ChatProvider,
  ChatLayout,
  ChatHeader,
  MessageList,
  Message,
  MessageComposer,
  WelcomeScreen,
} from "@miiflow/assistant-ui/styled";
import "@miiflow/assistant-ui/styles.css";

function Chat() {
  const {
    messages,
    isStreaming,
    streamingMessageId,
    sendMessage,
    uploadFile,
    startNewThread,
    branding,
    brandingCSSVars,
    loading,
  } = useMiiflowChat({
    // Find these in your Miiflow dashboard under Settings > Embed
    publicKey: "pk_live_...",
    assistantId: "ast_...",
  });

  if (loading) return <div>Loading...</div>;

  const isEmpty = messages.length === 0;

  return (
    <ChatProvider
      messages={messages}
      isStreaming={isStreaming}
      streamingMessageId={streamingMessageId}
      onSendMessage={sendMessage}
    >
      <div style={{ height: "100vh", ...brandingCSSVars }}>
        <ChatLayout
          isEmpty={isEmpty}
          header={
            <ChatHeader
              title={branding?.customName ?? "Assistant"}
              logo={branding?.chatbotLogo}
              actions={[
                { id: "new", label: "New chat", onClick: startNewThread },
              ]}
            />
          }
          welcomeScreen={
            <WelcomeScreen
              welcomeText={branding?.welcomeMessage}
              placeholders={branding?.rotatingPlaceholders}
              suggestions={branding?.presetQuestions}
              onSubmit={sendMessage}
              onSuggestionClick={sendMessage}
            />
          }
          messageList={
            <MessageList>
              {messages.map((msg) => (
                <Message
                  key={msg.id}
                  message={msg}
                  reasoning={msg.reasoning}
                  suggestedActions={msg.suggestedActions}
                  onSuggestedAction={(a) => sendMessage(a.value)}
                />
              ))}
            </MessageList>
          }
          composer={
            <MessageComposer
              onSubmit={sendMessage}
              onUploadFile={uploadFile}
              disabled={isStreaming}
              placeholder={branding?.chatboxPlaceholder}
            />
          }
        />
      </div>
    </ChatProvider>
  );
}

Configuration Reference

Pass a MiiflowChatConfig object to useMiiflowChat:

Field Type Required Description
publicKey string Yes Public API key from the Miiflow dashboard
assistantId string Yes Assistant ID from the Miiflow dashboard
userId string No User ID for identity tracking
userName string No User display name
userEmail string No User email
userMetadata string No JSON string of custom user metadata
hmac string No HMAC for identity verification
timestamp string No Timestamp for HMAC verification
baseUrl string No Override API endpoint (default: https://api.miiflow.ai/api)
webSocketUrl string No WebSocket URL for tool invocations (auto-derived from baseUrl if not set)
responseTimeout number No SSE stream timeout in ms (default: 60000)

Connecting to a Custom Backend

By default, the hook connects to https://api.miiflow.ai. To point to your own backend, pass a baseUrl:

useMiiflowChat({
  publicKey: "pk_live_...",
  assistantId: "ast_...",
  baseUrl: "https://your-server.example.com/api",
  // webSocketUrl is auto-derived from baseUrl; override if needed:
  // webSocketUrl: "wss://your-server.example.com/ws",
});

Your backend must implement the same API contract as the Miiflow platform (session init, SSE streaming, file upload, and tool-result endpoints).

Hook API — useMiiflowChat

import { useMiiflowChat } from "@miiflow/assistant-ui/client";

const result = useMiiflowChat(config);

State

Property Type Description
messages ChatMessage[] Messages in the conversation
isStreaming boolean Whether a response is currently streaming
streamingMessageId string | null ID of the message being streamed
loading boolean Whether the session is still initializing
error string | null Error message if initialization or sending failed
session EmbedSession | null Current session data
branding BrandingData | null Branding configuration from the dashboard
brandingCSSVars CSSProperties CSS custom properties derived from branding

Actions

Method Signature Description
sendMessage (content: string, attachmentIds?: string[]) => Promise<void> Send a message to the assistant
uploadFile (file: File) => Promise<string> Upload a file and get an attachment ID
startNewThread () => Promise<string> Start a new conversation thread
registerTool (tool: ClientToolDefinition) => Promise<void> Register a client-side tool
registerTools (tools: ClientToolDefinition[]) => Promise<void> Register multiple tools
sendSystemEvent (event: SystemEvent) => Promise<void> Send an invisible system event

Components Reference

ChatProvider

Wraps children and provides chat context via React context.

Prop Type Default Description
messages ChatMessage[] Messages to display
isStreaming boolean false Whether a response is streaming
streamingMessageId string | null null ID of the streaming message
viewerRole ParticipantRole "user" Viewer's role (determines message alignment)
onSendMessage (content: string, attachments?: File[]) => Promise<void> Message send handler
onStopStreaming () => void Stop streaming handler
onRetryLastMessage () => Promise<void> Retry last message handler
onVisualizationAction (event: VisualizationActionEvent) => void Callback for form/card interactions

ChatLayout

Handles the empty-to-active state transition with crossfade animation. Accepts render slots for each section.

Prop Type Default Description
isEmpty boolean Whether the chat has no messages
header ReactNode Header slot (rendered in both states)
welcomeScreen ReactNode Content for empty state
messageList ReactNode Message list for active state
composer ReactNode Composer for active state
footer ReactNode Extra content between list and composer
variant "standalone" | "embedded" | "widget" "standalone" Layout variant
className string Additional CSS classes

WelcomeScreen

Empty state with rotating placeholder text and suggestion cards.

Prop Type Default Description
placeholders string[] [] Rotating placeholder strings
suggestions string[] [] Preset suggestion cards
onSubmit (message: string) => void Submit handler for built-in input
onSuggestionClick (suggestion: string) => void Suggestion card click handler
welcomeText string "How can I help you today?" Heading text
composerSlot ReactNode Override default input with custom composer
className string Additional CSS classes

MessageList

Scrollable message container with auto-scroll.

Prop Type Default Description
children ReactNode Message elements
autoScroll boolean true Auto-scroll to bottom on new messages
className string Additional CSS classes

Message

Individual message with markdown rendering, reasoning panel, citations, and visualizations.

Prop Type Default Description
message MessageData Message data object
viewerRole ParticipantRole "user" Viewer's role (determines alignment)
showAvatar boolean true Show participant avatar
showTimestamp boolean true Show message timestamp
renderMarkdown boolean true Render content as markdown
reasoning StreamingChunk[] Reasoning/thinking chunks for collapsible panel
suggestedActions SuggestedAction[] Suggested follow-up actions
onSuggestedAction (action: SuggestedAction) => void Suggested action click handler
citations SourceReference[] Citation sources to display
visualizations VisualizationChunkData[] Inline visualizations
className string Additional CSS classes

MessageComposer

Rich text editor (Lexical) with file upload, drag-and-drop, and Enter-to-send.

Prop Type Default Description
onSubmit (content: string, attachments?: File[]) => Promise<void> Submit handler
onUploadFile (file: File) => Promise<string> File upload handler (returns attachment ID)
onAttach (files: File[]) => void Called when files are attached
disabled boolean false Disable the composer
supportsAttachments boolean true Enable file attachments
allowedFileTypes string[] images, docs, videos Allowed MIME types
maxFileSize number 104857600 (100MB) Max file size in bytes
placeholder string "Type a message..." Placeholder text
isSubmitting boolean false Show loading state on send button
className string Additional CSS classes

ChatHeader

Title bar with logo, subtitle, action menu, and close button.

Prop Type Default Description
title string Assistant name
subtitle string Description or status text
logo string | ReactNode Logo URL or custom element
actions ChatHeaderAction[] Menu items ({ id, label, icon?, onClick, disabled? })
showClose boolean Show close button
onClose () => void Close button handler
loading boolean Show loading skeleton
className string Additional CSS classes
style CSSProperties Inline styles

Styling & Theming

CSS Import

Import the stylesheet to get default styles for all components:

import "@miiflow/assistant-ui/styles.css";

Branding via CSS Variables

The brandingCSSVars object from useMiiflowChat contains CSS custom properties derived from your dashboard branding settings. Spread it onto the container element:

<div style={brandingCSSVars}>
  <ChatLayout ... />
</div>

Available CSS variables:

Variable Source Description
--chat-primary backgroundBubbleColor Primary accent color
--chat-user-message-bg backgroundBubbleColor User message bubble background
--chat-header-bg headerBackgroundColor Header background color
--chat-message-font-size messageFontSize Base message font size

TailwindCSS Customization

All components accept a className prop for Tailwind utility overrides:

<MessageComposer className="rounded-none border-0" />

File Uploads

Pass onUploadFile={uploadFile} to MessageComposer to enable server-side file uploads:

const { sendMessage, uploadFile } = useMiiflowChat(config);

<MessageComposer
  onSubmit={sendMessage}
  onUploadFile={uploadFile}
  supportsAttachments={true}
/>

The composer handles file picking, validation, drag-and-drop, and preview thumbnails. Files are uploaded via uploadFile() which returns an attachment ID. The IDs are passed along when sendMessage() is called.

Client-Side Tools

Register tools that the assistant can invoke on the client:

const { registerTool } = useMiiflowChat(config);

await registerTool({
  name: "get_weather",
  description: "Get current weather for a city",
  parameters: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name" },
    },
    required: ["city"],
  },
  handler: async (params) => {
    const response = await fetch(`/api/weather?city=${params.city}`);
    return response.json();
  },
});

Tools are automatically re-registered when starting a new thread via startNewThread().

The handler function receives the parameters as a Record<string, unknown> and must return a Promise. Results are sent back to the assistant automatically. A 30-second timeout is enforced per invocation.

System Events

Send invisible context events that the assistant can use to inform its responses:

const { sendSystemEvent } = useMiiflowChat(config);

await sendSystemEvent({
  action: "page_navigation",
  description: "User navigated to /pricing",
  followUpInstruction: "If relevant, mention our pricing plans",
});
Field Type Required Description
action string Yes Event identifier
description string Yes Human-readable description of what happened
followUpInstruction string Yes Instruction for the assistant
metadata Record<string, unknown> No Additional structured data

Identity Verification (HMAC)

For secure identity verification, compute an HMAC on your server and pass it to the config:

useMiiflowChat({
  publicKey: "pk_live_...",
  assistantId: "ast_...",
  userId: "user_123",
  userName: "Jane Doe",
  userEmail: "jane@example.com",
  hmac: serverComputedHmac,
  timestamp: serverTimestamp,
});

The hmac and timestamp should be generated server-side using your secret key. See the Miiflow dashboard for your HMAC secret.

Visualizations

Assistant messages can contain rich visualizations (charts, tables, forms, etc.) rendered inline via [VIZ:id] markers. The Message component handles this automatically when you pass the visualizations prop.

Built-in Types

Type Component Description
chart ChartVisualization Line, bar, pie, area, scatter charts (Recharts)
table TableVisualization Sortable, paginated data tables
card CardVisualization Structured cards with sections, actions, images
kpi KpiVisualization Key performance indicator metrics with trends
code_preview CodePreviewVisualization Syntax-highlighted code blocks
form FormVisualization Interactive forms with validation

Visualization Registry

Instead of a hardcoded switch, visualizations are resolved through a registry. You can register custom visualization types that the VisualizationRenderer will render automatically:

import {
  registerVisualization,
  getVisualization,
  getRegisteredTypes,
} from "@miiflow/assistant-ui/styled";

// Register a custom visualization type
registerVisualization("my_widget", {
  component: MyWidgetComponent,
  schema: myWidgetZodSchema, // optional — enables data validation
});

// Check what's registered
console.log(getRegisteredTypes());
// ["chart", "table", "card", "kpi", "code_preview", "form", "my_widget"]

Your component receives these props:

interface VisualizationComponentProps {
  data: any;
  config?: VisualizationConfig;
  isStreaming?: boolean;
  onAction?: (event: VisualizationActionEvent) => void;
}

Overriding built-ins: Call registerVisualization("chart", { component: MyChart }) to replace a built-in type with your own implementation. The last registration wins.

Schema Validation

Each built-in type has a Zod schema registered alongside its component. When a schema is present, VisualizationRenderer validates the data before rendering. Invalid data shows a descriptive error fallback instead of crashing.

You can import the schemas directly for use in your own code:

import {
  chartVisualizationSchema,
  tableVisualizationSchema,
  cardVisualizationSchema,
  kpiVisualizationSchema,
  codePreviewVisualizationSchema,
  formVisualizationSchema,
} from "@miiflow/assistant-ui/styled";

const result = chartVisualizationSchema.safeParse(data);
if (!result.success) {
  console.error("Invalid chart data:", result.error.issues);
}

To add validation to a custom type, pass a schema when registering:

import { z } from "zod";

const mySchema = z.object({
  message: z.string(),
  count: z.number().min(0),
});

registerVisualization("my_widget", {
  component: MyWidget,
  schema: mySchema,
});

Note: zod is a peer dependency (>= 3.0.0). Install it in your project if you haven't already.

Interaction Callbacks

Forms and cards can trigger user interactions (submit, cancel, button click). Instead of listening for global CustomEvents, pass a callback through ChatProvider:

function handleVisualizationAction(event: VisualizationActionEvent) {
  switch (event.type) {
    case "form_submit":
      console.log("Form submitted:", event.action, event.data);
      // Send the form data back to the assistant, save to DB, etc.
      break;
    case "form_cancel":
      console.log("Form cancelled:", event.action);
      break;
    case "card_action":
      console.log("Card action clicked:", event.action);
      break;
  }
}

<ChatProvider
  messages={messages}
  onSendMessage={sendMessage}
  onVisualizationAction={handleVisualizationAction}
>
  ...
</ChatProvider>

The VisualizationActionEvent type is a discriminated union:

type VisualizationActionEvent =
  | { type: "form_submit"; action: string; data: Record<string, unknown> }
  | { type: "form_cancel"; action: string }
  | { type: "card_action"; action: string };

Backward compatibility: If no onVisualizationAction callback is provided, components fall back to dispatching CustomEvents on window (visualization-form-submit, visualization-form-cancel, visualization-action).

Using VisualizationRenderer Standalone

You can render visualizations outside of Message by using VisualizationRenderer directly:

import { VisualizationRenderer } from "@miiflow/assistant-ui/styled";

<VisualizationRenderer
  data={{
    id: "viz-1",
    type: "chart",
    title: "Monthly Revenue",
    data: {
      chartType: "bar",
      series: [{ name: "Revenue", data: [{ x: "Jan", y: 100 }, { x: "Feb", y: 150 }] }],
    },
  }}
  onAction={(event) => console.log(event)}
/>

Package Exports

Import Description
@miiflow/assistant-ui Core types, context, hooks, primitives
@miiflow/assistant-ui/styled TailwindCSS-styled components, visualization registry, schemas
@miiflow/assistant-ui/client useMiiflowChat hook, session utilities, types
@miiflow/assistant-ui/primitives Headless unstyled component primitives
@miiflow/assistant-ui/styles.css Full CSS (includes Tailwind preflight)
@miiflow/assistant-ui/styles-no-preflight.css CSS without preflight (for embedding in existing pages)

Key Exports from @miiflow/assistant-ui/styled

Visualization Registry: registerVisualization, getVisualization, getRegisteredTypes, VisualizationEntry

Visualization Schemas: chartVisualizationSchema, tableVisualizationSchema, cardVisualizationSchema, kpiVisualizationSchema, codePreviewVisualizationSchema, formVisualizationSchema

Types: VisualizationActionEvent, VisualizationChunkData, VisualizationConfig, VisualizationType

About

React chat UI components for AI assistants

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors