@yak-io/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,36 @@
1
+ Yak Proprietary License
2
+
3
+ Copyright (c) 2025 Yak. All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ proprietary property of Yak and are protected by copyright law.
7
+
8
+ GRANT OF LICENSE:
9
+ Subject to the terms of this license and your valid subscription or agreement
10
+ with Yak, you are granted a limited, non-exclusive, non-transferable license
11
+ to use the Software solely for integrating the Yak chatbot widget into your
12
+ applications as intended and documented.
13
+
14
+ RESTRICTIONS:
15
+ You may NOT:
16
+ - Modify, adapt, alter, translate, or create derivative works of the Software
17
+ - Reverse engineer, disassemble, decompile, or otherwise attempt to derive
18
+ the source code of the Software
19
+ - Redistribute, sublicense, lease, rent, or lend the Software to third parties
20
+ - Remove or alter any proprietary notices, labels, or marks on the Software
21
+ - Use the Software for any purpose other than as expressly permitted herein
22
+
23
+ NO WARRANTY:
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL YAK
27
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
28
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
29
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30
+
31
+ TERMINATION:
32
+ This license is effective until terminated. Your rights under this license
33
+ will terminate automatically without notice if you fail to comply with any
34
+ of its terms.
35
+
36
+ For licensing inquiries, contact: support@yak.io
@@ -0,0 +1,112 @@
1
+ import React from "react";
2
+ import { type Theme, type ChatConfigProvider, type ToolCallHandler, type GraphQLSchemaHandler, type RESTSchemaHandler } from "@yak-io/javascript";
3
+ /**
4
+ * Props for YakProvider
5
+ */
6
+ export type YakProviderProps = {
7
+ /** App identifier in the yak SaaS */
8
+ appId: string;
9
+ /**
10
+ * Provider function for chat configuration (routes + tools).
11
+ * The consuming platform decides how to get the config (static, fetch, etc.)
12
+ * Called when the widget is opened.
13
+ *
14
+ * @example Static config
15
+ * ```tsx
16
+ * getConfig={() => ({
17
+ * routes: { routes: [...], generated_at: "..." },
18
+ * tools: { tools: [...], generated_at: "..." },
19
+ * })}
20
+ * ```
21
+ *
22
+ * @example Fetch from server
23
+ * ```tsx
24
+ * getConfig={async () => {
25
+ * const res = await fetch("/api/yak/config");
26
+ * return res.json();
27
+ * }}
28
+ * ```
29
+ */
30
+ getConfig?: ChatConfigProvider;
31
+ /**
32
+ * Handler for tool calls from the chat widget.
33
+ * The consuming platform decides how to execute (browser, server fetch, etc.)
34
+ *
35
+ * @example Browser-only execution
36
+ * ```tsx
37
+ * onToolCall={async (name, args) => {
38
+ * if (name === "ui.scrollTo") {
39
+ * document.getElementById((args as {id: string}).id)?.scrollIntoView();
40
+ * return { success: true };
41
+ * }
42
+ * throw new Error(`Unknown tool: ${name}`);
43
+ * }}
44
+ * ```
45
+ *
46
+ * @example Server delegation
47
+ * ```tsx
48
+ * onToolCall={async (name, args) => {
49
+ * const res = await fetch("/api/yak/tools", {
50
+ * method: "POST",
51
+ * headers: { "Content-Type": "application/json" },
52
+ * body: JSON.stringify({ name, args }),
53
+ * });
54
+ * const data = await res.json();
55
+ * if (!data.ok) throw new Error(data.error);
56
+ * return data.result;
57
+ * }}
58
+ * ```
59
+ */
60
+ onToolCall?: ToolCallHandler;
61
+ /**
62
+ * Handler for GraphQL schema tool calls.
63
+ * Called when the LLM generates a GraphQL operation based on provided schema context.
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * onGraphQLSchemaCall={async (schemaName, request) => {
68
+ * const response = await fetch("/graphql", {
69
+ * method: "POST",
70
+ * headers: { "Content-Type": "application/json" },
71
+ * body: JSON.stringify(request),
72
+ * });
73
+ * return response.json();
74
+ * }}
75
+ * ```
76
+ */
77
+ onGraphQLSchemaCall?: GraphQLSchemaHandler;
78
+ /**
79
+ * Handler for REST/OpenAPI schema tool calls.
80
+ * Called when the LLM generates a REST request based on provided OpenAPI schema context.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * onRESTSchemaCall={async (schemaName, request) => {
85
+ * const url = new URL(request.path, "https://api.example.com");
86
+ * if (request.query) {
87
+ * Object.entries(request.query).forEach(([k, v]) => url.searchParams.set(k, v));
88
+ * }
89
+ * const response = await fetch(url, {
90
+ * method: request.method,
91
+ * headers: request.body ? { "Content-Type": "application/json" } : undefined,
92
+ * body: request.body ? JSON.stringify(request.body) : undefined,
93
+ * });
94
+ * return response.json();
95
+ * }}
96
+ * ```
97
+ */
98
+ onRESTSchemaCall?: RESTSchemaHandler;
99
+ /** Optional theme configuration */
100
+ theme?: Theme;
101
+ /** Optional redirect handler (defaults to window.location.assign) */
102
+ onRedirect?: (path: string) => void;
103
+ /** Disable the restart session button in the header */
104
+ disableRestartButton?: boolean;
105
+ /** Children components */
106
+ children: React.ReactNode;
107
+ };
108
+ /**
109
+ * YakProvider sets up the context and message handling for the yak widget.
110
+ */
111
+ export declare function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, children, }: YakProviderProps): React.JSX.Element;
112
+ //# sourceMappingURL=YakProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YakProvider.d.ts","sourceRoot":"","sources":["../src/YakProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,OAAO,EAEL,KAAK,KAAK,EAGV,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;IAC3C;;;;;;;;;;;;;;;;;;;OAmBG;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,0BAA0B;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,UAAU,EACV,oBAAoB,EACpB,QAAQ,GACT,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAgMtC"}
@@ -0,0 +1,171 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
4
+ import { YakContext } from "./context.js";
5
+ import { YakClient, } from "@yak-io/javascript";
6
+ import { logger } from "./internal/logger.js";
7
+ /**
8
+ * YakProvider sets up the context and message handling for the yak widget.
9
+ */
10
+ export function YakProvider({ appId, getConfig, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, children, }) {
11
+ const [iframeWindow, setIframeWindow] = useState(null);
12
+ const [chatConfig, setChatConfig] = useState(null);
13
+ const [isWidgetOpen, setIsWidgetOpen] = useState(false);
14
+ const [pendingPrompt, setPendingPrompt] = useState(null);
15
+ const [isIframeReady, setIsIframeReady] = useState(false);
16
+ // Initialize YakClient
17
+ const clientRef = useRef(null);
18
+ if (!clientRef.current) {
19
+ clientRef.current = new YakClient({
20
+ appId,
21
+ onToolCall,
22
+ onGraphQLSchemaCall,
23
+ onRESTSchemaCall,
24
+ theme,
25
+ onRedirect,
26
+ options: { disableRestartButton },
27
+ onClose: () => setIsWidgetOpen(false),
28
+ onReady: () => setIsIframeReady(true),
29
+ });
30
+ }
31
+ const client = clientRef.current;
32
+ // Get the iframe origin from the client (computed based on environment)
33
+ const iframeOrigin = client.getIframeOrigin();
34
+ // Update client config when props change
35
+ useEffect(() => {
36
+ client.updateConfig({
37
+ appId,
38
+ onToolCall,
39
+ onGraphQLSchemaCall,
40
+ onRESTSchemaCall,
41
+ theme,
42
+ onRedirect,
43
+ options: { disableRestartButton },
44
+ chatConfig: chatConfig ?? undefined,
45
+ });
46
+ }, [appId, onToolCall, onGraphQLSchemaCall, onRESTSchemaCall, theme, onRedirect, disableRestartButton, chatConfig, client]);
47
+ // Update client iframe window
48
+ useEffect(() => {
49
+ client.setIframeWindow(iframeWindow);
50
+ }, [iframeWindow, client]);
51
+ // Update client widget open state
52
+ useEffect(() => {
53
+ client.setWidgetOpen(isWidgetOpen);
54
+ }, [isWidgetOpen, client]);
55
+ // Mount/Unmount client listeners
56
+ useEffect(() => {
57
+ client.mount();
58
+ return () => client.unmount();
59
+ }, [client]);
60
+ // Get chat config when widget is opened
61
+ useEffect(() => {
62
+ if (typeof window === "undefined" || !isWidgetOpen || !getConfig)
63
+ return;
64
+ logger.debug("Getting chat config");
65
+ let cancelled = false;
66
+ (async () => {
67
+ try {
68
+ const config = await getConfig();
69
+ if (!cancelled) {
70
+ logger.debug(`Chat config loaded with ${config.tools?.tools.length ?? 0} tools and ${config.routes?.routes.length ?? 0} routes`);
71
+ setChatConfig(config);
72
+ }
73
+ }
74
+ catch (err) {
75
+ logger.warn("Error getting chat config:", err);
76
+ }
77
+ })();
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [getConfig, isWidgetOpen]);
82
+ const resolvedRedirect = useMemo(() => {
83
+ if (onRedirect)
84
+ return onRedirect;
85
+ if (typeof window === "undefined")
86
+ return undefined;
87
+ return (path) => {
88
+ window.location.assign(path);
89
+ };
90
+ }, [onRedirect]);
91
+ const config = useMemo(() => ({
92
+ appId,
93
+ theme,
94
+ chatConfig,
95
+ onRedirect: resolvedRedirect,
96
+ }), [appId, theme, chatConfig, resolvedRedirect]);
97
+ // Methods to get URLs from the client
98
+ const getIframeOrigin = useCallback(() => client.getIframeOrigin(), [client]);
99
+ const getEmbedUrl = useCallback(() => client.getEmbedUrl(), [client]);
100
+ const registerIframeWindow = useCallback((win) => {
101
+ logger.debug("Iframe window registered");
102
+ setIframeWindow(win);
103
+ }, []);
104
+ const unregisterIframeWindow = useCallback(() => {
105
+ logger.debug("Iframe window unregistered");
106
+ setIframeWindow(null);
107
+ }, []);
108
+ const sendMessage = useCallback((message) => {
109
+ iframeWindow?.postMessage(message, iframeOrigin);
110
+ }, [iframeWindow, iframeOrigin]);
111
+ const open = useCallback(() => {
112
+ setIsWidgetOpen(true);
113
+ }, []);
114
+ const close = useCallback(() => {
115
+ setIsWidgetOpen(false);
116
+ }, []);
117
+ const openWithPrompt = useCallback((prompt) => {
118
+ logger.debug("Opening widget with prompt:", prompt);
119
+ setPendingPrompt(prompt);
120
+ setIsWidgetOpen(true);
121
+ }, []);
122
+ // Effect to send pending prompt when iframe becomes ready
123
+ useEffect(() => {
124
+ if (isIframeReady && pendingPrompt && iframeWindow) {
125
+ logger.debug("Sending pending prompt to iframe:", pendingPrompt);
126
+ const promptMessage = {
127
+ type: "yak:prompt",
128
+ payload: { prompt: pendingPrompt },
129
+ };
130
+ iframeWindow.postMessage(promptMessage, iframeOrigin);
131
+ setPendingPrompt(null);
132
+ }
133
+ }, [isIframeReady, pendingPrompt, iframeWindow, iframeOrigin]);
134
+ // Effect to send focus message when widget is opened
135
+ useEffect(() => {
136
+ if (isWidgetOpen && iframeWindow && isIframeReady) {
137
+ logger.debug("Sending focus request to iframe");
138
+ const focusMessage = {
139
+ type: "yak:focus",
140
+ };
141
+ iframeWindow.postMessage(focusMessage, iframeOrigin);
142
+ }
143
+ }, [isWidgetOpen, iframeWindow, isIframeReady, iframeOrigin]);
144
+ const contextValue = useMemo(() => ({
145
+ config,
146
+ getIframeOrigin,
147
+ getEmbedUrl,
148
+ registerIframeWindow,
149
+ unregisterIframeWindow,
150
+ sendMessage,
151
+ isOpen: isWidgetOpen,
152
+ isIframeReady,
153
+ open,
154
+ close,
155
+ openWithPrompt,
156
+ setIsIframeReady,
157
+ }), [
158
+ config,
159
+ getIframeOrigin,
160
+ getEmbedUrl,
161
+ registerIframeWindow,
162
+ unregisterIframeWindow,
163
+ sendMessage,
164
+ isWidgetOpen,
165
+ isIframeReady,
166
+ open,
167
+ close,
168
+ openWithPrompt,
169
+ ]);
170
+ return _jsx(YakContext.Provider, { value: contextValue, children: children });
171
+ }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ /**
3
+ * Props for YakWidget
4
+ */
5
+ export type YakWidgetProps = {
6
+ /** Optional CSS class name for the iframe */
7
+ iframeClassName?: string;
8
+ /** Text to display next to the logo */
9
+ triggerLabel?: string;
10
+ };
11
+ /**
12
+ * YakWidget renders a fixed-position launcher button and iframe panel.
13
+ * The iframe loads immediately and shows its own skeleton while waiting for config.
14
+ */
15
+ export declare function YakWidget({ iframeClassName, triggerLabel, }?: YakWidgetProps): React.JSX.Element;
16
+ //# sourceMappingURL=YakWidget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YakWidget.d.ts","sourceRoot":"","sources":["../src/YakWidget.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAqU3D;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,EACxB,eAAe,EACf,YAA4B,GAC7B,GAAE,cAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAwJzC"}
@@ -0,0 +1,385 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useRef, useEffect, useState } from "react";
4
+ import { useYak } from "./context.js";
5
+ /**
6
+ * All widget styles consolidated in one place
7
+ */
8
+ function getWidgetStyles() {
9
+ return `
10
+ /* ===========================================
11
+ YAK WIDGET STYLES
12
+ =========================================== */
13
+
14
+ /* Trigger Button Base */
15
+ .yak-widget-trigger {
16
+ position: fixed;
17
+ z-index: 9997;
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 12px;
21
+ border: none;
22
+ border-radius: 30px;
23
+ padding: 0 5px 0 20px;
24
+ height: 45px;
25
+ min-width: 45px;
26
+ width: auto;
27
+ cursor: pointer;
28
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
29
+ overflow: hidden;
30
+ background-color: #000;
31
+ color: #fff;
32
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
33
+ }
34
+
35
+ /* Trigger position variants */
36
+ .yak-widget-trigger[data-position="left"] {
37
+ bottom: 28px;
38
+ left: 28px;
39
+ flex-direction: row-reverse;
40
+ }
41
+ .yak-widget-trigger[data-position="right"] {
42
+ bottom: 28px;
43
+ right: 28px;
44
+ flex-direction: row;
45
+ }
46
+
47
+ .yak-widget-trigger-label {
48
+ font-size: 14px;
49
+ font-weight: 600;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .yak-widget-icon-bg {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ width: 36px;
58
+ height: 36px;
59
+ border-radius: 50%;
60
+ background-color: rgba(255, 255, 255, 0.1);
61
+ }
62
+
63
+ .yak-widget-icon {
64
+ width: 20px;
65
+ height: 20px;
66
+ color: currentColor;
67
+ }
68
+
69
+ /* Spinner animation for loading state */
70
+ .yak-widget-spinner {
71
+ width: 20px;
72
+ height: 20px;
73
+ border: 2px solid currentColor;
74
+ border-top-color: transparent;
75
+ border-radius: 50%;
76
+ animation: yak-widget-spin 0.8s linear infinite;
77
+ }
78
+
79
+ @keyframes yak-widget-spin {
80
+ to {
81
+ transform: rotate(360deg);
82
+ }
83
+ }
84
+
85
+ /* Loading/disabled state for trigger button */
86
+ .yak-widget-trigger:disabled {
87
+ cursor: wait;
88
+ opacity: 0.8;
89
+ }
90
+
91
+ /* Custom button styles for forced light mode */
92
+ .yak-widget-trigger.yak-widget-custom-light {
93
+ background-color: var(--yak-btn-light-bg, #fff);
94
+ color: var(--yak-btn-light-color, #000);
95
+ border: 1px solid var(--yak-btn-light-border, transparent);
96
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
97
+ }
98
+
99
+ /* Custom button styles for forced dark mode */
100
+ .yak-widget-trigger.yak-widget-custom-dark {
101
+ background-color: var(--yak-btn-dark-bg, #000);
102
+ color: var(--yak-btn-dark-color, #fff);
103
+ border: 1px solid var(--yak-btn-dark-border, transparent);
104
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
105
+ }
106
+
107
+ /* System mode with custom light colors */
108
+ @media (prefers-color-scheme: light) {
109
+ .yak-widget-trigger[data-has-light-custom]:not(.yak-widget-dark) {
110
+ background-color: var(--yak-btn-light-bg, #fff);
111
+ color: var(--yak-btn-light-color, #000);
112
+ border: 1px solid var(--yak-btn-light-border, transparent);
113
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
114
+ }
115
+ }
116
+
117
+ /* System mode with custom dark colors */
118
+ @media (prefers-color-scheme: dark) {
119
+ .yak-widget-trigger[data-has-dark-custom]:not(.yak-widget-light) {
120
+ background-color: var(--yak-btn-dark-bg, #000);
121
+ color: var(--yak-btn-dark-color, #fff);
122
+ border: 1px solid var(--yak-btn-dark-border, transparent);
123
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
124
+ }
125
+ }
126
+
127
+ /* Light mode via system preference (default styling without custom) */
128
+ @media (prefers-color-scheme: light) {
129
+ .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) {
130
+ background-color: #fff;
131
+ color: #000;
132
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
133
+ border: 1px solid #e5e5e5;
134
+ }
135
+ .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) .yak-widget-icon-bg {
136
+ background-color: rgba(0, 0, 0, 0.05);
137
+ }
138
+ }
139
+
140
+ /* Dark mode via system preference (default styling without custom) */
141
+ @media (prefers-color-scheme: dark) {
142
+ .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) {
143
+ background-color: #000;
144
+ color: #fff;
145
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
146
+ border: none;
147
+ }
148
+ .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) .yak-widget-icon-bg {
149
+ background-color: rgba(255, 255, 255, 0.1);
150
+ }
151
+ }
152
+
153
+ /* Forced light mode (default styling) */
154
+ .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) {
155
+ background-color: #fff;
156
+ color: #000;
157
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
158
+ border: 1px solid #e5e5e5;
159
+ }
160
+ .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) .yak-widget-icon-bg {
161
+ background-color: rgba(0, 0, 0, 0.05);
162
+ }
163
+
164
+ /* Forced dark mode (default styling) */
165
+ .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) {
166
+ background-color: #000;
167
+ color: #fff;
168
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
169
+ border: none;
170
+ }
171
+ .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) .yak-widget-icon-bg {
172
+ background-color: rgba(255, 255, 255, 0.1);
173
+ }
174
+
175
+ /* ===========================================
176
+ BACKDROP
177
+ =========================================== */
178
+ .yak-widget-backdrop {
179
+ position: fixed;
180
+ top: 0;
181
+ left: 0;
182
+ right: 0;
183
+ bottom: 0;
184
+ background-color: rgba(0, 0, 0, 0.5);
185
+ z-index: 9997;
186
+ opacity: 0;
187
+ visibility: hidden;
188
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
189
+ }
190
+ .yak-widget-backdrop[data-open="true"] {
191
+ opacity: 1;
192
+ visibility: visible;
193
+ }
194
+
195
+ /* ===========================================
196
+ CONTAINER
197
+ =========================================== */
198
+ .yak-widget-container {
199
+ position: fixed;
200
+ width: 500px;
201
+ height: 600px;
202
+ max-width: calc(100vw - 40px);
203
+ max-height: calc(100vh - 120px);
204
+ border-radius: 15px;
205
+ overflow: hidden;
206
+ z-index: 9998;
207
+ background-color: transparent;
208
+ }
209
+
210
+ /* Container position variants (chatbox mode) */
211
+ .yak-widget-container[data-position="left"]:not(.yak-widget-drawer) {
212
+ bottom: 16px;
213
+ left: 16px;
214
+ }
215
+ .yak-widget-container[data-position="right"]:not(.yak-widget-drawer) {
216
+ bottom: 16px;
217
+ right: 16px;
218
+ }
219
+
220
+ /* Container visibility (chatbox mode) */
221
+ .yak-widget-container:not(.yak-widget-drawer) {
222
+ display: none;
223
+ }
224
+ .yak-widget-container:not(.yak-widget-drawer)[data-open="true"] {
225
+ display: block;
226
+ }
227
+
228
+ /* ===========================================
229
+ DRAWER MODE
230
+ =========================================== */
231
+ .yak-widget-container.yak-widget-drawer {
232
+ height: calc(100% - 32px);
233
+ max-width: 100vw;
234
+ max-height: none;
235
+ border-radius: 15px;
236
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
237
+ }
238
+
239
+ /* Drawer position */
240
+ .yak-widget-container.yak-widget-drawer[data-position="left"] {
241
+ top: 16px;
242
+ left: 16px;
243
+ bottom: 16px;
244
+ transform: translateX(calc(-100% - 16px));
245
+ }
246
+ .yak-widget-container.yak-widget-drawer[data-position="right"] {
247
+ top: 16px;
248
+ right: 16px;
249
+ bottom: 16px;
250
+ transform: translateX(calc(100% + 16px));
251
+ }
252
+
253
+ /* Drawer open state */
254
+ .yak-widget-container.yak-widget-drawer[data-open="true"] {
255
+ transform: translateX(0);
256
+ }
257
+
258
+ /* ===========================================
259
+ IFRAME
260
+ =========================================== */
261
+ .yak-widget-iframe {
262
+ position: absolute;
263
+ inset: 0;
264
+ width: 100%;
265
+ height: 100%;
266
+ border: none;
267
+ }
268
+
269
+ /* ===========================================
270
+ MOBILE RESPONSIVE
271
+ =========================================== */
272
+ @media (max-width: 640px) {
273
+ .yak-widget-container:not(.yak-widget-drawer) {
274
+ width: 100% !important;
275
+ height: 100% !important;
276
+ height: 100dvh !important;
277
+ max-width: none !important;
278
+ max-height: none !important;
279
+ top: 0 !important;
280
+ left: 0 !important;
281
+ right: 0 !important;
282
+ bottom: 0 !important;
283
+ border-radius: 0 !important;
284
+ }
285
+ .yak-widget-container.yak-widget-drawer {
286
+ width: 100% !important;
287
+ max-width: none !important;
288
+ }
289
+ }
290
+ `;
291
+ }
292
+ /**
293
+ * Inline SVG for brain/circuit fallback icon
294
+ */
295
+ function BrainCircuitIcon({ size = 20, className }) {
296
+ return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [_jsx("path", { d: "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" }), _jsx("path", { d: "M9 13a4.5 4.5 0 0 0 3-4" }), _jsx("path", { d: "M6.003 5.125A3 3 0 0 0 6.401 6.5" }), _jsx("path", { d: "M3.477 10.896a4 4 0 0 1 .585-.396" }), _jsx("path", { d: "M6 18a4 4 0 0 1-1.967-.516" }), _jsx("path", { d: "M12 13h4" }), _jsx("path", { d: "M12 18h6a2 2 0 0 1 2 2v1" }), _jsx("path", { d: "M12 8h8" }), _jsx("path", { d: "M16 8V5a2 2 0 0 1 2-2" }), _jsx("circle", { cx: "16", cy: "13", r: ".5" }), _jsx("circle", { cx: "18", cy: "3", r: ".5" }), _jsx("circle", { cx: "20", cy: "21", r: ".5" }), _jsx("circle", { cx: "20", cy: "8", r: ".5" })] }));
297
+ }
298
+ /**
299
+ * YakWidget renders a fixed-position launcher button and iframe panel.
300
+ * The iframe loads immediately and shows its own skeleton while waiting for config.
301
+ */
302
+ export function YakWidget({ iframeClassName, triggerLabel = "Ask with AI", } = {}) {
303
+ const { config, getEmbedUrl, registerIframeWindow, unregisterIframeWindow, isOpen, isIframeReady, open, close, } = useYak();
304
+ const iframeRef = useRef(null);
305
+ const [hasBeenOpened, setHasBeenOpened] = useState(false);
306
+ // Track if we're in a loading state (open but iframe not ready)
307
+ const isLoading = isOpen && hasBeenOpened && !isIframeReady;
308
+ // Get URLs from the client (computed based on environment)
309
+ const iframeSrc = getEmbedUrl();
310
+ // Track when widget is first opened
311
+ useEffect(() => {
312
+ if (isOpen && !hasBeenOpened) {
313
+ setHasBeenOpened(true);
314
+ }
315
+ }, [isOpen, hasBeenOpened]);
316
+ // Register iframe window when loaded
317
+ useEffect(() => {
318
+ if (!hasBeenOpened)
319
+ return;
320
+ const iframe = iframeRef.current;
321
+ if (!iframe)
322
+ return;
323
+ const handleLoad = () => {
324
+ if (iframe.contentWindow) {
325
+ registerIframeWindow(iframe.contentWindow);
326
+ }
327
+ };
328
+ iframe.addEventListener("load", handleLoad);
329
+ return () => {
330
+ iframe.removeEventListener("load", handleLoad);
331
+ unregisterIframeWindow();
332
+ };
333
+ }, [registerIframeWindow, unregisterIframeWindow, hasBeenOpened]);
334
+ // Determine position styles based on theme
335
+ const position = config.theme?.position ?? "right";
336
+ const colorMode = config.theme?.colorMode;
337
+ const displayMode = config.theme?.displayMode ?? "chatbox";
338
+ const isDrawer = displayMode === "drawer";
339
+ // Determine color mode class for the widget
340
+ const colorModeClass = colorMode === "light" ? "yak-widget-light" : colorMode === "dark" ? "yak-widget-dark" : "";
341
+ // Check if custom button theme is provided (now nested in light/dark)
342
+ const lightButton = config.theme?.light?.button;
343
+ const darkButton = config.theme?.dark?.button;
344
+ const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
345
+ const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;
346
+ // Determine which custom class to apply based on color mode
347
+ let customButtonClass = "";
348
+ if (colorMode === "light" && hasLightCustom) {
349
+ customButtonClass = "yak-widget-custom-light";
350
+ }
351
+ else if (colorMode === "dark" && hasDarkCustom) {
352
+ customButtonClass = "yak-widget-custom-dark";
353
+ }
354
+ else if (colorMode === "system" || colorMode === undefined) {
355
+ // For system mode, we need to handle both via media queries
356
+ // We'll use a data attribute approach instead
357
+ }
358
+ // Build inline style for button CSS variables
359
+ const buttonStyle = {};
360
+ if (lightButton?.background)
361
+ buttonStyle["--yak-btn-light-bg"] = lightButton.background;
362
+ if (lightButton?.color)
363
+ buttonStyle["--yak-btn-light-color"] = lightButton.color;
364
+ if (lightButton?.border)
365
+ buttonStyle["--yak-btn-light-border"] = lightButton.border;
366
+ if (darkButton?.background)
367
+ buttonStyle["--yak-btn-dark-bg"] = darkButton.background;
368
+ if (darkButton?.color)
369
+ buttonStyle["--yak-btn-dark-color"] = darkButton.color;
370
+ if (darkButton?.border)
371
+ buttonStyle["--yak-btn-dark-border"] = darkButton.border;
372
+ // Build container class names
373
+ const containerClasses = [
374
+ "yak-widget-container",
375
+ isDrawer && "yak-widget-drawer",
376
+ colorModeClass,
377
+ ].filter(Boolean).join(" ");
378
+ // Build button class names
379
+ const buttonClasses = [
380
+ "yak-widget-trigger",
381
+ colorModeClass,
382
+ customButtonClass,
383
+ ].filter(Boolean).join(" ");
384
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: getWidgetStyles() }), _jsxs("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(BrainCircuitIcon, { size: 20, className: "yak-widget-icon" })) })] }), isDrawer && hasBeenOpened && (_jsx("div", { className: "yak-widget-backdrop", "data-open": isOpen && isIframeReady, onClick: close, "aria-hidden": "true" })), hasBeenOpened && (_jsx("div", { className: containerClasses, "data-position": position, "data-open": isOpen && isIframeReady, children: _jsx("iframe", { ref: iframeRef, src: iframeSrc, className: `yak-widget-iframe ${iframeClassName ?? ""}`.trim(), title: "yak-chat-host" }) }))] }));
385
+ }
@@ -0,0 +1,57 @@
1
+ import type { Theme, IframeMessageFromHost, ChatConfig } from "@yak-io/javascript";
2
+ /**
3
+ * Configuration for the yak provider
4
+ */
5
+ export type YakConfig = {
6
+ appId: string;
7
+ theme?: Theme;
8
+ chatConfig?: ChatConfig | null;
9
+ onRedirect?: (path: string) => void;
10
+ };
11
+ /**
12
+ * Context value with config and iframe management
13
+ */
14
+ export type YakContextValue = {
15
+ config: YakConfig;
16
+ /** Get the iframe origin URL (determined by environment) */
17
+ getIframeOrigin: () => string;
18
+ /** Get the full iframe embed URL */
19
+ getEmbedUrl: () => string;
20
+ registerIframeWindow: (win: Window) => void;
21
+ unregisterIframeWindow: () => void;
22
+ sendMessage: (message: IframeMessageFromHost) => void;
23
+ /** Whether the chat widget is currently open */
24
+ isOpen: boolean;
25
+ /** Whether the iframe is ready to receive messages */
26
+ isIframeReady: boolean;
27
+ /** Open the chat widget */
28
+ open: () => void;
29
+ /** Close the chat widget */
30
+ close: () => void;
31
+ /** Open the chat widget and trigger a specific prompt */
32
+ openWithPrompt: (prompt: string) => void;
33
+ setIsIframeReady: (ready: boolean) => void;
34
+ };
35
+ export declare const YakContext: import("react").Context<YakContextValue | null>;
36
+ /**
37
+ * Hook to access the Yak chat widget API.
38
+ * Provides methods for opening, closing, and triggering prompts.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * function MyComponent() {
43
+ * const { open, close, openWithPrompt, isOpen } = useYak();
44
+ *
45
+ * return (
46
+ * <div>
47
+ * <button onClick={() => open()}>Open Chat</button>
48
+ * <button onClick={() => openWithPrompt("Help me!")}>Get Help</button>
49
+ * </div>
50
+ * );
51
+ * }
52
+ * ```
53
+ *
54
+ * @throws {Error} if used outside YakProvider
55
+ */
56
+ export declare function useYak(): YakContextValue;
57
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEnF;;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,eAAe,GAAG;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,4DAA4D;IAC5D,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,oCAAoC;IACpC,WAAW,EAAE,MAAM,MAAM,CAAC;IAC1B,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,WAAW,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACtD,gDAAgD;IAChD,MAAM,EAAE,OAAO,CAAC;IAChB,sDAAsD;IACtD,aAAa,EAAE,OAAO,CAAC;IACvB,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,gBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C,CAAC;AAEF,eAAO,MAAM,UAAU,iDAA8C,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,MAAM,IAAI,eAAe,CAMxC"}
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ export const YakContext = createContext(null);
4
+ /**
5
+ * Hook to access the Yak chat widget API.
6
+ * Provides methods for opening, closing, and triggering prompts.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * function MyComponent() {
11
+ * const { open, close, openWithPrompt, isOpen } = useYak();
12
+ *
13
+ * return (
14
+ * <div>
15
+ * <button onClick={() => open()}>Open Chat</button>
16
+ * <button onClick={() => openWithPrompt("Help me!")}>Get Help</button>
17
+ * </div>
18
+ * );
19
+ * }
20
+ * ```
21
+ *
22
+ * @throws {Error} if used outside YakProvider
23
+ */
24
+ export function useYak() {
25
+ const context = useContext(YakContext);
26
+ if (!context) {
27
+ throw new Error("useYak must be used within YakProvider");
28
+ }
29
+ return context;
30
+ }
@@ -0,0 +1,8 @@
1
+ export { useYak } from "./context.js";
2
+ export type { YakConfig, YakContextValue } from "./context.js";
3
+ export { YakProvider } from "./YakProvider.js";
4
+ export type { YakProviderProps } from "./YakProvider.js";
5
+ export { YakWidget } from "./YakWidget.js";
6
+ export type { YakWidgetProps } from "./YakWidget.js";
7
+ export { type GraphQLSchemaHandler, type RESTSchemaHandler, type GraphQLRequest, type RESTRequest, type ToolCallHandler, type SchemaSource, type GraphQLSchemaSource, type OpenAPISchemaSource, type Theme, type ThemeColors, type ButtonColors, } from "@yak-io/javascript";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/D,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,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use client";
2
+ // Public API - only export what consumers need
3
+ export { useYak } from "./context.js";
4
+ export { YakProvider } from "./YakProvider.js";
5
+ export { YakWidget } from "./YakWidget.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Simple logger utility for the yak React SDK.
3
+ * Debug/info logs are only emitted in development mode.
4
+ * Warnings and errors are always logged.
5
+ */
6
+ export declare const logger: {
7
+ debug: (message: string, data?: unknown) => void;
8
+ info: (message: string, data?: unknown) => void;
9
+ warn: (message: string, data?: unknown) => void;
10
+ error: (message: string, data?: unknown) => void;
11
+ };
12
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/internal/logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,MAAM;qBACA,MAAM,SAAS,OAAO,KAAG,IAAI;oBAU9B,MAAM,SAAS,OAAO,KAAG,IAAI;oBAU7B,MAAM,SAAS,OAAO,KAAG,IAAI;qBAQ5B,MAAM,SAAS,OAAO,KAAG,IAAI;CAO/C,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Simple logger utility for the yak React SDK.
3
+ * Debug/info logs are only emitted in development mode.
4
+ * Warnings and errors are always logged.
5
+ */
6
+ const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
7
+ export const logger = {
8
+ debug: (message, data) => {
9
+ if (isDev) {
10
+ if (data !== undefined) {
11
+ console.log(`[yak-chat-host] ${message}`, data);
12
+ }
13
+ else {
14
+ console.log(`[yak-chat-host] ${message}`);
15
+ }
16
+ }
17
+ },
18
+ info: (message, data) => {
19
+ if (isDev) {
20
+ if (data !== undefined) {
21
+ console.info(`[yak-chat-host] ${message}`, data);
22
+ }
23
+ else {
24
+ console.info(`[yak-chat-host] ${message}`);
25
+ }
26
+ }
27
+ },
28
+ warn: (message, data) => {
29
+ if (data !== undefined) {
30
+ console.warn(`[yak-chat-host] ${message}`, data);
31
+ }
32
+ else {
33
+ console.warn(`[yak-chat-host] ${message}`);
34
+ }
35
+ },
36
+ error: (message, data) => {
37
+ if (data !== undefined) {
38
+ console.error(`[yak-chat-host] ${message}`, data);
39
+ }
40
+ else {
41
+ console.error(`[yak-chat-host] ${message}`);
42
+ }
43
+ },
44
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@yak-io/react",
3
+ "version": "0.1.0",
4
+ "description": "React SDK for embedding yak chatbot",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "author": "Yak <support@yak.io>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/9f-au/yak.git",
11
+ "directory": "packages/react"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "provenance": false
16
+ },
17
+ "keywords": [
18
+ "yak",
19
+ "chatbot",
20
+ "ai",
21
+ "widget",
22
+ "chat",
23
+ "react"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "LICENSE"
31
+ ],
32
+ "sideEffects": false,
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "dependencies": {
41
+ "@yak-io/javascript": "0.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^18.0.0 || ^19.0.0",
45
+ "react-dom": "^18.0.0 || ^19.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^24.10.2",
49
+ "@types/react": "^19.2.7",
50
+ "@types/react-dom": "^19.2.0",
51
+ "typescript": "^5.3.0",
52
+ "@repo/typescript-config": "0.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc",
56
+ "check-types": "tsc --noEmit"
57
+ }
58
+ }