@ozwell/react 1.0.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 ADDED
@@ -0,0 +1,207 @@
1
+ # @ozwell/react
2
+
3
+ React components for Ozwell chat widget.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ozwell/react
9
+ # or
10
+ yarn add @ozwell/react
11
+ # or
12
+ pnpm add @ozwell/react
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```tsx
18
+ import { OzwellChat } from '@ozwell/react';
19
+
20
+ function App() {
21
+ return (
22
+ <OzwellChat
23
+ apiKey="agnt_key-your-agent-key"
24
+ tools={[
25
+ {
26
+ type: 'function',
27
+ function: {
28
+ name: 'get_user_info',
29
+ description: 'Get current user information',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {},
33
+ required: []
34
+ }
35
+ }
36
+ }
37
+ ]}
38
+ />
39
+ );
40
+ }
41
+ ```
42
+
43
+ This package loads the hosted iframe widget and authenticates with an agent key.
44
+ Sign in to [Ozwell Manager](https://ozwellconsole.os.mieweb.org) to create an agent.
45
+ For another deployment, set `widgetUrl` to its `/widget/frame/` URL.
46
+ See the [React integration guide](https://docs.ozwell.ai/frontend/react) for credential
47
+ safety, a complete Vite + MIE UI Hello World example, and troubleshooting.
48
+
49
+ ## Feature Support
50
+
51
+ ### Currently Available
52
+ - Agent-key authentication (`apiKey` prop)
53
+ - Custom endpoints (`endpoint` prop)
54
+ - Model selection (`model` prop)
55
+ - MCP tool/function calling (`tools` prop, `onToolCall` callback)
56
+ - System prompts (`system` prop)
57
+ - Welcome messages (`welcomeMessage` prop)
58
+ - Debug mode (`debug` prop)
59
+ - OpenAI API compatibility (`openaiApiKey` prop)
60
+ - Custom headers (`headers` prop)
61
+ - Auto-open on AI reply (`autoOpenOnReply` prop)
62
+ - Lifecycle callbacks (`onReady`, `onOpen`, `onClose`)
63
+ - Error callback for loader and mount errors (`onError` - partial)
64
+
65
+ ### Coming Soon
66
+ - Agent management (`agentId` prop)
67
+ - Theme customization (`theme`, `primaryColor` props)
68
+ - Position control (`position` prop)
69
+ - Auto-open behavior (`autoOpen` prop)
70
+ - User share callbacks (`onUserShare` - requires widget support)
71
+ - Full error callback support (`onError` - additional error types)
72
+
73
+ ## Using Tools (MCP Function Calling)
74
+
75
+ The React package supports MCP (Model Context Protocol) tools for function calling. Here's how to define and handle tools:
76
+
77
+ ### Defining Tools
78
+
79
+ Tools are defined using the OpenAI-compatible format and passed via the `tools` prop:
80
+
81
+ ```tsx
82
+ import { OzwellChat } from '@ozwell/react';
83
+ import type { OzwellTool } from '@ozwell/react';
84
+
85
+ const tools: OzwellTool[] = [
86
+ {
87
+ type: 'function',
88
+ function: {
89
+ name: 'get_form_data',
90
+ description: 'Retrieves current user information including name, address, and zip code',
91
+ parameters: {
92
+ type: 'object',
93
+ properties: {},
94
+ required: []
95
+ }
96
+ }
97
+ },
98
+ {
99
+ type: 'function',
100
+ function: {
101
+ name: 'update_form_data',
102
+ description: 'Updates user profile information',
103
+ parameters: {
104
+ type: 'object',
105
+ properties: {
106
+ name: { type: 'string', description: 'The new name value' },
107
+ address: { type: 'string', description: 'The new address value' },
108
+ zipCode: { type: 'string', description: 'The new zip code value' }
109
+ },
110
+ required: []
111
+ }
112
+ }
113
+ }
114
+ ];
115
+
116
+ function App() {
117
+ return (
118
+ <OzwellChat
119
+ endpoint="http://localhost:3000/v1/chat/completions"
120
+ tools={tools}
121
+ debug={true}
122
+ system="You are a helpful assistant for managing user profile information."
123
+ />
124
+ );
125
+ }
126
+ ```
127
+
128
+ ### Handling Tool Calls
129
+
130
+ Use the `onToolCall` prop to handle tool calls with a simple callback:
131
+
132
+ ```tsx
133
+ import React, { useState } from 'react';
134
+ import { OzwellChat } from '@ozwell/react';
135
+
136
+ function App() {
137
+ const [formData, setFormData] = useState({
138
+ name: '',
139
+ address: '',
140
+ zipCode: ''
141
+ });
142
+
143
+ // Define your tool handlers
144
+ const toolHandlers: Record<string, (args: Record<string, unknown>) => unknown> = {
145
+ get_form_data: () => formData,
146
+
147
+ update_form_data: (args) => {
148
+ setFormData(prev => ({
149
+ name: (args.name as string) ?? prev.name,
150
+ address: (args.address as string) ?? prev.address,
151
+ zipCode: (args.zipCode as string) ?? prev.zipCode
152
+ }));
153
+ return { success: true, updated: args };
154
+ }
155
+ };
156
+
157
+ return (
158
+ <OzwellChat
159
+ endpoint="/v1/chat/completions"
160
+ tools={tools}
161
+ onToolCall={(tool, args, sendResult) => {
162
+ const handler = toolHandlers[tool];
163
+ if (handler) {
164
+ const result = handler(args);
165
+ sendResult(result);
166
+ } else {
167
+ sendResult({ error: `Unknown tool: ${tool}` });
168
+ }
169
+ }}
170
+ />
171
+ );
172
+ }
173
+ ```
174
+
175
+ The `onToolCall` callback receives:
176
+
177
+ - `tool` - The name of the tool being called
178
+ - `args` - The arguments passed to the tool
179
+ - `sendResult` - A function to send the result back to the AI
180
+
181
+ This handles all the postMessage complexity internally, so you just focus on your tool logic.
182
+
183
+ ## Documentation
184
+
185
+ For full documentation, see [React Integration Guide](../../docs/frontend/react.md).
186
+
187
+ > **Note:** The documentation describes the planned API including `apiKey` and `agentId` props. The current implementation supports `endpoint` and `tools` configuration matching the vanilla JS widget.
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ # Install dependencies
193
+ npm install
194
+
195
+ # Build the package
196
+ npm run build
197
+
198
+ # Development mode (watch)
199
+ npm run dev
200
+
201
+ # Type checking
202
+ npm run lint
203
+ ```
204
+
205
+ ## License
206
+
207
+ Apache-2.0
package/dist/index.cjs ADDED
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ OzwellChat: () => OzwellChat,
24
+ default: () => OzwellChat_default,
25
+ useOzwell: () => useOzwell
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/OzwellChat.tsx
30
+ var import_react = require("react");
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ function OzwellChat(props) {
33
+ const {
34
+ // Layout
35
+ width = 360,
36
+ height = 420,
37
+ containerId,
38
+ defaultUI = true,
39
+ // Configuration
40
+ endpoint,
41
+ model,
42
+ system,
43
+ welcomeMessage,
44
+ placeholder,
45
+ title,
46
+ tools,
47
+ debug,
48
+ openaiApiKey,
49
+ headers,
50
+ widgetUrl,
51
+ autoOpenOnReply,
52
+ apiKey,
53
+ // Future props (not yet implemented in vanilla widget)
54
+ // These are accepted but ignored until backend support is added
55
+ agentId,
56
+ theme: _theme,
57
+ // Prefix with _ to indicate intentionally unused
58
+ position: _position,
59
+ primaryColor: _primaryColor,
60
+ autoOpen: _autoOpen,
61
+ // Callbacks
62
+ onReady,
63
+ onOpen,
64
+ onClose,
65
+ onToolCall,
66
+ onUserShare,
67
+ onError,
68
+ // React-specific
69
+ children
70
+ } = props;
71
+ const [scriptStatus, setScriptStatus] = (0, import_react.useState)("idle");
72
+ const [isWidgetReady, setIsWidgetReady] = (0, import_react.useState)(false);
73
+ const instanceContainerId = (0, import_react.useRef)(
74
+ containerId || `ozwell-container-${Math.random().toString(36).slice(2, 11)}`
75
+ );
76
+ (0, import_react.useEffect)(() => {
77
+ if (window.OzwellChat) {
78
+ setScriptStatus("ready");
79
+ return;
80
+ }
81
+ const handleLoad = () => setScriptStatus("ready");
82
+ const handleError = () => {
83
+ setScriptStatus("error");
84
+ console.error("[OzwellChat] Failed to load ozwell-loader.js");
85
+ onError?.({
86
+ code: "SCRIPT_LOAD_ERROR",
87
+ message: "Failed to load Ozwell widget. Check the widget host URL and network access."
88
+ });
89
+ };
90
+ const existingScript = document.querySelector(
91
+ 'script[data-ozwell-loader], script[src*="ozwell-loader.js"]'
92
+ );
93
+ if (existingScript) {
94
+ setScriptStatus("loading");
95
+ existingScript.addEventListener("load", handleLoad);
96
+ existingScript.addEventListener("error", handleError);
97
+ return () => {
98
+ existingScript.removeEventListener("load", handleLoad);
99
+ existingScript.removeEventListener("error", handleError);
100
+ };
101
+ }
102
+ window.OzwellChatConfig = {
103
+ ...window.OzwellChatConfig,
104
+ autoMount: false
105
+ };
106
+ setScriptStatus("loading");
107
+ const scriptSrc = new URL(
108
+ "/widget",
109
+ widgetUrl ? new URL(widgetUrl, window.location.href) : "https://ozwellapi.os.mieweb.org"
110
+ ).href;
111
+ const script = document.createElement("script");
112
+ script.dataset.ozwellLoader = "true";
113
+ script.src = scriptSrc;
114
+ script.async = true;
115
+ script.addEventListener("load", handleLoad);
116
+ script.addEventListener("error", handleError);
117
+ document.head.appendChild(script);
118
+ return () => {
119
+ script.removeEventListener("load", handleLoad);
120
+ script.removeEventListener("error", handleError);
121
+ };
122
+ }, [widgetUrl]);
123
+ (0, import_react.useEffect)(() => {
124
+ if (scriptStatus !== "ready" || !window.OzwellChat) {
125
+ return;
126
+ }
127
+ const config = {
128
+ // Core config
129
+ endpoint,
130
+ model,
131
+ system,
132
+ welcomeMessage,
133
+ placeholder,
134
+ title,
135
+ tools,
136
+ debug,
137
+ openaiApiKey,
138
+ headers,
139
+ widgetUrl,
140
+ apiKey,
141
+ // Layout config
142
+ defaultUI,
143
+ autoMount: false,
144
+ // Prevent auto-mount, we'll mount manually
145
+ autoOpenOnReply,
146
+ agentId
147
+ };
148
+ if (!defaultUI) {
149
+ config.containerId = instanceContainerId.current;
150
+ }
151
+ const cleanConfig = Object.fromEntries(
152
+ Object.entries(config).filter(([, value]) => value !== void 0)
153
+ );
154
+ window.OzwellChatConfig = cleanConfig;
155
+ try {
156
+ const mountOptions = {};
157
+ if (!defaultUI) {
158
+ mountOptions.containerId = instanceContainerId.current;
159
+ }
160
+ if (width) {
161
+ mountOptions.width = typeof width === "string" ? parseInt(width) : width;
162
+ }
163
+ if (height) {
164
+ mountOptions.height = typeof height === "string" ? parseInt(height) : height;
165
+ }
166
+ if (window.OzwellChat.iframe) {
167
+ window.OzwellChat.configure(cleanConfig);
168
+ } else {
169
+ window.OzwellChat.mount(mountOptions);
170
+ }
171
+ window.OzwellChat.ready().then(() => {
172
+ setIsWidgetReady(true);
173
+ onReady?.();
174
+ });
175
+ } catch (error) {
176
+ console.error("[OzwellChat] Failed to mount widget:", error);
177
+ onError?.({
178
+ code: "MOUNT_ERROR",
179
+ message: "Failed to mount Ozwell widget",
180
+ details: error
181
+ });
182
+ }
183
+ }, [
184
+ scriptStatus,
185
+ endpoint,
186
+ model,
187
+ system,
188
+ welcomeMessage,
189
+ placeholder,
190
+ title,
191
+ tools,
192
+ debug,
193
+ openaiApiKey,
194
+ headers,
195
+ widgetUrl,
196
+ defaultUI,
197
+ autoOpenOnReply,
198
+ width,
199
+ height,
200
+ apiKey,
201
+ agentId,
202
+ onReady,
203
+ onError
204
+ ]);
205
+ (0, import_react.useEffect)(() => {
206
+ if (!isWidgetReady) {
207
+ return;
208
+ }
209
+ const handleMessage = (event) => {
210
+ const iframe = window.OzwellChat?.iframe;
211
+ if (!iframe || event.source !== iframe.contentWindow || event.origin !== new URL(iframe.src, window.location.href).origin) {
212
+ return;
213
+ }
214
+ const data = event.data;
215
+ if (!data || typeof data !== "object" || data.source !== "ozwell-chat-widget") {
216
+ return;
217
+ }
218
+ switch (data.type) {
219
+ case "closed":
220
+ onClose?.();
221
+ break;
222
+ case "opened":
223
+ onOpen?.();
224
+ break;
225
+ case "user-share":
226
+ onUserShare?.(data.payload);
227
+ break;
228
+ case "error":
229
+ onError?.(data.payload);
230
+ break;
231
+ case "tool_call":
232
+ if (onToolCall) {
233
+ const { tool, tool_call_id, payload: args } = data;
234
+ const sendResult = (result) => {
235
+ const iframe2 = window.OzwellChat?.iframe;
236
+ if (iframe2?.contentWindow) {
237
+ const targetOrigin = iframe2.src ? new URL(iframe2.src).origin : "*";
238
+ iframe2.contentWindow.postMessage(
239
+ {
240
+ source: "ozwell-chat-parent",
241
+ type: "tool_result",
242
+ tool_call_id,
243
+ result
244
+ },
245
+ targetOrigin
246
+ );
247
+ } else {
248
+ console.error("[OzwellChat] Could not find widget iframe to send tool result");
249
+ }
250
+ };
251
+ onToolCall(tool, args || {}, sendResult);
252
+ }
253
+ break;
254
+ }
255
+ };
256
+ window.addEventListener("message", handleMessage);
257
+ return () => {
258
+ window.removeEventListener("message", handleMessage);
259
+ };
260
+ }, [isWidgetReady, onClose, onOpen, onUserShare, onError, onToolCall]);
261
+ (0, import_react.useEffect)(() => {
262
+ const handleToolCall = (event) => {
263
+ const { name, arguments: args, respond, error } = event.detail;
264
+ if (!onToolCall) {
265
+ error(`No handler configured for tool "${name}".`);
266
+ return;
267
+ }
268
+ try {
269
+ const outcome = onToolCall(name, args || {}, respond);
270
+ Promise.resolve(outcome).catch(() => error(`Tool "${name}" failed.`));
271
+ } catch {
272
+ error(`Tool "${name}" failed.`);
273
+ }
274
+ };
275
+ document.addEventListener("ozwell-tool-call", handleToolCall);
276
+ return () => document.removeEventListener("ozwell-tool-call", handleToolCall);
277
+ }, [onToolCall]);
278
+ if (defaultUI) {
279
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
280
+ }
281
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
282
+ "div",
283
+ {
284
+ id: instanceContainerId.current,
285
+ style: {
286
+ width: typeof width === "number" ? `${width}px` : width,
287
+ height: typeof height === "number" ? `${height}px` : height
288
+ },
289
+ children
290
+ }
291
+ );
292
+ }
293
+ var OzwellChat_default = OzwellChat;
294
+
295
+ // src/useOzwell.ts
296
+ var import_react2 = require("react");
297
+ function useOzwell() {
298
+ const [isReady, setIsReady] = (0, import_react2.useState)(false);
299
+ const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
300
+ const [hasUnread, setHasUnread] = (0, import_react2.useState)(false);
301
+ const [iframe, setIframe] = (0, import_react2.useState)(null);
302
+ (0, import_react2.useEffect)(() => {
303
+ if (window.OzwellChat) {
304
+ setIsReady(true);
305
+ setIframe(window.OzwellChat.iframe);
306
+ return;
307
+ }
308
+ const handleReady = () => {
309
+ setIsReady(true);
310
+ setIframe(window.OzwellChat?.iframe || null);
311
+ };
312
+ document.addEventListener("ozwell-chat-ready", handleReady);
313
+ return () => {
314
+ document.removeEventListener("ozwell-chat-ready", handleReady);
315
+ };
316
+ }, []);
317
+ (0, import_react2.useEffect)(() => {
318
+ if (!isReady) {
319
+ return;
320
+ }
321
+ const handleMessage = (event) => {
322
+ const widgetIframe = window.OzwellChat?.iframe;
323
+ if (widgetIframe && event.source !== widgetIframe.contentWindow) {
324
+ return;
325
+ }
326
+ const data = event.data;
327
+ if (!data || typeof data !== "object" || data.source !== "ozwell-chat-widget") {
328
+ return;
329
+ }
330
+ if (data.type === "opened") {
331
+ setIsOpen(true);
332
+ setHasUnread(false);
333
+ } else if (data.type === "closed") {
334
+ setIsOpen(false);
335
+ }
336
+ };
337
+ const handleUnread = () => {
338
+ setHasUnread(true);
339
+ };
340
+ window.addEventListener("message", handleMessage);
341
+ document.addEventListener("ozwell-chat-unread", handleUnread);
342
+ return () => {
343
+ window.removeEventListener("message", handleMessage);
344
+ document.removeEventListener("ozwell-chat-unread", handleUnread);
345
+ };
346
+ }, [isReady]);
347
+ const open = (0, import_react2.useCallback)(() => {
348
+ if (!isReady) {
349
+ console.warn("[useOzwell] Widget not ready yet");
350
+ return;
351
+ }
352
+ window.OzwellChat?.open?.();
353
+ }, [isReady]);
354
+ const close = (0, import_react2.useCallback)(() => {
355
+ if (!isReady) {
356
+ console.warn("[useOzwell] Widget not ready yet");
357
+ return;
358
+ }
359
+ window.OzwellChat?.close?.();
360
+ }, [isReady]);
361
+ const toggle = (0, import_react2.useCallback)(() => {
362
+ if (isOpen) {
363
+ close();
364
+ } else {
365
+ open();
366
+ }
367
+ }, [isOpen, open, close]);
368
+ const sendMessage = (0, import_react2.useCallback)((content) => {
369
+ if (!isReady) {
370
+ console.warn("[useOzwell] Widget not ready yet");
371
+ return;
372
+ }
373
+ if (!content || typeof content !== "string") {
374
+ console.warn("[useOzwell] sendMessage requires a non-empty string");
375
+ return;
376
+ }
377
+ console.warn("[useOzwell] sendMessage() not yet implemented in vanilla widget");
378
+ }, [isReady]);
379
+ return {
380
+ isReady,
381
+ isOpen,
382
+ hasUnread,
383
+ open,
384
+ close,
385
+ toggle,
386
+ sendMessage,
387
+ iframe
388
+ };
389
+ }
390
+ // Annotate the CommonJS export names for ESM import in node:
391
+ 0 && (module.exports = {
392
+ OzwellChat,
393
+ useOzwell
394
+ });