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:
- 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.
- 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 | |
Argumenty, z którymi wywołano narzędzie. Tylko do odczytu. |
| State & data | |
Twój structuredContent z odpowiedzi MCP. To widzi widżet i model. |
| State & data | |
_meta z odpowiedzi. Widoczne tylko dla widżetu, model tego nie czyta. |
| State & data | |
Migawka stanu UI, którą ChatGPT przechowuje między renderami widżetu. |
| State & data | |
Zapisać nową migawkę widgetState synchronicznie. |
| Function | |
Wywołać narzędzie MCP z widżetu. |
| Function | |
Poprosić ChatGPT o wysłanie wiadomości na czat w imieniu widżetu. Model zacznie odpowiadać. |
| Function | |
Poprosić hosta o inline / fullscreen / pip. |
| Function | |
Poprosić o otwarcie okna modalnego. |
| Function | |
Zgłosić, że wysokość treści się zmieniła. |
| Function | |
Otwiera dialog płatności zgodny z protokołem ACP. |
| Function | |
Otworzyć zewnętrzny link w przeglądarce użytkownika. |
| Context | |
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:
- widgetState jest przechowywany i przekazywany modelowi wraz z kontekstem, więc nie wkładamy tam nic wrażliwego.
- 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.
- Użytkownik pisze wiadomość: „Dobierz prezent dla dziewczyny, budżet 50$”.
- Model ChatGPT decyduje się wywołać twoje narzędzie MCP search_gifts z argumentami { recipient: "girlfriend", budget: 50 }.
- 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).
- 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.
- Twój widżet:
- renderuje UI na podstawie toolOutput;
- przy interakcjach wywołuje callTool lub wysyła follow‑up;
- 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.
GO TO FULL VERSION