@apollo/client-ai-apps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/pr.yaml +24 -0
  4. package/.github/workflows/release.yaml +36 -0
  5. package/LICENSE +21 -0
  6. package/dist/apollo_client/client.d.ts +14 -0
  7. package/dist/apollo_client/provider.d.ts +5 -0
  8. package/dist/hooks/useOpenAiGlobal.d.ts +2 -0
  9. package/dist/hooks/useRequestDisplayMode.d.ts +4 -0
  10. package/dist/hooks/useSendFollowUpMessage.d.ts +1 -0
  11. package/dist/hooks/useToolEffect.d.ts +6 -0
  12. package/dist/hooks/useToolInput.d.ts +1 -0
  13. package/dist/hooks/useToolName.d.ts +1 -0
  14. package/dist/index.d.ts +11 -0
  15. package/dist/index.js +177 -0
  16. package/dist/types/application-manifest.d.ts +29 -0
  17. package/dist/types/openai.d.ts +73 -0
  18. package/dist/vite/index.d.ts +1 -0
  19. package/dist/vite/index.js +210 -0
  20. package/dist/vite/operation_manifest_plugin.d.ts +10 -0
  21. package/package.json +53 -0
  22. package/scripts/build-vite.mjs +18 -0
  23. package/scripts/build.mjs +7 -0
  24. package/scripts/dev.mjs +21 -0
  25. package/scripts/shared.mjs +9 -0
  26. package/src/apollo_client/client.test.ts +411 -0
  27. package/src/apollo_client/client.ts +90 -0
  28. package/src/apollo_client/provider.test.tsx +41 -0
  29. package/src/apollo_client/provider.tsx +32 -0
  30. package/src/hooks/useCallTool.test.ts +46 -0
  31. package/src/hooks/useCallTool.ts +8 -0
  32. package/src/hooks/useOpenAiGlobal.test.ts +54 -0
  33. package/src/hooks/useOpenAiGlobal.ts +26 -0
  34. package/src/hooks/useRequestDisplayMode.ts +7 -0
  35. package/src/hooks/useSendFollowUpMessage.ts +7 -0
  36. package/src/hooks/useToolEffect.tsx +41 -0
  37. package/src/hooks/useToolInput.ts +7 -0
  38. package/src/hooks/useToolName.ts +7 -0
  39. package/src/index.ts +12 -0
  40. package/src/types/application-manifest.ts +32 -0
  41. package/src/types/openai.ts +90 -0
  42. package/src/vite/index.ts +1 -0
  43. package/src/vite/operation_manifest_plugin.ts +274 -0
  44. package/vitest-setup.ts +1 -0
  45. package/vitest.config.ts +12 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@apollo/client-ai-apps",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": "./dist/index.js",
13
+ "./vite": "./dist/vite/index.js"
14
+ },
15
+ "scripts": {
16
+ "dev": "node ./scripts/dev.mjs",
17
+ "build": "npm run build:react && npm run build:vite",
18
+ "build:react": "node ./scripts/build.mjs && tsc src/index.ts --emitDeclarationOnly --declaration --outDir dist --skipLibCheck --lib ES2015,DOM --target ES2015 --moduleResolution bundler --jsx react",
19
+ "build:vite": "node ./scripts/build-vite.mjs && tsc src/vite/index.ts --emitDeclarationOnly --declaration --outDir dist/vite --skipLibCheck --lib ES2015,DOM --target ES2020 --module esnext --moduleResolution node --allowSyntheticDefaultImports",
20
+ "changeset:new": "npx @changesets/cli",
21
+ "changeset:version": "npx @changesets/cli version",
22
+ "test": "vitest run --coverage",
23
+ "test:watch": "vitest --coverage"
24
+ },
25
+ "keywords": [],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "description": "",
29
+ "devDependencies": {
30
+ "@testing-library/jest-dom": "^6.9.1",
31
+ "@testing-library/react": "^16.3.0",
32
+ "@types/node": "^24.10.0",
33
+ "@types/react": "^19.2.2",
34
+ "@vitejs/plugin-react": "^5.1.1",
35
+ "@vitest/coverage-v8": "^4.0.13",
36
+ "esbuild": "^0.25.12",
37
+ "graphql": "^16.9.0",
38
+ "happy-dom": "^20.0.10",
39
+ "rxjs": "^7.8.1",
40
+ "typescript": "^5.9.3",
41
+ "vitest": "^4.0.13"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^19.2.0",
45
+ "react-dom": "^19.2.0"
46
+ },
47
+ "dependencies": {
48
+ "@apollo/client": "^4.0.9",
49
+ "@graphql-tools/graphql-tag-pluck": "^8.3.23",
50
+ "crypto-hash": "^4.0.0",
51
+ "glob": "^11.0.3"
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import * as esbuild from "esbuild";
2
+
3
+ // Separately build Vite plugin
4
+ await esbuild.build({
5
+ entryPoints: ["src/vite/index.ts"],
6
+ bundle: true,
7
+ outdir: "dist/vite",
8
+ platform: "node",
9
+ format: "esm",
10
+ external: [
11
+ "glob",
12
+ "@graphql-tools/graphql-tag-pluck",
13
+ "@graphql-tools/graphql-tag-pluck",
14
+ "graphql",
15
+ "@apollo/client",
16
+ "rxjs",
17
+ ],
18
+ });
@@ -0,0 +1,7 @@
1
+ import * as esbuild from "esbuild";
2
+ import { sharedConfig } from "./shared.mjs";
3
+
4
+ // Build react components
5
+ await esbuild.build({
6
+ ...sharedConfig,
7
+ });
@@ -0,0 +1,21 @@
1
+ import * as esbuild from "esbuild";
2
+ import { sharedConfig } from "./shared.mjs";
3
+
4
+ console.log("Starting dev mode...");
5
+
6
+ let ctx = await esbuild.context({
7
+ ...sharedConfig,
8
+ plugins: [
9
+ {
10
+ name: "rebuild-logger",
11
+ setup(build) {
12
+ build.onEnd((result) => {
13
+ console.log(`Rebuilt at ${new Date().toLocaleTimeString()} (${result.errors.length} errors)`);
14
+ });
15
+ },
16
+ },
17
+ ],
18
+ });
19
+
20
+ console.log("Watching for changes...");
21
+ await ctx.watch();
@@ -0,0 +1,9 @@
1
+ export const sharedConfig = {
2
+ entryPoints: ["src/index.ts"],
3
+ bundle: true,
4
+ outdir: "dist",
5
+ platform: "browser",
6
+ format: "esm",
7
+ packages: "external",
8
+ external: ["react", "react-dom"],
9
+ };
@@ -0,0 +1,411 @@
1
+ import { expect, test, describe, vi } from "vitest";
2
+ import { ExtendedApolloClient } from "./client";
3
+ import { ApplicationManifest } from "../types/application-manifest";
4
+ import { parse } from "graphql";
5
+
6
+ describe("Client Basics", () => {
7
+ test("Should execute tool call when client.query is called", async () => {
8
+ vi.stubGlobal("openai", {
9
+ toolInput: {},
10
+ toolOutput: {},
11
+ toolResponseMetadata: {
12
+ toolName: "the-store--Get Product",
13
+ },
14
+ callTool: vi.fn(async (name: string, args: Record<string, unknown>) => {
15
+ return {
16
+ structuredContent: {
17
+ data: {
18
+ product: {
19
+ id: "1",
20
+ title: "Pen",
21
+ rating: 5,
22
+ price: 1.0,
23
+ description: "Awesome pen",
24
+ images: [],
25
+ __typename: "Product",
26
+ },
27
+ },
28
+ },
29
+ };
30
+ }),
31
+ });
32
+
33
+ const manifest = {
34
+ format: "apollo-ai-app-manifest",
35
+ version: "1",
36
+ name: "the-store",
37
+ description: "An online store selling a variety of high quality products across many different categories.",
38
+ hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
39
+ operations: [
40
+ {
41
+ id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
42
+ name: "Product",
43
+ type: "query",
44
+ body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
45
+ variables: { id: "ID" },
46
+ prefetch: false,
47
+ tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
48
+ },
49
+ ],
50
+ resource: "index.html",
51
+ };
52
+
53
+ const client = new ExtendedApolloClient({
54
+ manifest: manifest as ApplicationManifest,
55
+ });
56
+
57
+ const variables = { id: "1" };
58
+ await client.query({ query: parse(manifest.operations[0].body), variables });
59
+
60
+ expect(window.openai.callTool).toBeCalledWith("execute", { query: manifest.operations[0].body, variables });
61
+ expect(client.extract()).toMatchInlineSnapshot(`
62
+ {
63
+ "Product:1": {
64
+ "__typename": "Product",
65
+ "description": "Awesome pen",
66
+ "id": "1",
67
+ "images": [],
68
+ "price": 1,
69
+ "rating": 5,
70
+ "title": "Pen",
71
+ },
72
+ "ROOT_QUERY": {
73
+ "__typename": "Query",
74
+ "product({"id":"1"})": {
75
+ "__ref": "Product:1",
76
+ },
77
+ },
78
+ }
79
+ `);
80
+ });
81
+ });
82
+
83
+ describe("prefetchData", () => {
84
+ test("Should cache tool response when data is provided", async () => {
85
+ vi.stubGlobal("openai", {
86
+ toolInput: {
87
+ id: 1,
88
+ },
89
+ toolOutput: {
90
+ result: {
91
+ data: {
92
+ product: {
93
+ id: "1",
94
+ title: "Pen",
95
+ rating: 5,
96
+ price: 1.0,
97
+ description: "Awesome pen",
98
+ images: [],
99
+ __typename: "Product",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ toolResponseMetadata: {
105
+ toolName: "the-store--Get Product",
106
+ },
107
+ });
108
+
109
+ const manifest = {
110
+ format: "apollo-ai-app-manifest",
111
+ version: "1",
112
+ name: "the-store",
113
+ description: "An online store selling a variety of high quality products across many different categories.",
114
+ hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
115
+ operations: [
116
+ {
117
+ id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
118
+ name: "Product",
119
+ type: "query",
120
+ body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
121
+ variables: { id: "ID" },
122
+ prefetch: false,
123
+ tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
124
+ },
125
+ ],
126
+ resource: "index.html",
127
+ };
128
+
129
+ const client = new ExtendedApolloClient({
130
+ manifest: manifest as ApplicationManifest,
131
+ });
132
+ await client.prefetchData();
133
+
134
+ expect(client.extract()).toMatchInlineSnapshot(`
135
+ {
136
+ "Product:1": {
137
+ "__typename": "Product",
138
+ "description": "Awesome pen",
139
+ "id": "1",
140
+ "images": [],
141
+ "price": 1,
142
+ "rating": 5,
143
+ "title": "Pen",
144
+ },
145
+ "ROOT_QUERY": {
146
+ "__typename": "Query",
147
+ "product({"id":1})": {
148
+ "__ref": "Product:1",
149
+ },
150
+ },
151
+ }
152
+ `);
153
+ });
154
+
155
+ test("Should cache prefetched data when prefetched data is provided", async () => {
156
+ vi.stubGlobal("openai", {
157
+ toolInput: {},
158
+ toolOutput: {
159
+ result: {},
160
+ prefetch: {
161
+ __anonymous: {
162
+ data: {
163
+ topProducts: [
164
+ {
165
+ id: "2",
166
+ title: "iPhone 17",
167
+ rating: 5,
168
+ price: 999.99,
169
+ description: "Awesome phone",
170
+ images: [],
171
+ __typename: "Product",
172
+ },
173
+ ],
174
+ },
175
+ },
176
+ },
177
+ },
178
+ toolResponseMetadata: {
179
+ toolName: "the-store--Get Product",
180
+ },
181
+ });
182
+
183
+ const manifest = {
184
+ format: "apollo-ai-app-manifest",
185
+ version: "1",
186
+ name: "the-store",
187
+ description: "An online store selling a variety of high quality products across many different categories.",
188
+ hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
189
+ operations: [
190
+ {
191
+ id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
192
+ name: "TopProducts",
193
+ type: "query",
194
+ body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
195
+ variables: {},
196
+ prefetch: true,
197
+ prefetchID: "__anonymous",
198
+ tools: [{ name: "Top Products", description: "Shows the currently highest rated products." }],
199
+ },
200
+ ],
201
+ resource: "index.html",
202
+ };
203
+
204
+ const client = new ExtendedApolloClient({
205
+ manifest: manifest as ApplicationManifest,
206
+ });
207
+ await client.prefetchData();
208
+
209
+ expect(client.extract()).toMatchInlineSnapshot(`
210
+ {
211
+ "Product:2": {
212
+ "__typename": "Product",
213
+ "id": "2",
214
+ "price": 999.99,
215
+ "rating": 5,
216
+ "title": "iPhone 17",
217
+ },
218
+ "ROOT_QUERY": {
219
+ "__typename": "Query",
220
+ "topProducts": [
221
+ {
222
+ "__ref": "Product:2",
223
+ },
224
+ ],
225
+ },
226
+ }
227
+ `);
228
+ });
229
+
230
+ test("Should cache both prefetch and tool response when both are provided", async () => {
231
+ vi.stubGlobal("openai", {
232
+ toolInput: {
233
+ id: 1,
234
+ },
235
+ toolOutput: {
236
+ result: {
237
+ data: {
238
+ product: {
239
+ id: "1",
240
+ title: "Pen",
241
+ rating: 5,
242
+ price: 1.0,
243
+ description: "Awesome pen",
244
+ images: [],
245
+ __typename: "Product",
246
+ },
247
+ },
248
+ },
249
+ prefetch: {
250
+ __anonymous: {
251
+ data: {
252
+ topProducts: [
253
+ {
254
+ id: "2",
255
+ title: "iPhone 17",
256
+ rating: 5,
257
+ price: 999.99,
258
+ description: "Awesome phone",
259
+ images: [],
260
+ __typename: "Product",
261
+ },
262
+ ],
263
+ },
264
+ },
265
+ },
266
+ },
267
+ toolResponseMetadata: {
268
+ toolName: "the-store--Get Product",
269
+ },
270
+ });
271
+
272
+ const manifest = {
273
+ format: "apollo-ai-app-manifest",
274
+ version: "1",
275
+ name: "the-store",
276
+ description: "An online store selling a variety of high quality products across many different categories.",
277
+ hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
278
+ operations: [
279
+ {
280
+ id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
281
+ name: "Product",
282
+ type: "query",
283
+ body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
284
+ variables: { id: "ID" },
285
+ prefetch: false,
286
+ tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
287
+ },
288
+ {
289
+ id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
290
+ name: "TopProducts",
291
+ type: "query",
292
+ body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
293
+ variables: {},
294
+ prefetch: true,
295
+ prefetchID: "__anonymous",
296
+ tools: [{ name: "Top Products", description: "Shows the currently highest rated products." }],
297
+ },
298
+ ],
299
+ resource: "index.html",
300
+ };
301
+
302
+ const client = new ExtendedApolloClient({
303
+ manifest: manifest as ApplicationManifest,
304
+ });
305
+ await client.prefetchData();
306
+
307
+ expect(client.extract()).toMatchInlineSnapshot(`
308
+ {
309
+ "Product:1": {
310
+ "__typename": "Product",
311
+ "description": "Awesome pen",
312
+ "id": "1",
313
+ "images": [],
314
+ "price": 1,
315
+ "rating": 5,
316
+ "title": "Pen",
317
+ },
318
+ "Product:2": {
319
+ "__typename": "Product",
320
+ "id": "2",
321
+ "price": 999.99,
322
+ "rating": 5,
323
+ "title": "iPhone 17",
324
+ },
325
+ "ROOT_QUERY": {
326
+ "__typename": "Query",
327
+ "product({"id":1})": {
328
+ "__ref": "Product:1",
329
+ },
330
+ "topProducts": [
331
+ {
332
+ "__ref": "Product:2",
333
+ },
334
+ ],
335
+ },
336
+ }
337
+ `);
338
+ });
339
+
340
+ test("Should exclude extra inputs when writing to cache", async () => {
341
+ vi.stubGlobal("openai", {
342
+ toolInput: {
343
+ id: 1,
344
+ myOtherThing: 2,
345
+ },
346
+ toolOutput: {
347
+ result: {
348
+ data: {
349
+ product: {
350
+ id: "1",
351
+ title: "Pen",
352
+ rating: 5,
353
+ price: 1.0,
354
+ description: "Awesome pen",
355
+ images: [],
356
+ __typename: "Product",
357
+ },
358
+ },
359
+ },
360
+ },
361
+ toolResponseMetadata: {
362
+ toolName: "the-store--Get Product",
363
+ },
364
+ });
365
+
366
+ const manifest = {
367
+ format: "apollo-ai-app-manifest",
368
+ version: "1",
369
+ name: "the-store",
370
+ description: "An online store selling a variety of high quality products across many different categories.",
371
+ hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
372
+ operations: [
373
+ {
374
+ id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
375
+ name: "Product",
376
+ type: "query",
377
+ body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
378
+ variables: { id: "ID" },
379
+ prefetch: false,
380
+ tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
381
+ },
382
+ ],
383
+ resource: "index.html",
384
+ };
385
+
386
+ const client = new ExtendedApolloClient({
387
+ manifest: manifest as ApplicationManifest,
388
+ });
389
+ await client.prefetchData();
390
+
391
+ expect(client.extract()).toMatchInlineSnapshot(`
392
+ {
393
+ "Product:1": {
394
+ "__typename": "Product",
395
+ "description": "Awesome pen",
396
+ "id": "1",
397
+ "images": [],
398
+ "price": 1,
399
+ "rating": 5,
400
+ "title": "Pen",
401
+ },
402
+ "ROOT_QUERY": {
403
+ "__typename": "Query",
404
+ "product({"id":1})": {
405
+ "__ref": "Product:1",
406
+ },
407
+ },
408
+ }
409
+ `);
410
+ });
411
+ });
@@ -0,0 +1,90 @@
1
+ import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client";
2
+ import * as Observable from "rxjs";
3
+ import { selectHttpOptionsAndBody } from "@apollo/client/link/http";
4
+ import { fallbackHttpConfig } from "@apollo/client/link/http";
5
+ import { DocumentTransform } from "@apollo/client";
6
+ import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
7
+ import { parse } from "graphql";
8
+ import "../types/openai";
9
+ import { ApplicationManifest } from "../types/application-manifest";
10
+
11
+ // TODO: In the future if/when we support PQs again, do pqLink.concat(toolCallLink)
12
+ // Commenting this out for now.
13
+ // import { sha256 } from "crypto-hash";
14
+ // import { PersistedQueryLink } from "@apollo/client/link/persisted-queries";
15
+ // const pqLink = new PersistedQueryLink({
16
+ // sha256: (queryString) => sha256(queryString),
17
+ // });
18
+
19
+ // Normally, ApolloClient uses an HttpLink and sends the graphql request over HTTP
20
+ // In our case, we're are sending the graphql request over the "execute" tool call
21
+ const toolCallLink = new ApolloLink((operation) => {
22
+ const context = operation.getContext();
23
+ const contextConfig = {
24
+ http: context.http,
25
+ options: context.fetchOptions,
26
+ credentials: context.credentials,
27
+ headers: context.headers,
28
+ };
29
+ const { query, variables } = selectHttpOptionsAndBody(operation, fallbackHttpConfig, contextConfig).body;
30
+
31
+ return Observable.from(window.openai.callTool("execute", { query, variables })).pipe(
32
+ Observable.map((result) => ({ data: result.structuredContent.data }))
33
+ );
34
+ });
35
+
36
+ // This allows us to extend the options with the "manifest" option AND make link/cache optional (they are normally required)
37
+ type ExtendedApolloClientOptions = Omit<ApolloClient.Options, "link" | "cache"> & {
38
+ link?: ApolloClient.Options["link"];
39
+ cache?: ApolloClient.Options["cache"];
40
+ manifest: ApplicationManifest;
41
+ };
42
+
43
+ export class ExtendedApolloClient extends ApolloClient {
44
+ manifest: ApplicationManifest;
45
+
46
+ constructor(options: ExtendedApolloClientOptions) {
47
+ super({
48
+ link: toolCallLink,
49
+ cache: options.cache ?? new InMemoryCache(),
50
+ // Strip out the prefetch/tool directives so they don't get sent with the operation to the server
51
+ documentTransform: new DocumentTransform((document) => {
52
+ return removeDirectivesFromDocument([{ name: "prefetch" }, { name: "tool" }], document)!;
53
+ }),
54
+ });
55
+
56
+ this.manifest = options.manifest;
57
+ }
58
+
59
+ async prefetchData() {
60
+ // Write prefetched data to the cache
61
+ this.manifest.operations.forEach((operation) => {
62
+ if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch[operation.prefetchID]) {
63
+ this.writeQuery({
64
+ query: parse(operation.body),
65
+ data: window.openai.toolOutput.prefetch[operation.prefetchID].data,
66
+ });
67
+ }
68
+
69
+ // If this operation has the tool that matches up with the tool that was executed, write the tool result to the cache
70
+ if (
71
+ operation.tools?.find(
72
+ (tool) => `${this.manifest.name}--${tool.name}` === window.openai.toolResponseMetadata.toolName
73
+ )
74
+ ) {
75
+ // We need to include the variables that were used as part of the tool call so that we get a proper cache entry
76
+ // However, we only want to include toolInput's that were graphql operation (ignore extraInputs)
77
+ const variables = Object.keys(window.openai.toolInput).reduce(
78
+ (obj, key) => (operation.variables[key] ? { ...obj, [key]: window.openai.toolInput[key] } : obj),
79
+ {}
80
+ );
81
+
82
+ this.writeQuery({
83
+ query: parse(operation.body),
84
+ data: window.openai.toolOutput.result.data,
85
+ variables,
86
+ });
87
+ }
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,41 @@
1
+ import { expect, test, vi } from "vitest";
2
+ import { ExtendedApolloProvider } from "./provider";
3
+ import { render } from "@testing-library/react";
4
+ import { ExtendedApolloClient } from "./client";
5
+ import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
6
+
7
+ test("Should call prefetch data when window.open is immediately available", () => {
8
+ vi.stubGlobal("openai", {
9
+ toolOutput: {},
10
+ });
11
+
12
+ const client = {
13
+ prefetchData: vi.fn(async () => {}),
14
+ } as unknown as ExtendedApolloClient;
15
+
16
+ render(<ExtendedApolloProvider client={client} />);
17
+
18
+ expect(client.prefetchData).toBeCalled();
19
+ });
20
+
21
+ test("Should NOT call prefetch data when window.open is not immediately available", () => {
22
+ const client = {
23
+ prefetchData: vi.fn(async () => {}),
24
+ } as unknown as ExtendedApolloClient;
25
+
26
+ render(<ExtendedApolloProvider client={client} />);
27
+
28
+ expect(client.prefetchData).not.toBeCalled();
29
+ });
30
+
31
+ test("Should call prefetch data when window.open is not immediately available and event is sent", () => {
32
+ const client = {
33
+ prefetchData: vi.fn(async () => {}),
34
+ } as unknown as ExtendedApolloClient;
35
+
36
+ render(<ExtendedApolloProvider client={client} />);
37
+
38
+ window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
39
+
40
+ expect(client.prefetchData).toBeCalled();
41
+ });
@@ -0,0 +1,32 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { ApolloProvider } from "@apollo/client/react";
3
+ import { ExtendedApolloClient } from "./client";
4
+ import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
5
+
6
+ export const ExtendedApolloProvider = ({
7
+ children,
8
+ client,
9
+ }: React.PropsWithChildren<{ client: ExtendedApolloClient }>) => {
10
+ const [hasPreloaded, setHasPreloaded] = useState(false);
11
+
12
+ // This is to prevent against a race condition. We don't know if window.openai will be available when this loads or if it will become available shortly after.
13
+ // So... we create the event listener and whenever it is available, then we can process the prefetch/tool data.
14
+ // In practice, this should be pretty much instant
15
+ useEffect(() => {
16
+ const prefetchData = async () => {
17
+ await client.prefetchData();
18
+ setHasPreloaded(true);
19
+ window.removeEventListener(SET_GLOBALS_EVENT_TYPE, prefetchData);
20
+ };
21
+
22
+ window.addEventListener(SET_GLOBALS_EVENT_TYPE, prefetchData, {
23
+ passive: true,
24
+ });
25
+
26
+ if (window.openai?.toolOutput) {
27
+ window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
28
+ }
29
+ }, [setHasPreloaded]);
30
+
31
+ return hasPreloaded ? <ApolloProvider client={client}>{children}</ApolloProvider> : null;
32
+ };