@yak-io/react 0.8.0 → 0.10.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 CHANGED
@@ -1,18 +1,29 @@
1
1
  # @yak-io/react
2
2
 
3
- React integration for the Yak embeddable chat widget. Provides `YakProvider`, `YakWidget`, `useYak`, and `useYakToolEvent`.
3
+ > 📚 **Full documentation:** https://docs.yak.io/docs/sdks/react
4
+ >
5
+ > 🤖 **For LLMs / AI agents:** https://docs.yak.io/llms.txt
4
6
 
5
- ## Installation
7
+ React SDK for [Yak](https://docs.yak.io) — an embeddable AI assistant (text chat **and** push-to-talk voice) for web apps. Wrap your app in `YakProvider`, render `YakWidget` (or build your own trigger), and control everything with the `useYak()` hook.
8
+
9
+ **On Next.js, use [`@yak-io/nextjs`](https://docs.yak.io/docs/sdks/nextjs) instead** — it adds route scanning and server handler factories on top of this package.
6
10
 
7
11
  ```bash
8
12
  pnpm add @yak-io/react
9
13
  ```
10
14
 
11
- For Next.js, use [`@yak-io/nextjs`](../nextjs) instead — it adds route scanning and server handler factories on top of this package.
15
+ ## Exports
12
16
 
13
- ## Quickstart
17
+ | Export | Kind | Purpose |
18
+ | --- | --- | --- |
19
+ | `YakProvider` | component | Sets up the chat + voice runtime. Wrap your app once. |
20
+ | `YakWidget` | component | The floating launcher pill. Render it inside `YakProvider`, or omit it and build your own trigger. |
21
+ | `useYak()` | hook | Imperative control of chat + voice, plus reactive state. |
22
+ | `useYakToolEvent(handler)` | hook | Run a callback after each tool call (e.g. cache invalidation). |
23
+ | `enableYakLogging` / `disableYakLogging` / `isYakLoggingEnabled` | fn | Toggle verbose SDK logging. |
24
+ | Types | — | `YakProviderProps`, `YakWidgetProps`, `YakContextValue`, plus core types re-exported from `@yak-io/javascript`. |
14
25
 
15
- ### 1. Wrap your app with `YakProvider`
26
+ ## Quickstart
16
27
 
17
28
  ```tsx
18
29
  // src/App.tsx (or your root layout)
@@ -22,11 +33,14 @@ export default function App({ children }: { children: React.ReactNode }) {
22
33
  return (
23
34
  <YakProvider
24
35
  appId={import.meta.env.VITE_YAK_APP_ID}
36
+ mode="both" // "chat" | "voice" | "both" — default "chat"
25
37
  theme={{ position: "bottom-right", colorMode: "system" }}
38
+ // Routes + tools the assistant may use (usually your server endpoint).
26
39
  getConfig={async () => {
27
40
  const res = await fetch("/api/yak");
28
41
  return res.json();
29
42
  }}
43
+ // Execute a tool the assistant decides to call.
30
44
  onToolCall={async (name, args) => {
31
45
  const res = await fetch("/api/yak", {
32
46
  method: "POST",
@@ -45,142 +59,143 @@ export default function App({ children }: { children: React.ReactNode }) {
45
59
  }
46
60
  ```
47
61
 
48
- ### 2. Control the widget programmatically
62
+ ## Programmatic control
63
+
64
+ `useYak()` works from any component inside `YakProvider`. To skip the floating pill entirely, just don't render `<YakWidget>` and wire your own buttons:
49
65
 
50
66
  ```tsx
51
67
  import { useYak } from "@yak-io/react";
52
68
 
53
- export function HelpButton() {
69
+ function HelpButton() {
54
70
  const { open, openWithPrompt, isOpen } = useYak();
71
+ return (
72
+ <>
73
+ <button onClick={open}>Open chat</button>
74
+ <button onClick={() => openWithPrompt("How do I get started?")}>Get help</button>
75
+ {isOpen && <span>Chat is open</span>}
76
+ </>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## Voice
82
+
83
+ Set `mode="voice"` or `mode="both"` on the provider, then drive the session with the voice methods. `voiceStart()` must run from a user gesture (browser mic requirement).
84
+
85
+ ```tsx
86
+ import { useYak } from "@yak-io/react";
55
87
 
88
+ function VoiceButton() {
89
+ const { voiceToggle, voiceState, voiceIsActive, voiceLoading } = useYak();
56
90
  return (
57
- <div>
58
- <button onClick={open}>Open Chat</button>
59
- <button onClick={() => openWithPrompt("How do I get started?")}>
60
- Get Help
61
- </button>
62
- {isOpen && <p>Chat is open</p>}
63
- </div>
91
+ <button onClick={voiceToggle} disabled={voiceLoading}>
92
+ {voiceIsActive ? `Stop (${voiceState})` : "Start voice"}
93
+ </button>
64
94
  );
65
95
  }
66
96
  ```
67
97
 
68
- ### 3. Invalidate data after tool calls
98
+ ## Tool events
99
+
100
+ Re-sync your UI when the assistant changes data:
69
101
 
70
102
  ```tsx
71
103
  import { useYakToolEvent } from "@yak-io/react";
72
104
 
73
105
  function TasksPage() {
74
- const [tasks, setTasks] = useState([]);
75
-
76
- // Re-fetch when the chatbot modifies tasks
77
106
  useYakToolEvent((event) => {
78
- if (event.ok && event.name.startsWith("tasks.")) {
79
- fetchTasks().then(setTasks);
80
- }
107
+ // { name, args, ok, result?, error? }
108
+ if (event.ok && event.name.startsWith("tasks.")) refetchTasks();
81
109
  });
82
-
83
110
  // ...
84
111
  }
85
112
  ```
86
113
 
87
- ## API Reference
114
+ ## API reference
88
115
 
89
- ### `YakProvider`
116
+ ### `<YakProvider>`
90
117
 
91
- Sets up context, initializes the widget, and delegates DOM rendering to `@yak-io/javascript`.
118
+ | Prop | Type | Default | Description |
119
+ | --- | --- | --- | --- |
120
+ | `appId` | `string` | — | Your Yak app ID (required). |
121
+ | `children` | `ReactNode` | — | Your app (required). |
122
+ | `mode` | `"chat" \| "voice" \| "both"` | `"chat"` | Which surfaces are exposed. |
123
+ | `getConfig` | `ChatConfigProvider` | — | Async provider of routes + tools. Called on open / voice start. |
124
+ | `onToolCall` | `ToolCallHandler` | — | Executes a tool the assistant calls. |
125
+ | `onGraphQLSchemaCall` | `GraphQLSchemaHandler` | — | Handles GraphQL schema tool calls. |
126
+ | `onRESTSchemaCall` | `RESTSchemaHandler` | — | Handles REST/OpenAPI schema tool calls. |
127
+ | `theme` | `Theme` | — | Position, color mode, and colors. |
128
+ | `onRedirect` | `(path: string) => void` | `window.location.assign` | Navigation handler. |
129
+ | `disableRestartButton` | `boolean` | `false` | Hide the restart-session button. |
130
+ | `trigger` | `boolean \| TriggerButtonConfig` | `false` | Render the provider's built-in pill. Leave `false` and use `<YakWidget>` instead. |
131
+ | `user` | `UserIdentity` | — | Signed end-user identity for conversation persistence. See [end-user identity](https://docs.yak.io/docs/customization/end-user-identity). |
92
132
 
93
- **Props:**
133
+ ### `<YakWidget>`
94
134
 
95
- | Prop | Type | Required | Description |
96
- |------|------|----------|-------------|
97
- | `appId` | `string` | ✅ | Your Yak app ID |
98
- | `children` | `ReactNode` | ✅ | Your app content |
99
- | `getConfig` | `ChatConfigProvider` | — | Async function returning routes + tools config. Called when the widget opens. |
100
- | `onToolCall` | `ToolCallHandler` | — | Handle tool calls from the assistant |
101
- | `onGraphQLSchemaCall` | `GraphQLSchemaHandler` | — | Handle GraphQL schema tool calls |
102
- | `onRESTSchemaCall` | `RESTSchemaHandler` | — | Handle REST/OpenAPI schema tool calls |
103
- | `theme` | `Theme` | — | Widget theme (position, colorMode, colors) |
104
- | `onRedirect` | `(path: string) => void` | — | Custom navigation handler (defaults to `window.location.assign`) |
105
- | `disableRestartButton` | `boolean` | — | Hide the restart session button |
106
- | `trigger` | `boolean \| TriggerButtonConfig` | — | Built-in trigger button config (`false` by default — use `<YakWidget>` instead) |
135
+ The floating launcher pill. Shows a chat icon, a voice icon, or both, based on `mode`.
107
136
 
108
- ### `YakWidget`
137
+ | Prop | Type | Default | Description |
138
+ | --- | --- | --- | --- |
139
+ | `mode` | `"chat" \| "voice" \| "both"` | inherits provider | Override which icons appear. |
140
+ | `lightButton` | `{ background?, color?, border? }` | — | Pill colors in light mode. |
141
+ | `darkButton` | `{ background?, color?, border? }` | — | Pill colors in dark mode. |
109
142
 
110
- Renders a fixed-position launcher button. Place it anywhere inside `YakProvider`.
111
-
112
- **Props:**
113
-
114
- | Prop | Type | Description |
115
- |------|------|-------------|
116
- | `triggerLabel` | `string` | Button label (default: `"Ask with AI"`) |
117
- | `position` | `WidgetPosition` | Button position (default: `"bottom-right"`) |
118
- | `colorMode` | `"light" \| "dark" \| "system"` | Color mode override |
119
- | `lightButton` | `{ background?, color?, border? }` | Custom light mode button colors |
120
- | `darkButton` | `{ background?, color?, border? }` | Custom dark mode button colors |
143
+ > Position and color mode come from the provider's `theme` (`theme.position`, `theme.colorMode`) not from `YakWidget` props.
121
144
 
122
145
  ### `useYak()`
123
146
 
124
- Access the widget API from any component inside `YakProvider`.
147
+ Returns `YakContextValue`. Throws if used outside `YakProvider`.
125
148
 
126
149
  ```ts
127
150
  const {
128
- isOpen, // boolean — whether the widget is open
129
- isReady, // boolean — whether the iframe is ready
130
- open, // () => void
131
- close, // () => void
132
- openWithPrompt, // (prompt: string) => void
151
+ // chat
152
+ isOpen, // boolean
153
+ isReady, // boolean iframe ready to receive messages
154
+ chatLoading, // boolean — isOpen && !isReady (show a loading spinner)
155
+ open, // () => void
156
+ close, // () => void
157
+ openWithPrompt, // (prompt: string) => void
158
+ // voice
159
+ voiceState, // "idle" | "connecting" | "listening" | "thinking" | "speaking" | "error"
160
+ voiceMachine, // { state, errorMessage? }
161
+ voiceIsActive, // boolean — connecting/listening/thinking/speaking
162
+ voiceLoading, // boolean — voiceState === "connecting"
163
+ voiceErrorMessage, // string | undefined
164
+ voiceStart, // () => Promise<void>
165
+ voiceStop, // () => Promise<void>
166
+ voiceToggle, // () => Promise<void>
167
+ // misc
168
+ mode, // "chat" | "voice" | "both"
133
169
  subscribeToToolEvents, // (handler) => () => void
134
170
  } = useYak();
135
171
  ```
136
172
 
137
- Throws if called outside `YakProvider`.
138
-
139
- ### `useYakToolEvent(handler)`
140
-
141
- Subscribe to tool call completion events. Automatically unsubscribes on unmount. Useful for cache invalidation when the chatbot modifies data.
142
-
143
- ```ts
144
- useYakToolEvent((event) => {
145
- // event.name — the tool name called ("tasks.list")
146
- // event.args — arguments passed to the tool
147
- // event.ok — whether the call succeeded
148
- // event.result — the result (if ok)
149
- // event.error — error message (if not ok)
150
- });
151
- ```
152
-
153
173
  ## Logging
154
174
 
155
175
  ```ts
156
176
  import { enableYakLogging, disableYakLogging, isYakLoggingEnabled } from "@yak-io/react";
157
177
 
158
- enableYakLogging(); // Enable verbose SDK logs
159
- disableYakLogging(); // Disable SDK logs
160
- isYakLoggingEnabled(); // → boolean
178
+ enableYakLogging(); // verbose SDK logs
161
179
  ```
162
180
 
163
181
  ## Types
164
182
 
165
- All key types are re-exported for convenience:
166
-
167
183
  ```ts
168
184
  import type {
185
+ YakProviderProps,
186
+ YakWidgetProps,
187
+ YakContextValue,
169
188
  ChatConfigProvider,
170
189
  ToolCallHandler,
171
190
  ToolCallEvent,
172
- GraphQLSchemaHandler,
173
- RESTSchemaHandler,
174
191
  Theme,
175
- ThemeColors,
176
- TriggerButtonConfig,
192
+ WidgetMode,
177
193
  WidgetPosition,
178
- SchemaSource,
179
- GraphQLSchemaSource,
180
- OpenAPISchemaSource,
194
+ VoiceState,
195
+ VoiceMachine,
181
196
  } from "@yak-io/react";
182
197
  ```
183
198
 
184
199
  ## License
185
200
 
186
- Proprietary — see LICENSE file.
201
+ Proprietary — see [LICENSE](./LICENSE).
@@ -1,19 +1,27 @@
1
+ import { type ChatConfigProvider, type GraphQLSchemaHandler, type RESTSchemaHandler, type Theme, type ToolCallHandler, type TriggerButtonConfig, type UserIdentity, type WidgetMode } from "@yak-io/javascript";
1
2
  import type React from "react";
2
- import { type TriggerButtonConfig, type ChatConfigProvider, type ToolCallHandler, type GraphQLSchemaHandler, type RESTSchemaHandler, type Theme } from "@yak-io/javascript";
3
3
  /**
4
4
  * Props for YakProvider
5
5
  */
6
6
  export type YakProviderProps = {
7
7
  /** App identifier in the yak SaaS */
8
8
  appId: string;
9
+ /**
10
+ * Which experiences this widget exposes.
11
+ * "chat" — chat only (iframe).
12
+ * "voice" — voice only (WebRTC).
13
+ * "both" — both, sharing one trigger pill.
14
+ * Default: "chat".
15
+ */
16
+ mode?: WidgetMode;
9
17
  /**
10
18
  * Provider function for chat configuration (routes + tools).
11
19
  * The consuming platform decides how to get the config (static, fetch, etc.)
12
- * Called when the widget is opened.
20
+ * Called when the chat is opened or when a voice session starts.
13
21
  */
14
22
  getConfig?: ChatConfigProvider;
15
23
  /**
16
- * Handler for tool calls from the chat widget.
24
+ * Handler for tool calls from the chat or voice runtime.
17
25
  * The consuming platform decides how to execute (browser, server fetch, etc.)
18
26
  */
19
27
  onToolCall?: ToolCallHandler;
@@ -32,17 +40,27 @@ export type YakProviderProps = {
32
40
  /** Disable the restart session button in the header */
33
41
  disableRestartButton?: boolean;
34
42
  /**
35
- * Trigger button configuration. Pass `false` to disable the built-in trigger button
36
- * (useful when using `<YakWidget />` separately). Defaults to `false`.
43
+ * Trigger button configuration. Pass `false` to disable the built-in trigger
44
+ * (useful when rendering `<YakWidget />` separately). Defaults to `false`.
37
45
  */
38
46
  trigger?: boolean | TriggerButtonConfig;
47
+ /**
48
+ * Signed end-user identity for server-side conversation persistence.
49
+ *
50
+ * Compute the `hash` on your backend with
51
+ * `HMAC-SHA256(application.apiSecret, user.id)` and pass it down to the
52
+ * browser. When supplied, the widget stores conversations server-side and
53
+ * shows a history pane so end-users can resume past chats. When omitted,
54
+ * the widget runs in anonymous mode (no persistence).
55
+ */
56
+ user?: UserIdentity;
39
57
  /** Children components */
40
58
  children: React.ReactNode;
41
59
  };
42
60
  /**
43
- * YakProvider sets up the context, message handling, and renders the chat widget.
44
- * All DOM rendering (panel, iframe, styles, optional trigger) is delegated to YakEmbed
45
- * from the JavaScript SDK.
61
+ * YakProvider sets up the unified chat + voice runtime. All DOM rendering
62
+ * (panel, iframe, optional trigger) is delegated to YakEmbed. Consumers
63
+ * access both surfaces via `useYak()`.
46
64
  */
47
- export declare function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, trigger, children, }: YakProviderProps): React.JSX.Element;
65
+ export declare function YakProvider({ appId, mode, getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, trigger, user, children, }: YakProviderProps): React.JSX.Element;
48
66
  //# sourceMappingURL=YakProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"YakProvider.d.ts","sourceRoot":"","sources":["../src/YakProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAEpB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,KAAK,EACX,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;OAEG;IACH,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;IAC3C;;OAEG;IACH,gBAAgB,CAAC,EAAE,iBAAiB,CAAC;IACrC,mCAAmC;IACnC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;IACxC,0BAA0B;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,UAAU,EACV,oBAAoB,EACpB,OAAe,EACf,QAAQ,GACT,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoJtC"}
1
+ {"version":3,"file":"YakProvider.d.ts","sourceRoot":"","sources":["../src/YakProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAGzB,KAAK,iBAAiB,EACtB,KAAK,KAAK,EAEV,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAEjB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;OAEG;IACH,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;IAC3C;;OAEG;IACH,gBAAgB,CAAC,EAAE,iBAAiB,CAAC;IACrC,mCAAmC;IACnC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;IACxC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,0BAA0B;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,IAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,UAAU,EACV,oBAAoB,EACpB,OAAe,EACf,IAAI,EACJ,QAAQ,GACT,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAmNtC"}
@@ -1,17 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
3
+ import { INITIAL_VOICE_MACHINE, logger, YakEmbed, } from "@yak-io/javascript";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
5
  import { YakContext } from "./context.js";
5
- import { YakEmbed, } from "@yak-io/javascript";
6
- import { logger } from "@yak-io/javascript";
7
6
  /**
8
- * YakProvider sets up the context, message handling, and renders the chat widget.
9
- * All DOM rendering (panel, iframe, styles, optional trigger) is delegated to YakEmbed
10
- * from the JavaScript SDK.
7
+ * YakProvider sets up the unified chat + voice runtime. All DOM rendering
8
+ * (panel, iframe, optional trigger) is delegated to YakEmbed. Consumers
9
+ * access both surfaces via `useYak()`.
11
10
  */
12
- export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, trigger = false, children, }) {
11
+ export function YakProvider({ appId, mode = "chat", getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, trigger = false, user, children, }) {
13
12
  const [isOpen, setIsOpen] = useState(false);
14
13
  const [isReady, setIsReady] = useState(false);
14
+ const [voiceMachine, setVoiceMachine] = useState(INITIAL_VOICE_MACHINE);
15
15
  // Store event subscribers for tool call events
16
16
  const toolEventSubscribersRef = useRef(new Set());
17
17
  // Handler that notifies all subscribers when a tool call completes
@@ -45,30 +45,36 @@ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall,
45
45
  if (!embedRef.current) {
46
46
  embedRef.current = new YakEmbed({
47
47
  appId,
48
+ mode,
48
49
  theme,
49
50
  trigger,
51
+ getConfig,
50
52
  onToolCall,
51
53
  onGraphQLSchemaCall,
52
54
  onRESTSchemaCall,
53
55
  onRedirect: resolvedRedirect,
54
56
  options: { disableRestartButton },
55
57
  onToolCallComplete: handleToolCallComplete,
58
+ user,
56
59
  });
57
60
  }
58
61
  const embed = embedRef.current;
59
- // Mount/unmount embed and subscribe to state changes
62
+ // Mount/unmount embed and subscribe to chat + voice state changes
60
63
  useEffect(() => {
61
64
  embed.mount();
62
- const unsubscribe = embed.onStateChange((state) => {
65
+ const unsubscribeChat = embed.onStateChange((state) => {
63
66
  setIsOpen(state.isOpen);
64
67
  setIsReady(state.isReady);
65
68
  });
69
+ const unsubscribeVoice = embed.onVoiceStateChange((m) => setVoiceMachine(m));
66
70
  return () => {
67
- unsubscribe();
71
+ unsubscribeChat();
72
+ unsubscribeVoice();
68
73
  embed.destroy();
69
74
  };
70
75
  }, [embed]);
71
- // Update embed config when props change (exclude onReady/onClose which are managed by YakEmbed)
76
+ // Update embed config when props change. Chat config goes via the client;
77
+ // voice config goes via the voice session.
72
78
  useEffect(() => {
73
79
  embed.getClient().updateConfig({
74
80
  appId,
@@ -79,9 +85,19 @@ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall,
79
85
  onRedirect: resolvedRedirect,
80
86
  options: { disableRestartButton },
81
87
  onToolCallComplete: handleToolCallComplete,
88
+ user,
89
+ });
90
+ embed.getVoiceSession()?.updateConfig({
91
+ appId,
92
+ getConfig,
93
+ onToolCall,
94
+ onGraphQLSchemaCall,
95
+ onRESTSchemaCall,
96
+ onRedirect: resolvedRedirect,
82
97
  });
83
98
  }, [
84
99
  appId,
100
+ getConfig,
85
101
  onToolCall,
86
102
  onGraphQLSchemaCall,
87
103
  onRESTSchemaCall,
@@ -90,8 +106,9 @@ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall,
90
106
  disableRestartButton,
91
107
  embed,
92
108
  handleToolCallComplete,
109
+ user,
93
110
  ]);
94
- // Fetch chat config when widget is opened
111
+ // Fetch chat config when the chat is opened
95
112
  useEffect(() => {
96
113
  if (typeof window === "undefined" || !isOpen || !getConfig)
97
114
  return;
@@ -113,10 +130,21 @@ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall,
113
130
  cancelled = true;
114
131
  };
115
132
  }, [getConfig, isOpen, embed]);
116
- // Open/close methods
133
+ // Chat methods
117
134
  const open = useCallback(() => embed.open(), [embed]);
118
135
  const close = useCallback(() => embed.close(), [embed]);
119
136
  const openWithPrompt = useCallback((prompt) => embed.openWithPrompt(prompt), [embed]);
137
+ // Voice methods
138
+ const voiceStart = useCallback(async () => {
139
+ try {
140
+ await embed.voiceStart();
141
+ }
142
+ catch (err) {
143
+ logger.warn("Voice start failed", err);
144
+ }
145
+ }, [embed]);
146
+ const voiceStop = useCallback(() => embed.voiceStop(), [embed]);
147
+ const voiceToggle = useCallback(() => embed.voiceToggle(), [embed]);
120
148
  // Subscribe to tool call completion events
121
149
  const subscribeToToolEvents = useCallback((handler) => {
122
150
  toolEventSubscribersRef.current.add(handler);
@@ -128,14 +156,47 @@ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall,
128
156
  }, []);
129
157
  // Expose iframe origin for YakWidget
130
158
  const getIframeOrigin = useCallback(() => embed.getClient().getIframeOrigin(), [embed]);
159
+ const voiceState = voiceMachine.state;
160
+ const voiceIsActive = voiceState !== "idle" && voiceState !== "error";
161
+ const chatLoading = isOpen && !isReady;
162
+ const voiceLoading = voiceState === "connecting";
131
163
  const contextValue = useMemo(() => ({
164
+ mode,
132
165
  isOpen,
133
166
  isReady,
167
+ chatLoading,
134
168
  open,
135
169
  close,
136
170
  openWithPrompt,
137
171
  subscribeToToolEvents,
172
+ voiceMachine,
173
+ voiceState,
174
+ voiceErrorMessage: voiceMachine.errorMessage,
175
+ voiceIsActive,
176
+ voiceLoading,
177
+ voiceStart,
178
+ voiceStop,
179
+ voiceToggle,
138
180
  getIframeOrigin,
139
- }), [isOpen, isReady, open, close, openWithPrompt, subscribeToToolEvents, getIframeOrigin]);
181
+ theme,
182
+ }), [
183
+ mode,
184
+ isOpen,
185
+ isReady,
186
+ chatLoading,
187
+ open,
188
+ close,
189
+ openWithPrompt,
190
+ subscribeToToolEvents,
191
+ voiceMachine,
192
+ voiceState,
193
+ voiceIsActive,
194
+ voiceLoading,
195
+ voiceStart,
196
+ voiceStop,
197
+ voiceToggle,
198
+ getIframeOrigin,
199
+ theme,
200
+ ]);
140
201
  return _jsx(YakContext.Provider, { value: contextValue, children: children });
141
202
  }
@@ -1,15 +1,11 @@
1
+ import type { WidgetMode } from "@yak-io/javascript";
1
2
  import type React from "react";
2
- import type { WidgetPosition } from "@yak-io/javascript";
3
3
  /**
4
4
  * Props for YakWidget
5
5
  */
6
6
  export type YakWidgetProps = {
7
- /** Text to display next to the logo */
8
- triggerLabel?: string;
9
- /** Position of the button (defaults to theme position or "bottom-right") */
10
- position?: WidgetPosition;
11
- /** Color mode override */
12
- colorMode?: "light" | "dark" | "system";
7
+ /** Override the mode from the provider. */
8
+ mode?: WidgetMode;
13
9
  /** Custom button colors for light mode */
14
10
  lightButton?: {
15
11
  background?: string;
@@ -24,9 +20,10 @@ export type YakWidgetProps = {
24
20
  };
25
21
  };
26
22
  /**
27
- * YakWidget renders a fixed-position launcher button.
28
- * Trigger CSS is injected by YakEmbed (via YakProvider) this component
29
- * only provides the React button element using those shared class names.
23
+ * YakWidget renders a fixed-position launcher pill. The logo sits on the
24
+ * left; one or two icon buttons sit on the right based on `mode`. Trigger
25
+ * CSS is injected by YakEmbed (via YakProvider) this component only
26
+ * provides the React markup using those shared class names.
30
27
  */
31
- export declare function YakWidget({ triggerLabel, position, colorMode, lightButton, darkButton, }?: YakWidgetProps): React.JSX.Element;
28
+ export declare function YakWidget({ mode, lightButton, darkButton, }?: YakWidgetProps): React.JSX.Element;
32
29
  //# sourceMappingURL=YakWidget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"YakWidget.d.ts","sourceRoot":"","sources":["../src/YakWidget.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,0BAA0B;IAC1B,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACxC,0CAA0C;IAC1C,WAAW,CAAC,EAAE;QACZ,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,yCAAyC;IACzC,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAwCF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EACxB,YAA4B,EAC5B,QAAyB,EACzB,SAAS,EACT,WAAW,EACX,UAAU,GACX,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoCzC"}
1
+ {"version":3,"file":"YakWidget.d.ts","sourceRoot":"","sources":["../src/YakWidget.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,0CAA0C;IAC1C,WAAW,CAAC,EAAE;QACZ,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,yCAAyC;IACzC,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAsGF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,WAAW,EACX,UAAU,GACX,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAmEzC"}
package/dist/YakWidget.js CHANGED
@@ -1,10 +1,32 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useContext } from "react";
4
- import { YakContext, useYak } from "./context.js";
5
- /**
6
- * Compute button CSS variables from custom button color props
7
- */
3
+ import { useYakInternal } from "./context.js";
4
+ const VOICE_ARIA = {
5
+ idle: "Start voice mode",
6
+ connecting: "Connecting voice session",
7
+ listening: "Voice listening — tap to stop",
8
+ thinking: "Voice thinking — tap to stop",
9
+ speaking: "Voice speaking — tap to stop",
10
+ error: "Voice error — tap to retry",
11
+ };
12
+ function MessageCircleIcon() {
13
+ return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: 20, height: 20, "aria-hidden": "true", children: _jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
14
+ }
15
+ function AudioLinesIcon() {
16
+ return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: 20, height: 20, "aria-hidden": "true", children: [_jsx("path", { d: "M2 10v3" }), _jsx("path", { d: "M6 6v11" }), _jsx("path", { d: "M10 3v18" }), _jsx("path", { d: "M14 8v7" }), _jsx("path", { d: "M18 5v13" }), _jsx("path", { d: "M22 10v3" })] }));
17
+ }
18
+ function StopIcon() {
19
+ return (_jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: 20, height: 20, "aria-hidden": "true", children: _jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }));
20
+ }
21
+ function VoiceIcon({ state }) {
22
+ if (state === "connecting") {
23
+ return _jsx("span", { className: "yak-widget-spinner", "aria-hidden": "true" });
24
+ }
25
+ if (state === "listening" || state === "speaking" || state === "thinking") {
26
+ return _jsx(StopIcon, {});
27
+ }
28
+ return _jsx(AudioLinesIcon, {});
29
+ }
8
30
  function buildButtonStyle(lightButton, darkButton) {
9
31
  const style = {};
10
32
  if (lightButton?.background)
@@ -21,9 +43,6 @@ function buildButtonStyle(lightButton, darkButton) {
21
43
  style["--yak-btn-dark-border"] = darkButton.border;
22
44
  return style;
23
45
  }
24
- /**
25
- * Compute button class names from color mode and custom theme flags
26
- */
27
46
  function buildButtonClasses(colorMode, hasLightCustom, hasDarkCustom) {
28
47
  const colorModeClass = colorMode === "light" ? "yak-widget-light" : colorMode === "dark" ? "yak-widget-dark" : "";
29
48
  let customButtonClass = "";
@@ -36,18 +55,27 @@ function buildButtonClasses(colorMode, hasLightCustom, hasDarkCustom) {
36
55
  return ["yak-widget-trigger", colorModeClass, customButtonClass].filter(Boolean).join(" ");
37
56
  }
38
57
  /**
39
- * YakWidget renders a fixed-position launcher button.
40
- * Trigger CSS is injected by YakEmbed (via YakProvider) this component
41
- * only provides the React button element using those shared class names.
58
+ * YakWidget renders a fixed-position launcher pill. The logo sits on the
59
+ * left; one or two icon buttons sit on the right based on `mode`. Trigger
60
+ * CSS is injected by YakEmbed (via YakProvider) this component only
61
+ * provides the React markup using those shared class names.
42
62
  */
43
- export function YakWidget({ triggerLabel = "Ask with AI", position = "bottom-right", colorMode, lightButton, darkButton, } = {}) {
44
- const { open, isOpen, isReady } = useYak();
45
- const internal = useContext(YakContext);
46
- const logoUrl = internal ? `${internal.getIframeOrigin()}/logo.svg` : "";
47
- const isLoading = isOpen && !isReady;
63
+ export function YakWidget({ mode, lightButton, darkButton, } = {}) {
64
+ const ctx = useYakInternal();
65
+ const resolvedMode = mode ?? ctx.mode;
66
+ const position = ctx.theme?.position ?? "bottom-left";
67
+ const colorMode = ctx.theme?.colorMode;
68
+ const logoUrl = `${ctx.getIframeOrigin()}/logo.svg`;
69
+ const showChat = resolvedMode === "chat" || resolvedMode === "both";
70
+ const showVoice = resolvedMode === "voice" || resolvedMode === "both";
71
+ // Consume the shipped loading flags rather than re-deriving them here.
72
+ const chatLoading = ctx.chatLoading;
73
+ const voiceConnecting = ctx.voiceLoading;
48
74
  const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
49
75
  const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;
50
76
  const buttonStyle = buildButtonStyle(lightButton, darkButton);
51
77
  const buttonClasses = buildButtonClasses(colorMode, hasLightCustom, hasDarkCustom);
52
- return (_jsxs("button", { type: "button", onClick: open, className: buttonClasses, style: Object.keys(buttonStyle).length > 0 ? buttonStyle : undefined, "data-position": position, "data-has-light-custom": hasLightCustom || undefined, "data-has-dark-custom": hasDarkCustom || undefined, "aria-label": isLoading ? "Loading chat" : "Open chat", disabled: isLoading, children: [_jsx("span", { className: "yak-widget-trigger-label", children: triggerLabel }), _jsx("div", { className: "yak-widget-icon-bg", children: isLoading ? (_jsx("div", { className: "yak-widget-spinner", "aria-hidden": "true" })) : (_jsx("img", { src: logoUrl, alt: "", width: 20, height: 20, className: "yak-widget-icon" })) })] }));
78
+ return (_jsxs("div", { className: buttonClasses, style: Object.keys(buttonStyle).length > 0 ? buttonStyle : undefined, "data-position": position, "data-mode": resolvedMode, "data-has-light-custom": hasLightCustom || undefined, "data-has-dark-custom": hasDarkCustom || undefined, children: [_jsx("div", { className: "yak-widget-icon-bg", children: _jsx("img", { src: logoUrl, alt: "", width: 20, height: 20, className: "yak-widget-icon" }) }), showChat && (_jsx("button", { type: "button", className: "yak-widget-trigger-icon-btn", "data-action": "chat", "aria-label": chatLoading ? "Loading chat" : "Open chat", disabled: chatLoading, onClick: ctx.open, children: chatLoading ? (_jsx("span", { className: "yak-widget-spinner", "aria-hidden": "true" })) : (_jsx(MessageCircleIcon, {})) })), showVoice && (_jsx("button", { type: "button", className: "yak-widget-trigger-icon-btn", "data-action": "voice", "data-state": ctx.voiceState, "aria-label": VOICE_ARIA[ctx.voiceState], disabled: voiceConnecting, onClick: () => {
79
+ void ctx.voiceToggle();
80
+ }, children: _jsx(VoiceIcon, { state: ctx.voiceState }) }))] }));
53
81
  }
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Theme, ChatConfig, ToolCallEvent } from "@yak-io/javascript";
1
+ import type { ChatConfig, Theme, ToolCallEvent, VoiceMachine, VoiceState, WidgetMode } from "@yak-io/javascript";
2
2
  /**
3
3
  * Configuration for the yak provider
4
4
  */
@@ -13,14 +13,21 @@ export type YakConfig = {
13
13
  */
14
14
  export type ToolCallEventHandler = (event: ToolCallEvent) => void;
15
15
  /**
16
- * Public API for controlling the Yak chat widget.
16
+ * Public API for controlling the Yak widget — chat + voice combined.
17
17
  * This is what consumers get from useYak().
18
18
  */
19
19
  export type YakContextValue = {
20
+ /** Which modes are exposed: "chat", "voice", or "both". */
21
+ mode: WidgetMode;
20
22
  /** Whether the chat widget is currently open */
21
23
  isOpen: boolean;
22
24
  /** Whether the iframe is ready to receive messages */
23
25
  isReady: boolean;
26
+ /**
27
+ * Whether the chat is opening but not yet interactive (`isOpen && !isReady`).
28
+ * Drive a custom loading state off this instead of re-deriving it.
29
+ */
30
+ chatLoading: boolean;
24
31
  /** Open the chat widget */
25
32
  open: () => void;
26
33
  /** Close the chat widget */
@@ -29,6 +36,22 @@ export type YakContextValue = {
29
36
  openWithPrompt: (prompt: string) => void;
30
37
  /** Subscribe to tool call completion events */
31
38
  subscribeToToolEvents: (handler: ToolCallEventHandler) => () => void;
39
+ /** Current voice state machine snapshot. `idle` when mode === "chat". */
40
+ voiceMachine: VoiceMachine;
41
+ /** Convenience: `voiceMachine.state`. */
42
+ voiceState: VoiceState;
43
+ /** Convenience: error message when voiceState === "error". */
44
+ voiceErrorMessage: string | undefined;
45
+ /** Whether a voice session is currently live (connecting/listening/thinking/speaking). */
46
+ voiceIsActive: boolean;
47
+ /** Whether the voice session is still connecting (`voiceState === "connecting"`). */
48
+ voiceLoading: boolean;
49
+ /** Start a voice session. Must be invoked from a user gesture. */
50
+ voiceStart: () => Promise<void>;
51
+ /** Stop the current voice session. */
52
+ voiceStop: () => Promise<void>;
53
+ /** Toggle: start if idle/error, stop if active. */
54
+ voiceToggle: () => Promise<void>;
32
55
  };
33
56
  /**
34
57
  * Internal context with additional methods for widget internals.
@@ -37,21 +60,21 @@ export type YakContextValue = {
37
60
  export type YakInternalContextValue = YakContextValue & {
38
61
  /** Get the iframe origin URL (determined by environment) */
39
62
  getIframeOrigin: () => string;
63
+ /** Theme passed to YakProvider — used by YakWidget to mirror panel placement. */
64
+ theme: Theme | undefined;
40
65
  };
41
66
  export declare const YakContext: import("react").Context<YakInternalContextValue | null>;
42
67
  /**
43
- * Hook to access the Yak chat widget API.
44
- * Provides methods for opening, closing, and triggering prompts.
68
+ * Hook to access the Yak widget API — chat and voice.
45
69
  *
46
70
  * @example
47
71
  * ```tsx
48
72
  * function MyComponent() {
49
- * const { open, close, openWithPrompt, isOpen } = useYak();
50
- *
73
+ * const { open, voiceToggle, voiceState } = useYak();
51
74
  * return (
52
75
  * <div>
53
- * <button onClick={() => open()}>Open Chat</button>
54
- * <button onClick={() => openWithPrompt("Help me!")}>Get Help</button>
76
+ * <button onClick={open}>Open chat</button>
77
+ * <button onClick={voiceToggle}>{voiceState === "idle" ? "Start voice" : "Stop voice"}</button>
55
78
  * </div>
56
79
  * );
57
80
  * }
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE3E;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAElE;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,gDAAgD;IAChD,MAAM,EAAE,OAAO,CAAC;IAChB,sDAAsD;IACtD,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,yDAAyD;IACzD,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,+CAA+C;IAC/C,qBAAqB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;CACtE,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG;IACtD,4DAA4D;IAC5D,eAAe,EAAE,MAAM,MAAM,CAAC;CAC/B,CAAC;AAEF,eAAO,MAAM,UAAU,yDAAsD,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,MAAM,IAAI,eAAe,CAcxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,uBAAuB,CAMxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAkBnE"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,KAAK,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACX,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAElE;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,2DAA2D;IAC3D,IAAI,EAAE,UAAU,CAAC;IAGjB,gDAAgD;IAChD,MAAM,EAAE,OAAO,CAAC;IAChB,sDAAsD;IACtD,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,yDAAyD;IACzD,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,+CAA+C;IAC/C,qBAAqB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;IAGrE,yEAAyE;IACzE,YAAY,EAAE,YAAY,CAAC;IAC3B,yCAAyC;IACzC,UAAU,EAAE,UAAU,CAAC;IACvB,8DAA8D;IAC9D,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,0FAA0F;IAC1F,aAAa,EAAE,OAAO,CAAC;IACvB,qFAAqF;IACrF,YAAY,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAClE,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,sCAAsC;IACtC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,mDAAmD;IACnD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG;IACtD,4DAA4D;IAC5D,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,iFAAiF;IACjF,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,UAAU,yDAAsD,CAAC;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,IAAI,eAAe,CAuBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,uBAAuB,CAMxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAkBnE"}
package/dist/context.js CHANGED
@@ -2,18 +2,16 @@
2
2
  import { createContext, useContext, useEffect, useRef } from "react";
3
3
  export const YakContext = createContext(null);
4
4
  /**
5
- * Hook to access the Yak chat widget API.
6
- * Provides methods for opening, closing, and triggering prompts.
5
+ * Hook to access the Yak widget API — chat and voice.
7
6
  *
8
7
  * @example
9
8
  * ```tsx
10
9
  * function MyComponent() {
11
- * const { open, close, openWithPrompt, isOpen } = useYak();
12
- *
10
+ * const { open, voiceToggle, voiceState } = useYak();
13
11
  * return (
14
12
  * <div>
15
- * <button onClick={() => open()}>Open Chat</button>
16
- * <button onClick={() => openWithPrompt("Help me!")}>Get Help</button>
13
+ * <button onClick={open}>Open chat</button>
14
+ * <button onClick={voiceToggle}>{voiceState === "idle" ? "Start voice" : "Stop voice"}</button>
17
15
  * </div>
18
16
  * );
19
17
  * }
@@ -26,14 +24,23 @@ export function useYak() {
26
24
  if (!context) {
27
25
  throw new Error("useYak must be used within YakProvider");
28
26
  }
29
- // Return only the public API
30
27
  return {
28
+ mode: context.mode,
31
29
  isOpen: context.isOpen,
32
30
  isReady: context.isReady,
31
+ chatLoading: context.chatLoading,
33
32
  open: context.open,
34
33
  close: context.close,
35
34
  openWithPrompt: context.openWithPrompt,
36
35
  subscribeToToolEvents: context.subscribeToToolEvents,
36
+ voiceMachine: context.voiceMachine,
37
+ voiceState: context.voiceState,
38
+ voiceErrorMessage: context.voiceErrorMessage,
39
+ voiceIsActive: context.voiceIsActive,
40
+ voiceLoading: context.voiceLoading,
41
+ voiceStart: context.voiceStart,
42
+ voiceStop: context.voiceStop,
43
+ voiceToggle: context.voiceToggle,
37
44
  };
38
45
  }
39
46
  /**
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
+ export type { ChatConfigProvider, GraphQLRequest, GraphQLSchemaHandler, GraphQLSchemaSource, OpenAPISchemaSource, RESTRequest, RESTSchemaHandler, SchemaSource, Theme, ThemeColors, ToolCallEvent, ToolCallHandler, TriggerButtonConfig, VoiceMachine, VoiceState, WidgetMode, WidgetPosition, } from "@yak-io/javascript";
2
+ export { disableYakLogging, enableYakLogging, isYakLoggingEnabled } from "@yak-io/javascript";
3
+ export type { ToolCallEventHandler, YakConfig, YakContextValue } from "./context.js";
1
4
  export { useYak, useYakToolEvent } from "./context.js";
2
- export type { YakConfig, YakContextValue, ToolCallEventHandler } from "./context.js";
3
- export { YakProvider } from "./YakProvider.js";
4
5
  export type { YakProviderProps } from "./YakProvider.js";
5
- export { YakWidget } from "./YakWidget.js";
6
+ export { YakProvider } from "./YakProvider.js";
6
7
  export type { YakWidgetProps } from "./YakWidget.js";
7
- export { enableYakLogging, disableYakLogging, isYakLoggingEnabled } from "@yak-io/javascript";
8
- export type { GraphQLSchemaHandler, RESTSchemaHandler, GraphQLRequest, RESTRequest, ToolCallHandler, ToolCallEvent, SchemaSource, GraphQLSchemaSource, OpenAPISchemaSource, Theme, ThemeColors, TriggerButtonConfig, WidgetPosition, ChatConfigProvider, } from "@yak-io/javascript";
8
+ export { YakWidget } from "./YakWidget.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9F,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,kBAAkB,GACnB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,KAAK,EACL,WAAW,EACX,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,UAAU,EACV,UAAU,EACV,cAAc,GACf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9F,YAAY,EAAE,oBAAoB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
+ // Re-export useful types and constants from @yak-io/javascript for consumers
3
+ export { disableYakLogging, enableYakLogging, isYakLoggingEnabled } from "@yak-io/javascript";
2
4
  // Public API - only export what consumers need
3
5
  export { useYak, useYakToolEvent } from "./context.js";
4
6
  export { YakProvider } from "./YakProvider.js";
5
7
  export { YakWidget } from "./YakWidget.js";
6
- // Re-export useful types and constants from @yak-io/javascript for consumers
7
- export { enableYakLogging, disableYakLogging, isYakLoggingEnabled } from "@yak-io/javascript";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yak-io/react",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "React SDK for embedding yak chatbot",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -25,6 +25,7 @@
25
25
  "node": ">=18"
26
26
  },
27
27
  "files": [
28
+ "README.md",
28
29
  "dist",
29
30
  "LICENSE"
30
31
  ],
@@ -41,7 +42,7 @@
41
42
  "./package.json": "./package.json"
42
43
  },
43
44
  "dependencies": {
44
- "@yak-io/javascript": "0.7.0"
45
+ "@yak-io/javascript": "0.9.0"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "react": "^18.0.0 || ^19.0.0",
@@ -50,15 +51,16 @@
50
51
  "devDependencies": {
51
52
  "@testing-library/jest-dom": "^6.9.1",
52
53
  "@testing-library/react": "^16.3.2",
53
- "@types/node": "^24.12.0",
54
+ "@types/node": "^24.12.4",
54
55
  "@types/react": "^19.2.14",
55
56
  "@types/react-dom": "^19.2.0",
56
57
  "jsdom": "^28.1.0",
57
- "react": "^19.2.4",
58
- "react-dom": "^19.2.4",
58
+ "react": "^19.2.6",
59
+ "react-dom": "^19.2.6",
59
60
  "typescript": "^5.3.0",
60
61
  "@repo/typescript-config": "0.0.0"
61
62
  },
63
+ "homepage": "https://docs.yak.io/docs/sdks/react",
62
64
  "scripts": {
63
65
  "build": "tsc",
64
66
  "check-types": "tsc --noEmit",