@voidx-ai/react 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.
- package/README.md +263 -0
- package/dist/index.cjs +171 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +182 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# @voidx/react
|
|
2
|
+
|
|
3
|
+
React/Next.js wrapper for [VoidX AI Addon](https://voidx.ai). Install one package, drop one component, and get a 3D AI agent with chat and voice on your site.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @voidx/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Requires React 18 or later.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// app/layout.tsx (Next.js App Router)
|
|
17
|
+
import { VoidxAddon } from '@voidx/react';
|
|
18
|
+
|
|
19
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<html>
|
|
22
|
+
<body>
|
|
23
|
+
<VoidxAddon apiKey={process.env.NEXT_PUBLIC_VOIDX_API_KEY!} />
|
|
24
|
+
{children}
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This alone gives you a 3D AI agent with chat and voice.
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Required | Description |
|
|
36
|
+
|------|------|----------|-------------|
|
|
37
|
+
| `apiKey` | `string` | Yes | JWT API key from VoidX dashboard |
|
|
38
|
+
| `locale` | `'ko' \| 'en' \| 'ja' \| 'zh'` | No | Language (default: `'ko'`) |
|
|
39
|
+
| `features` | `VoidxFeatures` | No | Enable/disable features |
|
|
40
|
+
| `theme` | `VoidxTheme` | No | Color, font, border-radius customization |
|
|
41
|
+
| `position` | `'follow-cursor' \| 'bottom-right' \| 'bottom-left'` | No | Agent position |
|
|
42
|
+
| `actions` | `Record<string, (params) => Promise<any>>` | No | Custom actions the AI can invoke |
|
|
43
|
+
| `systemEvents` | `Record<string, SystemEventHandler>` | No | Window event listeners that trigger AI responses |
|
|
44
|
+
| `onReady` | `() => void` | No | Called when SDK is fully loaded |
|
|
45
|
+
| `onError` | `(error: Error) => void` | No | Called on SDK errors |
|
|
46
|
+
| `disabled` | `boolean` | No | Set `true` to skip SDK loading |
|
|
47
|
+
| `children` | `React.ReactNode` | No | Children can use `useVoidx()` hook |
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
Control which features are enabled:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<VoidxAddon
|
|
55
|
+
apiKey="..."
|
|
56
|
+
features={{
|
|
57
|
+
chat: true, // Chat popup (default: true)
|
|
58
|
+
cursor: true, // 3D cursor agent (default: true)
|
|
59
|
+
voice: true, // Voice agent (default: true)
|
|
60
|
+
hoverDetection: true, // Hover detection (default: true)
|
|
61
|
+
agentMode: '3d', // '3d' or 'parallax' (default: '3d')
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Actions
|
|
67
|
+
|
|
68
|
+
Actions are functions **you define** that the AI can call during a conversation. The AI decides when to call them based on what the user asks.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<VoidxAddon
|
|
72
|
+
apiKey="..."
|
|
73
|
+
actions={{
|
|
74
|
+
// AI calls this when user asks to search
|
|
75
|
+
search_products: async (params) => {
|
|
76
|
+
const res = await fetch(`/api/products?q=${params.query}`);
|
|
77
|
+
return res.json();
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// AI calls this when user wants to add to cart
|
|
81
|
+
add_to_cart: async (params) => {
|
|
82
|
+
cartStore.addItem(params.productId);
|
|
83
|
+
return { success: true, message: 'Added to cart' };
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// AI calls this when user wants to navigate
|
|
87
|
+
navigate_to: async (params) => {
|
|
88
|
+
router.push(params.path);
|
|
89
|
+
return { success: true };
|
|
90
|
+
},
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**How it works:**
|
|
96
|
+
|
|
97
|
+
1. You register action functions with names like `search_products`, `add_to_cart`
|
|
98
|
+
2. The AI learns that these actions are available
|
|
99
|
+
3. During conversation, the AI decides which action to call based on user intent
|
|
100
|
+
4. Your function runs, and the result is sent back to the AI
|
|
101
|
+
5. The AI uses the result to generate a natural response
|
|
102
|
+
|
|
103
|
+
You can put any JavaScript logic inside an action: API calls, state management, DOM manipulation, page navigation, etc.
|
|
104
|
+
|
|
105
|
+
## System Events
|
|
106
|
+
|
|
107
|
+
Listen for custom window events (e.g., order completed) and trigger an AI response automatically:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<VoidxAddon
|
|
111
|
+
apiKey="..."
|
|
112
|
+
systemEvents={{
|
|
113
|
+
'order-completed': {
|
|
114
|
+
handler: (detail, instance, locale) => ({
|
|
115
|
+
context: `Order ${detail.orderNumber} completed. Total: $${detail.total}`,
|
|
116
|
+
instruction: 'Thank the customer and mention shipping info.',
|
|
117
|
+
openChat: true, // Auto-open chat popup
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then dispatch the event from anywhere in your app:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// e.g., after checkout
|
|
128
|
+
window.dispatchEvent(
|
|
129
|
+
new CustomEvent('order-completed', {
|
|
130
|
+
detail: { orderNumber: 'ORD-123', total: 39.99 },
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The AI will automatically respond with a contextual message like "Thank you for your order!"
|
|
136
|
+
|
|
137
|
+
## useVoidx() Hook
|
|
138
|
+
|
|
139
|
+
Access the SDK instance from any child component:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { VoidxAddon, useVoidx } from '@voidx/react';
|
|
143
|
+
|
|
144
|
+
// Parent: wrap children with VoidxAddon
|
|
145
|
+
function Layout({ children }) {
|
|
146
|
+
return (
|
|
147
|
+
<VoidxAddon apiKey="...">
|
|
148
|
+
{children}
|
|
149
|
+
</VoidxAddon>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Child: use the hook
|
|
154
|
+
function AskAIButton() {
|
|
155
|
+
const voidx = useVoidx(); // null until SDK is ready
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<button onClick={() => voidx?.sendMessage('Recommend something')}>
|
|
159
|
+
Ask AI
|
|
160
|
+
</button>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Available methods on `voidx`:
|
|
166
|
+
|
|
167
|
+
| Method | Description |
|
|
168
|
+
|--------|-------------|
|
|
169
|
+
| `show()` | Open chat popup |
|
|
170
|
+
| `hide()` | Close chat popup |
|
|
171
|
+
| `sendMessage(text)` | Send a message as the user |
|
|
172
|
+
| `announceSystemEvent(context, options?)` | Trigger an AI response without user message |
|
|
173
|
+
| `setLocale(locale)` | Change language |
|
|
174
|
+
| `setTheme(theme)` | Update theme |
|
|
175
|
+
| `on(event, handler)` | Listen to SDK events |
|
|
176
|
+
| `off(event, handler)` | Remove event listener |
|
|
177
|
+
| `destroy()` | Remove SDK from page |
|
|
178
|
+
|
|
179
|
+
## Theme
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
<VoidxAddon
|
|
183
|
+
apiKey="..."
|
|
184
|
+
theme={{
|
|
185
|
+
primaryColor: '#6366f1',
|
|
186
|
+
gradientFrom: '#8b5cf6',
|
|
187
|
+
gradientTo: '#6366f1',
|
|
188
|
+
fontFamily: 'Pretendard, sans-serif',
|
|
189
|
+
borderRadius: 16,
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## SDK Events
|
|
195
|
+
|
|
196
|
+
Listen to SDK lifecycle events via `useVoidx()`:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const voidx = useVoidx();
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!voidx) return;
|
|
203
|
+
|
|
204
|
+
const onMessage = (msg) => console.log('New message:', msg);
|
|
205
|
+
voidx.on('voidx:message', onMessage);
|
|
206
|
+
|
|
207
|
+
return () => voidx.off('voidx:message', onMessage);
|
|
208
|
+
}, [voidx]);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Available events: `voidx:ready`, `voidx:error`, `voidx:message`, `voidx:chat:open`, `voidx:chat:close`, `voidx:voice:start`, `voidx:voice:end`, `voidx:voice:error`, `voidx:auth:expired`, `voidx:action:executed`, `voidx:action:error`, `voidx:destroy`
|
|
212
|
+
|
|
213
|
+
## Full Example
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// app/layout.tsx
|
|
217
|
+
import { VoidxAddon } from '@voidx/react';
|
|
218
|
+
|
|
219
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
220
|
+
return (
|
|
221
|
+
<html>
|
|
222
|
+
<body>
|
|
223
|
+
<VoidxAddon
|
|
224
|
+
apiKey={process.env.NEXT_PUBLIC_VOIDX_API_KEY!}
|
|
225
|
+
locale="ko"
|
|
226
|
+
features={{ chat: true, cursor: true, voice: true, agentMode: '3d' }}
|
|
227
|
+
theme={{ primaryColor: '#6366f1', borderRadius: 16 }}
|
|
228
|
+
actions={{
|
|
229
|
+
search: async ({ query }) => {
|
|
230
|
+
const res = await fetch(`/api/search?q=${query}`);
|
|
231
|
+
return res.json();
|
|
232
|
+
},
|
|
233
|
+
navigate: async ({ path }) => {
|
|
234
|
+
window.location.href = path;
|
|
235
|
+
return { success: true };
|
|
236
|
+
},
|
|
237
|
+
}}
|
|
238
|
+
systemEvents={{
|
|
239
|
+
'checkout-done': {
|
|
240
|
+
handler: (detail, _instance, locale) => ({
|
|
241
|
+
context: locale === 'en'
|
|
242
|
+
? `Order ${detail.id} placed, total $${detail.total}`
|
|
243
|
+
: `주문 ${detail.id} 완료, 합계 $${detail.total}`,
|
|
244
|
+
instruction: locale === 'en'
|
|
245
|
+
? 'Thank the user warmly.'
|
|
246
|
+
: '따뜻하게 감사 인사를 전해주세요.',
|
|
247
|
+
openChat: true,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
}}
|
|
251
|
+
onReady={() => console.log('VoidX ready')}
|
|
252
|
+
>
|
|
253
|
+
{children}
|
|
254
|
+
</VoidxAddon>
|
|
255
|
+
</body>
|
|
256
|
+
</html>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/VoidxAddon.tsx
|
|
7
|
+
var VoidxContext = react.createContext(null);
|
|
8
|
+
function useVoidx() {
|
|
9
|
+
return react.useContext(VoidxContext);
|
|
10
|
+
}
|
|
11
|
+
function useScript(src, disabled) {
|
|
12
|
+
const [status, setStatus] = react.useState("idle");
|
|
13
|
+
react.useEffect(() => {
|
|
14
|
+
if (disabled) {
|
|
15
|
+
setStatus("idle");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const existing = document.querySelector(
|
|
19
|
+
`script[src="${src}"]`
|
|
20
|
+
);
|
|
21
|
+
if (existing) {
|
|
22
|
+
if (window.VoidxAIAddon) {
|
|
23
|
+
setStatus("ready");
|
|
24
|
+
} else {
|
|
25
|
+
const onLoad = () => setStatus("ready");
|
|
26
|
+
const onError = () => setStatus("error");
|
|
27
|
+
existing.addEventListener("load", onLoad);
|
|
28
|
+
existing.addEventListener("error", onError);
|
|
29
|
+
setStatus("loading");
|
|
30
|
+
return () => {
|
|
31
|
+
existing.removeEventListener("load", onLoad);
|
|
32
|
+
existing.removeEventListener("error", onError);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const script = document.createElement("script");
|
|
38
|
+
script.src = src;
|
|
39
|
+
script.async = true;
|
|
40
|
+
script.addEventListener("load", () => setStatus("ready"));
|
|
41
|
+
script.addEventListener("error", () => setStatus("error"));
|
|
42
|
+
setStatus("loading");
|
|
43
|
+
document.body.appendChild(script);
|
|
44
|
+
return () => {
|
|
45
|
+
if (document.body.contains(script)) {
|
|
46
|
+
document.body.removeChild(script);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}, [src, disabled]);
|
|
50
|
+
return status;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/utils.ts
|
|
54
|
+
var DEFAULT_CDN_URL = "https://addon-cdn.voidx.ai/latest/voidx-ai-addon.standalone.js";
|
|
55
|
+
function normalizeLang(value) {
|
|
56
|
+
switch (value) {
|
|
57
|
+
case "en":
|
|
58
|
+
return "en";
|
|
59
|
+
case "ja":
|
|
60
|
+
return "ja";
|
|
61
|
+
case "zh":
|
|
62
|
+
return "zh";
|
|
63
|
+
default:
|
|
64
|
+
return "ko";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function VoidxAddon({
|
|
68
|
+
apiKey,
|
|
69
|
+
features,
|
|
70
|
+
theme,
|
|
71
|
+
locale,
|
|
72
|
+
position,
|
|
73
|
+
baseUrl,
|
|
74
|
+
scriptUrl,
|
|
75
|
+
cdnUrl,
|
|
76
|
+
voiceApiUrl,
|
|
77
|
+
hoverTargets,
|
|
78
|
+
actions,
|
|
79
|
+
systemEvents,
|
|
80
|
+
onReady,
|
|
81
|
+
onError,
|
|
82
|
+
onMessage,
|
|
83
|
+
disabled = false,
|
|
84
|
+
children
|
|
85
|
+
}) {
|
|
86
|
+
const instanceRef = react.useRef(null);
|
|
87
|
+
const [instance, setInstance] = react.useState(null);
|
|
88
|
+
const localeRef = react.useRef(normalizeLang(locale));
|
|
89
|
+
const resolvedScriptUrl = scriptUrl ?? DEFAULT_CDN_URL;
|
|
90
|
+
const scriptStatus = useScript(resolvedScriptUrl, disabled);
|
|
91
|
+
react.useEffect(() => {
|
|
92
|
+
if (disabled || scriptStatus !== "ready" || !window.VoidxAIAddon) return;
|
|
93
|
+
const normalizedLocale = normalizeLang(locale);
|
|
94
|
+
localeRef.current = normalizedLocale;
|
|
95
|
+
const config = {
|
|
96
|
+
apiKey,
|
|
97
|
+
locale: normalizedLocale,
|
|
98
|
+
...features && { features },
|
|
99
|
+
...theme && { theme },
|
|
100
|
+
...position && { position },
|
|
101
|
+
...baseUrl && { baseUrl },
|
|
102
|
+
...cdnUrl && { cdnUrl },
|
|
103
|
+
...voiceApiUrl && { voiceApiUrl },
|
|
104
|
+
...hoverTargets && { hoverTargets },
|
|
105
|
+
...actions && { actions },
|
|
106
|
+
...onReady && { onReady },
|
|
107
|
+
...onError && { onError },
|
|
108
|
+
...onMessage && { onMessage }
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const inst = window.VoidxAIAddon.init(config);
|
|
112
|
+
instanceRef.current = inst;
|
|
113
|
+
setInstance(inst);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
116
|
+
console.error("[VoidxAddon] init failed:", err);
|
|
117
|
+
onError?.(err);
|
|
118
|
+
}
|
|
119
|
+
return () => {
|
|
120
|
+
instanceRef.current?.destroy();
|
|
121
|
+
instanceRef.current = null;
|
|
122
|
+
setInstance(null);
|
|
123
|
+
};
|
|
124
|
+
}, [apiKey, scriptStatus, disabled]);
|
|
125
|
+
react.useEffect(() => {
|
|
126
|
+
const normalized = normalizeLang(locale);
|
|
127
|
+
localeRef.current = normalized;
|
|
128
|
+
instanceRef.current?.setLocale(normalized);
|
|
129
|
+
}, [locale]);
|
|
130
|
+
react.useEffect(() => {
|
|
131
|
+
if (theme && instanceRef.current) {
|
|
132
|
+
instanceRef.current.setTheme(theme);
|
|
133
|
+
}
|
|
134
|
+
}, [theme]);
|
|
135
|
+
const systemEventsRef = react.useRef(systemEvents);
|
|
136
|
+
systemEventsRef.current = systemEvents;
|
|
137
|
+
react.useEffect(() => {
|
|
138
|
+
const inst = instanceRef.current;
|
|
139
|
+
if (!inst || !systemEventsRef.current) return;
|
|
140
|
+
const entries = Object.entries(systemEventsRef.current);
|
|
141
|
+
if (entries.length === 0) return;
|
|
142
|
+
const cleanups = [];
|
|
143
|
+
for (const [eventName, { handler }] of entries) {
|
|
144
|
+
const listener = (event) => {
|
|
145
|
+
const detail = event.detail;
|
|
146
|
+
const result = handler(detail, inst, localeRef.current);
|
|
147
|
+
if (result) {
|
|
148
|
+
void inst.announceSystemEvent(result.context, {
|
|
149
|
+
instruction: result.instruction,
|
|
150
|
+
openChat: result.openChat
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
window.addEventListener(eventName, listener);
|
|
155
|
+
cleanups.push(() => window.removeEventListener(eventName, listener));
|
|
156
|
+
}
|
|
157
|
+
return () => {
|
|
158
|
+
for (const cleanup of cleanups) cleanup();
|
|
159
|
+
};
|
|
160
|
+
}, [instance]);
|
|
161
|
+
if (disabled) return null;
|
|
162
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(VoidxContext.Provider, { value: instance, children: [
|
|
163
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "voidx-ai-addon-root" }),
|
|
164
|
+
children
|
|
165
|
+
] });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
exports.VoidxAddon = VoidxAddon;
|
|
169
|
+
exports.useVoidx = useVoidx;
|
|
170
|
+
//# sourceMappingURL=index.cjs.map
|
|
171
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts","../src/use-script.ts","../src/utils.ts","../src/VoidxAddon.tsx"],"names":["createContext","useContext","useState","useEffect","useRef","jsxs","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext } from 'react';\nimport type { VoidxInstance } from './types';\n\nexport const VoidxContext = createContext<VoidxInstance | null>(null);\n\n/**\n * VoidxAddon 인스턴스에 접근하는 훅.\n * SDK 로딩 전이면 null을 반환한다.\n *\n * @example\n * ```tsx\n * function AskAIButton() {\n * const voidx = useVoidx();\n * return (\n * <button onClick={() => voidx?.sendMessage('추천해줘')}>\n * AI에게 물어보기\n * </button>\n * );\n * }\n * ```\n */\nexport function useVoidx(): VoidxInstance | null {\n return useContext(VoidxContext);\n}\n","import { useEffect, useState } from 'react';\n\ntype ScriptStatus = 'idle' | 'loading' | 'ready' | 'error';\n\n/**\n * CDN 스크립트를 동적으로 로딩하는 훅.\n * 이미 DOM에 동일 src가 있으면 중복 삽입하지 않는다.\n */\nexport function useScript(src: string, disabled: boolean): ScriptStatus {\n const [status, setStatus] = useState<ScriptStatus>('idle');\n\n useEffect(() => {\n if (disabled) {\n setStatus('idle');\n return;\n }\n\n // 이미 로드된 스크립트가 있으면 재사용\n const existing = document.querySelector(\n `script[src=\"${src}\"]`,\n ) as HTMLScriptElement | null;\n\n if (existing) {\n // window.VoidxAIAddon이 이미 있으면 ready\n if (window.VoidxAIAddon) {\n setStatus('ready');\n } else {\n // 아직 로딩 중인 스크립트 — 이벤트 대기\n const onLoad = () => setStatus('ready');\n const onError = () => setStatus('error');\n existing.addEventListener('load', onLoad);\n existing.addEventListener('error', onError);\n setStatus('loading');\n return () => {\n existing.removeEventListener('load', onLoad);\n existing.removeEventListener('error', onError);\n };\n }\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n\n script.addEventListener('load', () => setStatus('ready'));\n script.addEventListener('error', () => setStatus('error'));\n\n setStatus('loading');\n document.body.appendChild(script);\n\n return () => {\n if (document.body.contains(script)) {\n document.body.removeChild(script);\n }\n };\n }, [src, disabled]);\n\n return status;\n}\n","import type { VoidxLocale } from './types';\n\nexport const DEFAULT_CDN_URL =\n 'https://addon-cdn.voidx.ai/latest/voidx-ai-addon.standalone.js';\n\nexport function normalizeLang(value: string | undefined): VoidxLocale {\n switch (value) {\n case 'en':\n return 'en';\n case 'ja':\n return 'ja';\n case 'zh':\n return 'zh';\n default:\n return 'ko';\n }\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { VoidxContext } from './context';\nimport type {\n VoidxAddonProps,\n VoidxInitConfig,\n VoidxInstance,\n VoidxLocale,\n} from './types';\nimport { useScript } from './use-script';\nimport { DEFAULT_CDN_URL, normalizeLang } from './utils';\n\n/**\n * VoidX AI Addon React 래퍼 컴포넌트.\n *\n * CDN standalone 스크립트를 동적 로딩하고, SDK를 init/destroy 한다.\n * children에서 useVoidx() 훅으로 인스턴스에 접근할 수 있다.\n */\nexport function VoidxAddon({\n apiKey,\n features,\n theme,\n locale,\n position,\n baseUrl,\n scriptUrl,\n cdnUrl,\n voiceApiUrl,\n hoverTargets,\n actions,\n systemEvents,\n onReady,\n onError,\n onMessage,\n disabled = false,\n children,\n}: VoidxAddonProps) {\n const instanceRef = useRef<VoidxInstance | null>(null);\n const [instance, setInstance] = useState<VoidxInstance | null>(null);\n const localeRef = useRef<VoidxLocale>(normalizeLang(locale));\n\n const resolvedScriptUrl = scriptUrl ?? DEFAULT_CDN_URL;\n const scriptStatus = useScript(resolvedScriptUrl, disabled);\n\n // ── init / destroy ──\n useEffect(() => {\n if (disabled || scriptStatus !== 'ready' || !window.VoidxAIAddon) return;\n\n const normalizedLocale = normalizeLang(locale);\n localeRef.current = normalizedLocale;\n\n const config: VoidxInitConfig = {\n apiKey,\n locale: normalizedLocale,\n ...(features && { features }),\n ...(theme && { theme }),\n ...(position && { position }),\n ...(baseUrl && { baseUrl }),\n ...(cdnUrl && { cdnUrl }),\n ...(voiceApiUrl && { voiceApiUrl }),\n ...(hoverTargets && { hoverTargets }),\n ...(actions && { actions }),\n ...(onReady && { onReady }),\n ...(onError && { onError }),\n ...(onMessage && { onMessage }),\n };\n\n try {\n const inst = window.VoidxAIAddon!.init(config);\n instanceRef.current = inst;\n setInstance(inst);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.error('[VoidxAddon] init failed:', err);\n onError?.(err);\n }\n\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n setInstance(null);\n };\n // apiKey 변경 시 재초기화, actions/systemEvents는 ref로 관리하지 않고\n // init 시점에만 전달 (SDK가 런타임 교체를 지원하지 않음)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiKey, scriptStatus, disabled]);\n\n // ── locale sync ──\n useEffect(() => {\n const normalized = normalizeLang(locale);\n localeRef.current = normalized;\n instanceRef.current?.setLocale(normalized);\n }, [locale]);\n\n // ── theme sync ──\n useEffect(() => {\n if (theme && instanceRef.current) {\n instanceRef.current.setTheme(theme);\n }\n }, [theme]);\n\n // ── system events ──\n const systemEventsRef = useRef(systemEvents);\n systemEventsRef.current = systemEvents;\n\n useEffect(() => {\n const inst = instanceRef.current;\n if (!inst || !systemEventsRef.current) return;\n\n const entries = Object.entries(systemEventsRef.current);\n if (entries.length === 0) return;\n\n const cleanups: (() => void)[] = [];\n\n for (const [eventName, { handler }] of entries) {\n const listener = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n const result = handler(detail, inst, localeRef.current);\n if (result) {\n void inst.announceSystemEvent(result.context, {\n instruction: result.instruction,\n openChat: result.openChat,\n });\n }\n };\n\n window.addEventListener(eventName, listener);\n cleanups.push(() => window.removeEventListener(eventName, listener));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }, [instance]); // instance가 설정된 후에만 이벤트 등록\n\n if (disabled) return null;\n\n return (\n <VoidxContext.Provider value={instance}>\n <div id=\"voidx-ai-addon-root\" />\n {children}\n </VoidxContext.Provider>\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VoidX SDK 타입 정의
|
|
5
|
+
*
|
|
6
|
+
* voidx-ai-addon SDK의 src/types/sdk.ts를 미러링.
|
|
7
|
+
* SDK를 런타임에 import하면 React 버전 충돌이 발생하므로 자체 정의한다.
|
|
8
|
+
* 향후 SDK가 타입 전용 패키지를 배포하면 import type으로 전환 가능.
|
|
9
|
+
*/
|
|
10
|
+
type VoidxLocale = 'ko' | 'en' | 'ja' | 'zh';
|
|
11
|
+
interface VoidxFeatures {
|
|
12
|
+
/** 챗봇 팝업 (기본: true) */
|
|
13
|
+
chat?: boolean;
|
|
14
|
+
/** 커서 에이전트 (기본: true) */
|
|
15
|
+
cursor?: boolean;
|
|
16
|
+
/** 음성 에이전트 (기본: true) */
|
|
17
|
+
voice?: boolean;
|
|
18
|
+
/** 호버 감지 (기본: true) */
|
|
19
|
+
hoverDetection?: boolean;
|
|
20
|
+
/** 에이전트 렌더링 모드 (기본: '3d') */
|
|
21
|
+
agentMode?: '3d' | 'parallax';
|
|
22
|
+
}
|
|
23
|
+
interface VoidxTheme {
|
|
24
|
+
primaryColor?: string;
|
|
25
|
+
gradientFrom?: string;
|
|
26
|
+
gradientTo?: string;
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
borderRadius?: number;
|
|
29
|
+
}
|
|
30
|
+
type HostActionHandler = (params: any) => Promise<any>;
|
|
31
|
+
type HostActions = Record<string, HostActionHandler>;
|
|
32
|
+
interface AnnounceSystemEventOptions {
|
|
33
|
+
/** 에이전트 응답 가이드 메시지 */
|
|
34
|
+
instruction?: string;
|
|
35
|
+
/** 챗봇 팝업 자동 열기 (기본: false) */
|
|
36
|
+
openChat?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface VoidxEventMap {
|
|
39
|
+
'voidx:ready': void;
|
|
40
|
+
'voidx:error': Error;
|
|
41
|
+
'voidx:message': {
|
|
42
|
+
id: string;
|
|
43
|
+
role: string;
|
|
44
|
+
content: string;
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
'voidx:auth:expired': {
|
|
48
|
+
status: number;
|
|
49
|
+
};
|
|
50
|
+
'voidx:chat:open': void;
|
|
51
|
+
'voidx:chat:close': void;
|
|
52
|
+
'voidx:voice:start': void;
|
|
53
|
+
'voidx:voice:end': void;
|
|
54
|
+
'voidx:voice:error': {
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
'voidx:hover:detected': {
|
|
58
|
+
element: Element;
|
|
59
|
+
};
|
|
60
|
+
'voidx:action:executed': {
|
|
61
|
+
name: string;
|
|
62
|
+
params: any;
|
|
63
|
+
result: any;
|
|
64
|
+
};
|
|
65
|
+
'voidx:action:error': {
|
|
66
|
+
name: string;
|
|
67
|
+
params: any;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
'voidx:destroy': void;
|
|
71
|
+
}
|
|
72
|
+
interface VoidxInstance {
|
|
73
|
+
destroy: () => void;
|
|
74
|
+
show: () => void;
|
|
75
|
+
hide: () => void;
|
|
76
|
+
sendMessage: (message: string) => void;
|
|
77
|
+
announceSystemEvent: (context: string, options?: AnnounceSystemEventOptions) => Promise<void>;
|
|
78
|
+
setTheme: (theme: Partial<VoidxTheme>) => void;
|
|
79
|
+
setLocale: (locale: VoidxLocale) => void;
|
|
80
|
+
on: <K extends keyof VoidxEventMap>(event: K, handler: (data: VoidxEventMap[K]) => void) => void;
|
|
81
|
+
off: <K extends keyof VoidxEventMap>(event: K, handler: (data: VoidxEventMap[K]) => void) => void;
|
|
82
|
+
}
|
|
83
|
+
interface VoidxInitConfig {
|
|
84
|
+
apiKey: string;
|
|
85
|
+
baseUrl?: string;
|
|
86
|
+
cdnUrl?: string;
|
|
87
|
+
voiceApiUrl?: string;
|
|
88
|
+
features?: VoidxFeatures;
|
|
89
|
+
theme?: VoidxTheme;
|
|
90
|
+
locale?: VoidxLocale;
|
|
91
|
+
position?: 'follow-cursor' | 'bottom-right' | 'bottom-left';
|
|
92
|
+
hoverTargets?: {
|
|
93
|
+
classNames?: string[];
|
|
94
|
+
ids?: string[];
|
|
95
|
+
idPatterns?: RegExp[];
|
|
96
|
+
classNamePatterns?: RegExp[];
|
|
97
|
+
};
|
|
98
|
+
actions?: HostActions;
|
|
99
|
+
onError?: (error: Error) => void;
|
|
100
|
+
onMessage?: (message: VoidxEventMap['voidx:message']) => void;
|
|
101
|
+
onReady?: () => void;
|
|
102
|
+
}
|
|
103
|
+
interface VoidxAIAddonGlobal {
|
|
104
|
+
init: (config: VoidxInitConfig) => VoidxInstance;
|
|
105
|
+
}
|
|
106
|
+
declare global {
|
|
107
|
+
interface Window {
|
|
108
|
+
VoidxAIAddon?: VoidxAIAddonGlobal;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
interface SystemEventResult {
|
|
112
|
+
context: string;
|
|
113
|
+
instruction?: string;
|
|
114
|
+
openChat?: boolean;
|
|
115
|
+
}
|
|
116
|
+
interface SystemEventHandler<D = any> {
|
|
117
|
+
handler: (detail: D, instance: VoidxInstance, locale: VoidxLocale) => SystemEventResult | void | null;
|
|
118
|
+
}
|
|
119
|
+
interface VoidxAddonProps {
|
|
120
|
+
/** JWT API key — 필수 */
|
|
121
|
+
apiKey: string;
|
|
122
|
+
/** 기능 설정 */
|
|
123
|
+
features?: VoidxFeatures;
|
|
124
|
+
/** 테마 커스터마이징 */
|
|
125
|
+
theme?: VoidxTheme;
|
|
126
|
+
/** 언어 (반응형 — 변경 시 setLocale 자동 호출) */
|
|
127
|
+
locale?: VoidxLocale;
|
|
128
|
+
/** 팝업 위치 */
|
|
129
|
+
position?: 'follow-cursor' | 'bottom-right' | 'bottom-left';
|
|
130
|
+
/** API base URL 오버라이드 */
|
|
131
|
+
baseUrl?: string;
|
|
132
|
+
/** standalone.js 스크립트 URL 오버라이드 (기본: addon-cdn.voidx.ai) */
|
|
133
|
+
scriptUrl?: string;
|
|
134
|
+
/** SDK 내부 CDN URL 오버라이드 (3D 모델, 이미지 등) */
|
|
135
|
+
cdnUrl?: string;
|
|
136
|
+
/** Voice API URL 오버라이드 */
|
|
137
|
+
voiceApiUrl?: string;
|
|
138
|
+
/** 호버 감지 대상 */
|
|
139
|
+
hoverTargets?: VoidxInitConfig['hoverTargets'];
|
|
140
|
+
/** 호스트 액션 */
|
|
141
|
+
actions?: HostActions;
|
|
142
|
+
/** 시스템 이벤트 → announceSystemEvent 브리지 */
|
|
143
|
+
systemEvents?: Record<string, SystemEventHandler>;
|
|
144
|
+
/** SDK 준비 완료 콜백 */
|
|
145
|
+
onReady?: () => void;
|
|
146
|
+
/** 에러 콜백 */
|
|
147
|
+
onError?: (error: Error) => void;
|
|
148
|
+
/** 메시지 수신 콜백 */
|
|
149
|
+
onMessage?: (message: VoidxEventMap['voidx:message']) => void;
|
|
150
|
+
/** 비활성화 (true면 SDK 로딩 안 함) */
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
/** children (useVoidx() 훅으로 인스턴스 접근 가능) */
|
|
153
|
+
children?: React.ReactNode;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* VoidX AI Addon React 래퍼 컴포넌트.
|
|
158
|
+
*
|
|
159
|
+
* CDN standalone 스크립트를 동적 로딩하고, SDK를 init/destroy 한다.
|
|
160
|
+
* children에서 useVoidx() 훅으로 인스턴스에 접근할 수 있다.
|
|
161
|
+
*/
|
|
162
|
+
declare function VoidxAddon({ apiKey, features, theme, locale, position, baseUrl, scriptUrl, cdnUrl, voiceApiUrl, hoverTargets, actions, systemEvents, onReady, onError, onMessage, disabled, children, }: VoidxAddonProps): react_jsx_runtime.JSX.Element | null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* VoidxAddon 인스턴스에 접근하는 훅.
|
|
166
|
+
* SDK 로딩 전이면 null을 반환한다.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```tsx
|
|
170
|
+
* function AskAIButton() {
|
|
171
|
+
* const voidx = useVoidx();
|
|
172
|
+
* return (
|
|
173
|
+
* <button onClick={() => voidx?.sendMessage('추천해줘')}>
|
|
174
|
+
* AI에게 물어보기
|
|
175
|
+
* </button>
|
|
176
|
+
* );
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
declare function useVoidx(): VoidxInstance | null;
|
|
181
|
+
|
|
182
|
+
export { type AnnounceSystemEventOptions, type HostActionHandler, type HostActions, type SystemEventHandler, type SystemEventResult, VoidxAddon, type VoidxAddonProps, type VoidxEventMap, type VoidxFeatures, type VoidxInitConfig, type VoidxInstance, type VoidxLocale, type VoidxTheme, useVoidx };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VoidX SDK 타입 정의
|
|
5
|
+
*
|
|
6
|
+
* voidx-ai-addon SDK의 src/types/sdk.ts를 미러링.
|
|
7
|
+
* SDK를 런타임에 import하면 React 버전 충돌이 발생하므로 자체 정의한다.
|
|
8
|
+
* 향후 SDK가 타입 전용 패키지를 배포하면 import type으로 전환 가능.
|
|
9
|
+
*/
|
|
10
|
+
type VoidxLocale = 'ko' | 'en' | 'ja' | 'zh';
|
|
11
|
+
interface VoidxFeatures {
|
|
12
|
+
/** 챗봇 팝업 (기본: true) */
|
|
13
|
+
chat?: boolean;
|
|
14
|
+
/** 커서 에이전트 (기본: true) */
|
|
15
|
+
cursor?: boolean;
|
|
16
|
+
/** 음성 에이전트 (기본: true) */
|
|
17
|
+
voice?: boolean;
|
|
18
|
+
/** 호버 감지 (기본: true) */
|
|
19
|
+
hoverDetection?: boolean;
|
|
20
|
+
/** 에이전트 렌더링 모드 (기본: '3d') */
|
|
21
|
+
agentMode?: '3d' | 'parallax';
|
|
22
|
+
}
|
|
23
|
+
interface VoidxTheme {
|
|
24
|
+
primaryColor?: string;
|
|
25
|
+
gradientFrom?: string;
|
|
26
|
+
gradientTo?: string;
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
borderRadius?: number;
|
|
29
|
+
}
|
|
30
|
+
type HostActionHandler = (params: any) => Promise<any>;
|
|
31
|
+
type HostActions = Record<string, HostActionHandler>;
|
|
32
|
+
interface AnnounceSystemEventOptions {
|
|
33
|
+
/** 에이전트 응답 가이드 메시지 */
|
|
34
|
+
instruction?: string;
|
|
35
|
+
/** 챗봇 팝업 자동 열기 (기본: false) */
|
|
36
|
+
openChat?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface VoidxEventMap {
|
|
39
|
+
'voidx:ready': void;
|
|
40
|
+
'voidx:error': Error;
|
|
41
|
+
'voidx:message': {
|
|
42
|
+
id: string;
|
|
43
|
+
role: string;
|
|
44
|
+
content: string;
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
'voidx:auth:expired': {
|
|
48
|
+
status: number;
|
|
49
|
+
};
|
|
50
|
+
'voidx:chat:open': void;
|
|
51
|
+
'voidx:chat:close': void;
|
|
52
|
+
'voidx:voice:start': void;
|
|
53
|
+
'voidx:voice:end': void;
|
|
54
|
+
'voidx:voice:error': {
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
'voidx:hover:detected': {
|
|
58
|
+
element: Element;
|
|
59
|
+
};
|
|
60
|
+
'voidx:action:executed': {
|
|
61
|
+
name: string;
|
|
62
|
+
params: any;
|
|
63
|
+
result: any;
|
|
64
|
+
};
|
|
65
|
+
'voidx:action:error': {
|
|
66
|
+
name: string;
|
|
67
|
+
params: any;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
'voidx:destroy': void;
|
|
71
|
+
}
|
|
72
|
+
interface VoidxInstance {
|
|
73
|
+
destroy: () => void;
|
|
74
|
+
show: () => void;
|
|
75
|
+
hide: () => void;
|
|
76
|
+
sendMessage: (message: string) => void;
|
|
77
|
+
announceSystemEvent: (context: string, options?: AnnounceSystemEventOptions) => Promise<void>;
|
|
78
|
+
setTheme: (theme: Partial<VoidxTheme>) => void;
|
|
79
|
+
setLocale: (locale: VoidxLocale) => void;
|
|
80
|
+
on: <K extends keyof VoidxEventMap>(event: K, handler: (data: VoidxEventMap[K]) => void) => void;
|
|
81
|
+
off: <K extends keyof VoidxEventMap>(event: K, handler: (data: VoidxEventMap[K]) => void) => void;
|
|
82
|
+
}
|
|
83
|
+
interface VoidxInitConfig {
|
|
84
|
+
apiKey: string;
|
|
85
|
+
baseUrl?: string;
|
|
86
|
+
cdnUrl?: string;
|
|
87
|
+
voiceApiUrl?: string;
|
|
88
|
+
features?: VoidxFeatures;
|
|
89
|
+
theme?: VoidxTheme;
|
|
90
|
+
locale?: VoidxLocale;
|
|
91
|
+
position?: 'follow-cursor' | 'bottom-right' | 'bottom-left';
|
|
92
|
+
hoverTargets?: {
|
|
93
|
+
classNames?: string[];
|
|
94
|
+
ids?: string[];
|
|
95
|
+
idPatterns?: RegExp[];
|
|
96
|
+
classNamePatterns?: RegExp[];
|
|
97
|
+
};
|
|
98
|
+
actions?: HostActions;
|
|
99
|
+
onError?: (error: Error) => void;
|
|
100
|
+
onMessage?: (message: VoidxEventMap['voidx:message']) => void;
|
|
101
|
+
onReady?: () => void;
|
|
102
|
+
}
|
|
103
|
+
interface VoidxAIAddonGlobal {
|
|
104
|
+
init: (config: VoidxInitConfig) => VoidxInstance;
|
|
105
|
+
}
|
|
106
|
+
declare global {
|
|
107
|
+
interface Window {
|
|
108
|
+
VoidxAIAddon?: VoidxAIAddonGlobal;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
interface SystemEventResult {
|
|
112
|
+
context: string;
|
|
113
|
+
instruction?: string;
|
|
114
|
+
openChat?: boolean;
|
|
115
|
+
}
|
|
116
|
+
interface SystemEventHandler<D = any> {
|
|
117
|
+
handler: (detail: D, instance: VoidxInstance, locale: VoidxLocale) => SystemEventResult | void | null;
|
|
118
|
+
}
|
|
119
|
+
interface VoidxAddonProps {
|
|
120
|
+
/** JWT API key — 필수 */
|
|
121
|
+
apiKey: string;
|
|
122
|
+
/** 기능 설정 */
|
|
123
|
+
features?: VoidxFeatures;
|
|
124
|
+
/** 테마 커스터마이징 */
|
|
125
|
+
theme?: VoidxTheme;
|
|
126
|
+
/** 언어 (반응형 — 변경 시 setLocale 자동 호출) */
|
|
127
|
+
locale?: VoidxLocale;
|
|
128
|
+
/** 팝업 위치 */
|
|
129
|
+
position?: 'follow-cursor' | 'bottom-right' | 'bottom-left';
|
|
130
|
+
/** API base URL 오버라이드 */
|
|
131
|
+
baseUrl?: string;
|
|
132
|
+
/** standalone.js 스크립트 URL 오버라이드 (기본: addon-cdn.voidx.ai) */
|
|
133
|
+
scriptUrl?: string;
|
|
134
|
+
/** SDK 내부 CDN URL 오버라이드 (3D 모델, 이미지 등) */
|
|
135
|
+
cdnUrl?: string;
|
|
136
|
+
/** Voice API URL 오버라이드 */
|
|
137
|
+
voiceApiUrl?: string;
|
|
138
|
+
/** 호버 감지 대상 */
|
|
139
|
+
hoverTargets?: VoidxInitConfig['hoverTargets'];
|
|
140
|
+
/** 호스트 액션 */
|
|
141
|
+
actions?: HostActions;
|
|
142
|
+
/** 시스템 이벤트 → announceSystemEvent 브리지 */
|
|
143
|
+
systemEvents?: Record<string, SystemEventHandler>;
|
|
144
|
+
/** SDK 준비 완료 콜백 */
|
|
145
|
+
onReady?: () => void;
|
|
146
|
+
/** 에러 콜백 */
|
|
147
|
+
onError?: (error: Error) => void;
|
|
148
|
+
/** 메시지 수신 콜백 */
|
|
149
|
+
onMessage?: (message: VoidxEventMap['voidx:message']) => void;
|
|
150
|
+
/** 비활성화 (true면 SDK 로딩 안 함) */
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
/** children (useVoidx() 훅으로 인스턴스 접근 가능) */
|
|
153
|
+
children?: React.ReactNode;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* VoidX AI Addon React 래퍼 컴포넌트.
|
|
158
|
+
*
|
|
159
|
+
* CDN standalone 스크립트를 동적 로딩하고, SDK를 init/destroy 한다.
|
|
160
|
+
* children에서 useVoidx() 훅으로 인스턴스에 접근할 수 있다.
|
|
161
|
+
*/
|
|
162
|
+
declare function VoidxAddon({ apiKey, features, theme, locale, position, baseUrl, scriptUrl, cdnUrl, voiceApiUrl, hoverTargets, actions, systemEvents, onReady, onError, onMessage, disabled, children, }: VoidxAddonProps): react_jsx_runtime.JSX.Element | null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* VoidxAddon 인스턴스에 접근하는 훅.
|
|
166
|
+
* SDK 로딩 전이면 null을 반환한다.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```tsx
|
|
170
|
+
* function AskAIButton() {
|
|
171
|
+
* const voidx = useVoidx();
|
|
172
|
+
* return (
|
|
173
|
+
* <button onClick={() => voidx?.sendMessage('추천해줘')}>
|
|
174
|
+
* AI에게 물어보기
|
|
175
|
+
* </button>
|
|
176
|
+
* );
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
declare function useVoidx(): VoidxInstance | null;
|
|
181
|
+
|
|
182
|
+
export { type AnnounceSystemEventOptions, type HostActionHandler, type HostActions, type SystemEventHandler, type SystemEventResult, VoidxAddon, type VoidxAddonProps, type VoidxEventMap, type VoidxFeatures, type VoidxInitConfig, type VoidxInstance, type VoidxLocale, type VoidxTheme, useVoidx };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createContext, useContext, useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/VoidxAddon.tsx
|
|
5
|
+
var VoidxContext = createContext(null);
|
|
6
|
+
function useVoidx() {
|
|
7
|
+
return useContext(VoidxContext);
|
|
8
|
+
}
|
|
9
|
+
function useScript(src, disabled) {
|
|
10
|
+
const [status, setStatus] = useState("idle");
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (disabled) {
|
|
13
|
+
setStatus("idle");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const existing = document.querySelector(
|
|
17
|
+
`script[src="${src}"]`
|
|
18
|
+
);
|
|
19
|
+
if (existing) {
|
|
20
|
+
if (window.VoidxAIAddon) {
|
|
21
|
+
setStatus("ready");
|
|
22
|
+
} else {
|
|
23
|
+
const onLoad = () => setStatus("ready");
|
|
24
|
+
const onError = () => setStatus("error");
|
|
25
|
+
existing.addEventListener("load", onLoad);
|
|
26
|
+
existing.addEventListener("error", onError);
|
|
27
|
+
setStatus("loading");
|
|
28
|
+
return () => {
|
|
29
|
+
existing.removeEventListener("load", onLoad);
|
|
30
|
+
existing.removeEventListener("error", onError);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const script = document.createElement("script");
|
|
36
|
+
script.src = src;
|
|
37
|
+
script.async = true;
|
|
38
|
+
script.addEventListener("load", () => setStatus("ready"));
|
|
39
|
+
script.addEventListener("error", () => setStatus("error"));
|
|
40
|
+
setStatus("loading");
|
|
41
|
+
document.body.appendChild(script);
|
|
42
|
+
return () => {
|
|
43
|
+
if (document.body.contains(script)) {
|
|
44
|
+
document.body.removeChild(script);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}, [src, disabled]);
|
|
48
|
+
return status;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/utils.ts
|
|
52
|
+
var DEFAULT_CDN_URL = "https://addon-cdn.voidx.ai/latest/voidx-ai-addon.standalone.js";
|
|
53
|
+
function normalizeLang(value) {
|
|
54
|
+
switch (value) {
|
|
55
|
+
case "en":
|
|
56
|
+
return "en";
|
|
57
|
+
case "ja":
|
|
58
|
+
return "ja";
|
|
59
|
+
case "zh":
|
|
60
|
+
return "zh";
|
|
61
|
+
default:
|
|
62
|
+
return "ko";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function VoidxAddon({
|
|
66
|
+
apiKey,
|
|
67
|
+
features,
|
|
68
|
+
theme,
|
|
69
|
+
locale,
|
|
70
|
+
position,
|
|
71
|
+
baseUrl,
|
|
72
|
+
scriptUrl,
|
|
73
|
+
cdnUrl,
|
|
74
|
+
voiceApiUrl,
|
|
75
|
+
hoverTargets,
|
|
76
|
+
actions,
|
|
77
|
+
systemEvents,
|
|
78
|
+
onReady,
|
|
79
|
+
onError,
|
|
80
|
+
onMessage,
|
|
81
|
+
disabled = false,
|
|
82
|
+
children
|
|
83
|
+
}) {
|
|
84
|
+
const instanceRef = useRef(null);
|
|
85
|
+
const [instance, setInstance] = useState(null);
|
|
86
|
+
const localeRef = useRef(normalizeLang(locale));
|
|
87
|
+
const resolvedScriptUrl = scriptUrl ?? DEFAULT_CDN_URL;
|
|
88
|
+
const scriptStatus = useScript(resolvedScriptUrl, disabled);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (disabled || scriptStatus !== "ready" || !window.VoidxAIAddon) return;
|
|
91
|
+
const normalizedLocale = normalizeLang(locale);
|
|
92
|
+
localeRef.current = normalizedLocale;
|
|
93
|
+
const config = {
|
|
94
|
+
apiKey,
|
|
95
|
+
locale: normalizedLocale,
|
|
96
|
+
...features && { features },
|
|
97
|
+
...theme && { theme },
|
|
98
|
+
...position && { position },
|
|
99
|
+
...baseUrl && { baseUrl },
|
|
100
|
+
...cdnUrl && { cdnUrl },
|
|
101
|
+
...voiceApiUrl && { voiceApiUrl },
|
|
102
|
+
...hoverTargets && { hoverTargets },
|
|
103
|
+
...actions && { actions },
|
|
104
|
+
...onReady && { onReady },
|
|
105
|
+
...onError && { onError },
|
|
106
|
+
...onMessage && { onMessage }
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const inst = window.VoidxAIAddon.init(config);
|
|
110
|
+
instanceRef.current = inst;
|
|
111
|
+
setInstance(inst);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
114
|
+
console.error("[VoidxAddon] init failed:", err);
|
|
115
|
+
onError?.(err);
|
|
116
|
+
}
|
|
117
|
+
return () => {
|
|
118
|
+
instanceRef.current?.destroy();
|
|
119
|
+
instanceRef.current = null;
|
|
120
|
+
setInstance(null);
|
|
121
|
+
};
|
|
122
|
+
}, [apiKey, scriptStatus, disabled]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const normalized = normalizeLang(locale);
|
|
125
|
+
localeRef.current = normalized;
|
|
126
|
+
instanceRef.current?.setLocale(normalized);
|
|
127
|
+
}, [locale]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (theme && instanceRef.current) {
|
|
130
|
+
instanceRef.current.setTheme(theme);
|
|
131
|
+
}
|
|
132
|
+
}, [theme]);
|
|
133
|
+
const systemEventsRef = useRef(systemEvents);
|
|
134
|
+
systemEventsRef.current = systemEvents;
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const inst = instanceRef.current;
|
|
137
|
+
if (!inst || !systemEventsRef.current) return;
|
|
138
|
+
const entries = Object.entries(systemEventsRef.current);
|
|
139
|
+
if (entries.length === 0) return;
|
|
140
|
+
const cleanups = [];
|
|
141
|
+
for (const [eventName, { handler }] of entries) {
|
|
142
|
+
const listener = (event) => {
|
|
143
|
+
const detail = event.detail;
|
|
144
|
+
const result = handler(detail, inst, localeRef.current);
|
|
145
|
+
if (result) {
|
|
146
|
+
void inst.announceSystemEvent(result.context, {
|
|
147
|
+
instruction: result.instruction,
|
|
148
|
+
openChat: result.openChat
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
window.addEventListener(eventName, listener);
|
|
153
|
+
cleanups.push(() => window.removeEventListener(eventName, listener));
|
|
154
|
+
}
|
|
155
|
+
return () => {
|
|
156
|
+
for (const cleanup of cleanups) cleanup();
|
|
157
|
+
};
|
|
158
|
+
}, [instance]);
|
|
159
|
+
if (disabled) return null;
|
|
160
|
+
return /* @__PURE__ */ jsxs(VoidxContext.Provider, { value: instance, children: [
|
|
161
|
+
/* @__PURE__ */ jsx("div", { id: "voidx-ai-addon-root" }),
|
|
162
|
+
children
|
|
163
|
+
] });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { VoidxAddon, useVoidx };
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts","../src/use-script.ts","../src/utils.ts","../src/VoidxAddon.tsx"],"names":["useState","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext } from 'react';\nimport type { VoidxInstance } from './types';\n\nexport const VoidxContext = createContext<VoidxInstance | null>(null);\n\n/**\n * VoidxAddon 인스턴스에 접근하는 훅.\n * SDK 로딩 전이면 null을 반환한다.\n *\n * @example\n * ```tsx\n * function AskAIButton() {\n * const voidx = useVoidx();\n * return (\n * <button onClick={() => voidx?.sendMessage('추천해줘')}>\n * AI에게 물어보기\n * </button>\n * );\n * }\n * ```\n */\nexport function useVoidx(): VoidxInstance | null {\n return useContext(VoidxContext);\n}\n","import { useEffect, useState } from 'react';\n\ntype ScriptStatus = 'idle' | 'loading' | 'ready' | 'error';\n\n/**\n * CDN 스크립트를 동적으로 로딩하는 훅.\n * 이미 DOM에 동일 src가 있으면 중복 삽입하지 않는다.\n */\nexport function useScript(src: string, disabled: boolean): ScriptStatus {\n const [status, setStatus] = useState<ScriptStatus>('idle');\n\n useEffect(() => {\n if (disabled) {\n setStatus('idle');\n return;\n }\n\n // 이미 로드된 스크립트가 있으면 재사용\n const existing = document.querySelector(\n `script[src=\"${src}\"]`,\n ) as HTMLScriptElement | null;\n\n if (existing) {\n // window.VoidxAIAddon이 이미 있으면 ready\n if (window.VoidxAIAddon) {\n setStatus('ready');\n } else {\n // 아직 로딩 중인 스크립트 — 이벤트 대기\n const onLoad = () => setStatus('ready');\n const onError = () => setStatus('error');\n existing.addEventListener('load', onLoad);\n existing.addEventListener('error', onError);\n setStatus('loading');\n return () => {\n existing.removeEventListener('load', onLoad);\n existing.removeEventListener('error', onError);\n };\n }\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n\n script.addEventListener('load', () => setStatus('ready'));\n script.addEventListener('error', () => setStatus('error'));\n\n setStatus('loading');\n document.body.appendChild(script);\n\n return () => {\n if (document.body.contains(script)) {\n document.body.removeChild(script);\n }\n };\n }, [src, disabled]);\n\n return status;\n}\n","import type { VoidxLocale } from './types';\n\nexport const DEFAULT_CDN_URL =\n 'https://addon-cdn.voidx.ai/latest/voidx-ai-addon.standalone.js';\n\nexport function normalizeLang(value: string | undefined): VoidxLocale {\n switch (value) {\n case 'en':\n return 'en';\n case 'ja':\n return 'ja';\n case 'zh':\n return 'zh';\n default:\n return 'ko';\n }\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { VoidxContext } from './context';\nimport type {\n VoidxAddonProps,\n VoidxInitConfig,\n VoidxInstance,\n VoidxLocale,\n} from './types';\nimport { useScript } from './use-script';\nimport { DEFAULT_CDN_URL, normalizeLang } from './utils';\n\n/**\n * VoidX AI Addon React 래퍼 컴포넌트.\n *\n * CDN standalone 스크립트를 동적 로딩하고, SDK를 init/destroy 한다.\n * children에서 useVoidx() 훅으로 인스턴스에 접근할 수 있다.\n */\nexport function VoidxAddon({\n apiKey,\n features,\n theme,\n locale,\n position,\n baseUrl,\n scriptUrl,\n cdnUrl,\n voiceApiUrl,\n hoverTargets,\n actions,\n systemEvents,\n onReady,\n onError,\n onMessage,\n disabled = false,\n children,\n}: VoidxAddonProps) {\n const instanceRef = useRef<VoidxInstance | null>(null);\n const [instance, setInstance] = useState<VoidxInstance | null>(null);\n const localeRef = useRef<VoidxLocale>(normalizeLang(locale));\n\n const resolvedScriptUrl = scriptUrl ?? DEFAULT_CDN_URL;\n const scriptStatus = useScript(resolvedScriptUrl, disabled);\n\n // ── init / destroy ──\n useEffect(() => {\n if (disabled || scriptStatus !== 'ready' || !window.VoidxAIAddon) return;\n\n const normalizedLocale = normalizeLang(locale);\n localeRef.current = normalizedLocale;\n\n const config: VoidxInitConfig = {\n apiKey,\n locale: normalizedLocale,\n ...(features && { features }),\n ...(theme && { theme }),\n ...(position && { position }),\n ...(baseUrl && { baseUrl }),\n ...(cdnUrl && { cdnUrl }),\n ...(voiceApiUrl && { voiceApiUrl }),\n ...(hoverTargets && { hoverTargets }),\n ...(actions && { actions }),\n ...(onReady && { onReady }),\n ...(onError && { onError }),\n ...(onMessage && { onMessage }),\n };\n\n try {\n const inst = window.VoidxAIAddon!.init(config);\n instanceRef.current = inst;\n setInstance(inst);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.error('[VoidxAddon] init failed:', err);\n onError?.(err);\n }\n\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n setInstance(null);\n };\n // apiKey 변경 시 재초기화, actions/systemEvents는 ref로 관리하지 않고\n // init 시점에만 전달 (SDK가 런타임 교체를 지원하지 않음)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiKey, scriptStatus, disabled]);\n\n // ── locale sync ──\n useEffect(() => {\n const normalized = normalizeLang(locale);\n localeRef.current = normalized;\n instanceRef.current?.setLocale(normalized);\n }, [locale]);\n\n // ── theme sync ──\n useEffect(() => {\n if (theme && instanceRef.current) {\n instanceRef.current.setTheme(theme);\n }\n }, [theme]);\n\n // ── system events ──\n const systemEventsRef = useRef(systemEvents);\n systemEventsRef.current = systemEvents;\n\n useEffect(() => {\n const inst = instanceRef.current;\n if (!inst || !systemEventsRef.current) return;\n\n const entries = Object.entries(systemEventsRef.current);\n if (entries.length === 0) return;\n\n const cleanups: (() => void)[] = [];\n\n for (const [eventName, { handler }] of entries) {\n const listener = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n const result = handler(detail, inst, localeRef.current);\n if (result) {\n void inst.announceSystemEvent(result.context, {\n instruction: result.instruction,\n openChat: result.openChat,\n });\n }\n };\n\n window.addEventListener(eventName, listener);\n cleanups.push(() => window.removeEventListener(eventName, listener));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }, [instance]); // instance가 설정된 후에만 이벤트 등록\n\n if (disabled) return null;\n\n return (\n <VoidxContext.Provider value={instance}>\n <div id=\"voidx-ai-addon-root\" />\n {children}\n </VoidxContext.Provider>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voidx-ai/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React/Next.js wrapper for VoidX AI Addon (CDN standalone loader)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18.0.0",
|
|
33
|
+
"react-dom": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"react-dom": "^19.0.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "~5.7.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/upfall-co/voidx-react.git"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"voidx",
|
|
53
|
+
"ai",
|
|
54
|
+
"agent",
|
|
55
|
+
"chatbot",
|
|
56
|
+
"react",
|
|
57
|
+
"nextjs"
|
|
58
|
+
],
|
|
59
|
+
"license": "MIT"
|
|
60
|
+
}
|