CodeGym /Kursy /ChatGPT Apps /Praca w piaskownicy: ograniczenia, niuanse i window.opena...

Praca w piaskownicy: ograniczenia, niuanse i window.openai

ChatGPT Apps
Poziom 3 , Lekcja 0
Dostępny

1. Czym jest piaskownica i dlaczego twój widżet jest w klatce

Gdy ChatGPT wyświetla twój widżet, renderuje go nie jak zwykły <iframe src="https://twoja-strona">. Widżet uruchamiany jest w kontrolowanej „piaskownicy” — izolowanym iframe z osobnym origin i surowymi ustawieniami bezpieczeństwa.

Technicznie wygląda to mniej więcej tak:

flowchart TD
    User["Użytkownik w ChatGPT"]
    Chat["ChatGPT UI + model"]
    Iframe["Twój widżet
iframe w piaskownicy"] MCP["Twój MCP / backend"] User --> Chat Chat -->|wywołanie narzędzia| MCP MCP -->|structuredContent + _meta| Chat Chat -->|window.openai.*| Iframe Iframe -->|callTool / follow-up| Chat Chat --> MCP

Twój kod wykonywany jest wyłącznie wewnątrz tego iframe, a dostęp do reszty świata odbywa się przez wąsko kontrolowane API, które udostępnia host (ChatGPT). Widżet nie powinien:

  • psuć samego ChatGPT (DOM, style, wydajność);
  • naruszać prywatności użytkownika;
  • poruszać się po sieci bez kontroli.

Stąd wynikają kluczowe ograniczenia piaskownicy.

Izolacja DOM i origin

Widżet żyje na specjalnej domenie piaskownicy (na przykład https://sandbox-apps.oaiusercontent.com) z atrybutem sandbox na iframe. Oznacza to, że:

  • nie możesz odwoływać się do window.parent ani do document ChatGPT — dostaniesz SecurityError;
  • mechanizmy międzydomenowe, takie jak postMessage, są kontrolowane przez hosta;
  • jakiekolwiek próby „naprawiania interfejsu ChatGPT za pomocą CSS” są z góry skazane na porażkę.

Sieć i ograniczenia CSP

Przeglądarka i polityka CSP od hosta ograniczają dostęp do sieci dla twojego widżetu:

  • metoda fetch ma dostęp tylko do domen z whitelisty, które muszą przejść review;
  • jakie domeny można dotykać z widżetu, deklarujesz jawnie przez openai/widgetCSP w odpowiedziach MCP; w przeciwnym razie żądania po prostu nie przejdą;
  • zalecana ścieżka dla wszystkiego „poważnego” — w ogóle nie wywoływać sieci z widżetu, tylko korzystać z backendu przez narzędzia MCP i callTool (więcej o tym w module 4).

Praktycznie: myśl o widżecie jak o cienkiej warstwie UI. Rozmawia z ChatGPT i twoim serwerem ściśle określonymi kanałami, a nie jak zwykłe SPA swobodnie żyjące w internecie.

Magazyny i zasoby

Lokalne magazyny (localStorage, sessionStorage) są dostępne, ale cookie — już nie. Weź to pod uwagę przy tworzeniu aplikacji. Pamięć i CPU są ograniczone: jeśli wpadniesz na pomysł, by wewnątrz widżetu przeliczyć wszystkie liczby pierwsze do miliarda, host ma pełne prawo po prostu zabić twój iframe.

Wniosek: żadnych ciężkich obliczeń i długo żyjących „cache’y” w widżecie. Złożona logika — po stronie serwera, a nie w komponencie React.

2. window.openai: most między widżetem a ChatGPT

Aby widżet w ogóle mógł się czegoś dowiedzieć (wyniki narzędzia, tryb wyświetlania, lokalizacja, stan), ChatGPT przy inicjalizacji wstrzykuje do okna iframe jeden globalny obiekt — window.openai.

To nie jest twój kod ani paczka npm, tylko host object udostępniany przez samą platformę AI. Pod spodem opiera się na zdarzeniach i komunikatach między hostem a iframe, ale o tym prawie nie musisz myśleć. Ważne, by pamiętać kilka rzeczy.

Kto i kiedy tworzy window.openai

window.openai pojawia się tylko:

  • wewnątrz tego iframe, który ChatGPT stworzył dla twojego widżetu;
  • gdy szablon HTML zostanie zwrócony z poprawnym mimeType (text/html+skybridge) i przejdzie wszystkie kontrole.

Ten typ widziałeś już w module o HelloWorld App — to właśnie on jest zwracany przez stronę widżetu zamiast zwykłego text/html.

Jeśli po prostu otworzysz stronę widżetu bezpośrednio w przeglądarce, to:

console.log(window.openai); // undefined

i to normalne. Dlatego w kodzie widżetu zawsze warto sprawdzać, czy obiekt istnieje, jeśli zakładasz „standalone” tryb do lokalnego developmentu lub storybooka.

Prosty przykład (nie finalny, jedynie ilustracja):

if (typeof window !== "undefined" && (window as any).openai) {
  console.log("We are inside ChatGPT sandbox!");
}

Asynchroniczność inicjalizacji

Pod spodem ChatGPT aktualizuje window.openai w miarę napływu nowych danych (nowy toolOutput, zmiana displayMode itd.), używając wewnętrznego zdarzenia openai:set_globals.

Czyli „wartości” w nim nie są statyczne: model AI może wywołać narzędzie MCP, backend zwróci nowe structuredContent, a window.openai.toolOutput zmieni się wprost pod twoim komponentem React.

Stąd dwie rekomendacje:

  1. Nie rób „ślepych” snapshotów w stylu const toolOutput = window.openai.toolOutput tylko raz na początku i nie zakładaj, że będą wieczne. Ten sam widżet może być ponownie użyty przez ChatGPT.
  2. Używaj warstwy hooków (za chwilę), która potrafi subskrybować zmiany.

3. Anatomia window.openai: dane, API i kontekst

Oficjalna dokumentacja daje dość zwięzłą tabelę pól i metod window.openai. Zbierzmy to w bardziej „ludzkiej” formie.

Główne pola i metody

window.openai = {
  // State & data
  toolInput,               // JSON: parametry, które SI przekazała do twojego narzędzia MCP
  toolOutput,              // JSON: parametry, które twoje narzędzie MCP zwróciło SI
  toolResponseMetadata,    // Odpowiedź narzędzia MCP: część _meta: {...}
  widgetState,             // Można odczytać zapisany stan widżetu
  setWidgetState,          // Można zapisać stan twojego widżetu tutaj

  // Runtime APIs
  callTool,                // Można wywołać narzędzie MCP
  sendFollowUpMessage,     // Dyskretnie wysłać wiadomość do SI na czacie: zacznie odpowiadać.
  requestDisplayMode,      // Przełączyć widżet w inny tryb: fullscreen, pip, inline
  requestModal,            // Zmienić widżet w okno modalne.
  requestClose,            // Zamknąć widżet. Zamknięcie modala — powrót do widżetu.
  requestCheckout,         // Otwiera okno modalne płatności. Serwer musi zaimplementować ACP
  notifyIntrinsicHeight,   // Powiadomienie o zmianie wysokości widżetu
  openExternal,            // Otworzyć link w nowym oknie.

  // Context
  theme,                   // Ciemny lub jasny motyw
  displayMode,             // Bieżący tryb wyświetlania widżetu; może różnić się od requestDisplayMode
  maxHeight,               // Maksymalnie dopuszczalna wysokość widżetu
  safeArea,                // "Bezpieczny obszar renderowania" — istotne dla telefonów z "notchem"
  view,
  userAgent,               // userAgent przeglądarki
  locale                   // locale przeglądarki
}

To samo w tabeli:

Kategoria Właściwość / metoda Do czego służy
State & data
toolInput
Argumenty, z którymi wywołano narzędzie. Tylko do odczytu.
State & data
toolOutput
Twój structuredContent z odpowiedzi MCP. To widzi widżet i model.
State & data
toolResponseMetadata
_meta z odpowiedzi. Widoczne tylko dla widżetu, model tego nie czyta.
State & data
widgetState
Migawka stanu UI, którą ChatGPT przechowuje między renderami widżetu.
State & data
setWidgetState(state)
Zapisać nową migawkę widgetState synchronicznie.
Function
callTool(name, args)
Wywołać narzędzie MCP z widżetu.
Function
sendFollowUpMessage({prompt})
Poprosić ChatGPT o wysłanie wiadomości na czat w imieniu widżetu. Model zacznie odpowiadać.
Function
requestDisplayMode(...)
Poprosić hosta o inline / fullscreen / pip.
Function
requestModal({title})
Poprosić o otwarcie okna modalnego.
Function
notifyIntrinsicHeight()
Zgłosić, że wysokość treści się zmieniła.
Function
requestCheckout(...)
Otwiera dialog płatności zgodny z protokołem ACP.
Function
openExternal({href})
Otworzyć zewnętrzny link w przeglądarce użytkownika.
Context
theme, displayMode, maxHeight, safeArea, view, userAgent, locale
Sygnały środowiska: motyw, tryb, dostępna wysokość, lokalizacja itd.

Nie trzeba od razu zapamiętywać wszystkiego z tej tabeli — traktuj ją jak „mapę terenu”. Teraz rozbierzemy to nie jak „ściągę”, tylko po ludzku.

toolInput i toolOutput: skąd biorą się dane

Gdy model decyduje się wywołać twoje narzędzie, formuje argumenty JSON. Te argumenty:

  • trafiają na serwer MCP jako input do handlera;
  • jednocześnie lądują w window.openai.toolInput w widżecie.

Po wykonaniu narzędzia serwer zwraca:

  • structuredContent — dane strukturalne dla UI;
  • _meta — dane prywatne tylko dla widżetu;
  • content — tekst dla samego modelu, by mógł „opowiedzieć” użytkownikowi, co zaszło.

structuredContent staje się window.openai.toolOutput, a _meta staje się window.openai.toolResponseMetadata.

Mini-przykład (waniliowy JS, bez React):

const root = document.getElementById("root");

// Można bezpiecznie użyć operatora nullish
const gifts = window.openai.toolOutput?.gifts ?? [];

root.textContent = `Znaleziono prezentów: ${gifts.length}`;

widgetState i setWidgetState: pamięć widżetu

widgetState — to, co platforma jest gotowa zapamiętać o twoim UI między renderami, a nawet między kolejnymi krokami dialogu.

Przykłady naturalnych rzeczy do widgetState:

  • wybrany prezent;
  • bieżące sortowanie (po cenie / popularności);
  • numer strony na liście.

Nienaturalnych:

  • surowa odpowiedź z zewnętrznego API;
  • obraz w base64;
  • tajne tokeny.

Warto pamiętać o dwóch rzeczach:

  1. widgetState jest przechowywany i przekazywany modelowi wraz z kontekstem, więc nie wkładamy tam nic wrażliwego.
  2. Objętość jest ograniczona (około 4 tysięcy tokenów), więc nie rób z tego mini-bazy danych.

Najprostszy przykład użycia (wprost, bez hooków, w waniliowym JS):

const current = window.openai.widgetState ?? { selectedGiftId: null };

function selectGift(id) {
  window.openai.setWidgetState({ ...current, selectedGiftId: id });
}

W realnym kodzie opakujemy to w hooki React.

Runtime API: callTool, sendFollowUpMessage i podobne

Te metody pozwalają widżetowi nie tylko „rysować się”, ale też wchodzić w interakcję z dialogiem i serwerem.

Kilka typowych scenariuszy:

  • callTool("search_gifts", { budget: 50 }) — użytkownik kliknął przycisk „Zmień budżet”, wywołujesz serwer i aktualizujesz UI;
  • sendFollowUpMessage({ prompt: "Pokaż jeszcze droższe pomysły" }) — zamiast prosić użytkownika o ręczne wpisanie tekstu, dodajesz przycisk follow-up, który tworzy nową wiadomość na czacie;
  • requestDisplayMode({ mode: "fullscreen" }) — jeśli tryb inline robi się ciasny, widżet może grzecznie poprosić ChatGPT o pełny ekran;
  • openExternal({ href: "https://myshop.com/checkout?giftId=123" }) — skierowanie użytkownika na zewnętrzną stronę (checkout, profil itd.) przez zaufany kanał.

Wszystko to idzie „po kablu” przez ChatGPT, a nie bezpośrednio do internetu.

Kontekst środowiska: motyw, tryb, wysokość, lokalizacja

Pola takie jak theme, displayMode, maxHeight, locale dają obraz środowiska, w którym żyje widżet.

Na przykład:

const theme = window.openai.theme;          // "light" lub "dark"
const mode = window.openai.displayMode;     // "inline" | "fullscreen" | "pip"
const maxH = window.openai.maxHeight;       // dostępna wysokość
const locale = window.openai.locale;        // "en-US", "de-DE", ...

Za pomocą tych sygnałów możesz:

  • dostosowywać kolory i odstępy do motywu;
  • zmieniać layout w zależności od trybu (inline vs fullscreen);
  • lokalizować podpisy w UI pod język użytkownika (o tym będzie osobny moduł).

Platforma daje sygnały o dostępnej przestrzeni, motywie i lokalizacji. Rozsądnie używać ich przez useOpenAIGlobal, useDisplayMode, useMaxHeight i inne hooki, aby widżet wyglądał „rodzinnie” w ChatGPT.

4. Hooki nad window.openai: nie dotykajmy globalnego obiektu bezpośrednio

Czysty dostęp do window.openai jest wygodny na prototypie, ale szybko zamienia kod w chaos: subskrypcje zdarzeń, sprawdzanie undefined, powtarzalne obudowy. Dlatego w szablonie Next.js dla Apps SDK jest gotowy zestaw hooków React, które chowają szczegóły i czynią wszystko reaktywnym.

Typowy index hooków wygląda tak:

// app/hooks/openai/index.ts
export { useCallTool } from "./use-call-tool";
export { useSendMessage } from "./use-send-message";
export { useOpenExternal } from "./use-open-external";
export { useRequestDisplayMode, useRequestModal, useRequestClose } from "./use-request-display-mode";
export { useRequestCheckout } from "./use-request-checkout";

// State hooks
export { useDisplayMode } from "./use-display-mode";
export { useWidgetProps } from "./use-widget-props";
export { useWidgetState } from "./use-widget-state";
export { useOpenAIGlobal } from "./use-openai-global";

export { useMaxHeight } from "./use-max-height";
export { useIsChatGptApp } from "./use-is-chatgpt-app";

Nazwy i dokładna ścieżka mogą się nieco różnić w twoim szablonie, ale idea jest ta sama: zamiast window.openai.* używasz hooków. Omówmy kluczowe.

useWidgetProps: wejście i wyjście narzędzia

useWidgetProps zwykle zwraca obiekt z danymi potrzebnymi widżetowi: toolInput, toolOutput, toolResponseMetadata i czasem dodatkowymi flagami jak isLoading.

Przykład:

import { useWidgetProps } from "../hooks/openai";

type Gift = { id: string; title: string; price: number };

export function GiftList() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  if (!gifts.length) {
    return <div>Na razie brak propozycji prezentów.</div>;
  }

  return (
    <ul>
      {gifts.map((g) => (
        <li key={g.id}>{g.title} — ${g.price}</li>
      ))}
    </ul>
  );
}

W komponencie nie ma żadnego window.openai — i bardzo dobrze.

useWidgetState: „reaktywna nakładka” na widgetState

useWidgetState pozwala pracować z widgetState jak ze zwykłym stanem React: dostajesz [state, setState], a hook pod spodem synchronizuje go z window.openai.widgetState i setWidgetState.

Przykład:

import { useWidgetState } from "../hooks/openai";

type UiState = { selectedGiftId: string | null };

export function SelectedGiftIndicator() {
  const [uiState, setUiState] = useWidgetState<UiState>(() => ({
    selectedGiftId: null,
  }));

  if (!uiState?.selectedGiftId) {
    return <div>Prezent nie został jeszcze wybrany.</div>;
  }

  return (
    <div>
      Wybrano prezent o id={uiState.selectedGiftId}
      <button onClick={() => setUiState({ selectedGiftId: null })}>
        Resetuj
      </button>
    </div>
  );
}

Po kliknięciu setUiState nie tylko zaktualizuje stan React, ale też zapisze nowy stan po stronie ChatGPT.

useOpenAIGlobal: dostęp do dowolnego pola window.openai

Jeśli potrzebujesz dostępu do jednego globalnego pola (np. motywu lub trybu), jest uniwersalny hook useOpenAIGlobal(key). Subskrybuje zdarzenie openai:set_globals i zwraca zawsze aktualną wartość.

Przykład:

import { useOpenAIGlobal } from "../hooks/openai";

export function ThemeAwareBlock() {
  const theme = useOpenAIGlobal<"light" | "dark">("theme");

  const background = theme === "dark" ? "#222" : "#fff";
  const color = theme === "dark" ? "#fff" : "#000";

  return <div style={{ background, color }}>Szanuję motyw ChatGPT</div>;
}

useCallTool, useSendMessage, useOpenExternal i inne

  • useCallTool(name) — zwraca funkcję, która wywołuje narzędzie MCP o podanej nazwie. To obudowa nad callTool.
  • useSendMessage() — opakowuje sendFollowUpMessage, aby widżet mógł inicjować wiadomości.
  • useOpenExternal() — wygodny helper wokół openExternal({ href }).
  • useRequestDisplayMode() i useRequestModal() — obudowy do żądań zmiany trybu / otwarcia modala.

Bazowy przykład mini‑widżetu GiftGenius, który używa prawie wszystkiego naraz:

import {
  useWidgetProps,
  useWidgetState,
  useCallTool,
  useSendMessage,
  useOpenExternal,
} from "../hooks/openai";

type Gift = { id: string; title: string; url: string; price: number };

export function GiftWidget() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  const [ui, setUi] = useWidgetState<{ selectedId: string | null }>(() => ({
    selectedId: null,
  }));

  const callSearch = useCallTool("search_gifts");
  const sendMessage = useSendMessage();
  const openExternal = useOpenExternal();

  if (!gifts.length) {
    return <div>Na razie brak pomysłów. Spróbuj poprosić GPT o odświeżenie wyników.</div>;
  }

  return (
    <div>
      {gifts.map((g) => (
        <button
          key={g.id}
          style={{
            display: "block",
            fontWeight: ui?.selectedId === g.id ? "bold" : "normal",
          }}
          onClick={() => setUi({ selectedId: g.id })}
        >
          {g.title} — ${g.price}
        </button>
      ))}

      <div style={{ marginTop: 12 }}>
        <button
          onClick={() =>
            sendMessage({ prompt: "Pokaż prezenty droższe od obecnych." })
          }
        >
          Poproś o więcej pomysłów
        </button>

        <button
          onClick={async () => {
            await callSearch({ budget: 200 });
          }}
        >
          Odśwież z budżetem $200
        </button>

        {ui?.selectedId && (
          <button
            onClick={() =>
              openExternal({
                href: `https://giftgenius.example.com/checkout?id=${ui.selectedId}`,
              })
            }
          >
            Przejdź do zakupu
          </button>
        )}
      </div>
    </div>
  );
}

