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 +120 -0
- package/dist/index.cjs +463 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.mjs +426 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
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"]}
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|