Skip to content

gadget-inc/vite-plugin-chatgpt-widgets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚡️ vite-plugin-chatgpt-widgets

A vite plugin for automatically bundling ChatGPT widget outputs within a vite project.

Installation

npm install vite-plugin-chatgpt-widgets
# or
pnpm add vite-plugin-chatgpt-widgets

Usage

1. Configure Vite

Add the plugin to your vite.config.ts and enable the build manifest:

import { defineConfig } from "vite";
import { chatGPTWidgetPlugin } from "vite-plugin-chatgpt-widgets";

export default defineConfig({
  plugins: [
    chatGPTWidgetPlugin({
      widgetsDir: "web/chatgpt", // default: 'web/chatgpt'
      baseUrl: "https://example.com", // if not using a vite `base`, this is required because the chatgpt iframe is sandboxed and absolute URL links are required
    }),
  ],
  build: {
    manifest: true, // Required for production mode
  },
  server: {
    cors: {
      // allow cross origin requests for development assets so the ChatGPT sandbox can access dev-time assets
      origin: true,
    },
  },
});

2. Create Widget Components

Create React components in your widgets directory:

// in web/chatgpt/Hello.tsx
export default function Hello() {
  return <div>Hello from ChatGPT Widget!</div>;
}

// in web/chatgpt/ListFoobars.tsx
export default function ListFoobars() {
  return (
    <ul>
      {window.openai.tool_output.foobars.map((foobar) => (
        <li>{foobar}</li>
      ))}
    </ul>
  );
}

Optional: Root Layout Component

You can optionally create a root layout component that will wrap all widgets. If a file named root.tsx (or root.ts, root.jsx, root.js) exists in the widgets directory, it will automatically wrap all other widget components:

// web/chatgpt/root.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <SomeProvider>
      <header>Common Header</header>
      {children}
    </SomeProvider>
  );
}

The root layout component:

  • Must accept a children prop
  • Will automatically wrap every widget component
  • Is optional - if not present, widgets render without a wrapper

3. Serve Widgets in Your Application

After setting up the plugin and writing some widgets, you need to expose the widget HTML snippets generated by this plugin as MCP server resources. In development, the HTML snippets will be generated by Vite dynamically, and in production, they'll be built by your Vite build process and read off the disk.

import { getWidgets } from "vite-plugin-chatgpt-widgets";

// In development, pass the Vite dev server instance from wherever you can get it
const widgets = await getWidgets("web/chatgpt", viteDevServer);

// In production, pass a path to the vite manifest, where we'll load precompiled versions from
const widgets = await getWidgets("web/chatgpt", {
  manifestPath: "dist/.vite/manifest.json",
});

// Register each widget on an MCP server as a resource for exposure to ChatGPT
for (const widget of widgets) {
  const resourceName = `widget-${widget.name.toLowerCase()}`;
  const resourceUri = `ui://widget/${widget.name}.html`;

  // assuming you are using @modelcontextprotocol/sdk, will be similar for other MCP implementations
  mcpServer.registerResource(
    resourceName,
    resourceUri,
    {
      title: widget.name,
      description: `ChatGPT widget for ${widget.name}`,
    },
    async () => {
      return {
        contents: [
          {
            uri: resourceUri,
            mimeType: "text/html+skybridge",
            text: widget.content,
          },
        ],
      };
    }
  );
}

Sandboxed iframes require fully qualified URLs

When serving widgets in sandboxed iframes like ChatGPT's UI, asset links must be fully qualified URLs with protocol and domain. The user's browser loads up your widget on an OpenAI controlled domain, so asset loads must refer directly back to your hosting provider. The plugin enforces this requirement and will throw an error if an absolute base URL is not configured.

You must configure an absolute base URL in one of these ways:

  1. Vite's base config: If you've already configured Vite with base: "https://example.com/", the plugin will use it automatically.
// Option 1: In Vite config (affects both dev and build)
export default defineConfig({
  plugins: [chatGPTWidgetPlugin({})],
  base: "https://example.com",
});
  1. baseUrl option to this plugin:: If Vite's base is not set, or must be relative,, provide the baseUrl option to this plugin directly:
// Option 1: In Vite config (affects both dev and build)
export default defineConfig({
  plugins: [
    chatGPTWidgetPlugin({
      baseUrl: "https://example.com",
    }),
  ],
  base: "/",
});

How It Works

The plugin creates virtual modules for each widget component:

  1. Virtual HTML file: virtual:chatgpt-widget-{name}.html - A standalone HTML page
  2. Virtual JS entrypoint: virtual:chatgpt-widget-{name}.js - Imports and renders your React component

During build, these are added as entrypoints and bundled into separate HTML files with hashed asset names. The getWidgetHTML helper:

  • In dev mode: Uses Vite's plugin container to load and transform the HTML in real-time
  • In production: Reads the built HTML files using Vite's manifest.json to locate them

Support

Plain React SPAs: Well supported

React Router v6 or React Router v7 in Declarative mode: Well supported

React Router v7 in Data or Framework Mode: Hackily supported

API

chatGPTWidgetPlugin(options?)

The Vite plugin.

Options:

  • widgetsDir (string, optional): Directory containing widget components. Default: 'web/chatgpt'
  • baseUrl (string, optional): Base URL for widget assets. Required if Vite's base config is not an absolute URL and you need fully qualified URLs for sandboxed iframes. Should include protocol and domain (e.g., "https://example.com"). Note: Does not require trailing slash.

getWidgets(widgetsDir, viteHandle)

Get the HTML content for a widget.

Parameters:

  • widgetsDir (string): The path to the directory on disk with your widget components
  • viteHandle (DevelopmentViteBuild | ProductionViteBuild): A reference to a Vite context we can use for getting widget content.
    • In dev: Pass an object like { devServer: ViteDevServer } to give the Vite dev server to use to build HTML
    • In prod: Pass an object like { mainfest: "some/path/to/.vite/manifest.json" } to list all the entrypoints built by the vite build process