@apollo/client-ai-apps 0.2.4 → 0.3.1
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.
- package/.git-blame-ignore-revs +2 -0
- package/.github/workflows/compare-build-output.yml +28 -0
- package/.github/workflows/pr.yaml +23 -15
- package/.github/workflows/release.yaml +46 -46
- package/.prettierrc +9 -0
- package/config/compare-build-output-to.sh +90 -0
- package/dist/core/ApolloClient.d.ts +14 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.js +164 -62
- package/dist/link/ToolCallLink.d.ts +26 -0
- package/dist/react/ApolloProvider.d.ts +9 -0
- package/dist/react/context/ToolUseContext.d.ts +15 -0
- package/dist/{hooks → react/hooks}/useOpenAiGlobal.d.ts +1 -1
- package/dist/react/hooks/useOpenExternal.d.ts +3 -0
- package/dist/{hooks → react/hooks}/useRequestDisplayMode.d.ts +1 -1
- package/dist/{hooks → react/hooks}/useToolEffect.d.ts +0 -4
- package/dist/react/hooks/useToolOutput.d.ts +1 -0
- package/dist/react/hooks/useToolResponseMetadata.d.ts +1 -0
- package/dist/react/hooks/useWidgetState.d.ts +4 -0
- package/dist/types/openai.d.ts +1 -2
- package/dist/vite/index.js +74 -21
- package/package.json +9 -2
- package/scripts/dev.mjs +3 -1
- package/src/core/ApolloClient.ts +108 -0
- package/src/{apollo_client/client.test.ts → core/__tests__/ApolloClient.test.ts} +232 -20
- package/src/index.ts +36 -10
- package/src/link/ToolCallLink.ts +49 -0
- package/src/{apollo_client/provider.tsx → react/ApolloProvider.tsx} +19 -9
- package/src/{apollo_client/provider.test.tsx → react/__tests__/ApolloProvider.test.tsx} +9 -9
- package/src/react/context/ToolUseContext.tsx +30 -0
- package/src/{hooks → react/hooks/__tests__}/useCallTool.test.ts +1 -1
- package/src/{hooks → react/hooks/__tests__}/useOpenAiGlobal.test.ts +5 -3
- package/src/react/hooks/__tests__/useOpenExternal.test.tsx +24 -0
- package/src/{hooks → react/hooks/__tests__}/useRequestDisplayMode.test.ts +2 -2
- package/src/{hooks → react/hooks/__tests__}/useSendFollowUpMessage.test.ts +4 -2
- package/src/{hooks → react/hooks/__tests__}/useToolEffect.test.tsx +27 -10
- package/src/{hooks → react/hooks/__tests__}/useToolInput.test.ts +1 -1
- package/src/{hooks → react/hooks/__tests__}/useToolName.test.ts +1 -1
- package/src/react/hooks/__tests__/useToolOutput.test.tsx +49 -0
- package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +49 -0
- package/src/react/hooks/__tests__/useWidgetState.test.tsx +158 -0
- package/src/react/hooks/useCallTool.ts +13 -0
- package/src/{hooks → react/hooks}/useOpenAiGlobal.ts +11 -5
- package/src/react/hooks/useOpenExternal.ts +11 -0
- package/src/{hooks → react/hooks}/useRequestDisplayMode.ts +1 -1
- package/src/react/hooks/useToolEffect.tsx +37 -0
- package/src/{hooks → react/hooks}/useToolName.ts +1 -1
- package/src/react/hooks/useToolOutput.ts +5 -0
- package/src/react/hooks/useToolResponseMetadata.ts +5 -0
- package/src/react/hooks/useWidgetState.ts +48 -0
- package/src/testing/internal/index.ts +2 -0
- package/src/testing/internal/matchers/index.d.ts +9 -0
- package/src/testing/internal/matchers/index.ts +1 -0
- package/src/testing/internal/matchers/toRerender.ts +49 -0
- package/src/testing/internal/openai/dispatchStateChange.ts +9 -0
- package/src/testing/internal/openai/stubOpenAiGlobals.ts +13 -0
- package/src/types/openai.ts +6 -3
- package/src/vite/{absolute_asset_imports_plugin.test.ts → __tests__/absolute_asset_imports_plugin.test.ts} +4 -2
- package/src/vite/{application_manifest_plugin.test.ts → __tests__/application_manifest_plugin.test.ts} +176 -53
- package/src/vite/absolute_asset_imports_plugin.ts +3 -1
- package/src/vite/application_manifest_plugin.ts +84 -24
- package/vitest-setup.ts +1 -0
- package/dist/apollo_client/client.d.ts +0 -14
- package/dist/apollo_client/provider.d.ts +0 -5
- package/src/apollo_client/client.ts +0 -90
- package/src/hooks/useCallTool.ts +0 -8
- package/src/hooks/useToolEffect.tsx +0 -41
- /package/dist/{hooks → react/hooks}/useSendFollowUpMessage.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useToolInput.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useToolName.d.ts +0 -0
- /package/src/{hooks → react/hooks}/useSendFollowUpMessage.ts +0 -0
- /package/src/{hooks → react/hooks}/useToolInput.ts +0 -0
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { expect, test, vi } from "vitest";
|
|
2
|
-
import { useToolEffect
|
|
2
|
+
import { useToolEffect } from "../useToolEffect";
|
|
3
3
|
import { renderHook } from "@testing-library/react";
|
|
4
|
+
import { ToolUseProvider } from "../../context/ToolUseContext";
|
|
4
5
|
|
|
5
6
|
test("Should trigger effect when tool name matches toolResponseMetadata", async () => {
|
|
6
7
|
vi.stubGlobal("openai", {
|
|
7
8
|
toolResponseMetadata: { toolName: "my-app--my-tool" },
|
|
8
9
|
});
|
|
9
10
|
const navigate = vi.fn();
|
|
10
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
11
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
12
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
13
|
+
);
|
|
11
14
|
|
|
12
|
-
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
15
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
16
|
+
wrapper,
|
|
17
|
+
});
|
|
13
18
|
|
|
14
19
|
expect(navigate).toBeCalled();
|
|
15
20
|
});
|
|
@@ -19,9 +24,17 @@ test("Should trigger effect when one of multiple tool name matches toolResponseM
|
|
|
19
24
|
toolResponseMetadata: { toolName: "my-app--my-tool" },
|
|
20
25
|
});
|
|
21
26
|
const navigate = vi.fn();
|
|
22
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
27
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
28
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
29
|
+
);
|
|
23
30
|
|
|
24
|
-
renderHook(
|
|
31
|
+
renderHook(
|
|
32
|
+
() =>
|
|
33
|
+
useToolEffect(["my-tool", "my-similar-tool"], () => navigate(), [
|
|
34
|
+
navigate,
|
|
35
|
+
]),
|
|
36
|
+
{ wrapper }
|
|
37
|
+
);
|
|
25
38
|
|
|
26
39
|
expect(navigate).toBeCalled();
|
|
27
40
|
});
|
|
@@ -31,9 +44,13 @@ test("Should not trigger effect when tool name does not match toolResponseMetada
|
|
|
31
44
|
toolResponseMetadata: { toolName: "my-app--my-other-tool" },
|
|
32
45
|
});
|
|
33
46
|
const navigate = vi.fn();
|
|
34
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
47
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
48
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
49
|
+
);
|
|
35
50
|
|
|
36
|
-
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
51
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
52
|
+
wrapper,
|
|
53
|
+
});
|
|
37
54
|
|
|
38
55
|
expect(navigate).not.toBeCalled();
|
|
39
56
|
});
|
|
@@ -44,7 +61,7 @@ test("Should throw an error when used outside of a ToolUseProvider", async () =>
|
|
|
44
61
|
});
|
|
45
62
|
const navigate = vi.fn();
|
|
46
63
|
|
|
47
|
-
expect(() =>
|
|
48
|
-
"
|
|
49
|
-
);
|
|
64
|
+
expect(() =>
|
|
65
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]))
|
|
66
|
+
).toThrowError("useToolEffect must be used within ToolUseProvider");
|
|
50
67
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
dispatchStateChange,
|
|
4
|
+
stubOpenAiGlobals,
|
|
5
|
+
} from "../../../testing/internal";
|
|
6
|
+
import { renderHookToSnapshotStream } from "@testing-library/react-render-stream";
|
|
7
|
+
import { useToolOutput } from "../useToolOutput";
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllGlobals();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("returns the tool output set in window", async () => {
|
|
14
|
+
stubOpenAiGlobals({ toolOutput: { test: true } });
|
|
15
|
+
|
|
16
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
17
|
+
useToolOutput()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await expect(takeSnapshot()).resolves.toEqual({ test: true });
|
|
21
|
+
await expect(takeSnapshot).not.toRerender();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns null when not set", async () => {
|
|
25
|
+
stubOpenAiGlobals();
|
|
26
|
+
|
|
27
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
28
|
+
useToolOutput()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await expect(takeSnapshot()).resolves.toBeNull();
|
|
32
|
+
await expect(takeSnapshot).not.toRerender();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("reacts to changes in globals", async () => {
|
|
36
|
+
stubOpenAiGlobals({ toolOutput: { initial: true } });
|
|
37
|
+
|
|
38
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
39
|
+
useToolOutput()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await expect(takeSnapshot()).resolves.toEqual({ initial: true });
|
|
43
|
+
|
|
44
|
+
window.openai.toolOutput = { updated: true };
|
|
45
|
+
dispatchStateChange();
|
|
46
|
+
|
|
47
|
+
await expect(takeSnapshot()).resolves.toEqual({ updated: true });
|
|
48
|
+
await expect(takeSnapshot).not.toRerender();
|
|
49
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
dispatchStateChange,
|
|
4
|
+
stubOpenAiGlobals,
|
|
5
|
+
} from "../../../testing/internal";
|
|
6
|
+
import { renderHookToSnapshotStream } from "@testing-library/react-render-stream";
|
|
7
|
+
import { useToolResponseMetadata } from "../useToolResponseMetadata";
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllGlobals();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("returns the tool output set in window", async () => {
|
|
14
|
+
stubOpenAiGlobals({ toolResponseMetadata: { test: true } });
|
|
15
|
+
|
|
16
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
17
|
+
useToolResponseMetadata()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await expect(takeSnapshot()).resolves.toEqual({ test: true });
|
|
21
|
+
await expect(takeSnapshot).not.toRerender();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns null when not set", async () => {
|
|
25
|
+
stubOpenAiGlobals();
|
|
26
|
+
|
|
27
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
28
|
+
useToolResponseMetadata()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await expect(takeSnapshot()).resolves.toBeNull();
|
|
32
|
+
await expect(takeSnapshot).not.toRerender();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("reacts to changes in globals", async () => {
|
|
36
|
+
stubOpenAiGlobals({ toolResponseMetadata: { initial: true } });
|
|
37
|
+
|
|
38
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
39
|
+
useToolResponseMetadata()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await expect(takeSnapshot()).resolves.toEqual({ initial: true });
|
|
43
|
+
|
|
44
|
+
window.openai.toolResponseMetadata = { updated: true };
|
|
45
|
+
dispatchStateChange();
|
|
46
|
+
|
|
47
|
+
await expect(takeSnapshot()).resolves.toEqual({ updated: true });
|
|
48
|
+
await expect(takeSnapshot).not.toRerender();
|
|
49
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
disableActEnvironment,
|
|
4
|
+
renderHookToSnapshotStream,
|
|
5
|
+
} from "@testing-library/react-render-stream";
|
|
6
|
+
import { useWidgetState } from "../useWidgetState";
|
|
7
|
+
import { stubOpenAiGlobals } from "../../../testing/internal";
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllGlobals();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("returns state from global", async () => {
|
|
14
|
+
stubOpenAiGlobals({ widgetState: { test: true } });
|
|
15
|
+
|
|
16
|
+
using _disabledAct = disableActEnvironment();
|
|
17
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
18
|
+
useWidgetState()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const [widgetState] = await takeSnapshot();
|
|
22
|
+
|
|
23
|
+
expect(widgetState).toEqual({ test: true });
|
|
24
|
+
await expect(takeSnapshot).not.toRerender();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns null when global does not exist", async () => {
|
|
28
|
+
stubOpenAiGlobals();
|
|
29
|
+
|
|
30
|
+
using _disabledAct = disableActEnvironment();
|
|
31
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
32
|
+
useWidgetState()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const [widgetState] = await takeSnapshot();
|
|
36
|
+
|
|
37
|
+
expect(widgetState).toBeNull();
|
|
38
|
+
await expect(takeSnapshot).not.toRerender();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns provided default state when global does not exist", async () => {
|
|
42
|
+
stubOpenAiGlobals();
|
|
43
|
+
|
|
44
|
+
using _disabledAct = disableActEnvironment();
|
|
45
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
46
|
+
useWidgetState({ defaultValue: true })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const [widgetState] = await takeSnapshot();
|
|
50
|
+
|
|
51
|
+
expect(widgetState).toEqual({ defaultValue: true });
|
|
52
|
+
await expect(takeSnapshot).not.toRerender();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns provided default state returned from init function when global does not exist", async () => {
|
|
56
|
+
stubOpenAiGlobals();
|
|
57
|
+
|
|
58
|
+
using _disabledAct = disableActEnvironment();
|
|
59
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
60
|
+
useWidgetState(() => ({ defaultValueFromFunction: true }))
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const [widgetState] = await takeSnapshot();
|
|
64
|
+
|
|
65
|
+
expect(widgetState).toEqual({ defaultValueFromFunction: true });
|
|
66
|
+
await expect(takeSnapshot).not.toRerender();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("prefers global value over default value", async () => {
|
|
70
|
+
stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
|
|
71
|
+
|
|
72
|
+
using _disabledAct = disableActEnvironment();
|
|
73
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
74
|
+
useWidgetState({ defaultValue: true })
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const [widgetState] = await takeSnapshot();
|
|
78
|
+
|
|
79
|
+
expect(widgetState).toEqual({ globalWidgetState: true });
|
|
80
|
+
await expect(takeSnapshot).not.toRerender();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("rerenders with new value after setting new value", async () => {
|
|
84
|
+
stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
|
|
85
|
+
|
|
86
|
+
using _disabledAct = disableActEnvironment();
|
|
87
|
+
const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream(
|
|
88
|
+
() => useWidgetState()
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
const [widgetState] = await takeSnapshot();
|
|
93
|
+
|
|
94
|
+
expect(widgetState).toEqual({ globalWidgetState: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [, setWidgetState] = getCurrentSnapshot();
|
|
98
|
+
setWidgetState({ rerendered: true });
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
const [widgetState] = await takeSnapshot();
|
|
102
|
+
|
|
103
|
+
expect(widgetState).toEqual({ rerendered: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await expect(takeSnapshot).not.toRerender();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("allows state setter function with previous value", async () => {
|
|
110
|
+
stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
|
|
111
|
+
|
|
112
|
+
using _disabledAct = disableActEnvironment();
|
|
113
|
+
const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream(
|
|
114
|
+
() => useWidgetState()
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
const [widgetState] = await takeSnapshot();
|
|
119
|
+
|
|
120
|
+
expect(widgetState).toEqual({ globalWidgetState: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [, setWidgetState] = getCurrentSnapshot();
|
|
124
|
+
setWidgetState((prev) => ({ ...prev, rerendered: true }));
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
const [widgetState] = await takeSnapshot();
|
|
128
|
+
|
|
129
|
+
expect(widgetState).toEqual({ globalWidgetState: true, rerendered: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await expect(takeSnapshot).not.toRerender();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("updates value from window when changed globally", async () => {
|
|
136
|
+
stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
|
|
137
|
+
|
|
138
|
+
using _disabledAct = disableActEnvironment();
|
|
139
|
+
const { takeSnapshot } = await renderHookToSnapshotStream(() =>
|
|
140
|
+
useWidgetState()
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
{
|
|
144
|
+
const [widgetState] = await takeSnapshot();
|
|
145
|
+
|
|
146
|
+
expect(widgetState).toEqual({ globalWidgetState: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
window.openai.setWidgetState({ fromEvent: true });
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
const [widgetState] = await takeSnapshot();
|
|
153
|
+
|
|
154
|
+
expect(widgetState).toEqual({ fromEvent: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await expect(takeSnapshot).not.toRerender();
|
|
158
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type UseCallToolResult = <K>(
|
|
2
|
+
toolId: string,
|
|
3
|
+
variables?: Record<string, unknown> | undefined
|
|
4
|
+
) => Promise<K>;
|
|
5
|
+
|
|
6
|
+
export const useCallTool = (): UseCallToolResult => {
|
|
7
|
+
const callTool = async (
|
|
8
|
+
toolId: string,
|
|
9
|
+
variables: Record<string, unknown> | undefined = {}
|
|
10
|
+
) => await window.openai?.callTool(toolId, variables);
|
|
11
|
+
|
|
12
|
+
return callTool;
|
|
13
|
+
};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useSyncExternalStore, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
SET_GLOBALS_EVENT_TYPE,
|
|
4
|
+
SetGlobalsEvent,
|
|
5
|
+
OpenAiGlobals,
|
|
6
|
+
} from "../../types/openai";
|
|
3
7
|
|
|
4
|
-
export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
|
|
8
|
+
export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
|
|
9
|
+
key: K
|
|
10
|
+
): OpenAiGlobals[K] {
|
|
5
11
|
return useSyncExternalStore(
|
|
6
|
-
(onChange) => {
|
|
12
|
+
useCallback((onChange) => {
|
|
7
13
|
const handleSetGlobal = (event: SetGlobalsEvent) => {
|
|
8
14
|
const value = event.detail.globals[key];
|
|
9
15
|
if (value === undefined) {
|
|
@@ -20,7 +26,7 @@ export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGl
|
|
|
20
26
|
return () => {
|
|
21
27
|
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
|
|
22
28
|
};
|
|
23
|
-
},
|
|
29
|
+
}, []),
|
|
24
30
|
() => window.openai[key]
|
|
25
31
|
);
|
|
26
32
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { API } from "../../types/openai";
|
|
3
|
+
|
|
4
|
+
type OpenExternalFn = API<any>["openExternal"];
|
|
5
|
+
|
|
6
|
+
export function useOpenExternal() {
|
|
7
|
+
return useCallback<OpenExternalFn>(
|
|
8
|
+
(...args) => window.openai.openExternal(...args),
|
|
9
|
+
[]
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { useToolName } from "./useToolName";
|
|
3
|
+
import { useToolInput } from "./useToolInput";
|
|
4
|
+
import { useToolUseState } from "../context/ToolUseContext";
|
|
5
|
+
|
|
6
|
+
export const useToolEffect = (
|
|
7
|
+
toolName: string | string[],
|
|
8
|
+
effect: (toolInput: any) => void,
|
|
9
|
+
deps: React.DependencyList = []
|
|
10
|
+
) => {
|
|
11
|
+
const ctx = useToolUseState();
|
|
12
|
+
const fullToolName = useToolName();
|
|
13
|
+
const toolInput = useToolInput();
|
|
14
|
+
if (!ctx)
|
|
15
|
+
throw new Error("useToolEffect must be used within ToolUseProvider");
|
|
16
|
+
|
|
17
|
+
const toolNames = Array.isArray(toolName) ? toolName : [toolName];
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const matches = toolNames.some(
|
|
21
|
+
(name) => fullToolName === `${ctx.appName}--${name}`
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!ctx.hasNavigated && matches) {
|
|
25
|
+
effect(toolInput);
|
|
26
|
+
ctx.setHasNavigated(true);
|
|
27
|
+
}
|
|
28
|
+
}, [
|
|
29
|
+
ctx.hasNavigated,
|
|
30
|
+
ctx.setHasNavigated,
|
|
31
|
+
ctx.appName,
|
|
32
|
+
toolNames,
|
|
33
|
+
fullToolName,
|
|
34
|
+
toolInput,
|
|
35
|
+
...deps,
|
|
36
|
+
]);
|
|
37
|
+
};
|
|
@@ -3,5 +3,5 @@ import { useOpenAiGlobal } from "./useOpenAiGlobal";
|
|
|
3
3
|
export const useToolName = (): string | undefined => {
|
|
4
4
|
const toolResponseMetadata = useOpenAiGlobal("toolResponseMetadata");
|
|
5
5
|
|
|
6
|
-
return toolResponseMetadata?.toolName;
|
|
6
|
+
return toolResponseMetadata?.toolName as string;
|
|
7
7
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SetStateAction, useCallback, useState } from "react";
|
|
2
|
+
import { UnknownObject } from "../../types/openai";
|
|
3
|
+
import { useOpenAiGlobal } from "./useOpenAiGlobal";
|
|
4
|
+
|
|
5
|
+
export function useWidgetState<T extends UnknownObject>(
|
|
6
|
+
defaultState: T | (() => T)
|
|
7
|
+
): readonly [T, (state: SetStateAction<T>) => void];
|
|
8
|
+
|
|
9
|
+
export function useWidgetState<T extends UnknownObject>(
|
|
10
|
+
defaultState?: T | (() => T | null) | null
|
|
11
|
+
): readonly [T | null, (state: SetStateAction<T | null>) => void];
|
|
12
|
+
|
|
13
|
+
export function useWidgetState<T extends UnknownObject>(
|
|
14
|
+
defaultState?: T | (() => T | null) | null
|
|
15
|
+
): readonly [T | null, (state: SetStateAction<T | null>) => void] {
|
|
16
|
+
const widgetStateFromWindow = useOpenAiGlobal("widgetState") as T;
|
|
17
|
+
const [previousWidgetStateFromWindow, setPreviousWidgetStateFromWindow] =
|
|
18
|
+
useState(widgetStateFromWindow);
|
|
19
|
+
|
|
20
|
+
let [widgetState, _setWidgetState] = useState<T | null>(() => {
|
|
21
|
+
if (widgetStateFromWindow != null) {
|
|
22
|
+
return widgetStateFromWindow;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return typeof defaultState === "function" ? defaultState() : (
|
|
26
|
+
(defaultState ?? null)
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (previousWidgetStateFromWindow !== widgetStateFromWindow) {
|
|
31
|
+
_setWidgetState((widgetState = widgetStateFromWindow));
|
|
32
|
+
setPreviousWidgetStateFromWindow(widgetStateFromWindow);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const setWidgetState = useCallback((state: SetStateAction<T | null>) => {
|
|
36
|
+
_setWidgetState((prevState) => {
|
|
37
|
+
const newState = typeof state === "function" ? state(prevState) : state;
|
|
38
|
+
|
|
39
|
+
if (newState != null && typeof window !== "undefined") {
|
|
40
|
+
void window.openai?.setWidgetState?.(newState);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return newState;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return [widgetState, setWidgetState];
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextRenderOptions } from "@testing-library/react-render-stream";
|
|
2
|
+
|
|
3
|
+
interface CustomMatchers<R = unknown> {
|
|
4
|
+
toRerender: (options?: NextRenderOptions) => Promise<R>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module "vitest" {
|
|
8
|
+
interface Assertion<T = any> extends CustomMatchers<T> {}
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./toRerender";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Vitest port of toRerender from
|
|
2
|
+
// https://github.com/testing-library/react-render-stream-testing-library/blob/main/src/expect/renderStreamMatchers.ts
|
|
3
|
+
import {
|
|
4
|
+
Assertable,
|
|
5
|
+
NextRenderOptions,
|
|
6
|
+
RenderStream,
|
|
7
|
+
WaitForRenderTimeoutError,
|
|
8
|
+
} from "@testing-library/react-render-stream";
|
|
9
|
+
|
|
10
|
+
import { expect } from "vitest";
|
|
11
|
+
|
|
12
|
+
const assertableSymbol = Symbol.for(
|
|
13
|
+
"@testing-library/react-render-stream:assertable"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect.extend({
|
|
17
|
+
async toRerender(actual, options: NextRenderOptions) {
|
|
18
|
+
const _stream = actual as RenderStream<any> | Assertable;
|
|
19
|
+
const stream = (
|
|
20
|
+
assertableSymbol in _stream ?
|
|
21
|
+
_stream[assertableSymbol]
|
|
22
|
+
: _stream) as RenderStream<any>;
|
|
23
|
+
const hint = this.utils.matcherHint("toRerender", undefined, undefined, {
|
|
24
|
+
isNot: this.isNot,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let pass = true;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await stream.peekRender({ timeout: 100, ...options });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (e instanceof WaitForRenderTimeoutError) {
|
|
33
|
+
pass = false;
|
|
34
|
+
} else {
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
pass,
|
|
41
|
+
message() {
|
|
42
|
+
return (
|
|
43
|
+
`${hint}\n\nExpected component to${pass ? " not" : ""} rerender, ` +
|
|
44
|
+
`but it did${pass ? "" : " not"}.`
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { API, OpenAiGlobals, UnknownObject } from "../../../types/openai";
|
|
3
|
+
import { dispatchStateChange } from "./dispatchStateChange";
|
|
4
|
+
|
|
5
|
+
export function stubOpenAiGlobals(globals?: Partial<API<any> & OpenAiGlobals>) {
|
|
6
|
+
vi.stubGlobal("openai", {
|
|
7
|
+
setWidgetState: (state: UnknownObject) => {
|
|
8
|
+
window.openai.widgetState = state;
|
|
9
|
+
dispatchStateChange();
|
|
10
|
+
},
|
|
11
|
+
...globals,
|
|
12
|
+
});
|
|
13
|
+
}
|
package/src/types/openai.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type UnknownObject =
|
|
1
|
+
export type UnknownObject = Record<string, unknown>;
|
|
2
2
|
|
|
3
3
|
declare global {
|
|
4
4
|
interface Window {
|
|
@@ -14,7 +14,7 @@ export type OpenAiGlobals<
|
|
|
14
14
|
ToolInput extends UnknownObject = UnknownObject,
|
|
15
15
|
ToolOutput extends UnknownObject = UnknownObject,
|
|
16
16
|
ToolResponseMetadata extends UnknownObject = UnknownObject,
|
|
17
|
-
WidgetState extends UnknownObject = UnknownObject
|
|
17
|
+
WidgetState extends UnknownObject = UnknownObject,
|
|
18
18
|
> = {
|
|
19
19
|
theme: Theme;
|
|
20
20
|
userAgent: UserAgent;
|
|
@@ -62,7 +62,10 @@ export class SetGlobalsEvent extends CustomEvent<{
|
|
|
62
62
|
readonly type = SET_GLOBALS_EVENT_TYPE;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export type CallTool = (
|
|
65
|
+
export type CallTool = (
|
|
66
|
+
name: string,
|
|
67
|
+
args: Record<string, unknown>
|
|
68
|
+
) => Promise<any>;
|
|
66
69
|
|
|
67
70
|
export type DisplayMode = "pip" | "inline" | "fullscreen";
|
|
68
71
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
|
|
2
|
-
import { AbsoluteAssetImportsPlugin } from "
|
|
2
|
+
import { AbsoluteAssetImportsPlugin } from "../absolute_asset_imports_plugin";
|
|
3
3
|
|
|
4
4
|
test("Should replace root relative scripts with full url when origin is provided", () => {
|
|
5
5
|
const ctx = {
|
|
@@ -96,5 +96,7 @@ test("Should not modify html when not running a local server", () => {
|
|
|
96
96
|
|
|
97
97
|
let result = plugin.transformIndexHtml(html, ctx);
|
|
98
98
|
|
|
99
|
-
expect(result).toMatchInlineSnapshot(
|
|
99
|
+
expect(result).toMatchInlineSnapshot(
|
|
100
|
+
`"<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>"`
|
|
101
|
+
);
|
|
100
102
|
});
|