@zantopia/zephyr-widget 0.4.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,277 @@
1
+ # 🦊 @zephyr/widget — Embeddable AI Navigation Assistant
2
+
3
+ Composant réutilisable d'assistant IA pour guider les utilisateurs dans n'importe quelle application web.
4
+
5
+ ## Installation
6
+
7
+ ### Option 1 : Script tag (le plus simple)
8
+
9
+ ```html
10
+ <script src="https://your-zephyr-server/api/sdk/zephyr-widget.js"></script>
11
+ <script>
12
+ ZephyrWidget.init({
13
+ server: 'https://your-zephyr-server',
14
+ persona: 'minimal',
15
+ theme: 'dark',
16
+ position: 'bottom-right',
17
+ accentColor: '#ff6b35',
18
+ language: 'fr',
19
+ });
20
+ </script>
21
+ ```
22
+
23
+ ### Option 2 : npm
24
+
25
+ ```bash
26
+ npm install @zephyr/widget
27
+ ```
28
+
29
+ ## Utilisation React
30
+
31
+ ```jsx
32
+ import { ZephyrChat } from '@zephyr/widget/react';
33
+
34
+ function App() {
35
+ return (
36
+ <ZephyrChat
37
+ server="https://your-zephyr-server"
38
+ persona="spirit"
39
+ theme="auto"
40
+ position="bottom-right"
41
+ accentColor="#6c63ff"
42
+ language="fr"
43
+ onMessage={(msg) => console.log(msg)}
44
+ />
45
+ );
46
+ }
47
+ ```
48
+
49
+ ### Inline (dans un conteneur)
50
+
51
+ ```jsx
52
+ <ZephyrChat
53
+ server="https://your-zephyr-server"
54
+ inline
55
+ style={{ height: '500px', width: '100%' }}
56
+ />
57
+ ```
58
+
59
+ ### Hook impératif
60
+
61
+ ```jsx
62
+ import { useZephyr } from '@zephyr/widget/react';
63
+
64
+ function HelpButton() {
65
+ const { open, send } = useZephyr();
66
+ return <button onClick={() => { open(); send("Comment faire X ?"); }}>Aide</button>;
67
+ }
68
+ ```
69
+
70
+ ## Utilisation Vue 3
71
+
72
+ ```vue
73
+ <script setup>
74
+ import ZephyrWidget from '@zephyr/widget/vue';
75
+ </script>
76
+
77
+ <template>
78
+ <ZephyrWidget
79
+ server="https://your-zephyr-server"
80
+ persona="futuristic"
81
+ theme="dark"
82
+ position="bottom-left"
83
+ accent-color="#ff9f43"
84
+ @message="onMessage"
85
+ @toggle="onToggle"
86
+ />
87
+ </template>
88
+ ```
89
+
90
+ ### Inline + ref
91
+
92
+ ```vue
93
+ <template>
94
+ <ZephyrWidget
95
+ ref="zephyr"
96
+ server="..."
97
+ :inline="true"
98
+ style="height: 500px"
99
+ />
100
+ <button @click="$refs.zephyr.send('Bonjour')">Envoyer</button>
101
+ </template>
102
+ ```
103
+
104
+ ## Utilisation Vanilla JS (Web Component / n'importe quel framework)
105
+
106
+ ```js
107
+ import { ZephyrWidget } from '@zephyr/widget';
108
+
109
+ const widget = ZephyrWidget.init({
110
+ server: 'https://your-zephyr-server',
111
+ persona: 'mascot',
112
+ theme: 'light',
113
+ position: 'bottom-right',
114
+ });
115
+
116
+ // API impérative
117
+ widget.open();
118
+ widget.send("Où trouver les paramètres ?");
119
+ widget.setTheme('dark');
120
+ widget.setPersona('spirit');
121
+ widget.close();
122
+ widget.destroy();
123
+ ```
124
+
125
+ ## Props / Options
126
+
127
+ | Prop | Type | Default | Description |
128
+ |---|---|---|---|
129
+ | `server` | string | **required** | URL du serveur Zephyr |
130
+ | `apiKey` | string | `""` | Clé API pour l'authentification |
131
+ | `persona` | string | `"minimal"` | `"mascot"` \| `"spirit"` \| `"minimal"` \| `"futuristic"` \| URL image custom |
132
+ | `theme` | string | `"dark"` | `"dark"` \| `"light"` \| `"auto"` |
133
+ | `position` | string | `"bottom-right"` | `"bottom-right"` \| `"bottom-left"` \| `"top-right"` \| `"top-left"` |
134
+ | `size` | string | `"md"` | `"sm"` \| `"md"` \| `"lg"` |
135
+ | `language` | string | `"fr"` | `"fr"` \| `"en"` |
136
+ | `greeting` | string | `null` | Message d'accueil custom |
137
+ | `placeholder` | string | `null` | Placeholder du champ de saisie |
138
+ | `accentColor` | string | `"#ff6b35"` | Couleur d'accent (CSS) |
139
+ | `zIndex` | number | `99999` | Z-index CSS |
140
+ | `open` | boolean | `false` | Ouvrir au chargement |
141
+ | `showBadge` | boolean | `true` | Afficher le badge de notification |
142
+ | `features` | array | `["chat","guide","search"]` | Fonctionnalités activées |
143
+ | `appContext` | object | `null` | Contexte applicatif métier (voir ci-dessous) |
144
+ | `inline` | boolean | `false` | Mode inline (dans un conteneur) |
145
+ | `customCSS` | string | `""` | CSS additionnel |
146
+
147
+ ## Personas
148
+
149
+ | ID | Style | Description |
150
+ |---|---|---|
151
+ | `mascot` | Mascotte complète | Renard Zephyr avec moustaches, détails complets |
152
+ | `spirit` | Esprit flottant | Version éthérée avec glow radial |
153
+ | `minimal` | Ultra minimal SaaS | Fond arrondi, traits simples |
154
+ | `futuristic` | Futuriste | Dégradé, yeux rectangulaires, point lumineux |
155
+
156
+ Vous pouvez aussi passer une **URL d'image** pour un persona custom :
157
+ ```js
158
+ ZephyrWidget.init({
159
+ server: '...',
160
+ persona: 'https://example.com/my-custom-avatar.svg',
161
+ });
162
+ ```
163
+
164
+ ## Events / Callbacks
165
+
166
+ | Event | Payload | Description |
167
+ |---|---|---|
168
+ | `onReady` / `@ready` | `widget` | Widget initialisé |
169
+ | `onMessage` / `@message` | `{ role, text, expression }` | Nouveau message |
170
+ | `onError` / `@error` | `{ message }` | Erreur WebSocket/serveur |
171
+ | `onToggle` / `@toggle` | `boolean` | Panel ouvert/fermé |
172
+
173
+ ## API Impérative
174
+
175
+ | Méthode | Description |
176
+ |---|---|
177
+ | `widget.open()` | Ouvre le panel |
178
+ | `widget.close()` | Ferme le panel |
179
+ | `widget.toggle()` | Bascule |
180
+ | `widget.send(text)` | Envoie un message |
181
+ | `widget.setTheme(theme)` | Change le thème |
182
+ | `widget.setPersona(persona)` | Change le persona |
183
+ | `widget.setAccentColor(color)` | Change la couleur d'accent |
184
+ | `widget.setAppContext(ctx)` | Met Ă  jour le contexte applicatif (utile pour les SPAs) |
185
+ | `widget.destroy()` | Détruit le widget |
186
+
187
+ ## Application Context (`appContext`)
188
+
189
+ Le contexte applicatif permet au chatbot de répondre **instantanément** aux questions métier sans analyser le DOM à chaque fois.
190
+
191
+ Le développeur qui intègre Zephyr décrit son application : ses features, sa FAQ, sa terminologie et ses workflows.
192
+
193
+ ### Structure
194
+
195
+ ```js
196
+ ZephyrWidget.init({
197
+ server: 'https://...',
198
+ appContext: {
199
+ // Nom et description de l'application
200
+ name: 'MonApp',
201
+ description: 'Plateforme de gestion de projets collaboratifs',
202
+
203
+ // Fonctionnalités principales (utilisé pour guider la navigation)
204
+ features: [
205
+ { name: 'Dashboard', path: '/dashboard', description: 'Vue d\'ensemble des projets actifs' },
206
+ { name: 'Kanban', path: '/board', description: 'Tableau de suivi des tâches drag & drop' },
207
+ { name: 'Paramètres', path: '/settings', description: 'Configuration du compte et des projets' },
208
+ ],
209
+
210
+ // FAQ — réponses pré-écrites (le chatbot les renvoie directement)
211
+ faq: [
212
+ { question: 'Comment créer un projet ?', answer: 'Menu + > Nouveau projet > Remplir le formulaire' },
213
+ { question: 'Comment inviter un membre ?', answer: 'Paramètres du projet > Membres > Inviter' },
214
+ ],
215
+
216
+ // Terminologie métier (le chatbot utilisera ces définitions)
217
+ terminology: {
218
+ sprint: 'période de travail de 2 semaines',
219
+ story: 'tâche utilisateur à compléter',
220
+ backlog: 'liste des tâches à planifier',
221
+ },
222
+
223
+ // Workflows typiques (parcours utilisateur)
224
+ workflows: [
225
+ 'Créer un projet → Ajouter des membres → Créer des tâches → Suivre l\'avancement',
226
+ 'Se connecter → Dashboard → Sélectionner un projet → Vue Kanban',
227
+ ],
228
+
229
+ // Texte libre complémentaire
230
+ custom: 'L\'app supporte 3 rĂ´les : Admin, Manager et Contributeur.',
231
+ },
232
+ });
233
+ ```
234
+
235
+ ### Mise Ă  jour dynamique (SPA)
236
+
237
+ Pour les Single Page Applications, mettez Ă  jour le contexte lors des changements de route :
238
+
239
+ ```js
240
+ const widget = ZephyrWidget.init({ server: '...', appContext: globalContext });
241
+
242
+ // Quand l'utilisateur change de page
243
+ router.afterEach((to) => {
244
+ widget.setAppContext({
245
+ ...globalContext,
246
+ custom: `L'utilisateur est sur la page ${to.name}.`,
247
+ });
248
+ });
249
+ ```
250
+
251
+ ### API REST
252
+
253
+ Le contexte applicatif peut aussi être défini via l'API REST :
254
+
255
+ ```bash
256
+ # Définir le contexte
257
+ curl -X PUT https://your-server/api/guide/app-context \
258
+ -H 'Content-Type: application/json' \
259
+ -d '{ "session_id": "...", "app_context": { "name": "MonApp", ... } }'
260
+
261
+ # Récupérer le contexte
262
+ curl https://your-server/api/guide/app-context/{session_id}
263
+ ```
264
+
265
+ ### Comment ça marche
266
+
267
+ 1. Le widget envoie le `appContext` au serveur via WebSocket Ă  la connexion
268
+ 2. Le serveur le stocke dans la session utilisateur
269
+ 3. À chaque question, le contexte applicatif est injecté **en priorité** dans le prompt LLM
270
+ 4. Si la FAQ contient la réponse, le chatbot la renvoie directement
271
+ 5. Sinon, il combine le contexte applicatif + l'analyse DOM pour une réponse complète
272
+
273
+ **Avantages :**
274
+ - ⚡ Réponses instantanées aux questions métier (pas d'analyse DOM nécessaire)
275
+ - 🎯 Réponses plus précises (le dev connaît mieux son app que le DOM)
276
+ - 💰 Moins de tokens LLM consommés
277
+ - 🗣️ Le chatbot parle le "langage" de l'app
package/index.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @zephyr/widget — TypeScript type definitions
3
+ */
4
+
5
+ /** Application context provided by the integrator to enrich chatbot responses. */
6
+ export interface AppContext {
7
+ /** Application name */
8
+ name?: string;
9
+ /** Short description of what the application does */
10
+ description?: string;
11
+ /** Key features / pages of the application */
12
+ features?: Array<{
13
+ name: string;
14
+ path?: string;
15
+ description?: string;
16
+ }>;
17
+ /** Frequently asked questions with pre-written answers */
18
+ faq?: Array<{
19
+ question: string;
20
+ answer: string;
21
+ }>;
22
+ /** Domain-specific terminology definitions */
23
+ terminology?: Record<string, string>;
24
+ /** Typical user workflows described as steps */
25
+ workflows?: string[];
26
+ /** Any extra free-form context the integrator wants the chatbot to know */
27
+ custom?: string;
28
+ }
29
+
30
+ export interface ZephyrWidgetOptions {
31
+ /** URL of the Zephyr backend server */
32
+ server: string;
33
+ /** API key for authentication (optional) */
34
+ apiKey?: string;
35
+ /** Persona style: "mascot" | "spirit" | "minimal" | "futuristic" | custom image URL */
36
+ persona?: "mascot" | "spirit" | "minimal" | "futuristic" | string;
37
+ /** Color theme */
38
+ theme?: "dark" | "light" | "auto";
39
+ /** Widget position on screen */
40
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
41
+ /** Avatar size */
42
+ size?: "sm" | "md" | "lg";
43
+ /** Language */
44
+ language?: "fr" | "en";
45
+ /** Custom greeting message */
46
+ greeting?: string | null;
47
+ /** Input placeholder */
48
+ placeholder?: string | null;
49
+ /** Primary accent color (CSS) */
50
+ accentColor?: string;
51
+ /** CSS z-index */
52
+ zIndex?: number;
53
+ /** Allow dragging the trigger button */
54
+ draggable?: boolean;
55
+ /** Start with panel open */
56
+ open?: boolean;
57
+ /** Show notification badge */
58
+ showBadge?: boolean;
59
+ /** Badge count */
60
+ badgeCount?: number;
61
+ /** Enabled features */
62
+ features?: Array<"chat" | "guide" | "search">;
63
+ /**
64
+ * Application context — describe your app so the chatbot can answer
65
+ * user questions faster without needing to analyse the DOM every time.
66
+ */
67
+ appContext?: AppContext;
68
+ /** Mount inside a specific selector (inline mode — no floating trigger) */
69
+ containerSelector?: string | null;
70
+ /** Additional CSS to inject */
71
+ customCSS?: string;
72
+
73
+ // Callbacks
74
+ onReady?: (widget: ZephyrWidgetInstance) => void;
75
+ onMessage?: (msg: { role: string; text: string; expression: string }) => void;
76
+ onError?: (err: { message: string }) => void;
77
+ onToggle?: (isOpen: boolean) => void;
78
+ }
79
+
80
+ export interface ZephyrWidgetInstance {
81
+ mount(container?: string | HTMLElement | null): void;
82
+ destroy(): void;
83
+ open(): void;
84
+ close(): void;
85
+ toggle(): void;
86
+ send(text: string): void;
87
+ setTheme(theme: "dark" | "light" | "auto"): void;
88
+ setPersona(persona: string): void;
89
+ setAccentColor(color: string): void;
90
+ /** Update the application context at runtime (useful for SPAs). */
91
+ setAppContext(ctx: AppContext): void;
92
+ on(event: "ready" | "message" | "error" | "toggle", handler: Function): void;
93
+ readonly messages: Array<{ role: string; text: string; expression: string }>;
94
+ readonly isOpen: boolean;
95
+ readonly sessionId: string | null;
96
+ readonly expression: string;
97
+ }
98
+
99
+ export interface ZephyrWidgetStatic {
100
+ init(options: ZephyrWidgetOptions): ZephyrWidgetInstance;
101
+ getInstance(): ZephyrWidgetInstance | null;
102
+ Widget: new (options: ZephyrWidgetOptions) => ZephyrWidgetInstance;
103
+ PERSONAS: Record<string, { svg: (accent: string) => string }>;
104
+ THEMES: Record<string, Record<string, string>>;
105
+ }
106
+
107
+ declare const ZephyrWidget: ZephyrWidgetStatic;
108
+ export default ZephyrWidget;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@zantopia/zephyr-widget",
3
+ "version": "0.4.0",
4
+ "description": "Zephyr AI Navigation Assistant — Embeddable widget for any web project",
5
+ "type": "module",
6
+ "main": "zephyr-widget.js",
7
+ "module": "zephyr-widget.js",
8
+ "types": "index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./zephyr-widget.js",
12
+ "types": "./index.d.ts"
13
+ },
14
+ "./react": {
15
+ "import": "./react/index.jsx",
16
+ "types": "./react/index.d.ts"
17
+ },
18
+ "./vue": {
19
+ "import": "./vue/ZephyrWidget.vue"
20
+ }
21
+ },
22
+ "files": [
23
+ "zephyr-widget.js",
24
+ "index.d.ts",
25
+ "react/",
26
+ "vue/",
27
+ "README.md"
28
+ ],
29
+ "scripts": {},
30
+ "peerDependencies": {
31
+ "react": ">=17.0.0",
32
+ "react-dom": ">=17.0.0",
33
+ "vue": ">=3.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "react": { "optional": true },
37
+ "react-dom": { "optional": true },
38
+ "vue": { "optional": true }
39
+ },
40
+ "keywords": ["zephyr", "ai", "widget", "assistant", "navigation", "ux"],
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+
3
+ export interface ZephyrChatProps {
4
+ /** Zephyr backend server URL */
5
+ server: string;
6
+ /** API key */
7
+ apiKey?: string;
8
+ /** Persona: "mascot" | "spirit" | "minimal" | "futuristic" | custom URL */
9
+ persona?: "mascot" | "spirit" | "minimal" | "futuristic" | string;
10
+ /** Theme */
11
+ theme?: "dark" | "light" | "auto";
12
+ /** Position */
13
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
14
+ /** Size */
15
+ size?: "sm" | "md" | "lg";
16
+ /** Language */
17
+ language?: "fr" | "en";
18
+ /** Custom greeting */
19
+ greeting?: string;
20
+ /** Input placeholder */
21
+ placeholder?: string;
22
+ /** Accent color */
23
+ accentColor?: string;
24
+ /** z-index */
25
+ zIndex?: number;
26
+ /** Start open */
27
+ open?: boolean;
28
+ /** Show badge */
29
+ showBadge?: boolean;
30
+ /** Features */
31
+ features?: Array<"chat" | "guide" | "search">;
32
+ /** Inline mode (render inside container) */
33
+ inline?: boolean;
34
+ /** Custom CSS */
35
+ customCSS?: string;
36
+ /** CSS class */
37
+ className?: string;
38
+ /** Inline styles */
39
+ style?: React.CSSProperties;
40
+
41
+ // Callbacks
42
+ onReady?: (widget: any) => void;
43
+ onMessage?: (msg: { role: string; text: string; expression: string }) => void;
44
+ onError?: (err: { message: string }) => void;
45
+ onToggle?: (isOpen: boolean) => void;
46
+ }
47
+
48
+ export declare function ZephyrChat(props: ZephyrChatProps): React.ReactElement | null;
49
+ export declare function useZephyr(): {
50
+ send: (text: string) => void;
51
+ open: () => void;
52
+ close: () => void;
53
+ };
54
+
55
+ export default ZephyrChat;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 🦊 @kitsune/widget/react — React component wrapper
3
+ *
4
+ * Usage:
5
+ * import { KitsuneChat } from '@kitsune/widget/react';
6
+ *
7
+ * function App() {
8
+ * return (
9
+ * <KitsuneChat
10
+ * server="https://your-kitsune-server"
11
+ * persona="minimal"
12
+ * theme="dark"
13
+ * position="bottom-right"
14
+ * accentColor="#ff6b35"
15
+ * onMessage={(msg) => console.log(msg)}
16
+ * />
17
+ * );
18
+ * }
19
+ */
20
+
21
+ import React, { useEffect, useRef, useCallback } from "react";
22
+
23
+ // Widget is loaded as an IIFE — import the core
24
+ let WidgetModule = null;
25
+ try {
26
+ WidgetModule = require("../kitsune-widget.js");
27
+ } catch {
28
+ // Dynamic import fallback handled in useEffect
29
+ }
30
+
31
+ /**
32
+ * Configuration props — matches KitsuneWidgetOptions
33
+ */
34
+ const defaultProps = {
35
+ server: "",
36
+ persona: "minimal",
37
+ theme: "dark",
38
+ position: "bottom-right",
39
+ size: "md",
40
+ language: "fr",
41
+ accentColor: "#ff6b35",
42
+ zIndex: 99999,
43
+ open: false,
44
+ };
45
+
46
+ export function KitsuneChat(props) {
47
+ const containerRef = useRef(null);
48
+ const instanceRef = useRef(null);
49
+
50
+ const {
51
+ server,
52
+ apiKey,
53
+ persona,
54
+ theme,
55
+ position,
56
+ size,
57
+ language,
58
+ greeting,
59
+ placeholder,
60
+ accentColor,
61
+ zIndex,
62
+ draggable,
63
+ open,
64
+ showBadge,
65
+ features,
66
+ customCSS,
67
+ inline,
68
+ onReady,
69
+ onMessage,
70
+ onError,
71
+ onToggle,
72
+ className,
73
+ style,
74
+ ...rest
75
+ } = { ...defaultProps, ...props };
76
+
77
+ useEffect(() => {
78
+ let widget = null;
79
+
80
+ const init = async () => {
81
+ let Mod = WidgetModule;
82
+ if (!Mod) {
83
+ // Fallback: load from CDN or relative path
84
+ try {
85
+ Mod = await import("../kitsune-widget.js");
86
+ } catch {
87
+ console.error("[KitsuneChat] Could not load kitsune-widget.js");
88
+ return;
89
+ }
90
+ }
91
+
92
+ const Cls = Mod.Widget || Mod.default?.Widget;
93
+ if (!Cls) return;
94
+
95
+ widget = new Cls({
96
+ server,
97
+ apiKey,
98
+ persona,
99
+ theme,
100
+ position,
101
+ size,
102
+ language,
103
+ greeting,
104
+ placeholder,
105
+ accentColor,
106
+ zIndex,
107
+ draggable,
108
+ open,
109
+ showBadge,
110
+ features,
111
+ customCSS,
112
+ containerSelector: inline ? null : null,
113
+ onReady,
114
+ onMessage,
115
+ onError,
116
+ onToggle,
117
+ });
118
+
119
+ if (inline && containerRef.current) {
120
+ widget.mount(containerRef.current);
121
+ } else {
122
+ widget.mount();
123
+ }
124
+
125
+ instanceRef.current = widget;
126
+ };
127
+
128
+ init();
129
+
130
+ return () => {
131
+ if (widget) widget.destroy();
132
+ };
133
+ }, [server]); // Re-init only on server change
134
+
135
+ // React to prop changes
136
+ useEffect(() => {
137
+ if (!instanceRef.current) return;
138
+ instanceRef.current.setTheme(theme);
139
+ }, [theme]);
140
+
141
+ useEffect(() => {
142
+ if (!instanceRef.current) return;
143
+ instanceRef.current.setPersona(persona);
144
+ }, [persona]);
145
+
146
+ useEffect(() => {
147
+ if (!instanceRef.current) return;
148
+ instanceRef.current.setAccentColor(accentColor);
149
+ }, [accentColor]);
150
+
151
+ // Inline renders a container div; floating renders nothing
152
+ if (inline) {
153
+ return React.createElement("div", {
154
+ ref: containerRef,
155
+ className: `kitsune-react-container ${className || ""}`,
156
+ style: { width: "100%", height: "100%", ...style },
157
+ ...rest,
158
+ });
159
+ }
160
+
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Hook for imperative widget control
166
+ */
167
+ export function useKitsune() {
168
+ const send = useCallback((text) => {
169
+ const w = typeof KitsuneWidget !== "undefined" ? KitsuneWidget.getInstance() : null;
170
+ if (w) w.send(text);
171
+ }, []);
172
+
173
+ const open = useCallback(() => {
174
+ const w = typeof KitsuneWidget !== "undefined" ? KitsuneWidget.getInstance() : null;
175
+ if (w) w.open();
176
+ }, []);
177
+
178
+ const close = useCallback(() => {
179
+ const w = typeof KitsuneWidget !== "undefined" ? KitsuneWidget.getInstance() : null;
180
+ if (w) w.close();
181
+ }, []);
182
+
183
+ return { send, open, close };
184
+ }
185
+
186
+ export default KitsuneChat;