Ta strona jest jeszcze surowa (w kolejnych modułach dopracujemy UX, obsługę błędów itd.), ale już ilustruje podejście: żadnych bezpośrednich odwołań do window.openai, tylko hooki.

5. Praktyka: poznajemy piaskownicę i window.openai

Aby poczuć, czym jest „widżet nie jak zwykła strona”, warto zrobić kilka ćwiczeń.

Ćwiczenie: „Poznaj środowisko”

Weź swój bieżący app/page.tsx w widżecie i dodaj przy pierwszym renderze prosty efekt:

import { useEffect } from "react";
import { useIsChatGptApp } from "../hooks/openai";

export default function Root() {
  const isChatGpt = useIsChatGptApp();

  useEffect(() => {
    if (typeof window !== "undefined") {
      console.log("window.origin =", window.origin);
      console.log("window.openai =", (window as any).openai);
    }
  }, []);

  return (
    <main>
      <h1>GiftGenius widget</h1>
      <p>Uruchomione wewnątrz ChatGPT: {String(isChatGpt)}</p>
    </main>
  );
}

Otwórz DevTools: albo bezpośrednio w oknie ChatGPT (przez wbudowany viewer tunelu, jeśli to możliwe), albo w lokalnej przeglądarce przy bezpośrednim otwarciu strony. W obu wariantach porównaj:

  • przy uruchomieniu w zwykłej przeglądarce isChatGptApp będzie false, a window.openai najpewniej undefined;
  • przy uruchomieniu przez ChatGPT zobaczysz obiekt z polami toolInput, toolOutput, theme itd.

To dobre intuicyjne odczucie: ten sam kod React zachowuje się różnie w zależności od środowiska, i właśnie do tego wymyślono hooki.

Ćwiczenie: „Wyświetl wszystko, co daje platforma”

Dodaj tymczasowy komponent do debugowania:

import { useWidgetProps, useOpenAIGlobal } from "../hooks/openai";

export function DebugPanel() {
  const { toolInput, toolOutput, toolResponseMetadata } = useWidgetProps();
  const theme = useOpenAIGlobal("theme");
  const displayMode = useOpenAIGlobal("displayMode");

  return (
    <pre style={{ fontSize: 10, maxHeight: 200, overflow: "auto" }}>
      {JSON.stringify(
        { toolInput, toolOutput, toolResponseMetadata, theme, displayMode },
        null,
        2
      )}
    </pre>
  );
}

I tymczasowo wstaw <DebugPanel /> pod głównym UI. Dzięki temu zobaczysz:

  • jakie dokładnie pola przychodzą z MCP do toolOutput;
  • co znajduje się w _meta (np. locale, userLocation i inne);
  • jak zmienia się displayMode, gdy rozwijasz widżet.

Później ten komponent możesz usunąć albo zostawić włączany przez jakiś flag, np. DEBUG_WIDGET.

6. Relacje: ChatGPT ↔ widżet ↔ MCP/serwer

Aby nie traktować widżetu jak „głównego” uczestnika systemu, warto jeszcze raz utrwalić role.

  1. Użytkownik pisze wiadomość: „Dobierz prezent dla dziewczyny, budżet 50$”.
  2. Model ChatGPT decyduje się wywołać twoje narzędzie MCP search_gifts z argumentami { recipient: "girlfriend", budget: 50 }.
  3. Serwer MCP wykonuje logikę biznesową, zwraca:
    • content z krótkim opisem dla modelu;
    • structuredContent z tablicą prezentów;
    • _meta ze szczegółami technicznymi (np. source i waluta).
  4. ChatGPT:
    • pokazuje użytkownikowi wiadomość tekstową („Znalazłem kilka opcji...”);
    • tworzy widżet‑iframe i przekazuje do niego structuredContent i _meta przez window.openai.toolOutput oraz toolResponseMetadata.
  5. Twój widżet:
    • renderuje UI na podstawie toolOutput;
    • przy interakcjach wywołuje callTool lub wysyła follow‑up;
  6. Model dalej decyduje, co zrobić z wynikami tych działań.

To prowadzi do ważnej myśli: widżet nigdy nie jest jedynym gospodarzem procesu. To warstwa UI, która żyje w ekosystemie modelu i serwera MCP. Rzeczy złożone (autoryzacja, dostęp do danych prywatnych, poważna logika biznesowa) powinny pozostać po stronie serwera. Widżet odpowiada za wygodny interfejs i uważną komunikację z użytkownikiem.

7. Zasady i reguły gry w piaskownicy

Cała ta konstrukcja z izolowanym iframe i window.openai istnieje nie bez powodu, lecz z uwagi na wymagania bezpieczeństwa i prywatności. Oficjalne wytyczne OpenAI podkreślają kilka zasad.

Po pierwsze — minimalizacja danych. Nie powinieneś przez widżet próbować wyciągnąć od użytkownika jak najwięcej PII (personally identifiable information) i wysyłać ich do siebie. Wszystko, co naprawdę konieczne, musi być jasno opisane w narzędziach, a model i warstwa bezpieczeństwa będą uważnie przyglądać się takim wywołaniom.

Po drugie — zakaz ukrytego śledzenia i fingerprintingu. Nie wolno budować systemu „podglądania” urządzenia użytkownika, zbierać odcisków przeglądarki ani sposobów obchodzenia ograniczeń. Parametry takie jak userAgent, userLocation itd. — to wskazówki dla UX, a nie do autoryzacji czy identyfikacji.

Po trzecie — wszystko, co umieszczasz w structuredContent, _meta, widgetState, w pewnym sensie albo widzi użytkownik, albo może zobaczyć recenzent Store. Dlatego:

  • nie wolno tam umieszczać żadnych kluczy API, tokenów, haseł ani sekretów admina;
  • stan widżetu projektuj tak, by użytkownik nie był zaskoczony, widząc go w logach czy debugowaniu.

Po czwarte — wywołania sieciowe. Bezpośrednie żądania z widżetu do zewnętrznych API są dopuszczalne tylko do ściśle ograniczonej listy domen i w scenariuszach niewrażliwych. Gdy w grę wchodzą pieniądze, konta, dane prywatne — wszystko powinno iść przez MCP/backend.

8. Typowe błędy przy pracy w piaskownicy i z window.openai

Błąd nr 1: uważać, że widżet to „zwykła strona w iframe”.
Nowicjusze z przyzwyczajenia próbują odwoływać się do window.parent, zmieniać style ChatGPT albo używać localStorage „jak zawsze”. W piaskownicy to albo nie działa, albo jest niestabilne: inny origin, izolowane storage, zablokowany dostęp do DOM. Trzeba przyjąć, że żyjesz w środowisku zarządzanym i komunikujesz się z hostem tylko przez window.openai i hooki.

Błąd nr 2: dotykać window.openai bezpośrednio wszędzie.
Kod w stylu window.openai.toolOutput w dziesięciu komponentach to droga do trudnego debugowania. Przy tym samemu trzeba pilnować zdarzeń, asynchroniczności i sprawdzania undefined. Znacznie bezpieczniej od razu używać useWidgetProps, useWidgetState, useOpenAIGlobal i innych hooków, które już opakowują openai:set_globals i synchronizują stan.

Błąd nr 3: trzymać w widgetState wszystko (zwłaszcza sekrety).
Czasem kusi, by „na wszelki wypadek” wrzucić tam ogromny obiekt z wynikami API albo nawet token dostępu. W rezultacie rośnie kontekst, pogarsza się praca modelu, a ty łamiesz podstawowe zasady bezpieczeństwa. widgetState powinien być mały, zawierać tylko sygnały UI i nigdy — danych poufnych.

Błąd nr 4: próbować łączyć się z internetem bezpośrednio z widżetu.
Wywołania fetch("https://api.superbank.com/...") z piaskownicy niemal na pewno trafią na CORS, a nawet jeśli wszystko ustawisz idealnie, będzie to niebezpieczne i trudne do kontroli. Wszystko związane z realnymi kontami, pieniędzmi i danymi osobowymi należy implementować jako narzędzia MCP i wywoływać przez callTool lub warstwę serwerową.

Błąd nr 5: polegać na stabilności window.openai poza ChatGPT.
Czasem deweloperzy próbują uruchamiać widżet jako oddzielne SPA i nie dodają sprawdzeń, że window.openai może być undefined. W środowisku dev kończy się to crashem „Cannot read properties of undefined”. Używaj useIsChatGptApp, sprawdzenia typeof window !== "undefined" oraz fallback‑UI na przypadki, gdy widżetu jako takiego nie ma.

Błąd nr 6: ignorować kontekst środowiska (theme, displayMode, maxHeight, locale).
Można oczywiście ustawić na sztywno wysokość 2000px, zawsze ciemny motyw i projektować pod desktop — ale to da użytkownikowi dość dziwne wrażenia. Platforma daje sygnały o przestrzeni, motywie i lokalizacji — rozsądnie z nich korzystać przez useOpenAIGlobal, useDisplayMode, useMaxHeight itd., by widżet wyglądał „rodzinnie” w ChatGPT.

Błąd nr 7: próbować „obejść” politykę przez zewnętrzne skrypty.
Czasem kusi, by podpiąć jakiś tracker, zewnętrzny bundle JS albo wykonać kod z obcej domeny „po cichu”. Piaskownica i polityki CSP są właśnie po to, by temu zapobiec: skrypty zewnętrzne są blokowane, a próby obejścia systemu — prostą drogą do odrzucenia twojej aplikacji w Store.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION