@yak-io/react 0.11.0 → 0.12.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/dist/YakProvider.d.ts +1 -1
- package/dist/YakProvider.d.ts.map +1 -1
- package/dist/YakWidget.d.ts +1 -1
- package/dist/YakWidget.d.ts.map +1 -1
- package/dist/index.cjs +418 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +407 -6
- package/dist/index.js.map +7 -0
- package/package.json +6 -6
- package/dist/YakProvider.js +0 -195
- package/dist/YakWidget.js +0 -81
- package/dist/context.js +0 -103
package/dist/YakProvider.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { INITIAL_VOICE_MACHINE, logger, YakEmbed, } from "@yak-io/javascript";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import { YakContext } from "./context.js";
|
|
6
|
-
/**
|
|
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()`.
|
|
10
|
-
*/
|
|
11
|
-
export function YakProvider({ appId, origin, mode = "chat", getConfig, onToolCall, theme, onRedirect, disableRestartButton, trigger = false, user, children, }) {
|
|
12
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
13
|
-
const [isReady, setIsReady] = useState(false);
|
|
14
|
-
const [voiceMachine, setVoiceMachine] = useState(INITIAL_VOICE_MACHINE);
|
|
15
|
-
// Store event subscribers for tool call events
|
|
16
|
-
const toolEventSubscribersRef = useRef(new Set());
|
|
17
|
-
// Handler that notifies all subscribers when a tool call completes
|
|
18
|
-
const handleToolCallComplete = useCallback((event) => {
|
|
19
|
-
logger.debug("Tool call completed, notifying subscribers:", {
|
|
20
|
-
name: event.name,
|
|
21
|
-
ok: event.ok,
|
|
22
|
-
subscriberCount: toolEventSubscribersRef.current.size,
|
|
23
|
-
});
|
|
24
|
-
for (const handler of toolEventSubscribersRef.current) {
|
|
25
|
-
try {
|
|
26
|
-
handler(event);
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
logger.warn("Error in tool event subscriber:", err);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, []);
|
|
33
|
-
// Resolve redirect handler
|
|
34
|
-
const resolvedRedirect = useMemo(() => {
|
|
35
|
-
if (onRedirect)
|
|
36
|
-
return onRedirect;
|
|
37
|
-
if (typeof window === "undefined")
|
|
38
|
-
return undefined;
|
|
39
|
-
return (path) => {
|
|
40
|
-
window.location.assign(path);
|
|
41
|
-
};
|
|
42
|
-
}, [onRedirect]);
|
|
43
|
-
// Initialize YakEmbed — created once, never recreated
|
|
44
|
-
const embedRef = useRef(null);
|
|
45
|
-
if (!embedRef.current) {
|
|
46
|
-
embedRef.current = new YakEmbed({
|
|
47
|
-
appId,
|
|
48
|
-
origin,
|
|
49
|
-
mode,
|
|
50
|
-
theme,
|
|
51
|
-
trigger,
|
|
52
|
-
getConfig,
|
|
53
|
-
onToolCall,
|
|
54
|
-
onRedirect: resolvedRedirect,
|
|
55
|
-
options: { disableRestartButton },
|
|
56
|
-
onToolCallComplete: handleToolCallComplete,
|
|
57
|
-
user,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const embed = embedRef.current;
|
|
61
|
-
// Mount/unmount embed and subscribe to chat + voice state changes
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
embed.mount();
|
|
64
|
-
const unsubscribeChat = embed.onStateChange((state) => {
|
|
65
|
-
setIsOpen(state.isOpen);
|
|
66
|
-
setIsReady(state.isReady);
|
|
67
|
-
});
|
|
68
|
-
const unsubscribeVoice = embed.onVoiceStateChange((m) => setVoiceMachine(m));
|
|
69
|
-
return () => {
|
|
70
|
-
unsubscribeChat();
|
|
71
|
-
unsubscribeVoice();
|
|
72
|
-
embed.destroy();
|
|
73
|
-
};
|
|
74
|
-
}, [embed]);
|
|
75
|
-
// Update embed config when props change. Chat config goes via the client;
|
|
76
|
-
// voice config goes via the voice session.
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
embed.getClient().updateConfig({
|
|
79
|
-
appId,
|
|
80
|
-
onToolCall,
|
|
81
|
-
theme,
|
|
82
|
-
onRedirect: resolvedRedirect,
|
|
83
|
-
options: { disableRestartButton },
|
|
84
|
-
onToolCallComplete: handleToolCallComplete,
|
|
85
|
-
user,
|
|
86
|
-
});
|
|
87
|
-
embed.getVoiceSession()?.updateConfig({
|
|
88
|
-
appId,
|
|
89
|
-
getConfig,
|
|
90
|
-
onToolCall,
|
|
91
|
-
onRedirect: resolvedRedirect,
|
|
92
|
-
});
|
|
93
|
-
}, [
|
|
94
|
-
appId,
|
|
95
|
-
getConfig,
|
|
96
|
-
onToolCall,
|
|
97
|
-
theme,
|
|
98
|
-
resolvedRedirect,
|
|
99
|
-
disableRestartButton,
|
|
100
|
-
embed,
|
|
101
|
-
handleToolCallComplete,
|
|
102
|
-
user,
|
|
103
|
-
]);
|
|
104
|
-
// Fetch chat config when the chat is opened
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (typeof window === "undefined" || !isOpen || !getConfig)
|
|
107
|
-
return;
|
|
108
|
-
logger.debug("Getting chat config");
|
|
109
|
-
let cancelled = false;
|
|
110
|
-
(async () => {
|
|
111
|
-
try {
|
|
112
|
-
const config = await getConfig();
|
|
113
|
-
if (!cancelled) {
|
|
114
|
-
logger.debug(`Chat config loaded with ${config.tools?.tools.length ?? 0} tools and ${config.routes?.routes.length ?? 0} routes`);
|
|
115
|
-
embed.getClient().updateConfig({ chatConfig: config });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
logger.warn("Error getting chat config:", err);
|
|
120
|
-
}
|
|
121
|
-
})();
|
|
122
|
-
return () => {
|
|
123
|
-
cancelled = true;
|
|
124
|
-
};
|
|
125
|
-
}, [getConfig, isOpen, embed]);
|
|
126
|
-
// Chat methods
|
|
127
|
-
const open = useCallback(() => embed.open(), [embed]);
|
|
128
|
-
const close = useCallback(() => embed.close(), [embed]);
|
|
129
|
-
const openWithPrompt = useCallback((prompt) => embed.openWithPrompt(prompt), [embed]);
|
|
130
|
-
// Voice methods
|
|
131
|
-
const voiceStart = useCallback(async () => {
|
|
132
|
-
try {
|
|
133
|
-
await embed.voiceStart();
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
logger.warn("Voice start failed", err);
|
|
137
|
-
}
|
|
138
|
-
}, [embed]);
|
|
139
|
-
const voiceStop = useCallback(() => embed.voiceStop(), [embed]);
|
|
140
|
-
const voiceToggle = useCallback(() => embed.voiceToggle(), [embed]);
|
|
141
|
-
// Subscribe to tool call completion events
|
|
142
|
-
const subscribeToToolEvents = useCallback((handler) => {
|
|
143
|
-
toolEventSubscribersRef.current.add(handler);
|
|
144
|
-
logger.debug("Tool event subscriber added, total:", toolEventSubscribersRef.current.size);
|
|
145
|
-
return () => {
|
|
146
|
-
toolEventSubscribersRef.current.delete(handler);
|
|
147
|
-
logger.debug("Tool event subscriber removed, total:", toolEventSubscribersRef.current.size);
|
|
148
|
-
};
|
|
149
|
-
}, []);
|
|
150
|
-
// Expose iframe origin for YakWidget
|
|
151
|
-
const getIframeOrigin = useCallback(() => embed.getClient().getIframeOrigin(), [embed]);
|
|
152
|
-
const voiceState = voiceMachine.state;
|
|
153
|
-
const voiceIsActive = voiceState !== "idle" && voiceState !== "error";
|
|
154
|
-
const chatLoading = isOpen && !isReady;
|
|
155
|
-
const voiceLoading = voiceState === "connecting";
|
|
156
|
-
const contextValue = useMemo(() => ({
|
|
157
|
-
mode,
|
|
158
|
-
isOpen,
|
|
159
|
-
isReady,
|
|
160
|
-
chatLoading,
|
|
161
|
-
open,
|
|
162
|
-
close,
|
|
163
|
-
openWithPrompt,
|
|
164
|
-
subscribeToToolEvents,
|
|
165
|
-
voiceMachine,
|
|
166
|
-
voiceState,
|
|
167
|
-
voiceErrorMessage: voiceMachine.errorMessage,
|
|
168
|
-
voiceIsActive,
|
|
169
|
-
voiceLoading,
|
|
170
|
-
voiceStart,
|
|
171
|
-
voiceStop,
|
|
172
|
-
voiceToggle,
|
|
173
|
-
getIframeOrigin,
|
|
174
|
-
theme,
|
|
175
|
-
}), [
|
|
176
|
-
mode,
|
|
177
|
-
isOpen,
|
|
178
|
-
isReady,
|
|
179
|
-
chatLoading,
|
|
180
|
-
open,
|
|
181
|
-
close,
|
|
182
|
-
openWithPrompt,
|
|
183
|
-
subscribeToToolEvents,
|
|
184
|
-
voiceMachine,
|
|
185
|
-
voiceState,
|
|
186
|
-
voiceIsActive,
|
|
187
|
-
voiceLoading,
|
|
188
|
-
voiceStart,
|
|
189
|
-
voiceStop,
|
|
190
|
-
voiceToggle,
|
|
191
|
-
getIframeOrigin,
|
|
192
|
-
theme,
|
|
193
|
-
]);
|
|
194
|
-
return _jsx(YakContext.Provider, { value: contextValue, children: children });
|
|
195
|
-
}
|
package/dist/YakWidget.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
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
|
-
}
|
|
30
|
-
function buildButtonStyle(lightButton, darkButton) {
|
|
31
|
-
const style = {};
|
|
32
|
-
if (lightButton?.background)
|
|
33
|
-
style["--yak-btn-light-bg"] = lightButton.background;
|
|
34
|
-
if (lightButton?.color)
|
|
35
|
-
style["--yak-btn-light-color"] = lightButton.color;
|
|
36
|
-
if (lightButton?.border)
|
|
37
|
-
style["--yak-btn-light-border"] = lightButton.border;
|
|
38
|
-
if (darkButton?.background)
|
|
39
|
-
style["--yak-btn-dark-bg"] = darkButton.background;
|
|
40
|
-
if (darkButton?.color)
|
|
41
|
-
style["--yak-btn-dark-color"] = darkButton.color;
|
|
42
|
-
if (darkButton?.border)
|
|
43
|
-
style["--yak-btn-dark-border"] = darkButton.border;
|
|
44
|
-
return style;
|
|
45
|
-
}
|
|
46
|
-
function buildButtonClasses(colorMode, hasLightCustom, hasDarkCustom) {
|
|
47
|
-
const colorModeClass = colorMode === "light" ? "yak-widget-light" : colorMode === "dark" ? "yak-widget-dark" : "";
|
|
48
|
-
let customButtonClass = "";
|
|
49
|
-
if (colorMode === "light" && hasLightCustom) {
|
|
50
|
-
customButtonClass = "yak-widget-custom-light";
|
|
51
|
-
}
|
|
52
|
-
else if (colorMode === "dark" && hasDarkCustom) {
|
|
53
|
-
customButtonClass = "yak-widget-custom-dark";
|
|
54
|
-
}
|
|
55
|
-
return ["yak-widget-trigger", colorModeClass, customButtonClass].filter(Boolean).join(" ");
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
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.
|
|
62
|
-
*/
|
|
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;
|
|
74
|
-
const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
|
|
75
|
-
const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;
|
|
76
|
-
const buttonStyle = buildButtonStyle(lightButton, darkButton);
|
|
77
|
-
const buttonClasses = buildButtonClasses(colorMode, hasLightCustom, hasDarkCustom);
|
|
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 }) }))] }));
|
|
81
|
-
}
|
package/dist/context.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { createContext, useContext, useEffect, useRef } from "react";
|
|
3
|
-
export const YakContext = createContext(null);
|
|
4
|
-
/**
|
|
5
|
-
* Hook to access the Yak widget API — chat and voice.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* function MyComponent() {
|
|
10
|
-
* const { open, voiceToggle, voiceState } = useYak();
|
|
11
|
-
* return (
|
|
12
|
-
* <div>
|
|
13
|
-
* <button onClick={open}>Open chat</button>
|
|
14
|
-
* <button onClick={voiceToggle}>{voiceState === "idle" ? "Start voice" : "Stop voice"}</button>
|
|
15
|
-
* </div>
|
|
16
|
-
* );
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*
|
|
20
|
-
* @throws {Error} if used outside YakProvider
|
|
21
|
-
*/
|
|
22
|
-
export function useYak() {
|
|
23
|
-
const context = useContext(YakContext);
|
|
24
|
-
if (!context) {
|
|
25
|
-
throw new Error("useYak must be used within YakProvider");
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
mode: context.mode,
|
|
29
|
-
isOpen: context.isOpen,
|
|
30
|
-
isReady: context.isReady,
|
|
31
|
-
chatLoading: context.chatLoading,
|
|
32
|
-
open: context.open,
|
|
33
|
-
close: context.close,
|
|
34
|
-
openWithPrompt: context.openWithPrompt,
|
|
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,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Internal hook for components that need full context access.
|
|
48
|
-
* Not exported publicly.
|
|
49
|
-
*/
|
|
50
|
-
export function useYakInternal() {
|
|
51
|
-
const context = useContext(YakContext);
|
|
52
|
-
if (!context) {
|
|
53
|
-
throw new Error("useYakInternal must be used within YakProvider");
|
|
54
|
-
}
|
|
55
|
-
return context;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Hook to subscribe to tool call completion events.
|
|
59
|
-
* Useful for page-level cache invalidation when chatbot tools modify data.
|
|
60
|
-
*
|
|
61
|
-
* The handler receives an event with:
|
|
62
|
-
* - `name`: The tool name that was called (e.g., "order.cancel")
|
|
63
|
-
* - `args`: The arguments passed to the tool
|
|
64
|
-
* - `ok`: Whether the call succeeded
|
|
65
|
-
* - `result`: The result (if ok is true)
|
|
66
|
-
* - `error`: The error message (if ok is false)
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```tsx
|
|
70
|
-
* function PlanPage({ planId }: { planId: string }) {
|
|
71
|
-
* const utils = trpc.useUtils();
|
|
72
|
-
*
|
|
73
|
-
* useYakToolEvent((event) => {
|
|
74
|
-
* // Only invalidate if this tool touched plans
|
|
75
|
-
* if (event.ok && event.name.startsWith("plan.")) {
|
|
76
|
-
* utils.plan.get.invalidate({ id: planId });
|
|
77
|
-
* utils.planItem.list.invalidate({ planId });
|
|
78
|
-
* }
|
|
79
|
-
* });
|
|
80
|
-
*
|
|
81
|
-
* // ... rest of component
|
|
82
|
-
* }
|
|
83
|
-
* ```
|
|
84
|
-
*
|
|
85
|
-
* @param handler - Function called after each tool call completes
|
|
86
|
-
* @throws {Error} if used outside YakProvider
|
|
87
|
-
*/
|
|
88
|
-
export function useYakToolEvent(handler) {
|
|
89
|
-
const context = useContext(YakContext);
|
|
90
|
-
if (!context) {
|
|
91
|
-
throw new Error("useYakToolEvent must be used within YakProvider");
|
|
92
|
-
}
|
|
93
|
-
// Use ref to always have latest handler without re-subscribing
|
|
94
|
-
const handlerRef = useRef(handler);
|
|
95
|
-
handlerRef.current = handler;
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
// Wrap handler to use ref (stable reference)
|
|
98
|
-
const stableHandler = (event) => {
|
|
99
|
-
handlerRef.current(event);
|
|
100
|
-
};
|
|
101
|
-
return context.subscribeToToolEvents(stableHandler);
|
|
102
|
-
}, [context]);
|
|
103
|
-
}
|