Skip to content

Proposal: Support for Custom Components in Chatkit #53

@MatthieuBonnardot

Description

@MatthieuBonnardot

Hi team,

First off, I really appreciate the amazing work you’re doing with Chatkit — it’s been great to build on top of it.

I currently have a branch ready across chatkit, chatkit-python, and chatkit-react that enables support for custom components. The core idea is to introduce a new widget called CustomWidget, which accepts the necessary props and JSON configuration. On the frontend, these would then be hydrated with user-defined custom components.

In theory, everything looks good with my current changes. I’ve also implemented a hook to list available custom components and automatically hydrate them in chat. However, since the Chatkit CDN code is minified, I don’t have a straightforward way to fully test this end-to-end.

Would it still be useful for me to open these PRs so the team can review the approach and direction? Or is this not aligned with OpenAI’s current vision for widgets?

I genuinely believe that enabling custom components could significantly improve flexibility, enhance customization, and drive greater adoption of Chatkit.

Thanks for your time and the great work you’re doing!

==============================

Proposed Implementation

I have a working implementation across all three repos that I'm happy to contribute.

1. Core Widget Type (@openai/chatkit)

// packages/chatkit/types/widgets.d.ts

export type WidgetComponent =
  | TextComponent
  | Title
  | Caption
  // ... existing types
  | CustomComponent;  // New type

export type CustomComponent = {
  type: 'Custom';
  key?: string;
  id?: string;
  componentType: string;  // Identifier for the React component
  props?: Record<string, unknown>;  // Props to pass to the component
} & BlockProps;

2. Widget Options Extension

// packages/chatkit/types/index.d.ts

export type WidgetsOption = {
  onAction?: (...) => Promise<void>;
  
  // New callback for custom component rendering
  onRenderCustom?: (
    component: Widgets.CustomComponent,
    container: HTMLElement,
  ) => void | Promise<void>;
};

3. React Hook (@openai/chatkit-react)

// packages/chatkit-react/src/useCustomWidgets.tsx

export type CustomComponentRegistry = Record<string, React.ComponentType<any>>;

export function useCustomWidgets(components: CustomComponentRegistry) {
  const rootsRef = React.useRef<Map<HTMLElement, ReactDOM.Root>>(new Map());

  const onRenderCustom = React.useCallback(
    (component: Widgets.CustomComponent, container: HTMLElement) => {
      const Component = components[component.componentType];

      if (!Component) {
        console.warn(
          `Custom component "${component.componentType}" not found in registry.`
        );
        return;
      }

      // Clean up existing root if it exists
      const existingRoot = rootsRef.current.get(container);
      if (existingRoot) {
        existingRoot.unmount();
      }

      // Create new root and render
      const root = ReactDOM.createRoot(container);
      rootsRef.current.set(container, root);

      root.render(
        <React.StrictMode>
          <Component {...(component.props || {})} />
        </React.StrictMode>
      );
    },
    [components]
  );

  // Cleanup on unmount
  React.useEffect(() => {
    const roots = rootsRef.current;
    return () => {
      for (const root of roots.values()) {
        root.unmount();
      }
      roots.clear();
    };
  }, []);

  return { widgets: { onRenderCustom } };
}

Usage Example

Frontend

import { useChatKit, useCustomWidgets, ChatKit } from '@openai/chatkit-react';

// Define custom component
function ProductCard({ name, price, imageUrl }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={imageUrl} alt={name} />
      <h3>{name}</h3>
      <p>${price.toFixed(2)}</p>
      <button>Add to Cart</button>
    </div>
  );
}

function App() {
  // Register custom components
  const customWidgets = useCustomWidgets({
    'product-card': ProductCard,
    'user-profile': UserProfile,
    'chart': ChartWidget,
  });

  const chatKit = useChatKit({
    api: { url: '/api/chatkit', domainKey: 'key' },
    widgets: customWidgets.widgets,
  });

  return <ChatKit control={chatKit.control} />;
}

Backend (Python SDK)

from chatkit import Card
from chatkit.widgets import CustomComponent

widget = Card(
    children=[
        CustomComponent(
            component_type="product-card",
            props={
                "name": "Wireless Headphones",
                "price": 149.99,
                "imageUrl": "https://example.com/image.jpg"
            }
        )
    ]
)

Backend (JSON)

{
  "type": "Card",
  "children": [
    {
      "type": "Custom",
      "componentType": "product-card",
      "props": {
        "name": "Wireless Headphones",
        "price": 149.99,
        "imageUrl": "https://example.com/image.jpg"
      }
    }
  ]
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions