Created
December 7, 2025 14:09
-
-
Save jordan-cutler/36a96e28aa75e3846f71ba088e081e53 to your computer and use it in GitHub Desktop.
Build Your Own ChatGPT App - Colin Matthews + HGE Code Snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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