tunio-agent-widget 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/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Tunio Agents Widget
2
+
3
+ A Next.js demo app for the Tunio on‑air control widget. The widget is a compact chat UI that talks to the Tunio agent service and streams responses back in real time.
4
+
5
+ ## What’s in this repo
6
+
7
+ - `src/components/TunioWidget.tsx` — the widget component.
8
+ - `src/components/TunioWidget.module.css` — widget styles and theme tokens.
9
+ - `src/i18n/translations.ts` — UI translations (EN/RU) + fallback to English.
10
+ - `src/app/page.tsx` — demo page that mounts the widget.
11
+ - `src/app/fonts.ts` + `src/app/globals.css` — typography setup (Roboto / Russo One, etc.).
12
+
13
+ ## Widget structure ("player")
14
+
15
+ The widget is **not** an audio player. It’s a control surface that sends natural‑language commands to the Tunio agent and shows streaming responses.
16
+
17
+ UI sections:
18
+
19
+ - Header: title/subtitle + status pill (always Online/Онлайн).
20
+ - Message list: system, user, and assistant bubbles (assistant supports Markdown).
21
+ - Composer: quick prompt pills + textarea + send button.
22
+ - Typing indicator: animated 3‑dot bounce while the agent streams.
23
+
24
+ ## How streaming works
25
+
26
+ The widget expects the agent API to respond as **Server‑Sent Events (SSE)** over a `POST` request.
27
+
28
+ Request body:
29
+
30
+ ```json
31
+ {
32
+ "message": "user text",
33
+ "session_token": "...",
34
+ "user_lang": "ru"
35
+ }
36
+ ```
37
+
38
+ Streaming response format (examples):
39
+
40
+ ```
41
+ event: message
42
+ data: {"content":"partial or full response"}
43
+
44
+ event: error
45
+ data: {"message":"error text"}
46
+ ```
47
+
48
+ The widget accumulates `event: message` chunks and updates the last assistant bubble in real time.
49
+
50
+ ## Props
51
+
52
+ ```ts
53
+ export type TunioWidgetProps = {
54
+ apiUrl?: string;
55
+ sessionTokenUrl?: string;
56
+ lang?: string;
57
+ getSessionToken?: () => Promise<string>;
58
+ accessToken?: string;
59
+ title?: string;
60
+ subtitle?: string;
61
+ theme?: 'light' | 'dark';
62
+ className?: string;
63
+ onNavigate?: (href: string) => void;
64
+ };
65
+ ```
66
+
67
+ Notes:
68
+
69
+ - `lang` controls i18n (EN/RU). Unknown → English.
70
+ - `theme` defaults to `light`.
71
+ - `onNavigate` lets you intercept internal links in assistant Markdown.
72
+ - Session token resolution order: `getSessionToken` → `accessToken` → `sessionTokenUrl`.
73
+
74
+ ## Keyboard & focus behavior
75
+
76
+ - **Enter** sends the message.
77
+ - **Shift+Enter** inserts a newline.
78
+ - After send (or failed send), focus returns to the textarea.
79
+ - While streaming, sending is disabled.
80
+
81
+ ## Theming & overrides
82
+
83
+ CSS Modules are scoped. For external overrides, target the root data attribute:
84
+
85
+ ```css
86
+ [data-tunio-widget="root"][data-theme="dark"] {
87
+ --accent: #5b8cff;
88
+ }
89
+ ```
90
+
91
+ Key tokens are defined inside `.widget` and overridden by `.light` / `.dark`.
92
+
93
+ ## Running locally
94
+
95
+ ```bash
96
+ pnpm dev
97
+ ```
98
+
99
+ Then open `http://localhost:3000`.
100
+
101
+ ## Building the npm package
102
+
103
+ ```bash
104
+ pnpm build
105
+ ```
106
+
107
+ This runs `tsup` and produces `dist/` with ESM + CJS builds and injected CSS.
108
+
109
+ For the demo app build:
110
+
111
+ ```bash
112
+ pnpm build:app
113
+ ```
114
+
115
+ ## Environment variables
116
+
117
+ - `NEXT_PUBLIC_AGENT_API_URL`
118
+ - `NEXT_PUBLIC_SESSION_TOKEN_URL`
119
+
120
+ These are optional if you pass the props directly.
package/dist/index.cjs ADDED
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ TunioWidget: () => TunioWidget
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/components/TunioWidget.tsx
38
+ var import_react = require("react");
39
+ var import_react_markdown = __toESM(require("react-markdown"));
40
+ var import_remark_gfm = __toESM(require("remark-gfm"));
41
+
42
+ // #style-inject:#style-inject
43
+ function styleInject(css, { insertAt } = {}) {
44
+ if (!css || typeof document === "undefined") return;
45
+ const head = document.head || document.getElementsByTagName("head")[0];
46
+ const style = document.createElement("style");
47
+ style.type = "text/css";
48
+ if (insertAt === "top") {
49
+ if (head.firstChild) {
50
+ head.insertBefore(style, head.firstChild);
51
+ } else {
52
+ head.appendChild(style);
53
+ }
54
+ } else {
55
+ head.appendChild(style);
56
+ }
57
+ if (style.styleSheet) {
58
+ style.styleSheet.cssText = css;
59
+ } else {
60
+ style.appendChild(document.createTextNode(css));
61
+ }
62
+ }
63
+
64
+ // src/components/TunioWidget.module.css
65
+ styleInject(':global(.tw-widget) {\n --panel: #f7f9fc;\n --ink: #1b2430;\n --muted: #6c7a90;\n --accent: #3f6df6;\n --accent-soft: rgba(63, 109, 246, 0.14);\n --teal: #21b3a8;\n --surface: #ffffff;\n --border: rgba(16, 24, 40, 0.12);\n --widget-bg:\n linear-gradient(\n 135deg,\n #f9fbff 0%,\n #eef2f8 60%,\n #e8edf6 100%);\n --header-bg:\n linear-gradient(\n \n 120deg,\n rgba(255, 255, 255, 0.92),\n rgba(243, 246, 252, 0.82) );\n --composer-bg:\n linear-gradient(\n \n 180deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(244, 246, 252, 0.92) 80% );\n --shadow: 0 24px 70px rgba(18, 24, 40, 0.18);\n --glow:\n radial-gradient(\n circle,\n rgba(63, 109, 246, 0.18) 0%,\n rgba(63, 109, 246, 0) 70%);\n --accent-glow: rgba(63, 109, 246, 0.2);\n --bubble-user: #1b2430;\n --bubble-user-text: #f7f9ff;\n --bubble-assistant: #ffffff;\n --bubble-assistant-border: rgba(16, 24, 40, 0.08);\n --bubble-system: rgba(27, 36, 48, 0.08);\n --bubble-system-text: #6c7a90;\n --input-focus-border: rgba(63, 109, 246, 0.6);\n --input-focus-shadow: rgba(63, 109, 246, 0.2);\n --button-bg: #1b2430;\n --button-text: #f7f9ff;\n --button-shadow: rgba(27, 36, 48, 0.2);\n --pill-border: rgba(16, 24, 40, 0.2);\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n width: min(100%, 420px);\n height: min(80vh, 640px);\n min-height: 520px;\n border-radius: 12px;\n background: var(--widget-bg);\n border: 1px solid var(--border);\n box-shadow: var(--shadow);\n color: var(--ink);\n overflow: hidden;\n position: relative;\n}\n:global(.tw-light) {\n color-scheme: light;\n}\n:global(.tw-dark) {\n color-scheme: dark;\n --panel: #121826;\n --ink: #e6edf7;\n --muted: #a5b3c8;\n --accent: #4a7dff;\n --accent-soft: rgba(74, 125, 255, 0.18);\n --teal: #2db9c3;\n --surface: #1a2232;\n --border: rgba(230, 237, 247, 0.12);\n --widget-bg:\n linear-gradient(\n 135deg,\n #131a2a 0%,\n #101525 60%,\n #0d1220 100%);\n --header-bg:\n linear-gradient(\n \n 120deg,\n rgba(18, 24, 38, 0.95),\n rgba(23, 30, 48, 0.85) );\n --composer-bg:\n linear-gradient(\n \n 180deg,\n rgba(18, 24, 38, 0) 0%,\n rgba(18, 24, 38, 0.9) 80% );\n --shadow: 0 28px 90px rgba(2, 7, 14, 0.65);\n --glow:\n radial-gradient(\n circle,\n rgba(74, 125, 255, 0.2) 0%,\n rgba(74, 125, 255, 0) 70%);\n --accent-glow: rgba(74, 125, 255, 0.28);\n --bubble-user: #2f3e63;\n --bubble-user-text: #f4f7ff;\n --bubble-assistant: #1a2233;\n --bubble-assistant-border: rgba(230, 237, 247, 0.1);\n --bubble-system: rgba(230, 237, 247, 0.08);\n --bubble-system-text: #b7c3d7;\n --input-focus-border: rgba(74, 125, 255, 0.6);\n --input-focus-shadow: rgba(74, 125, 255, 0.25);\n --button-bg: #4a7dff;\n --button-text: #f4f7ff;\n --button-shadow: rgba(74, 125, 255, 0.35);\n --pill-border: rgba(230, 237, 247, 0.24);\n}\n:global(.tw-widget)::before {\n content: "";\n position: absolute;\n inset: -120px 40% auto -60px;\n height: 220px;\n background: var(--glow);\n pointer-events: none;\n}\n:global(.tw-header) {\n padding: 22px 26px 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--header-bg);\n backdrop-filter: blur(12px);\n}\n:global(.tw-brand) {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n:global(.tw-brand-title) {\n font-family: var(--font-display);\n font-size: 24px;\n letter-spacing: -0.02em;\n}\n:global(.tw-brand-subtitle) {\n color: var(--muted);\n font-size: 13px;\n}\n:global(.tw-status) {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: 8px;\n background: var(--accent-soft);\n color: var(--ink);\n font-size: 12px;\n font-weight: 600;\n}\n:global(.tw-status-dot) {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n box-shadow: 0 0 0 4px var(--accent-glow);\n}\n:global(.tw-messages) {\n padding: 16px 22px 8px;\n display: flex;\n flex-direction: column;\n gap: 14px;\n overflow-y: auto;\n min-height: 0;\n scroll-behavior: smooth;\n}\n:global(.tw-message) {\n max-width: 85%;\n padding: 14px 16px;\n border-radius: 8px;\n line-height: 1.45;\n font-size: 14px;\n animation: fadeUp 0.25s ease;\n}\n:global(.tw-message) p {\n margin: 0;\n}\n:global(.tw-message) p + p {\n margin-top: 8px;\n}\n:global(.tw-message) a {\n color: var(--accent);\n text-decoration: none;\n font-weight: 600;\n}\n:global(.tw-message) a:hover {\n text-decoration: underline;\n}\n:global(.tw-message) strong {\n font-weight: 700;\n}\n:global(.tw-message) ul,\n:global(.tw-message) ol {\n margin: 8px 0 0;\n padding-left: 18px;\n}\n:global(.tw-message) li {\n margin: 4px 0;\n}\n:global(.tw-typing) {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-height: 16px;\n color: var(--muted);\n}\n:global(.tw-typing-dot) {\n width: 6px;\n height: 6px;\n border-radius: 999px;\n background: currentColor;\n opacity: 0.6;\n animation: typingBounce 1.2s infinite ease-in-out;\n}\n:global(.tw-typing-dot):nth-child(2) {\n animation-delay: 0.2s;\n}\n:global(.tw-typing-dot):nth-child(3) {\n animation-delay: 0.4s;\n}\n:global(.tw-sr-only) {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n:global(.tw-user) {\n align-self: flex-end;\n background: var(--bubble-user);\n color: var(--bubble-user-text);\n border-bottom-right-radius: 8px;\n}\n:global(.tw-assistant) {\n align-self: flex-start;\n background: var(--bubble-assistant);\n border: 1px solid var(--bubble-assistant-border);\n border-bottom-left-radius: 8px;\n}\n:global(.tw-system) {\n align-self: center;\n max-width: 92%;\n text-align: center;\n background: var(--bubble-system);\n color: var(--bubble-system-text);\n}\n:global(.tw-composer) {\n padding: 16px 22px 22px;\n display: grid;\n gap: 10px;\n background: var(--composer-bg);\n}\n:global(.tw-input-wrap) {\n display: flex;\n gap: 10px;\n align-items: flex-end;\n}\n:global(.tw-textarea) {\n flex: 1;\n min-height: 54px;\n max-height: 140px;\n padding: 14px 16px;\n border-radius: 8px;\n border: 1px solid var(--border);\n background: var(--surface);\n color: var(--ink);\n resize: none;\n font-family: inherit;\n font-size: 14px;\n outline: none;\n transition: border 0.2s ease, box-shadow 0.2s ease;\n}\n:global(.tw-textarea)::placeholder {\n color: var(--muted);\n opacity: 0.85;\n}\n:global(.tw-textarea):focus {\n border-color: var(--input-focus-border);\n box-shadow: 0 0 0 3px var(--input-focus-shadow);\n}\n:global(.tw-send) {\n border: none;\n border-radius: 8px;\n padding: 12px 18px;\n background: var(--button-bg);\n color: var(--button-text);\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n:global(.tw-send):hover {\n transform: translateY(-1px);\n box-shadow: 0 12px 24px var(--button-shadow);\n}\n:global(.tw-send):disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n box-shadow: none;\n}\n:global(.tw-meta) {\n display: flex;\n justify-content: space-between;\n color: var(--muted);\n font-size: 12px;\n}\n:global(.tw-pill-row) {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n:global(.tw-pill) {\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px dashed var(--pill-border);\n color: var(--muted);\n font-size: 11px;\n}\n@keyframes fadeUp {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@keyframes typingBounce {\n 0%, 80%, 100% {\n transform: translateY(0);\n opacity: 0.4;\n }\n 40% {\n transform: translateY(-4px);\n opacity: 0.9;\n }\n}\n@media (max-width: 720px) {\n :global(.tw-widget) {\n width: 100%;\n height: 100vh;\n border-radius: 12px;\n }\n}\n');
66
+
67
+ // src/i18n/translations.ts
68
+ var en = {
69
+ titleDefault: "Tunio Assistant",
70
+ subtitleDefault: "Live on-air assistant",
71
+ systemMessage: "Tell me what station or playlist you want and I will set it up.",
72
+ statusStreaming: "Online",
73
+ statusNeedsAttention: "Online",
74
+ statusReady: "Online",
75
+ thinking: "Thinking...",
76
+ pillSynthwave: '"Create a synthwave radio"',
77
+ pillSchedule: '"Schedule a rock playlist at 6 PM"',
78
+ pillPlayNow: '"Set playlist to play now"',
79
+ inputPlaceholder: "Tell me what to do on air...",
80
+ sending: "Working...",
81
+ send: "Send",
82
+ sessionReady: "Session ready",
83
+ sessionRequesting: "Requesting session...",
84
+ missingApiUrl: "Missing API URL",
85
+ errorPrefix: "Error:",
86
+ errorMissingTokenUrl: "Missing session token URL",
87
+ errorFailedTokenFetch: "Failed to fetch session token",
88
+ errorMissingToken: "Session token missing in response",
89
+ errorTokenLoad: "Failed to load token",
90
+ errorTokenUnavailable: "Session token is not available",
91
+ errorServiceUnavailable: "Agent service unavailable",
92
+ errorUnknown: "Unknown error",
93
+ errorUnexpected: "Unexpected error"
94
+ };
95
+ var ru = {
96
+ titleDefault: "Tunio \u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442",
97
+ subtitleDefault: "\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442 \u043F\u0440\u044F\u043C\u043E\u0433\u043E \u044D\u0444\u0438\u0440\u0430",
98
+ systemMessage: "\u0421\u043A\u0430\u0436\u0438\u0442\u0435, \u043A\u0430\u043A\u0443\u044E \u0441\u0442\u0430\u043D\u0446\u0438\u044E \u0438\u043B\u0438 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u043D\u0443\u0436\u043D\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C, \u0438 \u044F \u0432\u0441\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u044E.",
99
+ statusStreaming: "\u041E\u043D\u043B\u0430\u0439\u043D",
100
+ statusNeedsAttention: "\u041E\u043D\u043B\u0430\u0439\u043D",
101
+ statusReady: "\u041E\u043D\u043B\u0430\u0439\u043D",
102
+ thinking: "\u0414\u0443\u043C\u0430\u044E...",
103
+ pillSynthwave: '"\u0421\u043E\u0437\u0434\u0430\u0439 \u0441\u0438\u043D\u0442\u0432\u0435\u0439\u0432-\u0440\u0430\u0434\u0438\u043E"',
104
+ pillSchedule: '"\u0417\u0430\u043F\u043B\u0430\u043D\u0438\u0440\u0443\u0439 \u0440\u043E\u043A-\u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u043D\u0430 18:00"',
105
+ pillPlayNow: '"\u0412\u043A\u043B\u044E\u0447\u0438 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u0441\u0435\u0439\u0447\u0430\u0441"',
106
+ inputPlaceholder: "\u0421\u043A\u0430\u0436\u0438, \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u0432 \u044D\u0444\u0438\u0440\u0435...",
107
+ sending: "\u0412\u044B\u043F\u043E\u043B\u043D\u044F\u044E...",
108
+ send: "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C",
109
+ sessionReady: "\u0421\u0435\u0441\u0441\u0438\u044F \u0433\u043E\u0442\u043E\u0432\u0430",
110
+ sessionRequesting: "\u0417\u0430\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C \u0441\u0435\u0441\u0441\u0438\u044E...",
111
+ missingApiUrl: "\u041D\u0435 \u0437\u0430\u0434\u0430\u043D API URL",
112
+ errorPrefix: "\u041E\u0448\u0438\u0431\u043A\u0430:",
113
+ errorMissingTokenUrl: "\u041D\u0435 \u0437\u0430\u0434\u0430\u043D URL \u0442\u043E\u043A\u0435\u043D\u0430 \u0441\u0435\u0441\u0441\u0438\u0438",
114
+ errorFailedTokenFetch: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438",
115
+ errorMissingToken: "\u0422\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435",
116
+ errorTokenLoad: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D",
117
+ errorTokenUnavailable: "\u0422\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
118
+ errorServiceUnavailable: "\u0421\u0435\u0440\u0432\u0438\u0441 \u0430\u0433\u0435\u043D\u0442\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
119
+ errorUnknown: "\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430",
120
+ errorUnexpected: "\u041D\u0435\u043F\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043D\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"
121
+ };
122
+ var translations = {
123
+ en,
124
+ ru
125
+ };
126
+ var getTranslations = (lang) => {
127
+ var _a;
128
+ if (!lang) return en;
129
+ const normalized = lang.toLowerCase();
130
+ if (translations[normalized]) return translations[normalized];
131
+ const base = normalized.split("-")[0];
132
+ return (_a = translations[base]) != null ? _a : en;
133
+ };
134
+
135
+ // src/components/TunioWidget.tsx
136
+ var import_jsx_runtime = require("react/jsx-runtime");
137
+ var createId = () => Math.random().toString(36).slice(2);
138
+ function TunioWidget({
139
+ apiUrl,
140
+ sessionTokenUrl,
141
+ lang,
142
+ getSessionToken,
143
+ accessToken,
144
+ title,
145
+ subtitle,
146
+ theme = "light",
147
+ className,
148
+ onNavigate
149
+ }) {
150
+ var _a, _b;
151
+ const [input, setInput] = (0, import_react.useState)("");
152
+ const [isStreaming, setIsStreaming] = (0, import_react.useState)(false);
153
+ const [error, setError] = (0, import_react.useState)(null);
154
+ const [sessionToken, setSessionToken] = (0, import_react.useState)(null);
155
+ const inputRef = (0, import_react.useRef)(null);
156
+ const messagesEndRef = (0, import_react.useRef)(null);
157
+ const resolvedApiUrl = (_a = apiUrl != null ? apiUrl : process.env.NEXT_PUBLIC_AGENT_API_URL) != null ? _a : "";
158
+ const resolvedTokenUrl = (_b = sessionTokenUrl != null ? sessionTokenUrl : process.env.NEXT_PUBLIC_SESSION_TOKEN_URL) != null ? _b : "";
159
+ const resolvedUserLang = (0, import_react.useMemo)(() => {
160
+ if (lang) return lang;
161
+ if (typeof navigator !== "undefined" && navigator.language) {
162
+ return navigator.language.split("-")[0];
163
+ }
164
+ return void 0;
165
+ }, [lang]);
166
+ const i18n = (0, import_react.useMemo)(() => getTranslations(resolvedUserLang), [resolvedUserLang]);
167
+ const [messages, setMessages] = (0, import_react.useState)(() => [
168
+ {
169
+ id: "welcome",
170
+ role: "system",
171
+ content: i18n.systemMessage
172
+ }
173
+ ]);
174
+ const hasUserMessage = (0, import_react.useMemo)(
175
+ () => messages.some((message) => message.role === "user"),
176
+ [messages]
177
+ );
178
+ const displayTitle = title != null ? title : i18n.titleDefault;
179
+ const displaySubtitle = subtitle != null ? subtitle : i18n.subtitleDefault;
180
+ const scrollToBottom = () => {
181
+ var _a2;
182
+ (_a2 = messagesEndRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
183
+ };
184
+ (0, import_react.useEffect)(() => {
185
+ scrollToBottom();
186
+ }, [messages]);
187
+ (0, import_react.useEffect)(() => {
188
+ setMessages((prev) => {
189
+ if (prev.length === 1 && prev[0].id === "welcome" && prev[0].role === "system") {
190
+ return [{ ...prev[0], content: i18n.systemMessage }];
191
+ }
192
+ return prev;
193
+ });
194
+ }, [i18n.systemMessage]);
195
+ const fetchSessionToken = (0, import_react.useCallback)(async () => {
196
+ if (getSessionToken) {
197
+ return getSessionToken();
198
+ }
199
+ if (accessToken) {
200
+ return accessToken;
201
+ }
202
+ if (!resolvedTokenUrl) {
203
+ throw new Error(i18n.errorMissingTokenUrl);
204
+ }
205
+ const response = await fetch(resolvedTokenUrl, {
206
+ credentials: accessToken ? "omit" : "include",
207
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
208
+ });
209
+ if (!response.ok) {
210
+ throw new Error(i18n.errorFailedTokenFetch);
211
+ }
212
+ const data = await response.json();
213
+ const token = data.session_token;
214
+ if (!token) {
215
+ throw new Error(i18n.errorMissingToken);
216
+ }
217
+ return token;
218
+ }, [getSessionToken, i18n, resolvedTokenUrl]);
219
+ (0, import_react.useEffect)(() => {
220
+ fetchSessionToken().then(setSessionToken).catch((err) => {
221
+ setError(err instanceof Error ? err.message : i18n.errorTokenLoad);
222
+ });
223
+ }, [fetchSessionToken, i18n.errorTokenLoad]);
224
+ const statusLabel = (0, import_react.useMemo)(() => {
225
+ if (isStreaming) return i18n.statusStreaming;
226
+ if (error) return i18n.statusNeedsAttention;
227
+ return i18n.statusReady;
228
+ }, [error, i18n, isStreaming]);
229
+ const handleLinkClick = (0, import_react.useCallback)(
230
+ (href, event) => {
231
+ if (!href) return;
232
+ const isInternal = href.startsWith("/") && !href.startsWith("//");
233
+ if (isInternal && onNavigate) {
234
+ event.preventDefault();
235
+ onNavigate(href);
236
+ }
237
+ },
238
+ [onNavigate]
239
+ );
240
+ const normalizeHref = (0, import_react.useCallback)((href) => {
241
+ if (!href) return href;
242
+ if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) {
243
+ return href;
244
+ }
245
+ if (href.startsWith("mailto:") || href.startsWith("tel:")) {
246
+ return href;
247
+ }
248
+ if (typeof window === "undefined") {
249
+ return href;
250
+ }
251
+ try {
252
+ const url = new URL(href);
253
+ if (url.host === window.location.host) {
254
+ return `${url.pathname}${url.search}${url.hash}`;
255
+ }
256
+ } catch (e) {
257
+ return href;
258
+ }
259
+ return href;
260
+ }, []);
261
+ const focusInput = () => {
262
+ requestAnimationFrame(() => {
263
+ var _a2;
264
+ (_a2 = inputRef.current) == null ? void 0 : _a2.focus();
265
+ });
266
+ };
267
+ const sendMessage = async () => {
268
+ var _a2, _b2;
269
+ if (isStreaming) {
270
+ focusInput();
271
+ return;
272
+ }
273
+ if (!input.trim() || !resolvedApiUrl) {
274
+ focusInput();
275
+ return;
276
+ }
277
+ if (!sessionToken) {
278
+ setError(i18n.errorTokenUnavailable);
279
+ focusInput();
280
+ return;
281
+ }
282
+ setError(null);
283
+ setIsStreaming(true);
284
+ const userMessage = {
285
+ id: createId(),
286
+ role: "user",
287
+ content: input.trim()
288
+ };
289
+ const assistantMessage = {
290
+ id: createId(),
291
+ role: "assistant",
292
+ content: ""
293
+ };
294
+ setMessages((prev) => [...prev, userMessage, assistantMessage]);
295
+ setInput("");
296
+ focusInput();
297
+ try {
298
+ const response = await fetch(resolvedApiUrl, {
299
+ method: "POST",
300
+ headers: { "Content-Type": "application/json" },
301
+ body: JSON.stringify({
302
+ message: userMessage.content,
303
+ session_token: sessionToken,
304
+ user_lang: resolvedUserLang
305
+ })
306
+ });
307
+ if (!response.ok || !response.body) {
308
+ throw new Error(i18n.errorServiceUnavailable);
309
+ }
310
+ const reader = response.body.getReader();
311
+ const decoder = new TextDecoder();
312
+ let buffer = "";
313
+ while (true) {
314
+ const { value, done } = await reader.read();
315
+ if (done) break;
316
+ buffer += decoder.decode(value, { stream: true });
317
+ const parts = buffer.split("\n\n");
318
+ buffer = (_a2 = parts.pop()) != null ? _a2 : "";
319
+ for (const part of parts) {
320
+ const lines = part.split("\n");
321
+ let eventName = "message";
322
+ let data = "";
323
+ for (const line of lines) {
324
+ if (line.startsWith("event:")) {
325
+ eventName = line.replace("event:", "").trim();
326
+ }
327
+ if (line.startsWith("data:")) {
328
+ data += line.replace("data:", "").trim();
329
+ }
330
+ }
331
+ if (!data) continue;
332
+ if (eventName === "message") {
333
+ const payload = JSON.parse(data);
334
+ if (payload.content !== void 0) {
335
+ setMessages((prev) => {
336
+ var _a3;
337
+ const next = [...prev];
338
+ const last = next[next.length - 1];
339
+ if (last && last.role === "assistant") {
340
+ last.content = (_a3 = payload.content) != null ? _a3 : "";
341
+ }
342
+ return next;
343
+ });
344
+ }
345
+ }
346
+ if (eventName === "error") {
347
+ const payload = JSON.parse(data);
348
+ setError((_b2 = payload.message) != null ? _b2 : i18n.errorUnknown);
349
+ }
350
+ }
351
+ }
352
+ } catch (err) {
353
+ setError(err instanceof Error ? err.message : i18n.errorUnexpected);
354
+ } finally {
355
+ setIsStreaming(false);
356
+ }
357
+ };
358
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
359
+ "section",
360
+ {
361
+ className: `tw-widget tw-${theme}${className ? ` ${className}` : ""}`,
362
+ "data-theme": theme,
363
+ "data-tunio-widget": "root",
364
+ children: [
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "tw-header", children: [
366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-brand", children: [
367
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tw-brand-title", children: displayTitle }),
368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tw-brand-subtitle", children: displaySubtitle })
369
+ ] }),
370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-status", children: [
371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-status-dot" }),
372
+ statusLabel
373
+ ] })
374
+ ] }),
375
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-messages", children: [
376
+ messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
377
+ "div",
378
+ {
379
+ className: `tw-message tw-${message.role}`,
380
+ children: message.content ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
381
+ import_react_markdown.default,
382
+ {
383
+ remarkPlugins: [import_remark_gfm.default],
384
+ components: {
385
+ a: ({ href, children, ...props }) => {
386
+ const normalizedHref = normalizeHref(href);
387
+ const isInternal = typeof normalizedHref === "string" && normalizedHref.startsWith("/") && !normalizedHref.startsWith("//");
388
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
389
+ "a",
390
+ {
391
+ ...props,
392
+ href: normalizedHref,
393
+ onClick: (event) => handleLinkClick(normalizedHref, event),
394
+ target: isInternal ? void 0 : "_blank",
395
+ rel: isInternal ? void 0 : "noreferrer",
396
+ children
397
+ }
398
+ );
399
+ }
400
+ },
401
+ children: message.content
402
+ }
403
+ ) : message.role === "assistant" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "tw-typing", role: "status", "aria-label": i18n.thinking, children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-typing-dot" }),
405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-typing-dot" }),
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-typing-dot" }),
407
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-sr-only", children: i18n.thinking })
408
+ ] }) : ""
409
+ },
410
+ message.id
411
+ )),
412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesEndRef })
413
+ ] }),
414
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-composer", children: [
415
+ !hasUserMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-pill-row", children: [
416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-pill", children: i18n.pillSynthwave }),
417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-pill", children: i18n.pillSchedule }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tw-pill", children: i18n.pillPlayNow })
419
+ ] }) : null,
420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-input-wrap", children: [
421
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
422
+ "textarea",
423
+ {
424
+ className: "tw-textarea",
425
+ placeholder: i18n.inputPlaceholder,
426
+ value: input,
427
+ onChange: (event) => setInput(event.target.value),
428
+ onKeyDown: (event) => {
429
+ if (event.nativeEvent.isComposing) return;
430
+ if (event.key !== "Enter") return;
431
+ if (event.shiftKey) return;
432
+ event.preventDefault();
433
+ sendMessage();
434
+ },
435
+ rows: 2,
436
+ ref: inputRef
437
+ }
438
+ ),
439
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
440
+ "button",
441
+ {
442
+ className: "tw-send",
443
+ onClick: sendMessage,
444
+ disabled: isStreaming || !input.trim(),
445
+ children: isStreaming ? i18n.sending : i18n.send
446
+ }
447
+ )
448
+ ] }),
449
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tw-meta", children: [
450
+ i18n.errorPrefix,
451
+ " ",
452
+ error
453
+ ] }) : null
454
+ ] })
455
+ ]
456
+ }
457
+ );
458
+ }
459
+ // Annotate the CommonJS export names for ESM import in node:
460
+ 0 && (module.exports = {
461
+ TunioWidget
462
+ });
463
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/components/TunioWidget.tsx","#style-inject:#style-inject","../src/components/TunioWidget.module.css","../src/i18n/translations.ts"],"sourcesContent":["export { TunioWidget } from './components/TunioWidget';\nexport type { TunioWidgetProps } from './components/TunioWidget';\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport './TunioWidget.module.css';\nimport { getTranslations } from '../i18n/translations';\n\ntype ChatMessage = {\n id: string;\n role: 'user' | 'assistant' | 'system';\n content: string;\n};\n\nexport type TunioWidgetProps = {\n apiUrl?: string;\n sessionTokenUrl?: string;\n lang?: string;\n getSessionToken?: () => Promise<string>;\n accessToken?: string;\n title?: string;\n subtitle?: string;\n theme?: 'light' | 'dark';\n className?: string;\n onNavigate?: (href: string) => void;\n};\n\nconst createId = () => Math.random().toString(36).slice(2);\n\nexport function TunioWidget({\n apiUrl,\n sessionTokenUrl,\n lang,\n getSessionToken,\n accessToken,\n title,\n subtitle,\n theme = 'light',\n className,\n onNavigate,\n}: TunioWidgetProps) {\n const [input, setInput] = useState('');\n const [isStreaming, setIsStreaming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [sessionToken, setSessionToken] = useState<string | null>(null);\n const inputRef = useRef<HTMLTextAreaElement | null>(null);\n const messagesEndRef = useRef<HTMLDivElement | null>(null);\n\n const resolvedApiUrl = apiUrl ?? process.env.NEXT_PUBLIC_AGENT_API_URL ?? '';\n const resolvedTokenUrl =\n sessionTokenUrl ?? process.env.NEXT_PUBLIC_SESSION_TOKEN_URL ?? '';\n const resolvedUserLang = useMemo(() => {\n if (lang) return lang;\n if (typeof navigator !== 'undefined' && navigator.language) {\n return navigator.language.split('-')[0];\n }\n return undefined;\n }, [lang]);\n const i18n = useMemo(() => getTranslations(resolvedUserLang), [resolvedUserLang]);\n const [messages, setMessages] = useState<ChatMessage[]>(() => [\n {\n id: 'welcome',\n role: 'system',\n content: i18n.systemMessage,\n },\n ]);\n const hasUserMessage = useMemo(\n () => messages.some((message) => message.role === 'user'),\n [messages]\n );\n const displayTitle = title ?? i18n.titleDefault;\n const displaySubtitle = subtitle ?? i18n.subtitleDefault;\n\n const scrollToBottom = () => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n };\n\n useEffect(() => {\n scrollToBottom();\n }, [messages]);\n\n useEffect(() => {\n setMessages((prev) => {\n if (prev.length === 1 && prev[0].id === 'welcome' && prev[0].role === 'system') {\n return [{ ...prev[0], content: i18n.systemMessage }];\n }\n return prev;\n });\n }, [i18n.systemMessage]);\n\n const fetchSessionToken = useCallback(async () => {\n if (getSessionToken) {\n return getSessionToken();\n }\n\n if (accessToken) {\n return accessToken;\n }\n\n if (!resolvedTokenUrl) {\n throw new Error(i18n.errorMissingTokenUrl);\n }\n\n const response = await fetch(resolvedTokenUrl, {\n credentials: accessToken ? 'omit' : 'include',\n headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,\n });\n\n if (!response.ok) {\n throw new Error(i18n.errorFailedTokenFetch);\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n const token = data.session_token as string;\n\n if (!token) {\n throw new Error(i18n.errorMissingToken);\n }\n\n return token;\n }, [getSessionToken, i18n, resolvedTokenUrl]);\n\n useEffect(() => {\n fetchSessionToken()\n .then(setSessionToken)\n .catch((err) => {\n setError(err instanceof Error ? err.message : i18n.errorTokenLoad);\n });\n }, [fetchSessionToken, i18n.errorTokenLoad]);\n\n const statusLabel = useMemo(() => {\n if (isStreaming) return i18n.statusStreaming;\n if (error) return i18n.statusNeedsAttention;\n return i18n.statusReady;\n }, [error, i18n, isStreaming]);\n\n const handleLinkClick = useCallback(\n (href: string | undefined, event: React.MouseEvent<HTMLAnchorElement>) => {\n if (!href) return;\n const isInternal = href.startsWith('/') && !href.startsWith('//');\n if (isInternal && onNavigate) {\n event.preventDefault();\n onNavigate(href);\n }\n },\n [onNavigate]\n );\n\n const normalizeHref = useCallback((href: string | undefined) => {\n if (!href) return href;\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return href;\n }\n if (href.startsWith('mailto:') || href.startsWith('tel:')) {\n return href;\n }\n if (typeof window === 'undefined') {\n return href;\n }\n try {\n const url = new URL(href);\n if (url.host === window.location.host) {\n return `${url.pathname}${url.search}${url.hash}`;\n }\n } catch {\n return href;\n }\n return href;\n }, []);\n\n const focusInput = () => {\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n };\n\n const sendMessage = async () => {\n if (isStreaming) {\n focusInput();\n return;\n }\n if (!input.trim() || !resolvedApiUrl) {\n focusInput();\n return;\n }\n if (!sessionToken) {\n setError(i18n.errorTokenUnavailable);\n focusInput();\n return;\n }\n\n setError(null);\n setIsStreaming(true);\n\n const userMessage: ChatMessage = {\n id: createId(),\n role: 'user',\n content: input.trim(),\n };\n\n const assistantMessage: ChatMessage = {\n id: createId(),\n role: 'assistant',\n content: '',\n };\n\n setMessages((prev) => [...prev, userMessage, assistantMessage]);\n setInput('');\n focusInput();\n\n try {\n const response = await fetch(resolvedApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n message: userMessage.content,\n session_token: sessionToken,\n user_lang: resolvedUserLang,\n }),\n });\n\n if (!response.ok || !response.body) {\n throw new Error(i18n.errorServiceUnavailable);\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const parts = buffer.split('\\n\\n');\n buffer = parts.pop() ?? '';\n\n for (const part of parts) {\n const lines = part.split('\\n');\n let eventName = 'message';\n let data = '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n eventName = line.replace('event:', '').trim();\n }\n if (line.startsWith('data:')) {\n data += line.replace('data:', '').trim();\n }\n }\n\n if (!data) continue;\n\n if (eventName === 'message') {\n const payload = JSON.parse(data) as { content?: string };\n if (payload.content !== undefined) {\n setMessages((prev) => {\n const next = [...prev];\n const last = next[next.length - 1];\n if (last && last.role === 'assistant') {\n last.content = payload.content ?? '';\n }\n return next;\n });\n }\n }\n\n if (eventName === 'error') {\n const payload = JSON.parse(data) as { message?: string };\n setError(payload.message ?? i18n.errorUnknown);\n }\n }\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : i18n.errorUnexpected);\n } finally {\n setIsStreaming(false);\n }\n };\n\n return (\n <section\n className={`tw-widget tw-${theme}${className ? ` ${className}` : ''}`}\n data-theme={theme}\n data-tunio-widget=\"root\"\n >\n <header className=\"tw-header\">\n <div className=\"tw-brand\">\n <div className=\"tw-brand-title\">{displayTitle}</div>\n <div className=\"tw-brand-subtitle\">{displaySubtitle}</div>\n </div>\n <div className=\"tw-status\">\n <span className=\"tw-status-dot\" />\n {statusLabel}\n </div>\n </header>\n\n <div className=\"tw-messages\">\n {messages.map((message) => (\n <div\n key={message.id}\n className={`tw-message tw-${message.role}`}\n >\n {message.content ? (\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n components={{\n a: ({ href, children, ...props }) => {\n const normalizedHref = normalizeHref(href);\n const isInternal =\n typeof normalizedHref === 'string' &&\n normalizedHref.startsWith('/') &&\n !normalizedHref.startsWith('//');\n return (\n <a\n {...props}\n href={normalizedHref}\n onClick={(event) => handleLinkClick(normalizedHref, event)}\n target={isInternal ? undefined : '_blank'}\n rel={isInternal ? undefined : 'noreferrer'}\n >\n {children}\n </a>\n );\n },\n }}\n >\n {message.content}\n </ReactMarkdown>\n ) : message.role === 'assistant' ? (\n <span className=\"tw-typing\" role=\"status\" aria-label={i18n.thinking}>\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-sr-only\">{i18n.thinking}</span>\n </span>\n ) : (\n ''\n )}\n </div>\n ))}\n <div ref={messagesEndRef} />\n </div>\n\n <div className=\"tw-composer\">\n {!hasUserMessage ? (\n <div className=\"tw-pill-row\">\n <span className=\"tw-pill\">{i18n.pillSynthwave}</span>\n <span className=\"tw-pill\">{i18n.pillSchedule}</span>\n <span className=\"tw-pill\">{i18n.pillPlayNow}</span>\n </div>\n ) : null}\n <div className=\"tw-input-wrap\">\n <textarea\n className=\"tw-textarea\"\n placeholder={i18n.inputPlaceholder}\n value={input}\n onChange={(event) => setInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.nativeEvent.isComposing) return;\n if (event.key !== 'Enter') return;\n if (event.shiftKey) return;\n event.preventDefault();\n sendMessage();\n }}\n rows={2}\n ref={inputRef}\n />\n <button\n className=\"tw-send\"\n onClick={sendMessage}\n disabled={isStreaming || !input.trim()}\n >\n {isStreaming ? i18n.sending : i18n.send}\n </button>\n </div>\n {error ? (\n <div className=\"tw-meta\">\n {i18n.errorPrefix} {error}\n </div>\n ) : null}\n </div>\n </section>\n );\n}\n","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\":global(.tw-widget) {\\n --panel: #f7f9fc;\\n --ink: #1b2430;\\n --muted: #6c7a90;\\n --accent: #3f6df6;\\n --accent-soft: rgba(63, 109, 246, 0.14);\\n --teal: #21b3a8;\\n --surface: #ffffff;\\n --border: rgba(16, 24, 40, 0.12);\\n --widget-bg:\\n linear-gradient(\\n 135deg,\\n #f9fbff 0%,\\n #eef2f8 60%,\\n #e8edf6 100%);\\n --header-bg:\\n linear-gradient(\\n \\n 120deg,\\n rgba(255, 255, 255, 0.92),\\n rgba(243, 246, 252, 0.82) );\\n --composer-bg:\\n linear-gradient(\\n \\n 180deg,\\n rgba(255, 255, 255, 0) 0%,\\n rgba(244, 246, 252, 0.92) 80% );\\n --shadow: 0 24px 70px rgba(18, 24, 40, 0.18);\\n --glow:\\n radial-gradient(\\n circle,\\n rgba(63, 109, 246, 0.18) 0%,\\n rgba(63, 109, 246, 0) 70%);\\n --accent-glow: rgba(63, 109, 246, 0.2);\\n --bubble-user: #1b2430;\\n --bubble-user-text: #f7f9ff;\\n --bubble-assistant: #ffffff;\\n --bubble-assistant-border: rgba(16, 24, 40, 0.08);\\n --bubble-system: rgba(27, 36, 48, 0.08);\\n --bubble-system-text: #6c7a90;\\n --input-focus-border: rgba(63, 109, 246, 0.6);\\n --input-focus-shadow: rgba(63, 109, 246, 0.2);\\n --button-bg: #1b2430;\\n --button-text: #f7f9ff;\\n --button-shadow: rgba(27, 36, 48, 0.2);\\n --pill-border: rgba(16, 24, 40, 0.2);\\n display: grid;\\n grid-template-rows: auto minmax(0, 1fr) auto;\\n width: min(100%, 420px);\\n height: min(80vh, 640px);\\n min-height: 520px;\\n border-radius: 12px;\\n background: var(--widget-bg);\\n border: 1px solid var(--border);\\n box-shadow: var(--shadow);\\n color: var(--ink);\\n overflow: hidden;\\n position: relative;\\n}\\n:global(.tw-light) {\\n color-scheme: light;\\n}\\n:global(.tw-dark) {\\n color-scheme: dark;\\n --panel: #121826;\\n --ink: #e6edf7;\\n --muted: #a5b3c8;\\n --accent: #4a7dff;\\n --accent-soft: rgba(74, 125, 255, 0.18);\\n --teal: #2db9c3;\\n --surface: #1a2232;\\n --border: rgba(230, 237, 247, 0.12);\\n --widget-bg:\\n linear-gradient(\\n 135deg,\\n #131a2a 0%,\\n #101525 60%,\\n #0d1220 100%);\\n --header-bg:\\n linear-gradient(\\n \\n 120deg,\\n rgba(18, 24, 38, 0.95),\\n rgba(23, 30, 48, 0.85) );\\n --composer-bg:\\n linear-gradient(\\n \\n 180deg,\\n rgba(18, 24, 38, 0) 0%,\\n rgba(18, 24, 38, 0.9) 80% );\\n --shadow: 0 28px 90px rgba(2, 7, 14, 0.65);\\n --glow:\\n radial-gradient(\\n circle,\\n rgba(74, 125, 255, 0.2) 0%,\\n rgba(74, 125, 255, 0) 70%);\\n --accent-glow: rgba(74, 125, 255, 0.28);\\n --bubble-user: #2f3e63;\\n --bubble-user-text: #f4f7ff;\\n --bubble-assistant: #1a2233;\\n --bubble-assistant-border: rgba(230, 237, 247, 0.1);\\n --bubble-system: rgba(230, 237, 247, 0.08);\\n --bubble-system-text: #b7c3d7;\\n --input-focus-border: rgba(74, 125, 255, 0.6);\\n --input-focus-shadow: rgba(74, 125, 255, 0.25);\\n --button-bg: #4a7dff;\\n --button-text: #f4f7ff;\\n --button-shadow: rgba(74, 125, 255, 0.35);\\n --pill-border: rgba(230, 237, 247, 0.24);\\n}\\n:global(.tw-widget)::before {\\n content: \\\"\\\";\\n position: absolute;\\n inset: -120px 40% auto -60px;\\n height: 220px;\\n background: var(--glow);\\n pointer-events: none;\\n}\\n:global(.tw-header) {\\n padding: 22px 26px 16px;\\n display: flex;\\n align-items: center;\\n justify-content: space-between;\\n background: var(--header-bg);\\n backdrop-filter: blur(12px);\\n}\\n:global(.tw-brand) {\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n:global(.tw-brand-title) {\\n font-family: var(--font-display);\\n font-size: 24px;\\n letter-spacing: -0.02em;\\n}\\n:global(.tw-brand-subtitle) {\\n color: var(--muted);\\n font-size: 13px;\\n}\\n:global(.tw-status) {\\n display: inline-flex;\\n align-items: center;\\n gap: 8px;\\n padding: 6px 12px;\\n border-radius: 8px;\\n background: var(--accent-soft);\\n color: var(--ink);\\n font-size: 12px;\\n font-weight: 600;\\n}\\n:global(.tw-status-dot) {\\n width: 8px;\\n height: 8px;\\n border-radius: 50%;\\n background: var(--accent);\\n box-shadow: 0 0 0 4px var(--accent-glow);\\n}\\n:global(.tw-messages) {\\n padding: 16px 22px 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 14px;\\n overflow-y: auto;\\n min-height: 0;\\n scroll-behavior: smooth;\\n}\\n:global(.tw-message) {\\n max-width: 85%;\\n padding: 14px 16px;\\n border-radius: 8px;\\n line-height: 1.45;\\n font-size: 14px;\\n animation: fadeUp 0.25s ease;\\n}\\n:global(.tw-message) p {\\n margin: 0;\\n}\\n:global(.tw-message) p + p {\\n margin-top: 8px;\\n}\\n:global(.tw-message) a {\\n color: var(--accent);\\n text-decoration: none;\\n font-weight: 600;\\n}\\n:global(.tw-message) a:hover {\\n text-decoration: underline;\\n}\\n:global(.tw-message) strong {\\n font-weight: 700;\\n}\\n:global(.tw-message) ul,\\n:global(.tw-message) ol {\\n margin: 8px 0 0;\\n padding-left: 18px;\\n}\\n:global(.tw-message) li {\\n margin: 4px 0;\\n}\\n:global(.tw-typing) {\\n display: inline-flex;\\n align-items: center;\\n gap: 6px;\\n min-height: 16px;\\n color: var(--muted);\\n}\\n:global(.tw-typing-dot) {\\n width: 6px;\\n height: 6px;\\n border-radius: 999px;\\n background: currentColor;\\n opacity: 0.6;\\n animation: typingBounce 1.2s infinite ease-in-out;\\n}\\n:global(.tw-typing-dot):nth-child(2) {\\n animation-delay: 0.2s;\\n}\\n:global(.tw-typing-dot):nth-child(3) {\\n animation-delay: 0.4s;\\n}\\n:global(.tw-sr-only) {\\n position: absolute;\\n width: 1px;\\n height: 1px;\\n padding: 0;\\n margin: -1px;\\n overflow: hidden;\\n clip: rect(0, 0, 0, 0);\\n border: 0;\\n}\\n:global(.tw-user) {\\n align-self: flex-end;\\n background: var(--bubble-user);\\n color: var(--bubble-user-text);\\n border-bottom-right-radius: 8px;\\n}\\n:global(.tw-assistant) {\\n align-self: flex-start;\\n background: var(--bubble-assistant);\\n border: 1px solid var(--bubble-assistant-border);\\n border-bottom-left-radius: 8px;\\n}\\n:global(.tw-system) {\\n align-self: center;\\n max-width: 92%;\\n text-align: center;\\n background: var(--bubble-system);\\n color: var(--bubble-system-text);\\n}\\n:global(.tw-composer) {\\n padding: 16px 22px 22px;\\n display: grid;\\n gap: 10px;\\n background: var(--composer-bg);\\n}\\n:global(.tw-input-wrap) {\\n display: flex;\\n gap: 10px;\\n align-items: flex-end;\\n}\\n:global(.tw-textarea) {\\n flex: 1;\\n min-height: 54px;\\n max-height: 140px;\\n padding: 14px 16px;\\n border-radius: 8px;\\n border: 1px solid var(--border);\\n background: var(--surface);\\n color: var(--ink);\\n resize: none;\\n font-family: inherit;\\n font-size: 14px;\\n outline: none;\\n transition: border 0.2s ease, box-shadow 0.2s ease;\\n}\\n:global(.tw-textarea)::placeholder {\\n color: var(--muted);\\n opacity: 0.85;\\n}\\n:global(.tw-textarea):focus {\\n border-color: var(--input-focus-border);\\n box-shadow: 0 0 0 3px var(--input-focus-shadow);\\n}\\n:global(.tw-send) {\\n border: none;\\n border-radius: 8px;\\n padding: 12px 18px;\\n background: var(--button-bg);\\n color: var(--button-text);\\n font-weight: 600;\\n cursor: pointer;\\n transition: transform 0.2s ease, box-shadow 0.2s ease;\\n}\\n:global(.tw-send):hover {\\n transform: translateY(-1px);\\n box-shadow: 0 12px 24px var(--button-shadow);\\n}\\n:global(.tw-send):disabled {\\n opacity: 0.5;\\n cursor: not-allowed;\\n transform: none;\\n box-shadow: none;\\n}\\n:global(.tw-meta) {\\n display: flex;\\n justify-content: space-between;\\n color: var(--muted);\\n font-size: 12px;\\n}\\n:global(.tw-pill-row) {\\n display: flex;\\n gap: 8px;\\n flex-wrap: wrap;\\n}\\n:global(.tw-pill) {\\n padding: 6px 10px;\\n border-radius: 8px;\\n border: 1px dashed var(--pill-border);\\n color: var(--muted);\\n font-size: 11px;\\n}\\n@keyframes fadeUp {\\n from {\\n opacity: 0;\\n transform: translateY(8px);\\n }\\n to {\\n opacity: 1;\\n transform: translateY(0);\\n }\\n}\\n@keyframes typingBounce {\\n 0%, 80%, 100% {\\n transform: translateY(0);\\n opacity: 0.4;\\n }\\n 40% {\\n transform: translateY(-4px);\\n opacity: 0.9;\\n }\\n}\\n@media (max-width: 720px) {\\n :global(.tw-widget) {\\n width: 100%;\\n height: 100vh;\\n border-radius: 12px;\\n }\\n}\\n\")","export type Translations = {\n titleDefault: string;\n subtitleDefault: string;\n systemMessage: string;\n statusStreaming: string;\n statusNeedsAttention: string;\n statusReady: string;\n thinking: string;\n pillSynthwave: string;\n pillSchedule: string;\n pillPlayNow: string;\n inputPlaceholder: string;\n sending: string;\n send: string;\n sessionReady: string;\n sessionRequesting: string;\n missingApiUrl: string;\n errorPrefix: string;\n errorMissingTokenUrl: string;\n errorFailedTokenFetch: string;\n errorMissingToken: string;\n errorTokenLoad: string;\n errorTokenUnavailable: string;\n errorServiceUnavailable: string;\n errorUnknown: string;\n errorUnexpected: string;\n};\n\nexport const en: Translations = {\n titleDefault: 'Tunio Assistant',\n subtitleDefault: 'Live on-air assistant',\n systemMessage: 'Tell me what station or playlist you want and I will set it up.',\n statusStreaming: 'Online',\n statusNeedsAttention: 'Online',\n statusReady: 'Online',\n thinking: 'Thinking...',\n pillSynthwave: '\"Create a synthwave radio\"',\n pillSchedule: '\"Schedule a rock playlist at 6 PM\"',\n pillPlayNow: '\"Set playlist to play now\"',\n inputPlaceholder: 'Tell me what to do on air...',\n sending: 'Working...',\n send: 'Send',\n sessionReady: 'Session ready',\n sessionRequesting: 'Requesting session...',\n missingApiUrl: 'Missing API URL',\n errorPrefix: 'Error:',\n errorMissingTokenUrl: 'Missing session token URL',\n errorFailedTokenFetch: 'Failed to fetch session token',\n errorMissingToken: 'Session token missing in response',\n errorTokenLoad: 'Failed to load token',\n errorTokenUnavailable: 'Session token is not available',\n errorServiceUnavailable: 'Agent service unavailable',\n errorUnknown: 'Unknown error',\n errorUnexpected: 'Unexpected error',\n};\n\nexport const ru: Translations = {\n titleDefault: 'Tunio Ассистент',\n subtitleDefault: 'Ассистент прямого эфира',\n systemMessage: 'Скажите, какую станцию или плейлист нужно поставить, и я все настрою.',\n statusStreaming: 'Онлайн',\n statusNeedsAttention: 'Онлайн',\n statusReady: 'Онлайн',\n thinking: 'Думаю...',\n pillSynthwave: '\"Создай синтвейв-радио\"',\n pillSchedule: '\"Запланируй рок-плейлист на 18:00\"',\n pillPlayNow: '\"Включи плейлист сейчас\"',\n inputPlaceholder: 'Скажи, что сделать в эфире...',\n sending: 'Выполняю...',\n send: 'Отправить',\n sessionReady: 'Сессия готова',\n sessionRequesting: 'Запрашиваем сессию...',\n missingApiUrl: 'Не задан API URL',\n errorPrefix: 'Ошибка:',\n errorMissingTokenUrl: 'Не задан URL токена сессии',\n errorFailedTokenFetch: 'Не удалось получить токен сессии',\n errorMissingToken: 'Токен сессии отсутствует в ответе',\n errorTokenLoad: 'Не удалось загрузить токен',\n errorTokenUnavailable: 'Токен сессии недоступен',\n errorServiceUnavailable: 'Сервис агента недоступен',\n errorUnknown: 'Неизвестная ошибка',\n errorUnexpected: 'Непредвиденная ошибка',\n};\n\nconst translations: Record<string, Translations> = {\n en,\n ru,\n};\n\nexport const getTranslations = (lang?: string): Translations => {\n if (!lang) return en;\n const normalized = lang.toLowerCase();\n if (translations[normalized]) return translations[normalized];\n const base = normalized.split('-')[0];\n return translations[base] ?? en;\n};\n"],"mappings":"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","names":["_a","_b","ReactMarkdown","remarkGfm"]}
@@ -0,0 +1,17 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type TunioWidgetProps = {
4
+ apiUrl?: string;
5
+ sessionTokenUrl?: string;
6
+ lang?: string;
7
+ getSessionToken?: () => Promise<string>;
8
+ accessToken?: string;
9
+ title?: string;
10
+ subtitle?: string;
11
+ theme?: 'light' | 'dark';
12
+ className?: string;
13
+ onNavigate?: (href: string) => void;
14
+ };
15
+ declare function TunioWidget({ apiUrl, sessionTokenUrl, lang, getSessionToken, accessToken, title, subtitle, theme, className, onNavigate, }: TunioWidgetProps): react_jsx_runtime.JSX.Element;
16
+
17
+ export { TunioWidget, type TunioWidgetProps };
@@ -0,0 +1,17 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type TunioWidgetProps = {
4
+ apiUrl?: string;
5
+ sessionTokenUrl?: string;
6
+ lang?: string;
7
+ getSessionToken?: () => Promise<string>;
8
+ accessToken?: string;
9
+ title?: string;
10
+ subtitle?: string;
11
+ theme?: 'light' | 'dark';
12
+ className?: string;
13
+ onNavigate?: (href: string) => void;
14
+ };
15
+ declare function TunioWidget({ apiUrl, sessionTokenUrl, lang, getSessionToken, accessToken, title, subtitle, theme, className, onNavigate, }: TunioWidgetProps): react_jsx_runtime.JSX.Element;
16
+
17
+ export { TunioWidget, type TunioWidgetProps };
package/dist/index.mjs ADDED
@@ -0,0 +1,426 @@
1
+ // src/components/TunioWidget.tsx
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+
6
+ // #style-inject:#style-inject
7
+ function styleInject(css, { insertAt } = {}) {
8
+ if (!css || typeof document === "undefined") return;
9
+ const head = document.head || document.getElementsByTagName("head")[0];
10
+ const style = document.createElement("style");
11
+ style.type = "text/css";
12
+ if (insertAt === "top") {
13
+ if (head.firstChild) {
14
+ head.insertBefore(style, head.firstChild);
15
+ } else {
16
+ head.appendChild(style);
17
+ }
18
+ } else {
19
+ head.appendChild(style);
20
+ }
21
+ if (style.styleSheet) {
22
+ style.styleSheet.cssText = css;
23
+ } else {
24
+ style.appendChild(document.createTextNode(css));
25
+ }
26
+ }
27
+
28
+ // src/components/TunioWidget.module.css
29
+ styleInject(':global(.tw-widget) {\n --panel: #f7f9fc;\n --ink: #1b2430;\n --muted: #6c7a90;\n --accent: #3f6df6;\n --accent-soft: rgba(63, 109, 246, 0.14);\n --teal: #21b3a8;\n --surface: #ffffff;\n --border: rgba(16, 24, 40, 0.12);\n --widget-bg:\n linear-gradient(\n 135deg,\n #f9fbff 0%,\n #eef2f8 60%,\n #e8edf6 100%);\n --header-bg:\n linear-gradient(\n \n 120deg,\n rgba(255, 255, 255, 0.92),\n rgba(243, 246, 252, 0.82) );\n --composer-bg:\n linear-gradient(\n \n 180deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(244, 246, 252, 0.92) 80% );\n --shadow: 0 24px 70px rgba(18, 24, 40, 0.18);\n --glow:\n radial-gradient(\n circle,\n rgba(63, 109, 246, 0.18) 0%,\n rgba(63, 109, 246, 0) 70%);\n --accent-glow: rgba(63, 109, 246, 0.2);\n --bubble-user: #1b2430;\n --bubble-user-text: #f7f9ff;\n --bubble-assistant: #ffffff;\n --bubble-assistant-border: rgba(16, 24, 40, 0.08);\n --bubble-system: rgba(27, 36, 48, 0.08);\n --bubble-system-text: #6c7a90;\n --input-focus-border: rgba(63, 109, 246, 0.6);\n --input-focus-shadow: rgba(63, 109, 246, 0.2);\n --button-bg: #1b2430;\n --button-text: #f7f9ff;\n --button-shadow: rgba(27, 36, 48, 0.2);\n --pill-border: rgba(16, 24, 40, 0.2);\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n width: min(100%, 420px);\n height: min(80vh, 640px);\n min-height: 520px;\n border-radius: 12px;\n background: var(--widget-bg);\n border: 1px solid var(--border);\n box-shadow: var(--shadow);\n color: var(--ink);\n overflow: hidden;\n position: relative;\n}\n:global(.tw-light) {\n color-scheme: light;\n}\n:global(.tw-dark) {\n color-scheme: dark;\n --panel: #121826;\n --ink: #e6edf7;\n --muted: #a5b3c8;\n --accent: #4a7dff;\n --accent-soft: rgba(74, 125, 255, 0.18);\n --teal: #2db9c3;\n --surface: #1a2232;\n --border: rgba(230, 237, 247, 0.12);\n --widget-bg:\n linear-gradient(\n 135deg,\n #131a2a 0%,\n #101525 60%,\n #0d1220 100%);\n --header-bg:\n linear-gradient(\n \n 120deg,\n rgba(18, 24, 38, 0.95),\n rgba(23, 30, 48, 0.85) );\n --composer-bg:\n linear-gradient(\n \n 180deg,\n rgba(18, 24, 38, 0) 0%,\n rgba(18, 24, 38, 0.9) 80% );\n --shadow: 0 28px 90px rgba(2, 7, 14, 0.65);\n --glow:\n radial-gradient(\n circle,\n rgba(74, 125, 255, 0.2) 0%,\n rgba(74, 125, 255, 0) 70%);\n --accent-glow: rgba(74, 125, 255, 0.28);\n --bubble-user: #2f3e63;\n --bubble-user-text: #f4f7ff;\n --bubble-assistant: #1a2233;\n --bubble-assistant-border: rgba(230, 237, 247, 0.1);\n --bubble-system: rgba(230, 237, 247, 0.08);\n --bubble-system-text: #b7c3d7;\n --input-focus-border: rgba(74, 125, 255, 0.6);\n --input-focus-shadow: rgba(74, 125, 255, 0.25);\n --button-bg: #4a7dff;\n --button-text: #f4f7ff;\n --button-shadow: rgba(74, 125, 255, 0.35);\n --pill-border: rgba(230, 237, 247, 0.24);\n}\n:global(.tw-widget)::before {\n content: "";\n position: absolute;\n inset: -120px 40% auto -60px;\n height: 220px;\n background: var(--glow);\n pointer-events: none;\n}\n:global(.tw-header) {\n padding: 22px 26px 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--header-bg);\n backdrop-filter: blur(12px);\n}\n:global(.tw-brand) {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n:global(.tw-brand-title) {\n font-family: var(--font-display);\n font-size: 24px;\n letter-spacing: -0.02em;\n}\n:global(.tw-brand-subtitle) {\n color: var(--muted);\n font-size: 13px;\n}\n:global(.tw-status) {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: 8px;\n background: var(--accent-soft);\n color: var(--ink);\n font-size: 12px;\n font-weight: 600;\n}\n:global(.tw-status-dot) {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n box-shadow: 0 0 0 4px var(--accent-glow);\n}\n:global(.tw-messages) {\n padding: 16px 22px 8px;\n display: flex;\n flex-direction: column;\n gap: 14px;\n overflow-y: auto;\n min-height: 0;\n scroll-behavior: smooth;\n}\n:global(.tw-message) {\n max-width: 85%;\n padding: 14px 16px;\n border-radius: 8px;\n line-height: 1.45;\n font-size: 14px;\n animation: fadeUp 0.25s ease;\n}\n:global(.tw-message) p {\n margin: 0;\n}\n:global(.tw-message) p + p {\n margin-top: 8px;\n}\n:global(.tw-message) a {\n color: var(--accent);\n text-decoration: none;\n font-weight: 600;\n}\n:global(.tw-message) a:hover {\n text-decoration: underline;\n}\n:global(.tw-message) strong {\n font-weight: 700;\n}\n:global(.tw-message) ul,\n:global(.tw-message) ol {\n margin: 8px 0 0;\n padding-left: 18px;\n}\n:global(.tw-message) li {\n margin: 4px 0;\n}\n:global(.tw-typing) {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-height: 16px;\n color: var(--muted);\n}\n:global(.tw-typing-dot) {\n width: 6px;\n height: 6px;\n border-radius: 999px;\n background: currentColor;\n opacity: 0.6;\n animation: typingBounce 1.2s infinite ease-in-out;\n}\n:global(.tw-typing-dot):nth-child(2) {\n animation-delay: 0.2s;\n}\n:global(.tw-typing-dot):nth-child(3) {\n animation-delay: 0.4s;\n}\n:global(.tw-sr-only) {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n:global(.tw-user) {\n align-self: flex-end;\n background: var(--bubble-user);\n color: var(--bubble-user-text);\n border-bottom-right-radius: 8px;\n}\n:global(.tw-assistant) {\n align-self: flex-start;\n background: var(--bubble-assistant);\n border: 1px solid var(--bubble-assistant-border);\n border-bottom-left-radius: 8px;\n}\n:global(.tw-system) {\n align-self: center;\n max-width: 92%;\n text-align: center;\n background: var(--bubble-system);\n color: var(--bubble-system-text);\n}\n:global(.tw-composer) {\n padding: 16px 22px 22px;\n display: grid;\n gap: 10px;\n background: var(--composer-bg);\n}\n:global(.tw-input-wrap) {\n display: flex;\n gap: 10px;\n align-items: flex-end;\n}\n:global(.tw-textarea) {\n flex: 1;\n min-height: 54px;\n max-height: 140px;\n padding: 14px 16px;\n border-radius: 8px;\n border: 1px solid var(--border);\n background: var(--surface);\n color: var(--ink);\n resize: none;\n font-family: inherit;\n font-size: 14px;\n outline: none;\n transition: border 0.2s ease, box-shadow 0.2s ease;\n}\n:global(.tw-textarea)::placeholder {\n color: var(--muted);\n opacity: 0.85;\n}\n:global(.tw-textarea):focus {\n border-color: var(--input-focus-border);\n box-shadow: 0 0 0 3px var(--input-focus-shadow);\n}\n:global(.tw-send) {\n border: none;\n border-radius: 8px;\n padding: 12px 18px;\n background: var(--button-bg);\n color: var(--button-text);\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n:global(.tw-send):hover {\n transform: translateY(-1px);\n box-shadow: 0 12px 24px var(--button-shadow);\n}\n:global(.tw-send):disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n box-shadow: none;\n}\n:global(.tw-meta) {\n display: flex;\n justify-content: space-between;\n color: var(--muted);\n font-size: 12px;\n}\n:global(.tw-pill-row) {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n:global(.tw-pill) {\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px dashed var(--pill-border);\n color: var(--muted);\n font-size: 11px;\n}\n@keyframes fadeUp {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@keyframes typingBounce {\n 0%, 80%, 100% {\n transform: translateY(0);\n opacity: 0.4;\n }\n 40% {\n transform: translateY(-4px);\n opacity: 0.9;\n }\n}\n@media (max-width: 720px) {\n :global(.tw-widget) {\n width: 100%;\n height: 100vh;\n border-radius: 12px;\n }\n}\n');
30
+
31
+ // src/i18n/translations.ts
32
+ var en = {
33
+ titleDefault: "Tunio Assistant",
34
+ subtitleDefault: "Live on-air assistant",
35
+ systemMessage: "Tell me what station or playlist you want and I will set it up.",
36
+ statusStreaming: "Online",
37
+ statusNeedsAttention: "Online",
38
+ statusReady: "Online",
39
+ thinking: "Thinking...",
40
+ pillSynthwave: '"Create a synthwave radio"',
41
+ pillSchedule: '"Schedule a rock playlist at 6 PM"',
42
+ pillPlayNow: '"Set playlist to play now"',
43
+ inputPlaceholder: "Tell me what to do on air...",
44
+ sending: "Working...",
45
+ send: "Send",
46
+ sessionReady: "Session ready",
47
+ sessionRequesting: "Requesting session...",
48
+ missingApiUrl: "Missing API URL",
49
+ errorPrefix: "Error:",
50
+ errorMissingTokenUrl: "Missing session token URL",
51
+ errorFailedTokenFetch: "Failed to fetch session token",
52
+ errorMissingToken: "Session token missing in response",
53
+ errorTokenLoad: "Failed to load token",
54
+ errorTokenUnavailable: "Session token is not available",
55
+ errorServiceUnavailable: "Agent service unavailable",
56
+ errorUnknown: "Unknown error",
57
+ errorUnexpected: "Unexpected error"
58
+ };
59
+ var ru = {
60
+ titleDefault: "Tunio \u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442",
61
+ subtitleDefault: "\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442 \u043F\u0440\u044F\u043C\u043E\u0433\u043E \u044D\u0444\u0438\u0440\u0430",
62
+ systemMessage: "\u0421\u043A\u0430\u0436\u0438\u0442\u0435, \u043A\u0430\u043A\u0443\u044E \u0441\u0442\u0430\u043D\u0446\u0438\u044E \u0438\u043B\u0438 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u043D\u0443\u0436\u043D\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C, \u0438 \u044F \u0432\u0441\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u044E.",
63
+ statusStreaming: "\u041E\u043D\u043B\u0430\u0439\u043D",
64
+ statusNeedsAttention: "\u041E\u043D\u043B\u0430\u0439\u043D",
65
+ statusReady: "\u041E\u043D\u043B\u0430\u0439\u043D",
66
+ thinking: "\u0414\u0443\u043C\u0430\u044E...",
67
+ pillSynthwave: '"\u0421\u043E\u0437\u0434\u0430\u0439 \u0441\u0438\u043D\u0442\u0432\u0435\u0439\u0432-\u0440\u0430\u0434\u0438\u043E"',
68
+ pillSchedule: '"\u0417\u0430\u043F\u043B\u0430\u043D\u0438\u0440\u0443\u0439 \u0440\u043E\u043A-\u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u043D\u0430 18:00"',
69
+ pillPlayNow: '"\u0412\u043A\u043B\u044E\u0447\u0438 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u0441\u0435\u0439\u0447\u0430\u0441"',
70
+ inputPlaceholder: "\u0421\u043A\u0430\u0436\u0438, \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u0432 \u044D\u0444\u0438\u0440\u0435...",
71
+ sending: "\u0412\u044B\u043F\u043E\u043B\u043D\u044F\u044E...",
72
+ send: "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C",
73
+ sessionReady: "\u0421\u0435\u0441\u0441\u0438\u044F \u0433\u043E\u0442\u043E\u0432\u0430",
74
+ sessionRequesting: "\u0417\u0430\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C \u0441\u0435\u0441\u0441\u0438\u044E...",
75
+ missingApiUrl: "\u041D\u0435 \u0437\u0430\u0434\u0430\u043D API URL",
76
+ errorPrefix: "\u041E\u0448\u0438\u0431\u043A\u0430:",
77
+ errorMissingTokenUrl: "\u041D\u0435 \u0437\u0430\u0434\u0430\u043D URL \u0442\u043E\u043A\u0435\u043D\u0430 \u0441\u0435\u0441\u0441\u0438\u0438",
78
+ errorFailedTokenFetch: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438",
79
+ errorMissingToken: "\u0422\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435",
80
+ errorTokenLoad: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D",
81
+ errorTokenUnavailable: "\u0422\u043E\u043A\u0435\u043D \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
82
+ errorServiceUnavailable: "\u0421\u0435\u0440\u0432\u0438\u0441 \u0430\u0433\u0435\u043D\u0442\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
83
+ errorUnknown: "\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430",
84
+ errorUnexpected: "\u041D\u0435\u043F\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043D\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"
85
+ };
86
+ var translations = {
87
+ en,
88
+ ru
89
+ };
90
+ var getTranslations = (lang) => {
91
+ var _a;
92
+ if (!lang) return en;
93
+ const normalized = lang.toLowerCase();
94
+ if (translations[normalized]) return translations[normalized];
95
+ const base = normalized.split("-")[0];
96
+ return (_a = translations[base]) != null ? _a : en;
97
+ };
98
+
99
+ // src/components/TunioWidget.tsx
100
+ import { jsx, jsxs } from "react/jsx-runtime";
101
+ var createId = () => Math.random().toString(36).slice(2);
102
+ function TunioWidget({
103
+ apiUrl,
104
+ sessionTokenUrl,
105
+ lang,
106
+ getSessionToken,
107
+ accessToken,
108
+ title,
109
+ subtitle,
110
+ theme = "light",
111
+ className,
112
+ onNavigate
113
+ }) {
114
+ var _a, _b;
115
+ const [input, setInput] = useState("");
116
+ const [isStreaming, setIsStreaming] = useState(false);
117
+ const [error, setError] = useState(null);
118
+ const [sessionToken, setSessionToken] = useState(null);
119
+ const inputRef = useRef(null);
120
+ const messagesEndRef = useRef(null);
121
+ const resolvedApiUrl = (_a = apiUrl != null ? apiUrl : process.env.NEXT_PUBLIC_AGENT_API_URL) != null ? _a : "";
122
+ const resolvedTokenUrl = (_b = sessionTokenUrl != null ? sessionTokenUrl : process.env.NEXT_PUBLIC_SESSION_TOKEN_URL) != null ? _b : "";
123
+ const resolvedUserLang = useMemo(() => {
124
+ if (lang) return lang;
125
+ if (typeof navigator !== "undefined" && navigator.language) {
126
+ return navigator.language.split("-")[0];
127
+ }
128
+ return void 0;
129
+ }, [lang]);
130
+ const i18n = useMemo(() => getTranslations(resolvedUserLang), [resolvedUserLang]);
131
+ const [messages, setMessages] = useState(() => [
132
+ {
133
+ id: "welcome",
134
+ role: "system",
135
+ content: i18n.systemMessage
136
+ }
137
+ ]);
138
+ const hasUserMessage = useMemo(
139
+ () => messages.some((message) => message.role === "user"),
140
+ [messages]
141
+ );
142
+ const displayTitle = title != null ? title : i18n.titleDefault;
143
+ const displaySubtitle = subtitle != null ? subtitle : i18n.subtitleDefault;
144
+ const scrollToBottom = () => {
145
+ var _a2;
146
+ (_a2 = messagesEndRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
147
+ };
148
+ useEffect(() => {
149
+ scrollToBottom();
150
+ }, [messages]);
151
+ useEffect(() => {
152
+ setMessages((prev) => {
153
+ if (prev.length === 1 && prev[0].id === "welcome" && prev[0].role === "system") {
154
+ return [{ ...prev[0], content: i18n.systemMessage }];
155
+ }
156
+ return prev;
157
+ });
158
+ }, [i18n.systemMessage]);
159
+ const fetchSessionToken = useCallback(async () => {
160
+ if (getSessionToken) {
161
+ return getSessionToken();
162
+ }
163
+ if (accessToken) {
164
+ return accessToken;
165
+ }
166
+ if (!resolvedTokenUrl) {
167
+ throw new Error(i18n.errorMissingTokenUrl);
168
+ }
169
+ const response = await fetch(resolvedTokenUrl, {
170
+ credentials: accessToken ? "omit" : "include",
171
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(i18n.errorFailedTokenFetch);
175
+ }
176
+ const data = await response.json();
177
+ const token = data.session_token;
178
+ if (!token) {
179
+ throw new Error(i18n.errorMissingToken);
180
+ }
181
+ return token;
182
+ }, [getSessionToken, i18n, resolvedTokenUrl]);
183
+ useEffect(() => {
184
+ fetchSessionToken().then(setSessionToken).catch((err) => {
185
+ setError(err instanceof Error ? err.message : i18n.errorTokenLoad);
186
+ });
187
+ }, [fetchSessionToken, i18n.errorTokenLoad]);
188
+ const statusLabel = useMemo(() => {
189
+ if (isStreaming) return i18n.statusStreaming;
190
+ if (error) return i18n.statusNeedsAttention;
191
+ return i18n.statusReady;
192
+ }, [error, i18n, isStreaming]);
193
+ const handleLinkClick = useCallback(
194
+ (href, event) => {
195
+ if (!href) return;
196
+ const isInternal = href.startsWith("/") && !href.startsWith("//");
197
+ if (isInternal && onNavigate) {
198
+ event.preventDefault();
199
+ onNavigate(href);
200
+ }
201
+ },
202
+ [onNavigate]
203
+ );
204
+ const normalizeHref = useCallback((href) => {
205
+ if (!href) return href;
206
+ if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) {
207
+ return href;
208
+ }
209
+ if (href.startsWith("mailto:") || href.startsWith("tel:")) {
210
+ return href;
211
+ }
212
+ if (typeof window === "undefined") {
213
+ return href;
214
+ }
215
+ try {
216
+ const url = new URL(href);
217
+ if (url.host === window.location.host) {
218
+ return `${url.pathname}${url.search}${url.hash}`;
219
+ }
220
+ } catch (e) {
221
+ return href;
222
+ }
223
+ return href;
224
+ }, []);
225
+ const focusInput = () => {
226
+ requestAnimationFrame(() => {
227
+ var _a2;
228
+ (_a2 = inputRef.current) == null ? void 0 : _a2.focus();
229
+ });
230
+ };
231
+ const sendMessage = async () => {
232
+ var _a2, _b2;
233
+ if (isStreaming) {
234
+ focusInput();
235
+ return;
236
+ }
237
+ if (!input.trim() || !resolvedApiUrl) {
238
+ focusInput();
239
+ return;
240
+ }
241
+ if (!sessionToken) {
242
+ setError(i18n.errorTokenUnavailable);
243
+ focusInput();
244
+ return;
245
+ }
246
+ setError(null);
247
+ setIsStreaming(true);
248
+ const userMessage = {
249
+ id: createId(),
250
+ role: "user",
251
+ content: input.trim()
252
+ };
253
+ const assistantMessage = {
254
+ id: createId(),
255
+ role: "assistant",
256
+ content: ""
257
+ };
258
+ setMessages((prev) => [...prev, userMessage, assistantMessage]);
259
+ setInput("");
260
+ focusInput();
261
+ try {
262
+ const response = await fetch(resolvedApiUrl, {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json" },
265
+ body: JSON.stringify({
266
+ message: userMessage.content,
267
+ session_token: sessionToken,
268
+ user_lang: resolvedUserLang
269
+ })
270
+ });
271
+ if (!response.ok || !response.body) {
272
+ throw new Error(i18n.errorServiceUnavailable);
273
+ }
274
+ const reader = response.body.getReader();
275
+ const decoder = new TextDecoder();
276
+ let buffer = "";
277
+ while (true) {
278
+ const { value, done } = await reader.read();
279
+ if (done) break;
280
+ buffer += decoder.decode(value, { stream: true });
281
+ const parts = buffer.split("\n\n");
282
+ buffer = (_a2 = parts.pop()) != null ? _a2 : "";
283
+ for (const part of parts) {
284
+ const lines = part.split("\n");
285
+ let eventName = "message";
286
+ let data = "";
287
+ for (const line of lines) {
288
+ if (line.startsWith("event:")) {
289
+ eventName = line.replace("event:", "").trim();
290
+ }
291
+ if (line.startsWith("data:")) {
292
+ data += line.replace("data:", "").trim();
293
+ }
294
+ }
295
+ if (!data) continue;
296
+ if (eventName === "message") {
297
+ const payload = JSON.parse(data);
298
+ if (payload.content !== void 0) {
299
+ setMessages((prev) => {
300
+ var _a3;
301
+ const next = [...prev];
302
+ const last = next[next.length - 1];
303
+ if (last && last.role === "assistant") {
304
+ last.content = (_a3 = payload.content) != null ? _a3 : "";
305
+ }
306
+ return next;
307
+ });
308
+ }
309
+ }
310
+ if (eventName === "error") {
311
+ const payload = JSON.parse(data);
312
+ setError((_b2 = payload.message) != null ? _b2 : i18n.errorUnknown);
313
+ }
314
+ }
315
+ }
316
+ } catch (err) {
317
+ setError(err instanceof Error ? err.message : i18n.errorUnexpected);
318
+ } finally {
319
+ setIsStreaming(false);
320
+ }
321
+ };
322
+ return /* @__PURE__ */ jsxs(
323
+ "section",
324
+ {
325
+ className: `tw-widget tw-${theme}${className ? ` ${className}` : ""}`,
326
+ "data-theme": theme,
327
+ "data-tunio-widget": "root",
328
+ children: [
329
+ /* @__PURE__ */ jsxs("header", { className: "tw-header", children: [
330
+ /* @__PURE__ */ jsxs("div", { className: "tw-brand", children: [
331
+ /* @__PURE__ */ jsx("div", { className: "tw-brand-title", children: displayTitle }),
332
+ /* @__PURE__ */ jsx("div", { className: "tw-brand-subtitle", children: displaySubtitle })
333
+ ] }),
334
+ /* @__PURE__ */ jsxs("div", { className: "tw-status", children: [
335
+ /* @__PURE__ */ jsx("span", { className: "tw-status-dot" }),
336
+ statusLabel
337
+ ] })
338
+ ] }),
339
+ /* @__PURE__ */ jsxs("div", { className: "tw-messages", children: [
340
+ messages.map((message) => /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ className: `tw-message tw-${message.role}`,
344
+ children: message.content ? /* @__PURE__ */ jsx(
345
+ ReactMarkdown,
346
+ {
347
+ remarkPlugins: [remarkGfm],
348
+ components: {
349
+ a: ({ href, children, ...props }) => {
350
+ const normalizedHref = normalizeHref(href);
351
+ const isInternal = typeof normalizedHref === "string" && normalizedHref.startsWith("/") && !normalizedHref.startsWith("//");
352
+ return /* @__PURE__ */ jsx(
353
+ "a",
354
+ {
355
+ ...props,
356
+ href: normalizedHref,
357
+ onClick: (event) => handleLinkClick(normalizedHref, event),
358
+ target: isInternal ? void 0 : "_blank",
359
+ rel: isInternal ? void 0 : "noreferrer",
360
+ children
361
+ }
362
+ );
363
+ }
364
+ },
365
+ children: message.content
366
+ }
367
+ ) : message.role === "assistant" ? /* @__PURE__ */ jsxs("span", { className: "tw-typing", role: "status", "aria-label": i18n.thinking, children: [
368
+ /* @__PURE__ */ jsx("span", { className: "tw-typing-dot" }),
369
+ /* @__PURE__ */ jsx("span", { className: "tw-typing-dot" }),
370
+ /* @__PURE__ */ jsx("span", { className: "tw-typing-dot" }),
371
+ /* @__PURE__ */ jsx("span", { className: "tw-sr-only", children: i18n.thinking })
372
+ ] }) : ""
373
+ },
374
+ message.id
375
+ )),
376
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
377
+ ] }),
378
+ /* @__PURE__ */ jsxs("div", { className: "tw-composer", children: [
379
+ !hasUserMessage ? /* @__PURE__ */ jsxs("div", { className: "tw-pill-row", children: [
380
+ /* @__PURE__ */ jsx("span", { className: "tw-pill", children: i18n.pillSynthwave }),
381
+ /* @__PURE__ */ jsx("span", { className: "tw-pill", children: i18n.pillSchedule }),
382
+ /* @__PURE__ */ jsx("span", { className: "tw-pill", children: i18n.pillPlayNow })
383
+ ] }) : null,
384
+ /* @__PURE__ */ jsxs("div", { className: "tw-input-wrap", children: [
385
+ /* @__PURE__ */ jsx(
386
+ "textarea",
387
+ {
388
+ className: "tw-textarea",
389
+ placeholder: i18n.inputPlaceholder,
390
+ value: input,
391
+ onChange: (event) => setInput(event.target.value),
392
+ onKeyDown: (event) => {
393
+ if (event.nativeEvent.isComposing) return;
394
+ if (event.key !== "Enter") return;
395
+ if (event.shiftKey) return;
396
+ event.preventDefault();
397
+ sendMessage();
398
+ },
399
+ rows: 2,
400
+ ref: inputRef
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsx(
404
+ "button",
405
+ {
406
+ className: "tw-send",
407
+ onClick: sendMessage,
408
+ disabled: isStreaming || !input.trim(),
409
+ children: isStreaming ? i18n.sending : i18n.send
410
+ }
411
+ )
412
+ ] }),
413
+ error ? /* @__PURE__ */ jsxs("div", { className: "tw-meta", children: [
414
+ i18n.errorPrefix,
415
+ " ",
416
+ error
417
+ ] }) : null
418
+ ] })
419
+ ]
420
+ }
421
+ );
422
+ }
423
+ export {
424
+ TunioWidget
425
+ };
426
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/TunioWidget.tsx","#style-inject:#style-inject","../src/components/TunioWidget.module.css","../src/i18n/translations.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport './TunioWidget.module.css';\nimport { getTranslations } from '../i18n/translations';\n\ntype ChatMessage = {\n id: string;\n role: 'user' | 'assistant' | 'system';\n content: string;\n};\n\nexport type TunioWidgetProps = {\n apiUrl?: string;\n sessionTokenUrl?: string;\n lang?: string;\n getSessionToken?: () => Promise<string>;\n accessToken?: string;\n title?: string;\n subtitle?: string;\n theme?: 'light' | 'dark';\n className?: string;\n onNavigate?: (href: string) => void;\n};\n\nconst createId = () => Math.random().toString(36).slice(2);\n\nexport function TunioWidget({\n apiUrl,\n sessionTokenUrl,\n lang,\n getSessionToken,\n accessToken,\n title,\n subtitle,\n theme = 'light',\n className,\n onNavigate,\n}: TunioWidgetProps) {\n const [input, setInput] = useState('');\n const [isStreaming, setIsStreaming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [sessionToken, setSessionToken] = useState<string | null>(null);\n const inputRef = useRef<HTMLTextAreaElement | null>(null);\n const messagesEndRef = useRef<HTMLDivElement | null>(null);\n\n const resolvedApiUrl = apiUrl ?? process.env.NEXT_PUBLIC_AGENT_API_URL ?? '';\n const resolvedTokenUrl =\n sessionTokenUrl ?? process.env.NEXT_PUBLIC_SESSION_TOKEN_URL ?? '';\n const resolvedUserLang = useMemo(() => {\n if (lang) return lang;\n if (typeof navigator !== 'undefined' && navigator.language) {\n return navigator.language.split('-')[0];\n }\n return undefined;\n }, [lang]);\n const i18n = useMemo(() => getTranslations(resolvedUserLang), [resolvedUserLang]);\n const [messages, setMessages] = useState<ChatMessage[]>(() => [\n {\n id: 'welcome',\n role: 'system',\n content: i18n.systemMessage,\n },\n ]);\n const hasUserMessage = useMemo(\n () => messages.some((message) => message.role === 'user'),\n [messages]\n );\n const displayTitle = title ?? i18n.titleDefault;\n const displaySubtitle = subtitle ?? i18n.subtitleDefault;\n\n const scrollToBottom = () => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n };\n\n useEffect(() => {\n scrollToBottom();\n }, [messages]);\n\n useEffect(() => {\n setMessages((prev) => {\n if (prev.length === 1 && prev[0].id === 'welcome' && prev[0].role === 'system') {\n return [{ ...prev[0], content: i18n.systemMessage }];\n }\n return prev;\n });\n }, [i18n.systemMessage]);\n\n const fetchSessionToken = useCallback(async () => {\n if (getSessionToken) {\n return getSessionToken();\n }\n\n if (accessToken) {\n return accessToken;\n }\n\n if (!resolvedTokenUrl) {\n throw new Error(i18n.errorMissingTokenUrl);\n }\n\n const response = await fetch(resolvedTokenUrl, {\n credentials: accessToken ? 'omit' : 'include',\n headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,\n });\n\n if (!response.ok) {\n throw new Error(i18n.errorFailedTokenFetch);\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n const token = data.session_token as string;\n\n if (!token) {\n throw new Error(i18n.errorMissingToken);\n }\n\n return token;\n }, [getSessionToken, i18n, resolvedTokenUrl]);\n\n useEffect(() => {\n fetchSessionToken()\n .then(setSessionToken)\n .catch((err) => {\n setError(err instanceof Error ? err.message : i18n.errorTokenLoad);\n });\n }, [fetchSessionToken, i18n.errorTokenLoad]);\n\n const statusLabel = useMemo(() => {\n if (isStreaming) return i18n.statusStreaming;\n if (error) return i18n.statusNeedsAttention;\n return i18n.statusReady;\n }, [error, i18n, isStreaming]);\n\n const handleLinkClick = useCallback(\n (href: string | undefined, event: React.MouseEvent<HTMLAnchorElement>) => {\n if (!href) return;\n const isInternal = href.startsWith('/') && !href.startsWith('//');\n if (isInternal && onNavigate) {\n event.preventDefault();\n onNavigate(href);\n }\n },\n [onNavigate]\n );\n\n const normalizeHref = useCallback((href: string | undefined) => {\n if (!href) return href;\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return href;\n }\n if (href.startsWith('mailto:') || href.startsWith('tel:')) {\n return href;\n }\n if (typeof window === 'undefined') {\n return href;\n }\n try {\n const url = new URL(href);\n if (url.host === window.location.host) {\n return `${url.pathname}${url.search}${url.hash}`;\n }\n } catch {\n return href;\n }\n return href;\n }, []);\n\n const focusInput = () => {\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n };\n\n const sendMessage = async () => {\n if (isStreaming) {\n focusInput();\n return;\n }\n if (!input.trim() || !resolvedApiUrl) {\n focusInput();\n return;\n }\n if (!sessionToken) {\n setError(i18n.errorTokenUnavailable);\n focusInput();\n return;\n }\n\n setError(null);\n setIsStreaming(true);\n\n const userMessage: ChatMessage = {\n id: createId(),\n role: 'user',\n content: input.trim(),\n };\n\n const assistantMessage: ChatMessage = {\n id: createId(),\n role: 'assistant',\n content: '',\n };\n\n setMessages((prev) => [...prev, userMessage, assistantMessage]);\n setInput('');\n focusInput();\n\n try {\n const response = await fetch(resolvedApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n message: userMessage.content,\n session_token: sessionToken,\n user_lang: resolvedUserLang,\n }),\n });\n\n if (!response.ok || !response.body) {\n throw new Error(i18n.errorServiceUnavailable);\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const parts = buffer.split('\\n\\n');\n buffer = parts.pop() ?? '';\n\n for (const part of parts) {\n const lines = part.split('\\n');\n let eventName = 'message';\n let data = '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n eventName = line.replace('event:', '').trim();\n }\n if (line.startsWith('data:')) {\n data += line.replace('data:', '').trim();\n }\n }\n\n if (!data) continue;\n\n if (eventName === 'message') {\n const payload = JSON.parse(data) as { content?: string };\n if (payload.content !== undefined) {\n setMessages((prev) => {\n const next = [...prev];\n const last = next[next.length - 1];\n if (last && last.role === 'assistant') {\n last.content = payload.content ?? '';\n }\n return next;\n });\n }\n }\n\n if (eventName === 'error') {\n const payload = JSON.parse(data) as { message?: string };\n setError(payload.message ?? i18n.errorUnknown);\n }\n }\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : i18n.errorUnexpected);\n } finally {\n setIsStreaming(false);\n }\n };\n\n return (\n <section\n className={`tw-widget tw-${theme}${className ? ` ${className}` : ''}`}\n data-theme={theme}\n data-tunio-widget=\"root\"\n >\n <header className=\"tw-header\">\n <div className=\"tw-brand\">\n <div className=\"tw-brand-title\">{displayTitle}</div>\n <div className=\"tw-brand-subtitle\">{displaySubtitle}</div>\n </div>\n <div className=\"tw-status\">\n <span className=\"tw-status-dot\" />\n {statusLabel}\n </div>\n </header>\n\n <div className=\"tw-messages\">\n {messages.map((message) => (\n <div\n key={message.id}\n className={`tw-message tw-${message.role}`}\n >\n {message.content ? (\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n components={{\n a: ({ href, children, ...props }) => {\n const normalizedHref = normalizeHref(href);\n const isInternal =\n typeof normalizedHref === 'string' &&\n normalizedHref.startsWith('/') &&\n !normalizedHref.startsWith('//');\n return (\n <a\n {...props}\n href={normalizedHref}\n onClick={(event) => handleLinkClick(normalizedHref, event)}\n target={isInternal ? undefined : '_blank'}\n rel={isInternal ? undefined : 'noreferrer'}\n >\n {children}\n </a>\n );\n },\n }}\n >\n {message.content}\n </ReactMarkdown>\n ) : message.role === 'assistant' ? (\n <span className=\"tw-typing\" role=\"status\" aria-label={i18n.thinking}>\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-typing-dot\" />\n <span className=\"tw-sr-only\">{i18n.thinking}</span>\n </span>\n ) : (\n ''\n )}\n </div>\n ))}\n <div ref={messagesEndRef} />\n </div>\n\n <div className=\"tw-composer\">\n {!hasUserMessage ? (\n <div className=\"tw-pill-row\">\n <span className=\"tw-pill\">{i18n.pillSynthwave}</span>\n <span className=\"tw-pill\">{i18n.pillSchedule}</span>\n <span className=\"tw-pill\">{i18n.pillPlayNow}</span>\n </div>\n ) : null}\n <div className=\"tw-input-wrap\">\n <textarea\n className=\"tw-textarea\"\n placeholder={i18n.inputPlaceholder}\n value={input}\n onChange={(event) => setInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.nativeEvent.isComposing) return;\n if (event.key !== 'Enter') return;\n if (event.shiftKey) return;\n event.preventDefault();\n sendMessage();\n }}\n rows={2}\n ref={inputRef}\n />\n <button\n className=\"tw-send\"\n onClick={sendMessage}\n disabled={isStreaming || !input.trim()}\n >\n {isStreaming ? i18n.sending : i18n.send}\n </button>\n </div>\n {error ? (\n <div className=\"tw-meta\">\n {i18n.errorPrefix} {error}\n </div>\n ) : null}\n </div>\n </section>\n );\n}\n","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\":global(.tw-widget) {\\n --panel: #f7f9fc;\\n --ink: #1b2430;\\n --muted: #6c7a90;\\n --accent: #3f6df6;\\n --accent-soft: rgba(63, 109, 246, 0.14);\\n --teal: #21b3a8;\\n --surface: #ffffff;\\n --border: rgba(16, 24, 40, 0.12);\\n --widget-bg:\\n linear-gradient(\\n 135deg,\\n #f9fbff 0%,\\n #eef2f8 60%,\\n #e8edf6 100%);\\n --header-bg:\\n linear-gradient(\\n \\n 120deg,\\n rgba(255, 255, 255, 0.92),\\n rgba(243, 246, 252, 0.82) );\\n --composer-bg:\\n linear-gradient(\\n \\n 180deg,\\n rgba(255, 255, 255, 0) 0%,\\n rgba(244, 246, 252, 0.92) 80% );\\n --shadow: 0 24px 70px rgba(18, 24, 40, 0.18);\\n --glow:\\n radial-gradient(\\n circle,\\n rgba(63, 109, 246, 0.18) 0%,\\n rgba(63, 109, 246, 0) 70%);\\n --accent-glow: rgba(63, 109, 246, 0.2);\\n --bubble-user: #1b2430;\\n --bubble-user-text: #f7f9ff;\\n --bubble-assistant: #ffffff;\\n --bubble-assistant-border: rgba(16, 24, 40, 0.08);\\n --bubble-system: rgba(27, 36, 48, 0.08);\\n --bubble-system-text: #6c7a90;\\n --input-focus-border: rgba(63, 109, 246, 0.6);\\n --input-focus-shadow: rgba(63, 109, 246, 0.2);\\n --button-bg: #1b2430;\\n --button-text: #f7f9ff;\\n --button-shadow: rgba(27, 36, 48, 0.2);\\n --pill-border: rgba(16, 24, 40, 0.2);\\n display: grid;\\n grid-template-rows: auto minmax(0, 1fr) auto;\\n width: min(100%, 420px);\\n height: min(80vh, 640px);\\n min-height: 520px;\\n border-radius: 12px;\\n background: var(--widget-bg);\\n border: 1px solid var(--border);\\n box-shadow: var(--shadow);\\n color: var(--ink);\\n overflow: hidden;\\n position: relative;\\n}\\n:global(.tw-light) {\\n color-scheme: light;\\n}\\n:global(.tw-dark) {\\n color-scheme: dark;\\n --panel: #121826;\\n --ink: #e6edf7;\\n --muted: #a5b3c8;\\n --accent: #4a7dff;\\n --accent-soft: rgba(74, 125, 255, 0.18);\\n --teal: #2db9c3;\\n --surface: #1a2232;\\n --border: rgba(230, 237, 247, 0.12);\\n --widget-bg:\\n linear-gradient(\\n 135deg,\\n #131a2a 0%,\\n #101525 60%,\\n #0d1220 100%);\\n --header-bg:\\n linear-gradient(\\n \\n 120deg,\\n rgba(18, 24, 38, 0.95),\\n rgba(23, 30, 48, 0.85) );\\n --composer-bg:\\n linear-gradient(\\n \\n 180deg,\\n rgba(18, 24, 38, 0) 0%,\\n rgba(18, 24, 38, 0.9) 80% );\\n --shadow: 0 28px 90px rgba(2, 7, 14, 0.65);\\n --glow:\\n radial-gradient(\\n circle,\\n rgba(74, 125, 255, 0.2) 0%,\\n rgba(74, 125, 255, 0) 70%);\\n --accent-glow: rgba(74, 125, 255, 0.28);\\n --bubble-user: #2f3e63;\\n --bubble-user-text: #f4f7ff;\\n --bubble-assistant: #1a2233;\\n --bubble-assistant-border: rgba(230, 237, 247, 0.1);\\n --bubble-system: rgba(230, 237, 247, 0.08);\\n --bubble-system-text: #b7c3d7;\\n --input-focus-border: rgba(74, 125, 255, 0.6);\\n --input-focus-shadow: rgba(74, 125, 255, 0.25);\\n --button-bg: #4a7dff;\\n --button-text: #f4f7ff;\\n --button-shadow: rgba(74, 125, 255, 0.35);\\n --pill-border: rgba(230, 237, 247, 0.24);\\n}\\n:global(.tw-widget)::before {\\n content: \\\"\\\";\\n position: absolute;\\n inset: -120px 40% auto -60px;\\n height: 220px;\\n background: var(--glow);\\n pointer-events: none;\\n}\\n:global(.tw-header) {\\n padding: 22px 26px 16px;\\n display: flex;\\n align-items: center;\\n justify-content: space-between;\\n background: var(--header-bg);\\n backdrop-filter: blur(12px);\\n}\\n:global(.tw-brand) {\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n:global(.tw-brand-title) {\\n font-family: var(--font-display);\\n font-size: 24px;\\n letter-spacing: -0.02em;\\n}\\n:global(.tw-brand-subtitle) {\\n color: var(--muted);\\n font-size: 13px;\\n}\\n:global(.tw-status) {\\n display: inline-flex;\\n align-items: center;\\n gap: 8px;\\n padding: 6px 12px;\\n border-radius: 8px;\\n background: var(--accent-soft);\\n color: var(--ink);\\n font-size: 12px;\\n font-weight: 600;\\n}\\n:global(.tw-status-dot) {\\n width: 8px;\\n height: 8px;\\n border-radius: 50%;\\n background: var(--accent);\\n box-shadow: 0 0 0 4px var(--accent-glow);\\n}\\n:global(.tw-messages) {\\n padding: 16px 22px 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 14px;\\n overflow-y: auto;\\n min-height: 0;\\n scroll-behavior: smooth;\\n}\\n:global(.tw-message) {\\n max-width: 85%;\\n padding: 14px 16px;\\n border-radius: 8px;\\n line-height: 1.45;\\n font-size: 14px;\\n animation: fadeUp 0.25s ease;\\n}\\n:global(.tw-message) p {\\n margin: 0;\\n}\\n:global(.tw-message) p + p {\\n margin-top: 8px;\\n}\\n:global(.tw-message) a {\\n color: var(--accent);\\n text-decoration: none;\\n font-weight: 600;\\n}\\n:global(.tw-message) a:hover {\\n text-decoration: underline;\\n}\\n:global(.tw-message) strong {\\n font-weight: 700;\\n}\\n:global(.tw-message) ul,\\n:global(.tw-message) ol {\\n margin: 8px 0 0;\\n padding-left: 18px;\\n}\\n:global(.tw-message) li {\\n margin: 4px 0;\\n}\\n:global(.tw-typing) {\\n display: inline-flex;\\n align-items: center;\\n gap: 6px;\\n min-height: 16px;\\n color: var(--muted);\\n}\\n:global(.tw-typing-dot) {\\n width: 6px;\\n height: 6px;\\n border-radius: 999px;\\n background: currentColor;\\n opacity: 0.6;\\n animation: typingBounce 1.2s infinite ease-in-out;\\n}\\n:global(.tw-typing-dot):nth-child(2) {\\n animation-delay: 0.2s;\\n}\\n:global(.tw-typing-dot):nth-child(3) {\\n animation-delay: 0.4s;\\n}\\n:global(.tw-sr-only) {\\n position: absolute;\\n width: 1px;\\n height: 1px;\\n padding: 0;\\n margin: -1px;\\n overflow: hidden;\\n clip: rect(0, 0, 0, 0);\\n border: 0;\\n}\\n:global(.tw-user) {\\n align-self: flex-end;\\n background: var(--bubble-user);\\n color: var(--bubble-user-text);\\n border-bottom-right-radius: 8px;\\n}\\n:global(.tw-assistant) {\\n align-self: flex-start;\\n background: var(--bubble-assistant);\\n border: 1px solid var(--bubble-assistant-border);\\n border-bottom-left-radius: 8px;\\n}\\n:global(.tw-system) {\\n align-self: center;\\n max-width: 92%;\\n text-align: center;\\n background: var(--bubble-system);\\n color: var(--bubble-system-text);\\n}\\n:global(.tw-composer) {\\n padding: 16px 22px 22px;\\n display: grid;\\n gap: 10px;\\n background: var(--composer-bg);\\n}\\n:global(.tw-input-wrap) {\\n display: flex;\\n gap: 10px;\\n align-items: flex-end;\\n}\\n:global(.tw-textarea) {\\n flex: 1;\\n min-height: 54px;\\n max-height: 140px;\\n padding: 14px 16px;\\n border-radius: 8px;\\n border: 1px solid var(--border);\\n background: var(--surface);\\n color: var(--ink);\\n resize: none;\\n font-family: inherit;\\n font-size: 14px;\\n outline: none;\\n transition: border 0.2s ease, box-shadow 0.2s ease;\\n}\\n:global(.tw-textarea)::placeholder {\\n color: var(--muted);\\n opacity: 0.85;\\n}\\n:global(.tw-textarea):focus {\\n border-color: var(--input-focus-border);\\n box-shadow: 0 0 0 3px var(--input-focus-shadow);\\n}\\n:global(.tw-send) {\\n border: none;\\n border-radius: 8px;\\n padding: 12px 18px;\\n background: var(--button-bg);\\n color: var(--button-text);\\n font-weight: 600;\\n cursor: pointer;\\n transition: transform 0.2s ease, box-shadow 0.2s ease;\\n}\\n:global(.tw-send):hover {\\n transform: translateY(-1px);\\n box-shadow: 0 12px 24px var(--button-shadow);\\n}\\n:global(.tw-send):disabled {\\n opacity: 0.5;\\n cursor: not-allowed;\\n transform: none;\\n box-shadow: none;\\n}\\n:global(.tw-meta) {\\n display: flex;\\n justify-content: space-between;\\n color: var(--muted);\\n font-size: 12px;\\n}\\n:global(.tw-pill-row) {\\n display: flex;\\n gap: 8px;\\n flex-wrap: wrap;\\n}\\n:global(.tw-pill) {\\n padding: 6px 10px;\\n border-radius: 8px;\\n border: 1px dashed var(--pill-border);\\n color: var(--muted);\\n font-size: 11px;\\n}\\n@keyframes fadeUp {\\n from {\\n opacity: 0;\\n transform: translateY(8px);\\n }\\n to {\\n opacity: 1;\\n transform: translateY(0);\\n }\\n}\\n@keyframes typingBounce {\\n 0%, 80%, 100% {\\n transform: translateY(0);\\n opacity: 0.4;\\n }\\n 40% {\\n transform: translateY(-4px);\\n opacity: 0.9;\\n }\\n}\\n@media (max-width: 720px) {\\n :global(.tw-widget) {\\n width: 100%;\\n height: 100vh;\\n border-radius: 12px;\\n }\\n}\\n\")","export type Translations = {\n titleDefault: string;\n subtitleDefault: string;\n systemMessage: string;\n statusStreaming: string;\n statusNeedsAttention: string;\n statusReady: string;\n thinking: string;\n pillSynthwave: string;\n pillSchedule: string;\n pillPlayNow: string;\n inputPlaceholder: string;\n sending: string;\n send: string;\n sessionReady: string;\n sessionRequesting: string;\n missingApiUrl: string;\n errorPrefix: string;\n errorMissingTokenUrl: string;\n errorFailedTokenFetch: string;\n errorMissingToken: string;\n errorTokenLoad: string;\n errorTokenUnavailable: string;\n errorServiceUnavailable: string;\n errorUnknown: string;\n errorUnexpected: string;\n};\n\nexport const en: Translations = {\n titleDefault: 'Tunio Assistant',\n subtitleDefault: 'Live on-air assistant',\n systemMessage: 'Tell me what station or playlist you want and I will set it up.',\n statusStreaming: 'Online',\n statusNeedsAttention: 'Online',\n statusReady: 'Online',\n thinking: 'Thinking...',\n pillSynthwave: '\"Create a synthwave radio\"',\n pillSchedule: '\"Schedule a rock playlist at 6 PM\"',\n pillPlayNow: '\"Set playlist to play now\"',\n inputPlaceholder: 'Tell me what to do on air...',\n sending: 'Working...',\n send: 'Send',\n sessionReady: 'Session ready',\n sessionRequesting: 'Requesting session...',\n missingApiUrl: 'Missing API URL',\n errorPrefix: 'Error:',\n errorMissingTokenUrl: 'Missing session token URL',\n errorFailedTokenFetch: 'Failed to fetch session token',\n errorMissingToken: 'Session token missing in response',\n errorTokenLoad: 'Failed to load token',\n errorTokenUnavailable: 'Session token is not available',\n errorServiceUnavailable: 'Agent service unavailable',\n errorUnknown: 'Unknown error',\n errorUnexpected: 'Unexpected error',\n};\n\nexport const ru: Translations = {\n titleDefault: 'Tunio Ассистент',\n subtitleDefault: 'Ассистент прямого эфира',\n systemMessage: 'Скажите, какую станцию или плейлист нужно поставить, и я все настрою.',\n statusStreaming: 'Онлайн',\n statusNeedsAttention: 'Онлайн',\n statusReady: 'Онлайн',\n thinking: 'Думаю...',\n pillSynthwave: '\"Создай синтвейв-радио\"',\n pillSchedule: '\"Запланируй рок-плейлист на 18:00\"',\n pillPlayNow: '\"Включи плейлист сейчас\"',\n inputPlaceholder: 'Скажи, что сделать в эфире...',\n sending: 'Выполняю...',\n send: 'Отправить',\n sessionReady: 'Сессия готова',\n sessionRequesting: 'Запрашиваем сессию...',\n missingApiUrl: 'Не задан API URL',\n errorPrefix: 'Ошибка:',\n errorMissingTokenUrl: 'Не задан URL токена сессии',\n errorFailedTokenFetch: 'Не удалось получить токен сессии',\n errorMissingToken: 'Токен сессии отсутствует в ответе',\n errorTokenLoad: 'Не удалось загрузить токен',\n errorTokenUnavailable: 'Токен сессии недоступен',\n errorServiceUnavailable: 'Сервис агента недоступен',\n errorUnknown: 'Неизвестная ошибка',\n errorUnexpected: 'Непредвиденная ошибка',\n};\n\nconst translations: Record<string, Translations> = {\n en,\n ru,\n};\n\nexport const getTranslations = (lang?: string): Translations => {\n if (!lang) return en;\n const normalized = lang.toLowerCase();\n if (translations[normalized]) return translations[normalized];\n const base = normalized.split('-')[0];\n return translations[base] ?? en;\n};\n"],"mappings":"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","names":["_a","_b"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "tunio-agent-widget",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": [
19
+ "**/*.css"
20
+ ],
21
+ "dependencies": {
22
+ "react-markdown": "^10.1.0",
23
+ "remark-gfm": "^4.0.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20",
27
+ "@types/react": "^19",
28
+ "@types/react-dom": "^19",
29
+ "eslint": "^9",
30
+ "eslint-config-next": "16.1.6",
31
+ "next": "16.1.6",
32
+ "react": "19.2.3",
33
+ "react-dom": "19.2.3",
34
+ "tsup": "^8.5.0",
35
+ "typescript": "^5"
36
+ },
37
+ "peerDependencies": {
38
+ "react": ">=18",
39
+ "react-dom": ">=18"
40
+ },
41
+ "scripts": {
42
+ "dev": "next dev -p 3010",
43
+ "build": "tsup",
44
+ "build:app": "next build",
45
+ "start": "next start",
46
+ "start:3010": "next start -p 3010",
47
+ "lint": "eslint"
48
+ }
49
+ }