The Frontend: The OpenAI Bridge

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

The frontend code lives in 05-building-a-chatgpt-app/note-taking-app/frontend in the materials repo. It’s a standard React app bundled with Vite, designed to run in ChatGPT’s iframe. Direct database access is impossible due to sandboxing, so all communication flows through the host via window.openai—the bridge injected by ChatGPT.

Prerequisites: Ensure the backend is configured (from the previous section). This project ships with a prebuilt dist/index.html, so you can preview via the backend’s /dashboard route right away. Only run npm run build if you change the React code and need to regenerate the bundle.

Think of window.openai as a secure portal: It provides data injection, tool calling, and state persistence, but with restrictions for security (e.g., no arbitrary network access without CSP).

The Bridge Object

When rendering the widget, ChatGPT injects window.openai into the iframe. You abstract it with React hooks in hooks/useOpenAi.js for easy subscription and re-renders.

export const useToolOutput = () => {
  return useOpenAiGlobal('toolOutput');
};
export const useOpenAiGlobal = (key) => {
  return useSyncExternalStore(
    (onChange) => {
      const handleSetGlobal = (event) => {
        const value = event.detail?.globals?.[key];
        if (value === undefined) return;
        onChange();
      };

      window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
        passive: true,
      });

      return () => {
        window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
      };
    },
    () => window.openai?.[key]
  );
};

Receiving Data with toolOutput

The backend’s structuredContent is injected as window.openai.toolOutput. In App.jsx, hydrate once on mount to avoid overwriting user changes:

const toolOutput = useToolOutput();
const hasHydratedFromToolOutput = useRef(false);

useEffect(() => {
  if (toolOutput?.notes && !hasHydratedFromToolOutput.current) {
    setNotes(toolOutput.notes);
    setError(null);
    hasHydratedFromToolOutput.current = true;
    return;
  }
  if (isInChatGPT && !hasHydratedFromToolOutput.current) {
    refreshNotes();
  }
}, [toolOutput, isInChatGPT]);

Calling Tools from the UI

UI events invoke MCP tools via the host. Abstract in lib/sdk.js:

export const AppSDK = {
  callTool: async (toolName, args = {}) => {
    if (window.openai?.callTool) {
      try {
        const result = await window.openai.callTool(toolName, args);
        return result?.structuredContent ?? result ?? null;
      } catch (e) {
        throw new Error(`Tool call failed: ${e.message}`);
      }
    }

    throw new Error('ChatGPT host is not available.');
  },
};
const handleSave = async (note) => {
  const result = await AppSDK.callTool('create_note', {
    title: note.title,
    content: note.content
  });

  if (result?.notes && Array.isArray(result.notes)) {
    setNotes(result.notes);
  } else {
    await refreshNotes();
  }
  setWidgetState(prev => ({ ...prev, view: 'list' }));
};

Managing UI State with widgetState

Persist ephemeral UI state (e.g., current view) via window.openai.widgetState and setWidgetState:

const [widgetState, setWidgetState] = useWidgetState(() => ({
  view: 'list'
}));
const handleCreate = () => {
  setActiveNote({ title: '', content: '' });
  setWidgetState(prev => ({ ...prev, view: 'create' }));
};

const handleSelect = (note) => {
  setActiveNote(note);
  setWidgetState(prev => ({ ...prev, view: 'detail' }));
};

Summary

  1. Backend sends structuredContent.
  2. Host injects as toolOutput; subscribe via hooks.
  3. Render from data; call tools on events.
  4. Persist UI with widgetState.
See forum comments
Download course materials from Github
Previous: The Backend: Serving Resources Next: Server & Tunnel Setup