Skip to content

Instantly share code, notes, and snippets.

@jordan-cutler
Created December 7, 2025 14:09
Show Gist options
  • Select an option

  • Save jordan-cutler/36a96e28aa75e3846f71ba088e081e53 to your computer and use it in GitHub Desktop.

Select an option

Save jordan-cutler/36a96e28aa75e3846f71ba088e081e53 to your computer and use it in GitHub Desktop.
Build Your Own ChatGPT App - Colin Matthews + HGE Code Snippets
// App.tsx
import React, { useState, useEffect } from "react";
export default function RestaurantApp() {
// 1. Initialize with data if available (handling re-renders)
const [data, setData] = useState(window.openai?.toolOutput?.data || []);
const [selected, setSelected] = useState(null);
// 2. The Listener: Update state when the tool finishes running
useEffect(() => {
const handleUpdate = () => {
// "structuredContent" from the backend becomes "toolOutput" here
const output = window.openai?.toolOutput;
if (output?.data) setData(output.data);
};
// Listen for the specific OpenAI event
window.addEventListener("openai:set_globals", handleUpdate);
return () => window.removeEventListener("openai:set_globals", handleUpdate);
}, []);
if (data.length === 0) return <div>Loading results...</div>;
return (
<div className="p-4 grid gap-4">
{data.map(restaurant => (
<div
key={restaurant.id}
className="border rounded p-3 hover:bg-gray-50 cursor-pointer"
onClick={() => setSelected(restaurant)}
>
<h3 className="font-bold">{restaurant.name}</h3>
<p className="text-sm text-gray-600">Rating: {restaurant.rating} stars</p>
</div>
))}
{selected && (
<div className="mt-4 p-2 bg-blue-50 text-blue-800 rounded">
You selected: {selected.name}
</div>
)}
</div>
);
}
import React, { useState, useEffect } from 'react'
import { RestaurantListUI } from './RestaurantListUI'
export default function BookingWrapper() {
// 1. Initialize data from the conversation (Input Context)
// Inline apps often start with data passed from the LLM, like "partySize"
const [partySize, setPartySize] = useState(
window.openai?.toolInput?.partySize || 2
)
const [data, setData] = useState(window.openai?.toolOutput)
// 2. Listener: Update data if the LLM sends new information while the component is alive
useEffect(() => {
const handleUpdate = () => {
setData(window.openai?.toolOutput)
if (window.openai?.toolInput?.partySize) {
setPartySize(window.openai.toolInput.partySize)
}
}
window.addEventListener('openai:set_globals', handleUpdate)
return () => window.removeEventListener('openai:set_globals', handleUpdate)
}, [])
// 3. Output Action: Sending a message back to the chat
// This is crucial for inline apps to confirm actions without leaving the flow
const handleBookingConfirmation = async (
restaurantName: string,
time: string
) => {
await window.openai?.sendFollowUpMessage({
prompt: `I just booked a table at ${restaurantName} for ${partySize} guests at ${time}.`,
})
}
return (
<div className="w-full bg-white rounded-2xl border border-black/10 p-4">
{/* 4. Pass context down to the UI */}
<RestaurantListUI
data={data}
partySize={partySize}
onBook={handleBookingConfirmation}
/>
</div>
)
}
import React, { useState, useEffect } from "react";
import { Maximize2 } from "lucide-react";
import { SpreadsheetUI } from "./SpreadsheetUI";
export default function FullscreenWrapper() {
// 1. Initialize state from global, fallback to "inline"
const [mode, setMode] = useState(window.openai?.displayMode || "inline");
// 2. Listen for "openai:set_globals" to auto-update state
useEffect(() => {
const sync = () => setMode(window.openai?.displayMode || "inline");
window.addEventListener("openai:set_globals", sync);
return () => window.removeEventListener("openai:set_globals", sync);
}, []);
return (
<div className="h-full w-full flex flex-col">
{/* 3. Show button only in inline mode to request fullscreen */}
{mode === "inline" && (
<button onClick={() => window.openai?.requestDisplayMode?.({ mode: "fullscreen" })}
className="bg-blue-500 text-white px-3 py-1 flex items-center gap-1">
<Maximize2 className="h-4 w-4" /> Go Fullscreen
</button>
)}
<SpreadsheetUI />
</div>
);
}
import React, { useState, useEffect } from "react";
import { PictureInPicture2 } from "lucide-react";
import { MusicPlayerUI } from "./MusicPlayerUI";
export default function MusicPlayerWrapper() {
// 1. Initialize state (PiP is distinct from inline/fullscreen)
const [mode, setMode] = useState(window.openai?.displayMode || "inline");
// 2. Listener: Sync state when ChatGPT (or user) toggles PiP externally
useEffect(() => {
const sync = () => setMode(window.openai?.displayMode || "inline");
window.addEventListener("openai:set_globals", sync);
return () => window.removeEventListener("openai:set_globals", sync);
}, []);
return (
<div className="w-full h-full relative">
<MusicPlayerUI
// 3. Pass the mode down so the UI can adapt (e.g., hide lyrics in PiP)
isPiP={mode === "pip"}
// 4. The Trigger: Request 'pip' mode explicitly
piPButton={
mode === "inline" && (
<button onClick={() => window.openai?.requestDisplayMode?.({ mode: "pip" })}
className="absolute top-2 right-2 p-2 text-white hover:bg-white/10 rounded-full">
<PictureInPicture2 className="w-4 h-4" />
</button>
)
}
/>
</div>
);
}
// server.js
server.tool('search_restaurants', {
description: 'Search for restaurants based on location and cuisine',
parameters: {
location: 'string',
cuisine: 'string'
},
_meta: {
// This tells ChatGPT "When this tool finishes, load this UI."
// You must register this URI as a resource (see Step 1.5).
"openai/outputTemplate": "ui://widget/restaurant-list.html",
// 2. Status Updates: Keeps the user informed while your tool is loading.
"openai/toolInvocation/invoking": "Checking the Michelin guide...",
"openai/toolInvocation/invoked": "Here are the top matches."
}
}, async (params) => {
const restaurants = await fetchRestaurants(params);
return {
// 3. For the LLM: A text summary so the AI knows what happened.
content: [
{ type: "text", text: `Found ${restaurants.length} restaurants matching ${params.cuisine}.` }
],
// 4. For the UI: The raw JSON data your React component will read.
structuredContent: {
data: restaurants,
initialState: { selectedRestaurant: null }
}
};
});
// server.js (continued)
// The SDK serves this HTML file to the ChatGPT iframe
server.resource('restaurant-list', 'ui://widget/restaurant-list.html', {
mimeType: 'text/html+skybridge' // +skybridge enables the window.openai bridge
}, async () => {
return {
text: fs.readFileSync('./dist/restaurant-list.html', 'utf8')
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment