@perspective-ai/sdk-react 1.0.0-alpha.2
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/README.md +316 -0
- package/dist/index.cjs +529 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +521 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/FloatBubble.test.tsx +173 -0
- package/src/FloatBubble.tsx +83 -0
- package/src/Fullpage.test.tsx +164 -0
- package/src/Fullpage.tsx +83 -0
- package/src/PopupButton.test.tsx +273 -0
- package/src/PopupButton.tsx +208 -0
- package/src/SliderButton.test.tsx +279 -0
- package/src/SliderButton.tsx +208 -0
- package/src/Widget.test.tsx +308 -0
- package/src/Widget.tsx +100 -0
- package/src/hooks/useStableCallback.test.ts +83 -0
- package/src/hooks/useStableCallback.ts +20 -0
- package/src/hooks/useThemeSync.test.ts +127 -0
- package/src/hooks/useThemeSync.ts +36 -0
- package/src/index.ts +52 -0
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@perspective-ai/sdk-react",
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "React components for Perspective AI embed SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.cts",
|
|
15
|
+
"default": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"perspective",
|
|
28
|
+
"embed",
|
|
29
|
+
"react",
|
|
30
|
+
"interview",
|
|
31
|
+
"widget"
|
|
32
|
+
],
|
|
33
|
+
"author": "Perspective AI",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Perspective-AI/perspective.git",
|
|
38
|
+
"directory": "packages/sdk-react"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Perspective-AI/perspective/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://getperspective.ai",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
49
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@perspective-ai/sdk": "^1.0.0-alpha.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@testing-library/dom": "^10.4.1",
|
|
56
|
+
"@testing-library/react": "^16.3.1",
|
|
57
|
+
"@types/react": "^19.2.3",
|
|
58
|
+
"@types/react-dom": "^19.2.3",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsup",
|
|
66
|
+
"dev": "tsup --watch",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"test": "vitest run"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, cleanup } from "@testing-library/react";
|
|
3
|
+
import { createRef } from "react";
|
|
4
|
+
import { FloatBubble } from "./FloatBubble";
|
|
5
|
+
import type { FloatHandle } from "@perspective-ai/sdk";
|
|
6
|
+
|
|
7
|
+
const mockUnmount = vi.fn();
|
|
8
|
+
const mockOpen = vi.fn();
|
|
9
|
+
const mockClose = vi.fn();
|
|
10
|
+
const mockToggle = vi.fn();
|
|
11
|
+
const mockUpdate = vi.fn();
|
|
12
|
+
|
|
13
|
+
vi.mock("@perspective-ai/sdk", () => ({
|
|
14
|
+
createFloatBubble: vi.fn(() => ({
|
|
15
|
+
unmount: mockUnmount,
|
|
16
|
+
update: mockUpdate,
|
|
17
|
+
destroy: mockUnmount,
|
|
18
|
+
open: mockOpen,
|
|
19
|
+
close: mockClose,
|
|
20
|
+
toggle: mockToggle,
|
|
21
|
+
researchId: "test-research-id",
|
|
22
|
+
type: "float",
|
|
23
|
+
iframe: null,
|
|
24
|
+
container: null,
|
|
25
|
+
isOpen: false,
|
|
26
|
+
})),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { createFloatBubble } from "@perspective-ai/sdk";
|
|
30
|
+
const mockCreateFloatBubble = vi.mocked(createFloatBubble);
|
|
31
|
+
|
|
32
|
+
describe("FloatBubble", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
cleanup();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders nothing (bubble is added to document.body)", () => {
|
|
42
|
+
const { container } = render(<FloatBubble researchId="test-research-id" />);
|
|
43
|
+
|
|
44
|
+
expect(container.innerHTML).toBe("");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("calls createFloatBubble on mount", () => {
|
|
48
|
+
render(<FloatBubble researchId="test-research-id" />);
|
|
49
|
+
|
|
50
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledWith(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
researchId: "test-research-id",
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("calls unmount on cleanup", () => {
|
|
59
|
+
const { unmount } = render(<FloatBubble researchId="test-research-id" />);
|
|
60
|
+
|
|
61
|
+
expect(mockUnmount).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
unmount();
|
|
64
|
+
|
|
65
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("passes config to createFloatBubble", () => {
|
|
69
|
+
const onReady = vi.fn();
|
|
70
|
+
const onSubmit = vi.fn();
|
|
71
|
+
const onClose = vi.fn();
|
|
72
|
+
const onError = vi.fn();
|
|
73
|
+
|
|
74
|
+
render(
|
|
75
|
+
<FloatBubble
|
|
76
|
+
researchId="test-research-id"
|
|
77
|
+
params={{ source: "test" }}
|
|
78
|
+
theme="dark"
|
|
79
|
+
host="https://custom.example.com"
|
|
80
|
+
onReady={onReady}
|
|
81
|
+
onSubmit={onSubmit}
|
|
82
|
+
onClose={onClose}
|
|
83
|
+
onError={onError}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(1);
|
|
88
|
+
const config = mockCreateFloatBubble.mock.calls[0]![0];
|
|
89
|
+
expect(config.researchId).toBe("test-research-id");
|
|
90
|
+
expect(config.params).toEqual({ source: "test" });
|
|
91
|
+
expect(config.theme).toBe("dark");
|
|
92
|
+
expect(config.host).toBe("https://custom.example.com");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("exposes handle via embedRef", () => {
|
|
96
|
+
const mockHandle: FloatHandle = {
|
|
97
|
+
unmount: mockUnmount,
|
|
98
|
+
update: mockUpdate,
|
|
99
|
+
destroy: mockUnmount,
|
|
100
|
+
open: mockOpen,
|
|
101
|
+
close: mockClose,
|
|
102
|
+
toggle: mockToggle,
|
|
103
|
+
researchId: "test-research-id",
|
|
104
|
+
type: "float",
|
|
105
|
+
iframe: null,
|
|
106
|
+
container: null,
|
|
107
|
+
isOpen: false,
|
|
108
|
+
};
|
|
109
|
+
mockCreateFloatBubble.mockReturnValueOnce(mockHandle);
|
|
110
|
+
|
|
111
|
+
const embedRef = createRef<FloatHandle | null>();
|
|
112
|
+
|
|
113
|
+
render(<FloatBubble researchId="test-research-id" embedRef={embedRef} />);
|
|
114
|
+
|
|
115
|
+
expect(embedRef.current).toBe(mockHandle);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("clears embedRef on unmount", () => {
|
|
119
|
+
const embedRef = createRef<FloatHandle | null>();
|
|
120
|
+
|
|
121
|
+
const { unmount } = render(
|
|
122
|
+
<FloatBubble researchId="test-research-id" embedRef={embedRef} />
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(embedRef.current).not.toBeNull();
|
|
126
|
+
|
|
127
|
+
unmount();
|
|
128
|
+
|
|
129
|
+
expect(embedRef.current).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("re-creates float bubble when researchId changes", () => {
|
|
133
|
+
const { rerender } = render(<FloatBubble researchId="research-1" />);
|
|
134
|
+
|
|
135
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(1);
|
|
136
|
+
|
|
137
|
+
rerender(<FloatBubble researchId="research-2" />);
|
|
138
|
+
|
|
139
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("re-creates float bubble when theme changes", () => {
|
|
144
|
+
const { rerender } = render(
|
|
145
|
+
<FloatBubble researchId="test-research-id" theme="light" />
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(1);
|
|
149
|
+
|
|
150
|
+
rerender(<FloatBubble researchId="test-research-id" theme="dark" />);
|
|
151
|
+
|
|
152
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(mockCreateFloatBubble).toHaveBeenCalledTimes(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("passes brand colors to createFloatBubble", () => {
|
|
157
|
+
render(
|
|
158
|
+
<FloatBubble
|
|
159
|
+
researchId="test-research-id"
|
|
160
|
+
brand={{
|
|
161
|
+
light: { primary: "#ff0000", bg: "#ffffff" },
|
|
162
|
+
dark: { primary: "#0000ff", bg: "#000000" },
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const config = mockCreateFloatBubble.mock.calls[0]![0];
|
|
168
|
+
expect(config.brand).toEqual({
|
|
169
|
+
light: { primary: "#ff0000", bg: "#ffffff" },
|
|
170
|
+
dark: { primary: "#0000ff", bg: "#000000" },
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useRef, useEffect, type RefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
createFloatBubble,
|
|
4
|
+
type EmbedConfig,
|
|
5
|
+
type FloatHandle,
|
|
6
|
+
} from "@perspective-ai/sdk";
|
|
7
|
+
import { useStableCallback } from "./hooks/useStableCallback";
|
|
8
|
+
|
|
9
|
+
export interface FloatBubbleProps extends Omit<EmbedConfig, "type"> {
|
|
10
|
+
/** Ref to access the handle for programmatic control */
|
|
11
|
+
embedRef?: RefObject<FloatHandle | null>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Floating bubble widget that expands into a chat window.
|
|
16
|
+
* Renders a floating button in the corner of the screen.
|
|
17
|
+
*/
|
|
18
|
+
export function FloatBubble({
|
|
19
|
+
researchId,
|
|
20
|
+
params,
|
|
21
|
+
brand,
|
|
22
|
+
theme,
|
|
23
|
+
host,
|
|
24
|
+
onReady,
|
|
25
|
+
onSubmit,
|
|
26
|
+
onNavigate,
|
|
27
|
+
onClose,
|
|
28
|
+
onError,
|
|
29
|
+
embedRef,
|
|
30
|
+
}: FloatBubbleProps) {
|
|
31
|
+
const handleRef = useRef<FloatHandle | null>(null);
|
|
32
|
+
|
|
33
|
+
// Stable callbacks
|
|
34
|
+
const stableOnReady = useStableCallback(onReady);
|
|
35
|
+
const stableOnSubmit = useStableCallback(onSubmit);
|
|
36
|
+
const stableOnNavigate = useStableCallback(onNavigate);
|
|
37
|
+
const stableOnClose = useStableCallback(onClose);
|
|
38
|
+
const stableOnError = useStableCallback(onError);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handle = createFloatBubble({
|
|
42
|
+
researchId,
|
|
43
|
+
params,
|
|
44
|
+
brand,
|
|
45
|
+
theme,
|
|
46
|
+
host,
|
|
47
|
+
onReady: stableOnReady,
|
|
48
|
+
onSubmit: stableOnSubmit,
|
|
49
|
+
onNavigate: stableOnNavigate,
|
|
50
|
+
onClose: stableOnClose,
|
|
51
|
+
onError: stableOnError,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
handleRef.current = handle;
|
|
55
|
+
|
|
56
|
+
if (embedRef) {
|
|
57
|
+
embedRef.current = handle;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
handle.unmount();
|
|
62
|
+
handleRef.current = null;
|
|
63
|
+
if (embedRef) {
|
|
64
|
+
embedRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [
|
|
68
|
+
researchId,
|
|
69
|
+
params,
|
|
70
|
+
brand,
|
|
71
|
+
theme,
|
|
72
|
+
host,
|
|
73
|
+
stableOnReady,
|
|
74
|
+
stableOnSubmit,
|
|
75
|
+
stableOnNavigate,
|
|
76
|
+
stableOnClose,
|
|
77
|
+
stableOnError,
|
|
78
|
+
embedRef,
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// This component doesn't render anything - the bubble is added to document.body
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, cleanup } from "@testing-library/react";
|
|
3
|
+
import { createRef } from "react";
|
|
4
|
+
import { Fullpage } from "./Fullpage";
|
|
5
|
+
import type { EmbedHandle } from "@perspective-ai/sdk";
|
|
6
|
+
|
|
7
|
+
const mockUnmount = vi.fn();
|
|
8
|
+
const mockUpdate = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock("@perspective-ai/sdk", () => ({
|
|
11
|
+
createFullpage: vi.fn(() => ({
|
|
12
|
+
unmount: mockUnmount,
|
|
13
|
+
update: mockUpdate,
|
|
14
|
+
destroy: mockUnmount,
|
|
15
|
+
researchId: "test-research-id",
|
|
16
|
+
type: "fullpage",
|
|
17
|
+
iframe: null,
|
|
18
|
+
container: null,
|
|
19
|
+
})),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { createFullpage } from "@perspective-ai/sdk";
|
|
23
|
+
const mockCreateFullpage = vi.mocked(createFullpage);
|
|
24
|
+
|
|
25
|
+
describe("Fullpage", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
cleanup();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders nothing (fullpage overlay is added to document.body)", () => {
|
|
35
|
+
const { container } = render(<Fullpage researchId="test-research-id" />);
|
|
36
|
+
|
|
37
|
+
expect(container.innerHTML).toBe("");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("calls createFullpage on mount", () => {
|
|
41
|
+
render(<Fullpage researchId="test-research-id" />);
|
|
42
|
+
|
|
43
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(mockCreateFullpage).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
researchId: "test-research-id",
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("calls unmount on cleanup", () => {
|
|
52
|
+
const { unmount } = render(<Fullpage researchId="test-research-id" />);
|
|
53
|
+
|
|
54
|
+
expect(mockUnmount).not.toHaveBeenCalled();
|
|
55
|
+
|
|
56
|
+
unmount();
|
|
57
|
+
|
|
58
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("passes config to createFullpage", () => {
|
|
62
|
+
const onReady = vi.fn();
|
|
63
|
+
const onSubmit = vi.fn();
|
|
64
|
+
const onClose = vi.fn();
|
|
65
|
+
const onNavigate = vi.fn();
|
|
66
|
+
const onError = vi.fn();
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<Fullpage
|
|
70
|
+
researchId="test-research-id"
|
|
71
|
+
params={{ source: "test" }}
|
|
72
|
+
theme="dark"
|
|
73
|
+
host="https://custom.example.com"
|
|
74
|
+
onReady={onReady}
|
|
75
|
+
onSubmit={onSubmit}
|
|
76
|
+
onClose={onClose}
|
|
77
|
+
onNavigate={onNavigate}
|
|
78
|
+
onError={onError}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(1);
|
|
83
|
+
const config = mockCreateFullpage.mock.calls[0]![0];
|
|
84
|
+
expect(config.researchId).toBe("test-research-id");
|
|
85
|
+
expect(config.params).toEqual({ source: "test" });
|
|
86
|
+
expect(config.theme).toBe("dark");
|
|
87
|
+
expect(config.host).toBe("https://custom.example.com");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("exposes handle via embedRef", () => {
|
|
91
|
+
const mockHandle: EmbedHandle = {
|
|
92
|
+
unmount: mockUnmount,
|
|
93
|
+
update: mockUpdate,
|
|
94
|
+
destroy: mockUnmount,
|
|
95
|
+
researchId: "test-research-id",
|
|
96
|
+
type: "fullpage",
|
|
97
|
+
iframe: null,
|
|
98
|
+
container: null,
|
|
99
|
+
};
|
|
100
|
+
mockCreateFullpage.mockReturnValueOnce(mockHandle);
|
|
101
|
+
|
|
102
|
+
const embedRef = createRef<EmbedHandle | null>();
|
|
103
|
+
|
|
104
|
+
render(<Fullpage researchId="test-research-id" embedRef={embedRef} />);
|
|
105
|
+
|
|
106
|
+
expect(embedRef.current).toBe(mockHandle);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("clears embedRef on unmount", () => {
|
|
110
|
+
const embedRef = createRef<EmbedHandle | null>();
|
|
111
|
+
|
|
112
|
+
const { unmount } = render(
|
|
113
|
+
<Fullpage researchId="test-research-id" embedRef={embedRef} />
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(embedRef.current).not.toBeNull();
|
|
117
|
+
|
|
118
|
+
unmount();
|
|
119
|
+
|
|
120
|
+
expect(embedRef.current).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("re-creates fullpage when researchId changes", () => {
|
|
124
|
+
const { rerender } = render(<Fullpage researchId="research-1" />);
|
|
125
|
+
|
|
126
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(1);
|
|
127
|
+
|
|
128
|
+
rerender(<Fullpage researchId="research-2" />);
|
|
129
|
+
|
|
130
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("re-creates fullpage when theme changes", () => {
|
|
135
|
+
const { rerender } = render(
|
|
136
|
+
<Fullpage researchId="test-research-id" theme="light" />
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(1);
|
|
140
|
+
|
|
141
|
+
rerender(<Fullpage researchId="test-research-id" theme="dark" />);
|
|
142
|
+
|
|
143
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(mockCreateFullpage).toHaveBeenCalledTimes(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("passes brand colors to createFullpage", () => {
|
|
148
|
+
render(
|
|
149
|
+
<Fullpage
|
|
150
|
+
researchId="test-research-id"
|
|
151
|
+
brand={{
|
|
152
|
+
light: { primary: "#ff0000", bg: "#ffffff" },
|
|
153
|
+
dark: { primary: "#0000ff", bg: "#000000" },
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const config = mockCreateFullpage.mock.calls[0]![0];
|
|
159
|
+
expect(config.brand).toEqual({
|
|
160
|
+
light: { primary: "#ff0000", bg: "#ffffff" },
|
|
161
|
+
dark: { primary: "#0000ff", bg: "#000000" },
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/Fullpage.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useRef, useEffect, type RefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
createFullpage,
|
|
4
|
+
type EmbedConfig,
|
|
5
|
+
type EmbedHandle,
|
|
6
|
+
} from "@perspective-ai/sdk";
|
|
7
|
+
import { useStableCallback } from "./hooks/useStableCallback";
|
|
8
|
+
|
|
9
|
+
export interface FullpageProps extends Omit<EmbedConfig, "type"> {
|
|
10
|
+
/** Ref to access the embed handle for programmatic control */
|
|
11
|
+
embedRef?: RefObject<EmbedHandle | null>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Full viewport embed component.
|
|
16
|
+
* Takes over the entire screen with the interview.
|
|
17
|
+
*/
|
|
18
|
+
export function Fullpage({
|
|
19
|
+
researchId,
|
|
20
|
+
params,
|
|
21
|
+
brand,
|
|
22
|
+
theme,
|
|
23
|
+
host,
|
|
24
|
+
onReady,
|
|
25
|
+
onSubmit,
|
|
26
|
+
onNavigate,
|
|
27
|
+
onClose,
|
|
28
|
+
onError,
|
|
29
|
+
embedRef,
|
|
30
|
+
}: FullpageProps) {
|
|
31
|
+
const handleRef = useRef<EmbedHandle | null>(null);
|
|
32
|
+
|
|
33
|
+
// Stable callbacks
|
|
34
|
+
const stableOnReady = useStableCallback(onReady);
|
|
35
|
+
const stableOnSubmit = useStableCallback(onSubmit);
|
|
36
|
+
const stableOnNavigate = useStableCallback(onNavigate);
|
|
37
|
+
const stableOnClose = useStableCallback(onClose);
|
|
38
|
+
const stableOnError = useStableCallback(onError);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handle = createFullpage({
|
|
42
|
+
researchId,
|
|
43
|
+
params,
|
|
44
|
+
brand,
|
|
45
|
+
theme,
|
|
46
|
+
host,
|
|
47
|
+
onReady: stableOnReady,
|
|
48
|
+
onSubmit: stableOnSubmit,
|
|
49
|
+
onNavigate: stableOnNavigate,
|
|
50
|
+
onClose: stableOnClose,
|
|
51
|
+
onError: stableOnError,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
handleRef.current = handle;
|
|
55
|
+
|
|
56
|
+
if (embedRef) {
|
|
57
|
+
embedRef.current = handle;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
handle.unmount();
|
|
62
|
+
handleRef.current = null;
|
|
63
|
+
if (embedRef) {
|
|
64
|
+
embedRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [
|
|
68
|
+
researchId,
|
|
69
|
+
params,
|
|
70
|
+
brand,
|
|
71
|
+
theme,
|
|
72
|
+
host,
|
|
73
|
+
stableOnReady,
|
|
74
|
+
stableOnSubmit,
|
|
75
|
+
stableOnNavigate,
|
|
76
|
+
stableOnClose,
|
|
77
|
+
stableOnError,
|
|
78
|
+
embedRef,
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// This component doesn't render anything - the fullpage overlay is added to document.body
|
|
82
|
+
return null;
|
|
83
|
+
}
|