-
Notifications
You must be signed in to change notification settings - Fork 84
Description
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"
}
}
]
}