@invergent/website-widget 2.7.5 → 2.7.6
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 +50 -18
- package/dist/agent-ia0JiKMG.d.cts +174 -0
- package/dist/agent-ia0JiKMG.d.ts +174 -0
- package/dist/chunk-MAUJDM7P.js +700 -0
- package/dist/chunk-MAUJDM7P.js.map +1 -0
- package/dist/index.cjs +22 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -173
- package/dist/index.d.ts +4 -173
- package/dist/index.js +1 -691
- package/dist/index.js.map +1 -1
- package/dist/surogates-widget.global.js +221 -9
- package/dist/surogates-widget.global.js.map +1 -1
- package/dist/ui.cjs +1254 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +67 -0
- package/dist/ui.d.ts +67 -0
- package/dist/ui.js +556 -0
- package/dist/ui.js.map +1 -0
- package/package.json +9 -1
package/dist/ui.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { WebsiteAgent, SurogatesAuthError, SurogatesRateLimitError } from './chunk-MAUJDM7P.js';
|
|
2
|
+
import { render } from 'preact';
|
|
3
|
+
import { useState, useRef, useEffect } from 'preact/hooks';
|
|
4
|
+
import { jsx, jsxs } from 'preact/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/ui/styles.ts
|
|
7
|
+
var WIDGET_STYLES = `
|
|
8
|
+
:host { all: initial; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
|
|
11
|
+
.surg-root {
|
|
12
|
+
--surg-radius: 16px;
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
14
|
+
font-size: 14px;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: #0f172a;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.surg-launcher {
|
|
20
|
+
position: fixed;
|
|
21
|
+
bottom: 20px;
|
|
22
|
+
width: 56px;
|
|
23
|
+
height: 56px;
|
|
24
|
+
border-radius: 50%;
|
|
25
|
+
border: none;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
background: var(--surg-accent);
|
|
28
|
+
color: var(--surg-accent-fg);
|
|
29
|
+
box-shadow: 0 6px 24px rgba(15, 23, 42, 0.25);
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
34
|
+
z-index: 2147483000;
|
|
35
|
+
}
|
|
36
|
+
.surg-launcher:hover { transform: scale(1.06); box-shadow: 0 8px 28px rgba(15, 23, 42, 0.3); }
|
|
37
|
+
.surg-launcher svg { width: 26px; height: 26px; }
|
|
38
|
+
.surg-pos-bottom-right { right: 20px; }
|
|
39
|
+
.surg-pos-bottom-left { left: 20px; }
|
|
40
|
+
|
|
41
|
+
.surg-panel {
|
|
42
|
+
position: fixed;
|
|
43
|
+
bottom: 88px;
|
|
44
|
+
width: 380px;
|
|
45
|
+
max-width: calc(100vw - 40px);
|
|
46
|
+
height: 560px;
|
|
47
|
+
max-height: calc(100vh - 120px);
|
|
48
|
+
background: #ffffff;
|
|
49
|
+
border-radius: var(--surg-radius);
|
|
50
|
+
box-shadow: 0 12px 48px rgba(15, 23, 42, 0.28);
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
z-index: 2147483000;
|
|
55
|
+
animation: surg-rise 0.18s ease;
|
|
56
|
+
}
|
|
57
|
+
@keyframes surg-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
|
58
|
+
|
|
59
|
+
.surg-inline {
|
|
60
|
+
position: relative;
|
|
61
|
+
bottom: auto; right: auto; left: auto;
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 100%;
|
|
64
|
+
box-shadow: none;
|
|
65
|
+
border: 1px solid #e2e8f0;
|
|
66
|
+
animation: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.surg-header {
|
|
70
|
+
background: var(--surg-accent);
|
|
71
|
+
color: var(--surg-accent-fg);
|
|
72
|
+
padding: 14px 16px;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
}
|
|
78
|
+
.surg-header-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
79
|
+
.surg-header-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
|
|
80
|
+
.surg-title { font-weight: 600; font-size: 15px; }
|
|
81
|
+
.surg-subtitle { font-size: 11px; font-weight: 400; opacity: 0.85; }
|
|
82
|
+
.surg-logo {
|
|
83
|
+
width: 32px; height: 32px; border-radius: 50%; object-fit: cover;
|
|
84
|
+
background: rgba(255, 255, 255, 0.2); flex-shrink: 0;
|
|
85
|
+
}
|
|
86
|
+
.surg-launcher-logo {
|
|
87
|
+
width: 32px; height: 32px; border-radius: 50%; object-fit: cover;
|
|
88
|
+
}
|
|
89
|
+
.surg-close {
|
|
90
|
+
background: transparent;
|
|
91
|
+
border: none;
|
|
92
|
+
color: inherit;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
opacity: 0.85;
|
|
95
|
+
padding: 4px;
|
|
96
|
+
display: flex;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
}
|
|
99
|
+
.surg-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }
|
|
100
|
+
|
|
101
|
+
.surg-messages {
|
|
102
|
+
flex: 1;
|
|
103
|
+
overflow-y: auto;
|
|
104
|
+
padding: 16px;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 10px;
|
|
108
|
+
background: #f8fafc;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.surg-bubble {
|
|
112
|
+
max-width: 82%;
|
|
113
|
+
padding: 9px 13px;
|
|
114
|
+
border-radius: 14px;
|
|
115
|
+
word-wrap: break-word;
|
|
116
|
+
white-space: normal;
|
|
117
|
+
}
|
|
118
|
+
.surg-bubble p { margin: 0 0 8px; }
|
|
119
|
+
.surg-bubble p:last-child { margin-bottom: 0; }
|
|
120
|
+
.surg-bubble ul, .surg-bubble ol { margin: 4px 0; padding-left: 20px; }
|
|
121
|
+
.surg-bubble code {
|
|
122
|
+
background: rgba(15, 23, 42, 0.08);
|
|
123
|
+
padding: 1px 5px;
|
|
124
|
+
border-radius: 5px;
|
|
125
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
126
|
+
font-size: 0.9em;
|
|
127
|
+
}
|
|
128
|
+
.surg-bubble pre {
|
|
129
|
+
background: #0f172a;
|
|
130
|
+
color: #e2e8f0;
|
|
131
|
+
padding: 10px 12px;
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
overflow-x: auto;
|
|
134
|
+
margin: 6px 0;
|
|
135
|
+
}
|
|
136
|
+
.surg-bubble pre code { background: transparent; padding: 0; color: inherit; }
|
|
137
|
+
.surg-bubble a { color: var(--surg-accent); }
|
|
138
|
+
|
|
139
|
+
.surg-user {
|
|
140
|
+
align-self: flex-end;
|
|
141
|
+
background: var(--surg-accent);
|
|
142
|
+
color: var(--surg-accent-fg);
|
|
143
|
+
border-bottom-right-radius: 4px;
|
|
144
|
+
white-space: pre-wrap;
|
|
145
|
+
}
|
|
146
|
+
.surg-user a { color: var(--surg-accent-fg); text-decoration: underline; }
|
|
147
|
+
.surg-assistant {
|
|
148
|
+
align-self: flex-start;
|
|
149
|
+
background: #ffffff;
|
|
150
|
+
border: 1px solid #e2e8f0;
|
|
151
|
+
border-bottom-left-radius: 4px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.surg-typing { display: flex; gap: 4px; padding: 4px 2px; }
|
|
155
|
+
.surg-typing span {
|
|
156
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
157
|
+
background: #94a3b8;
|
|
158
|
+
animation: surg-blink 1.2s infinite ease-in-out;
|
|
159
|
+
}
|
|
160
|
+
.surg-typing span:nth-child(2) { animation-delay: 0.2s; }
|
|
161
|
+
.surg-typing span:nth-child(3) { animation-delay: 0.4s; }
|
|
162
|
+
@keyframes surg-blink { 0%, 60%, 100% { opacity: 0.3; } 30% { opacity: 1; } }
|
|
163
|
+
|
|
164
|
+
.surg-error {
|
|
165
|
+
margin: 0 16px;
|
|
166
|
+
padding: 8px 12px;
|
|
167
|
+
background: #fef2f2;
|
|
168
|
+
border: 1px solid #fecaca;
|
|
169
|
+
color: #b91c1c;
|
|
170
|
+
border-radius: 10px;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
flex-shrink: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.surg-composer {
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 8px;
|
|
178
|
+
padding: 12px;
|
|
179
|
+
border-top: 1px solid #e2e8f0;
|
|
180
|
+
background: #ffffff;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
}
|
|
183
|
+
.surg-input {
|
|
184
|
+
flex: 1;
|
|
185
|
+
resize: none;
|
|
186
|
+
border: 1px solid #cbd5e1;
|
|
187
|
+
border-radius: 12px;
|
|
188
|
+
padding: 9px 12px;
|
|
189
|
+
font: inherit;
|
|
190
|
+
color: inherit;
|
|
191
|
+
max-height: 120px;
|
|
192
|
+
outline: none;
|
|
193
|
+
}
|
|
194
|
+
.surg-input:focus { border-color: var(--surg-accent); }
|
|
195
|
+
.surg-send {
|
|
196
|
+
border: none;
|
|
197
|
+
background: var(--surg-accent);
|
|
198
|
+
color: var(--surg-accent-fg);
|
|
199
|
+
border-radius: 12px;
|
|
200
|
+
width: 40px;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
}
|
|
207
|
+
.surg-send:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
208
|
+
.surg-send svg { width: 18px; height: 18px; }
|
|
209
|
+
|
|
210
|
+
.surg-powered {
|
|
211
|
+
text-align: center;
|
|
212
|
+
font-size: 11px;
|
|
213
|
+
color: #94a3b8;
|
|
214
|
+
padding: 0 0 8px;
|
|
215
|
+
background: #ffffff;
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
// src/ui/markdown.ts
|
|
220
|
+
var ESCAPE_MAP = {
|
|
221
|
+
"&": "&",
|
|
222
|
+
"<": "<",
|
|
223
|
+
">": ">",
|
|
224
|
+
'"': """,
|
|
225
|
+
"'": "'"
|
|
226
|
+
};
|
|
227
|
+
function escapeHtml(s) {
|
|
228
|
+
return s.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);
|
|
229
|
+
}
|
|
230
|
+
function safeHref(raw) {
|
|
231
|
+
const url = raw.trim();
|
|
232
|
+
if (/^(https?:|mailto:)/i.test(url)) return url;
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
function renderInline(escaped) {
|
|
236
|
+
let out = escaped;
|
|
237
|
+
out = out.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
|
|
238
|
+
out = out.replace(
|
|
239
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
240
|
+
(_m, text, href) => {
|
|
241
|
+
const safe = safeHref(href);
|
|
242
|
+
if (!safe) return `${text}`;
|
|
243
|
+
return `<a href="${safe}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, (_m, t) => `<strong>${t}</strong>`);
|
|
247
|
+
out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, pre, t) => `${pre}<em>${t}</em>`);
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
function renderMarkdown(src) {
|
|
251
|
+
const parts = src.split(/```/);
|
|
252
|
+
const html = [];
|
|
253
|
+
parts.forEach((part, index) => {
|
|
254
|
+
const isCodeFence = index % 2 === 1;
|
|
255
|
+
if (isCodeFence) {
|
|
256
|
+
const body = part.replace(/^[^\n]*\n/, (m) => part.includes("\n") ? "" : m);
|
|
257
|
+
html.push(`<pre><code>${escapeHtml(body.replace(/\n$/, ""))}</code></pre>`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const blocks = part.split(/\n{2,}/);
|
|
261
|
+
for (const block of blocks) {
|
|
262
|
+
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
263
|
+
if (lines.length === 0) continue;
|
|
264
|
+
const isUnordered = lines.every((l) => /^\s*[-*]\s+/.test(l));
|
|
265
|
+
const isOrdered = lines.every((l) => /^\s*\d+\.\s+/.test(l));
|
|
266
|
+
if (isUnordered || isOrdered) {
|
|
267
|
+
const items = lines.map((l) => l.replace(/^\s*(?:[-*]|\d+\.)\s+/, "")).map((l) => `<li>${renderInline(escapeHtml(l))}</li>`).join("");
|
|
268
|
+
html.push(isOrdered ? `<ol>${items}</ol>` : `<ul>${items}</ul>`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const paragraph = lines.map((l) => renderInline(escapeHtml(l))).join("<br>");
|
|
272
|
+
html.push(`<p>${paragraph}</p>`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return html.join("");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/ui/next-action.ts
|
|
279
|
+
var NEXT_ACTION_RE = /<next_action([^>]*)>([\s\S]*?)<\/next_action>\s*/gi;
|
|
280
|
+
var PARTIAL_FOOTER_RE = /(?:^|\n)\s*<(?:n(?:e(?:x(?:t(?:_(?:a(?:c(?:t(?:i(?:o(?:n[\s\S]*)?)?)?)?)?)?)?)?)?)?)?$/i;
|
|
281
|
+
function stripNextAction(text) {
|
|
282
|
+
if (!text) return text;
|
|
283
|
+
const withoutBlocks = text.replace(NEXT_ACTION_RE, "").trimEnd();
|
|
284
|
+
const match = PARTIAL_FOOTER_RE.exec(withoutBlocks);
|
|
285
|
+
return match ? withoutBlocks.slice(0, match.index).trimEnd() : withoutBlocks;
|
|
286
|
+
}
|
|
287
|
+
var DEFAULT_SUBTITLE = "Typically replies instantly";
|
|
288
|
+
var DEFAULT_WELCOME = "Hi! \u{1F44B} How can I help you today?";
|
|
289
|
+
function uid() {
|
|
290
|
+
return `u-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
291
|
+
}
|
|
292
|
+
function snapshot(messages) {
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const m of messages) {
|
|
295
|
+
if (m.role !== "user" && m.role !== "assistant") continue;
|
|
296
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
297
|
+
out.push({ id: m.id ?? uid(), role: m.role, content });
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
function friendlyError(err) {
|
|
302
|
+
if (err instanceof SurogatesAuthError) {
|
|
303
|
+
return "This chat isn\u2019t accepting messages from this site yet. The site owner needs to allow this origin.";
|
|
304
|
+
}
|
|
305
|
+
if (err instanceof SurogatesRateLimitError) {
|
|
306
|
+
return "You\u2019re sending messages too quickly. Please wait a moment and try again.";
|
|
307
|
+
}
|
|
308
|
+
if (err && typeof err === "object") {
|
|
309
|
+
const code = err.code;
|
|
310
|
+
if (code === "auth") {
|
|
311
|
+
return "This chat isn\u2019t accepting messages from this site yet. The site owner needs to allow this origin.";
|
|
312
|
+
}
|
|
313
|
+
if (code === "sse_closed") return "Connection lost. Please send your message again.";
|
|
314
|
+
const message = err.message;
|
|
315
|
+
if (message && message.length < 160) return message;
|
|
316
|
+
}
|
|
317
|
+
return "Something went wrong. Please try again.";
|
|
318
|
+
}
|
|
319
|
+
var ICON_CHAT = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) });
|
|
320
|
+
var ICON_CLOSE = /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
321
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
322
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
323
|
+
] });
|
|
324
|
+
var ICON_SEND = /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
325
|
+
/* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
|
|
326
|
+
/* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
|
|
327
|
+
] });
|
|
328
|
+
function Widget({ agent, config, registerOpenControl, onOpenChange }) {
|
|
329
|
+
const inline = !!config.inline;
|
|
330
|
+
const [open, setOpen] = useState(inline || !!config.openByDefault);
|
|
331
|
+
const [messages, setMessages] = useState(() => snapshot(agent.messages));
|
|
332
|
+
const [running, setRunning] = useState(false);
|
|
333
|
+
const [error, setError] = useState(null);
|
|
334
|
+
const [input, setInput] = useState("");
|
|
335
|
+
const [agentName, setAgentName] = useState("");
|
|
336
|
+
const title = config.title || agentName || "Assistant";
|
|
337
|
+
const listRef = useRef(null);
|
|
338
|
+
const bootstrapStarted = useRef(false);
|
|
339
|
+
useEffect(() => registerOpenControl(setOpen), [registerOpenControl]);
|
|
340
|
+
useEffect(() => onOpenChange?.(open), [open, onOpenChange]);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const sub = agent.subscribe({
|
|
343
|
+
onMessagesChanged: () => setMessages(snapshot(agent.messages)),
|
|
344
|
+
onRunStartedEvent: () => {
|
|
345
|
+
setRunning(true);
|
|
346
|
+
setError(null);
|
|
347
|
+
},
|
|
348
|
+
onRunFinishedEvent: () => setRunning(false),
|
|
349
|
+
onRunErrorEvent: ({ event }) => {
|
|
350
|
+
setRunning(false);
|
|
351
|
+
setError(friendlyError(event));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return () => sub.unsubscribe();
|
|
355
|
+
}, [agent]);
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (!open || bootstrapStarted.current) return;
|
|
358
|
+
bootstrapStarted.current = true;
|
|
359
|
+
agent.ensureBootstrapped().then((res) => {
|
|
360
|
+
if (res.agentName) setAgentName(res.agentName);
|
|
361
|
+
}).catch((err) => {
|
|
362
|
+
bootstrapStarted.current = false;
|
|
363
|
+
setError(friendlyError(err));
|
|
364
|
+
});
|
|
365
|
+
}, [open, agent]);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
const el = listRef.current;
|
|
368
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
369
|
+
}, [messages, running]);
|
|
370
|
+
const send = () => {
|
|
371
|
+
const content = input.trim();
|
|
372
|
+
if (!content || running) return;
|
|
373
|
+
setInput("");
|
|
374
|
+
agent.addMessage({ id: uid(), role: "user", content });
|
|
375
|
+
setMessages(snapshot(agent.messages));
|
|
376
|
+
void agent.runAgent().catch(() => {
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
const onKeyDown = (e) => {
|
|
380
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
send();
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const subtitle = config.subtitle ?? DEFAULT_SUBTITLE;
|
|
386
|
+
const welcomeText = config.welcomeMessage ?? DEFAULT_WELCOME;
|
|
387
|
+
const hasAssistant = messages.some((m) => m.role === "assistant");
|
|
388
|
+
const showWelcome = !!welcomeText && !hasAssistant;
|
|
389
|
+
const showTyping = running && messages[messages.length - 1]?.role !== "assistant";
|
|
390
|
+
if (!inline && !open) {
|
|
391
|
+
return /* @__PURE__ */ jsx(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
type: "button",
|
|
395
|
+
class: `surg-launcher surg-pos-${config.position ?? "bottom-right"}`,
|
|
396
|
+
"aria-label": "Open chat",
|
|
397
|
+
onClick: () => setOpen(true),
|
|
398
|
+
children: config.logoUrl ? /* @__PURE__ */ jsx("img", { class: "surg-launcher-logo", src: config.logoUrl, alt: "" }) : ICON_CHAT
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const panelClass = inline ? "surg-panel surg-inline" : `surg-panel surg-pos-${config.position ?? "bottom-right"}`;
|
|
403
|
+
return /* @__PURE__ */ jsxs("div", { class: panelClass, role: "dialog", "aria-label": title, children: [
|
|
404
|
+
/* @__PURE__ */ jsxs("div", { class: "surg-header", children: [
|
|
405
|
+
/* @__PURE__ */ jsxs("div", { class: "surg-header-id", children: [
|
|
406
|
+
config.logoUrl && /* @__PURE__ */ jsx("img", { class: "surg-logo", src: config.logoUrl, alt: "" }),
|
|
407
|
+
/* @__PURE__ */ jsxs("div", { class: "surg-header-text", children: [
|
|
408
|
+
/* @__PURE__ */ jsx("span", { class: "surg-title", children: title }),
|
|
409
|
+
subtitle && /* @__PURE__ */ jsx("span", { class: "surg-subtitle", children: subtitle })
|
|
410
|
+
] })
|
|
411
|
+
] }),
|
|
412
|
+
!inline && /* @__PURE__ */ jsx("button", { type: "button", class: "surg-close", "aria-label": "Close chat", onClick: () => setOpen(false), children: ICON_CLOSE })
|
|
413
|
+
] }),
|
|
414
|
+
/* @__PURE__ */ jsxs("div", { class: "surg-messages", ref: listRef, children: [
|
|
415
|
+
showWelcome && /* @__PURE__ */ jsx(
|
|
416
|
+
"div",
|
|
417
|
+
{
|
|
418
|
+
class: "surg-bubble surg-assistant",
|
|
419
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(welcomeText) }
|
|
420
|
+
}
|
|
421
|
+
),
|
|
422
|
+
messages.map(
|
|
423
|
+
(m) => m.role === "assistant" ? /* @__PURE__ */ jsx(
|
|
424
|
+
"div",
|
|
425
|
+
{
|
|
426
|
+
class: "surg-bubble surg-assistant",
|
|
427
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(stripNextAction(m.content)) }
|
|
428
|
+
},
|
|
429
|
+
m.id
|
|
430
|
+
) : /* @__PURE__ */ jsx("div", { class: "surg-bubble surg-user", children: m.content }, m.id)
|
|
431
|
+
),
|
|
432
|
+
showTyping && /* @__PURE__ */ jsxs("div", { class: "surg-bubble surg-assistant surg-typing", children: [
|
|
433
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
434
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
435
|
+
/* @__PURE__ */ jsx("span", {})
|
|
436
|
+
] })
|
|
437
|
+
] }),
|
|
438
|
+
error && /* @__PURE__ */ jsx("div", { class: "surg-error", children: error }),
|
|
439
|
+
/* @__PURE__ */ jsxs("div", { class: "surg-composer", children: [
|
|
440
|
+
/* @__PURE__ */ jsx(
|
|
441
|
+
"textarea",
|
|
442
|
+
{
|
|
443
|
+
class: "surg-input",
|
|
444
|
+
rows: 1,
|
|
445
|
+
placeholder: "Type a message\u2026",
|
|
446
|
+
value: input,
|
|
447
|
+
disabled: running,
|
|
448
|
+
onInput: (e) => setInput(e.target.value),
|
|
449
|
+
onKeyDown
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
/* @__PURE__ */ jsx("button", { type: "button", class: "surg-send", "aria-label": "Send", disabled: running || !input.trim(), onClick: send, children: ICON_SEND })
|
|
453
|
+
] }),
|
|
454
|
+
/* @__PURE__ */ jsx("div", { class: "surg-powered", children: "Powered by Surogate" })
|
|
455
|
+
] });
|
|
456
|
+
}
|
|
457
|
+
var DEFAULT_ACCENT = "#4f46e5";
|
|
458
|
+
function accentForeground(hex) {
|
|
459
|
+
const v = hex.replace("#", "");
|
|
460
|
+
const full = v.length === 3 ? v.split("").map((c) => c + c).join("") : v;
|
|
461
|
+
if (full.length !== 6 || /[^0-9a-f]/i.test(full)) return "#ffffff";
|
|
462
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
463
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
464
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
465
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
466
|
+
return luminance > 0.62 ? "#0f172a" : "#ffffff";
|
|
467
|
+
}
|
|
468
|
+
function mount(config) {
|
|
469
|
+
if (!config || !config.apiUrl || !config.publishableKey) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
'SurogatesWidget.mount: "apiUrl" and "publishableKey" are required.'
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const accent = config.accentColor ?? DEFAULT_ACCENT;
|
|
475
|
+
let destroyed = false;
|
|
476
|
+
const host = document.createElement("div");
|
|
477
|
+
host.setAttribute("data-surogates-widget", "");
|
|
478
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
479
|
+
const style = document.createElement("style");
|
|
480
|
+
style.textContent = WIDGET_STYLES;
|
|
481
|
+
shadow.appendChild(style);
|
|
482
|
+
const root = document.createElement("div");
|
|
483
|
+
root.className = "surg-root";
|
|
484
|
+
shadow.appendChild(root);
|
|
485
|
+
const applyAccent = (accentColor) => {
|
|
486
|
+
const a = accentColor ?? DEFAULT_ACCENT;
|
|
487
|
+
root.style.setProperty("--surg-accent", a);
|
|
488
|
+
root.style.setProperty("--surg-accent-fg", accentForeground(a));
|
|
489
|
+
};
|
|
490
|
+
applyAccent(accent);
|
|
491
|
+
const attach = () => (config.target ?? document.body)?.appendChild(host);
|
|
492
|
+
if (config.target || document.body) {
|
|
493
|
+
attach();
|
|
494
|
+
} else {
|
|
495
|
+
document.addEventListener(
|
|
496
|
+
"DOMContentLoaded",
|
|
497
|
+
() => {
|
|
498
|
+
if (!destroyed) attach();
|
|
499
|
+
},
|
|
500
|
+
{ once: true }
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const agent = new WebsiteAgent(config);
|
|
504
|
+
let setOpen;
|
|
505
|
+
let isOpen = config.inline || !!config.openByDefault;
|
|
506
|
+
const registerOpenControl = (fn) => {
|
|
507
|
+
setOpen = fn;
|
|
508
|
+
};
|
|
509
|
+
const onOpenChange = (o) => {
|
|
510
|
+
isOpen = o;
|
|
511
|
+
};
|
|
512
|
+
let current = config;
|
|
513
|
+
const renderWidget = () => {
|
|
514
|
+
render(
|
|
515
|
+
/* @__PURE__ */ jsx(
|
|
516
|
+
Widget,
|
|
517
|
+
{
|
|
518
|
+
agent,
|
|
519
|
+
config: current,
|
|
520
|
+
registerOpenControl,
|
|
521
|
+
onOpenChange
|
|
522
|
+
}
|
|
523
|
+
),
|
|
524
|
+
root
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
renderWidget();
|
|
528
|
+
const endOnUnload = () => {
|
|
529
|
+
void agent.end();
|
|
530
|
+
};
|
|
531
|
+
window.addEventListener("beforeunload", endOnUnload);
|
|
532
|
+
return {
|
|
533
|
+
agent,
|
|
534
|
+
open: () => setOpen?.(true),
|
|
535
|
+
close: () => setOpen?.(false),
|
|
536
|
+
toggle: () => setOpen?.(!isOpen),
|
|
537
|
+
update: (partial) => {
|
|
538
|
+
if (destroyed) return;
|
|
539
|
+
current = { ...current, ...partial };
|
|
540
|
+
if ("accentColor" in partial) applyAccent(current.accentColor);
|
|
541
|
+
renderWidget();
|
|
542
|
+
},
|
|
543
|
+
destroy: () => {
|
|
544
|
+
if (destroyed) return;
|
|
545
|
+
destroyed = true;
|
|
546
|
+
window.removeEventListener("beforeunload", endOnUnload);
|
|
547
|
+
void agent.end();
|
|
548
|
+
render(null, root);
|
|
549
|
+
host.remove();
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export { mount };
|
|
555
|
+
//# sourceMappingURL=ui.js.map
|
|
556
|
+
//# sourceMappingURL=ui.js.map
|
package/dist/ui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/styles.ts","../src/ui/markdown.ts","../src/ui/next-action.ts","../src/ui/widget.tsx","../src/ui/mount.tsx"],"names":["jsx"],"mappings":"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","file":"ui.js","sourcesContent":["/**\n * Scoped CSS for the chat widget, injected into the Shadow DOM root so\n * it can neither leak into nor be overridden by the host page. All\n * colours derive from two custom properties (``--surg-accent`` and its\n * readable foreground) set at mount time from the caller's\n * ``accentColor`` so theming is a one-line change.\n *\n * Returned as a string (rather than a constructable stylesheet) because\n * a ``<style>`` element in the shadow root is the most broadly supported\n * path across the browsers a public embed has to serve.\n */\nexport const WIDGET_STYLES = `\n:host { all: initial; }\n* { box-sizing: border-box; }\n\n.surg-root {\n --surg-radius: 16px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n color: #0f172a;\n}\n\n.surg-launcher {\n position: fixed;\n bottom: 20px;\n width: 56px;\n height: 56px;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n background: var(--surg-accent);\n color: var(--surg-accent-fg);\n box-shadow: 0 6px 24px rgba(15, 23, 42, 0.25);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n z-index: 2147483000;\n}\n.surg-launcher:hover { transform: scale(1.06); box-shadow: 0 8px 28px rgba(15, 23, 42, 0.3); }\n.surg-launcher svg { width: 26px; height: 26px; }\n.surg-pos-bottom-right { right: 20px; }\n.surg-pos-bottom-left { left: 20px; }\n\n.surg-panel {\n position: fixed;\n bottom: 88px;\n width: 380px;\n max-width: calc(100vw - 40px);\n height: 560px;\n max-height: calc(100vh - 120px);\n background: #ffffff;\n border-radius: var(--surg-radius);\n box-shadow: 0 12px 48px rgba(15, 23, 42, 0.28);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n z-index: 2147483000;\n animation: surg-rise 0.18s ease;\n}\n@keyframes surg-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }\n\n.surg-inline {\n position: relative;\n bottom: auto; right: auto; left: auto;\n width: 100%;\n height: 100%;\n box-shadow: none;\n border: 1px solid #e2e8f0;\n animation: none;\n}\n\n.surg-header {\n background: var(--surg-accent);\n color: var(--surg-accent-fg);\n padding: 14px 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-shrink: 0;\n}\n.surg-header-id { display: flex; align-items: center; gap: 10px; min-width: 0; }\n.surg-header-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }\n.surg-title { font-weight: 600; font-size: 15px; }\n.surg-subtitle { font-size: 11px; font-weight: 400; opacity: 0.85; }\n.surg-logo {\n width: 32px; height: 32px; border-radius: 50%; object-fit: cover;\n background: rgba(255, 255, 255, 0.2); flex-shrink: 0;\n}\n.surg-launcher-logo {\n width: 32px; height: 32px; border-radius: 50%; object-fit: cover;\n}\n.surg-close {\n background: transparent;\n border: none;\n color: inherit;\n cursor: pointer;\n opacity: 0.85;\n padding: 4px;\n display: flex;\n border-radius: 6px;\n}\n.surg-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }\n\n.surg-messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n background: #f8fafc;\n}\n\n.surg-bubble {\n max-width: 82%;\n padding: 9px 13px;\n border-radius: 14px;\n word-wrap: break-word;\n white-space: normal;\n}\n.surg-bubble p { margin: 0 0 8px; }\n.surg-bubble p:last-child { margin-bottom: 0; }\n.surg-bubble ul, .surg-bubble ol { margin: 4px 0; padding-left: 20px; }\n.surg-bubble code {\n background: rgba(15, 23, 42, 0.08);\n padding: 1px 5px;\n border-radius: 5px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.9em;\n}\n.surg-bubble pre {\n background: #0f172a;\n color: #e2e8f0;\n padding: 10px 12px;\n border-radius: 10px;\n overflow-x: auto;\n margin: 6px 0;\n}\n.surg-bubble pre code { background: transparent; padding: 0; color: inherit; }\n.surg-bubble a { color: var(--surg-accent); }\n\n.surg-user {\n align-self: flex-end;\n background: var(--surg-accent);\n color: var(--surg-accent-fg);\n border-bottom-right-radius: 4px;\n white-space: pre-wrap;\n}\n.surg-user a { color: var(--surg-accent-fg); text-decoration: underline; }\n.surg-assistant {\n align-self: flex-start;\n background: #ffffff;\n border: 1px solid #e2e8f0;\n border-bottom-left-radius: 4px;\n}\n\n.surg-typing { display: flex; gap: 4px; padding: 4px 2px; }\n.surg-typing span {\n width: 7px; height: 7px; border-radius: 50%;\n background: #94a3b8;\n animation: surg-blink 1.2s infinite ease-in-out;\n}\n.surg-typing span:nth-child(2) { animation-delay: 0.2s; }\n.surg-typing span:nth-child(3) { animation-delay: 0.4s; }\n@keyframes surg-blink { 0%, 60%, 100% { opacity: 0.3; } 30% { opacity: 1; } }\n\n.surg-error {\n margin: 0 16px;\n padding: 8px 12px;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #b91c1c;\n border-radius: 10px;\n font-size: 13px;\n flex-shrink: 0;\n}\n\n.surg-composer {\n display: flex;\n gap: 8px;\n padding: 12px;\n border-top: 1px solid #e2e8f0;\n background: #ffffff;\n flex-shrink: 0;\n}\n.surg-input {\n flex: 1;\n resize: none;\n border: 1px solid #cbd5e1;\n border-radius: 12px;\n padding: 9px 12px;\n font: inherit;\n color: inherit;\n max-height: 120px;\n outline: none;\n}\n.surg-input:focus { border-color: var(--surg-accent); }\n.surg-send {\n border: none;\n background: var(--surg-accent);\n color: var(--surg-accent-fg);\n border-radius: 12px;\n width: 40px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.surg-send:disabled { opacity: 0.5; cursor: not-allowed; }\n.surg-send svg { width: 18px; height: 18px; }\n\n.surg-powered {\n text-align: center;\n font-size: 11px;\n color: #94a3b8;\n padding: 0 0 8px;\n background: #ffffff;\n}\n`;\n","/**\n * Minimal, dependency-free Markdown → HTML renderer for assistant text.\n *\n * Assistant output is *semi-trusted* (it comes from the agent, not the\n * visitor), but the widget renders into a Shadow DOM via\n * ``dangerouslySetInnerHTML``, so a careless renderer would still be an\n * XSS vector if the model ever echoed attacker-supplied HTML. The\n * contract here is therefore: **escape first, then re-introduce only the\n * small, known-safe subset of HTML the formatter produces.** No raw\n * HTML from the source string ever survives — every ``<`` in the input\n * is entity-escaped before any tag is emitted.\n *\n * Supported: fenced code blocks, inline code, bold, italic, links\n * (http/https/mailto only), unordered + ordered lists, and paragraphs\n * with line breaks. Everything else degrades to escaped plain text.\n */\n\nconst ESCAPE_MAP: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ESCAPE_MAP[c] ?? c);\n}\n\n/** Allow only links the browser can't be tricked into executing. */\nfunction safeHref(raw: string): string | undefined {\n const url = raw.trim();\n if (/^(https?:|mailto:)/i.test(url)) return url;\n return undefined;\n}\n\n/**\n * Apply inline formatting to an *already HTML-escaped* fragment. Order\n * matters: links before emphasis so ``[a](b)`` brackets aren't eaten by\n * the italic rule, and bold (``**``) before italic (``*``).\n */\nfunction renderInline(escaped: string): string {\n let out = escaped;\n\n // Inline code first — its contents must not be touched by later rules.\n // The buffer is already escaped, so the captured group is literal text.\n out = out.replace(/`([^`]+)`/g, (_m, code: string) => `<code>${code}</code>`);\n\n // Links: [text](href). ``href`` is validated; bad schemes render as\n // the original (escaped) text without an anchor.\n out = out.replace(\n /\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g,\n (_m, text: string, href: string) => {\n const safe = safeHref(href);\n if (!safe) return `${text}`;\n return `<a href=\"${safe}\" target=\"_blank\" rel=\"noopener noreferrer\">${text}</a>`;\n },\n );\n\n out = out.replace(/\\*\\*([^*]+)\\*\\*/g, (_m, t: string) => `<strong>${t}</strong>`);\n out = out.replace(/(^|[^*])\\*([^*\\n]+)\\*/g, (_m, pre: string, t: string) => `${pre}<em>${t}</em>`);\n\n return out;\n}\n\n/**\n * Render ``src`` Markdown to a safe HTML string. Splits on fenced code\n * blocks so their bodies bypass inline formatting entirely, then groups\n * the remaining text into list and paragraph blocks.\n */\nexport function renderMarkdown(src: string): string {\n const parts = src.split(/```/);\n const html: string[] = [];\n\n parts.forEach((part, index) => {\n const isCodeFence = index % 2 === 1;\n if (isCodeFence) {\n // Drop an optional language hint on the first line.\n const body = part.replace(/^[^\\n]*\\n/, (m) => (part.includes('\\n') ? '' : m));\n html.push(`<pre><code>${escapeHtml(body.replace(/\\n$/, ''))}</code></pre>`);\n return;\n }\n\n const blocks = part.split(/\\n{2,}/);\n for (const block of blocks) {\n const lines = block.split('\\n').filter((l) => l.trim().length > 0);\n if (lines.length === 0) continue;\n\n const isUnordered = lines.every((l) => /^\\s*[-*]\\s+/.test(l));\n const isOrdered = lines.every((l) => /^\\s*\\d+\\.\\s+/.test(l));\n if (isUnordered || isOrdered) {\n const items = lines\n .map((l) => l.replace(/^\\s*(?:[-*]|\\d+\\.)\\s+/, ''))\n .map((l) => `<li>${renderInline(escapeHtml(l))}</li>`)\n .join('');\n html.push(isOrdered ? `<ol>${items}</ol>` : `<ul>${items}</ul>`);\n continue;\n }\n\n const paragraph = lines.map((l) => renderInline(escapeHtml(l))).join('<br>');\n html.push(`<p>${paragraph}</p>`);\n }\n });\n\n return html.join('');\n}\n","/**\n * Strip the per-turn ``<next_action>`` footer from assistant text.\n *\n * The harness instructs the assistant to end every final response with a\n * ``<next_action complexity=\"...\" summary=\"...\">...</next_action>`` block\n * (see ``surogates/harness/prompts/guidance/next_action.md``). It is\n * harness/UI metadata, not user-facing content — by design the *client*\n * strips it: the full chat console does so in\n * ``agent-chat-react/src/lib/next-action.ts``, and messaging channels\n * strip it server-side. This widget streams raw ``llm.delta`` frames, so\n * it must strip it too or the XML footer flashes in the bubble.\n *\n * We only need the stripping half here (the console additionally parses\n * the block into a status pill). Streaming-safe: a dangling, not-yet-\n * closed footer is hidden the moment its opener appears so partial tags\n * never flicker.\n */\n\n// Complete ``<next_action ...>...</next_action>`` blocks (+ trailing ws).\nconst NEXT_ACTION_RE = /<next_action([^>]*)>([\\s\\S]*?)<\\/next_action>\\s*/gi;\n\n// A trailing, still-streaming footer: any prefix of the literal\n// ``<next_action`` token at the start of a line through end-of-text,\n// then everything after the whole word arrives (attributes + partial\n// body, closing tag not yet seen). Anchored to start-of-line so a stray\n// ``<`` mid-sentence is never mistaken for the footer.\nconst PARTIAL_FOOTER_RE =\n /(?:^|\\n)\\s*<(?:n(?:e(?:x(?:t(?:_(?:a(?:c(?:t(?:i(?:o(?:n[\\s\\S]*)?)?)?)?)?)?)?)?)?)?)?$/i;\n\n/** Return *text* with every (complete or streaming-partial) footer removed. */\nexport function stripNextAction(text: string): string {\n if (!text) return text;\n const withoutBlocks = text.replace(NEXT_ACTION_RE, '').trimEnd();\n const match = PARTIAL_FOOTER_RE.exec(withoutBlocks);\n return match ? withoutBlocks.slice(0, match.index).trimEnd() : withoutBlocks;\n}\n","/**\n * The Preact chat UI rendered inside the widget's Shadow DOM.\n *\n * It is a thin presentation layer over the headless :class:`WebsiteAgent`:\n * all transport, auth, and streaming live in the agent; this component\n * only subscribes to the AG-UI event stream, mirrors ``agent.messages``\n * into local state, and drives ``runAgent`` from the composer.\n */\nimport { useEffect, useRef, useState } from 'preact/hooks';\n\nimport type { WebsiteAgent } from '../agent.js';\nimport { SurogatesAuthError, SurogatesRateLimitError } from '../errors.js';\nimport { renderMarkdown } from './markdown.js';\nimport { stripNextAction } from './next-action.js';\nimport type { MountConfig } from './types.js';\n\n// Shown when the operator hasn't set their own. Kept friendly and\n// provider-neutral so an unconfigured widget still looks finished.\nconst DEFAULT_SUBTITLE = 'Typically replies instantly';\nconst DEFAULT_WELCOME = 'Hi! 👋 How can I help you today?';\n\ninterface ChatMessage {\n id: string;\n role: 'user' | 'assistant';\n content: string;\n}\n\ninterface WidgetProps {\n agent: WebsiteAgent;\n config: MountConfig;\n /** Lets the imperative handle in ``mount`` drive the open/closed state. */\n registerOpenControl: (setOpen: (open: boolean) => void) => void;\n /** Reports open-state changes back so ``mount``'s ``toggle()`` stays in sync. */\n onOpenChange?: (open: boolean) => void;\n}\n\nfunction uid(): string {\n return `u-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\n/** Project ``agent.messages`` down to the renderable user/assistant turns. */\nfunction snapshot(messages: ReadonlyArray<{ id?: string; role?: string; content?: unknown }>): ChatMessage[] {\n const out: ChatMessage[] = [];\n for (const m of messages) {\n if (m.role !== 'user' && m.role !== 'assistant') continue;\n const content = typeof m.content === 'string' ? m.content : '';\n out.push({ id: m.id ?? uid(), role: m.role, content });\n }\n return out;\n}\n\n/** Map an SDK error (thrown or via RUN_ERROR) to visitor-facing copy. */\nfunction friendlyError(err: { code?: string; message?: string } | unknown): string {\n if (err instanceof SurogatesAuthError) {\n return 'This chat isn’t accepting messages from this site yet. The site owner needs to allow this origin.';\n }\n if (err instanceof SurogatesRateLimitError) {\n return 'You’re sending messages too quickly. Please wait a moment and try again.';\n }\n if (err && typeof err === 'object') {\n const code = (err as { code?: string }).code;\n if (code === 'auth') {\n return 'This chat isn’t accepting messages from this site yet. The site owner needs to allow this origin.';\n }\n if (code === 'sse_closed') return 'Connection lost. Please send your message again.';\n const message = (err as { message?: string }).message;\n if (message && message.length < 160) return message;\n }\n return 'Something went wrong. Please try again.';\n}\n\nconst ICON_CHAT = (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\" />\n </svg>\n);\nconst ICON_CLOSE = (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\nconst ICON_SEND = (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\" /><polygon points=\"22 2 15 22 11 13 2 9 22 2\" />\n </svg>\n);\n\nexport function Widget({ agent, config, registerOpenControl, onOpenChange }: WidgetProps) {\n const inline = !!config.inline;\n const [open, setOpen] = useState(inline || !!config.openByDefault);\n const [messages, setMessages] = useState<ChatMessage[]>(() => snapshot(agent.messages));\n const [running, setRunning] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [input, setInput] = useState('');\n // Only the async input is state; the title is derived at render so a\n // configured title (incl. live handle.update()) always wins, falling back\n // to the bootstrapped agent name, then a generic label.\n const [agentName, setAgentName] = useState('');\n const title = config.title || agentName || 'Assistant';\n\n const listRef = useRef<HTMLDivElement>(null);\n const bootstrapStarted = useRef(false);\n\n // Expose the open setter to the mount() imperative handle.\n useEffect(() => registerOpenControl(setOpen), [registerOpenControl]);\n useEffect(() => onOpenChange?.(open), [open, onOpenChange]);\n\n // Mirror the agent's event stream into local UI state.\n useEffect(() => {\n const sub = agent.subscribe({\n onMessagesChanged: () => setMessages(snapshot(agent.messages)),\n onRunStartedEvent: () => {\n setRunning(true);\n setError(null);\n },\n onRunFinishedEvent: () => setRunning(false),\n onRunErrorEvent: ({ event }) => {\n setRunning(false);\n setError(friendlyError(event));\n },\n });\n return () => sub.unsubscribe();\n }, [agent]);\n\n // Validate config + resolve the header title the first time the panel\n // is opened. Eager bootstrap on open (not page-load) means visitors\n // who never open the chat don't create server sessions; opening is a\n // clear intent signal and surfaces misconfiguration immediately.\n useEffect(() => {\n if (!open || bootstrapStarted.current) return;\n bootstrapStarted.current = true;\n agent\n .ensureBootstrapped()\n .then((res) => {\n if (res.agentName) setAgentName(res.agentName);\n })\n .catch((err) => {\n // Allow a retry the next time the panel opens (transient network, etc.).\n bootstrapStarted.current = false;\n setError(friendlyError(err));\n });\n }, [open, agent]);\n\n // Keep the transcript pinned to the latest message.\n useEffect(() => {\n const el = listRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [messages, running]);\n\n const send = () => {\n const content = input.trim();\n if (!content || running) return;\n setInput('');\n agent.addMessage({ id: uid(), role: 'user', content } as never);\n setMessages(snapshot(agent.messages));\n void agent.runAgent().catch(() => {\n /* Failures surface through onRunErrorEvent; swallow the rejection. */\n });\n };\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n };\n\n // An unconfigured widget always shows sensible default copy: a missing\n // subtitle/welcome falls back to the defaults. Note Studio/the embed omit\n // empty fields, so *clearing* the field in Studio is indistinguishable from\n // never setting it — both show the default. There is intentionally no\n // \"show nothing\" state today; if suppression is ever needed, add an explicit\n // sentinel rather than relying on empty string.\n const subtitle = config.subtitle ?? DEFAULT_SUBTITLE;\n const welcomeText = config.welcomeMessage ?? DEFAULT_WELCOME;\n const hasAssistant = messages.some((m) => m.role === 'assistant');\n const showWelcome = !!welcomeText && !hasAssistant;\n const showTyping = running && messages[messages.length - 1]?.role !== 'assistant';\n\n if (!inline && !open) {\n return (\n <button\n type=\"button\"\n class={`surg-launcher surg-pos-${config.position ?? 'bottom-right'}`}\n aria-label=\"Open chat\"\n onClick={() => setOpen(true)}\n >\n {config.logoUrl ? (\n <img class=\"surg-launcher-logo\" src={config.logoUrl} alt=\"\" />\n ) : (\n ICON_CHAT\n )}\n </button>\n );\n }\n\n const panelClass = inline\n ? 'surg-panel surg-inline'\n : `surg-panel surg-pos-${config.position ?? 'bottom-right'}`;\n\n return (\n <div class={panelClass} role=\"dialog\" aria-label={title}>\n <div class=\"surg-header\">\n <div class=\"surg-header-id\">\n {config.logoUrl && <img class=\"surg-logo\" src={config.logoUrl} alt=\"\" />}\n <div class=\"surg-header-text\">\n <span class=\"surg-title\">{title}</span>\n {subtitle && <span class=\"surg-subtitle\">{subtitle}</span>}\n </div>\n </div>\n {!inline && (\n <button type=\"button\" class=\"surg-close\" aria-label=\"Close chat\" onClick={() => setOpen(false)}>\n {ICON_CLOSE}\n </button>\n )}\n </div>\n\n <div class=\"surg-messages\" ref={listRef}>\n {showWelcome && (\n <div\n class=\"surg-bubble surg-assistant\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: renderMarkdown(welcomeText) }}\n />\n )}\n {messages.map((m) =>\n m.role === 'assistant' ? (\n <div\n key={m.id}\n class=\"surg-bubble surg-assistant\"\n dangerouslySetInnerHTML={{ __html: renderMarkdown(stripNextAction(m.content)) }}\n />\n ) : (\n <div key={m.id} class=\"surg-bubble surg-user\">\n {m.content}\n </div>\n ),\n )}\n {showTyping && (\n <div class=\"surg-bubble surg-assistant surg-typing\">\n <span /><span /><span />\n </div>\n )}\n </div>\n\n {error && <div class=\"surg-error\">{error}</div>}\n\n <div class=\"surg-composer\">\n <textarea\n class=\"surg-input\"\n rows={1}\n placeholder=\"Type a message…\"\n value={input}\n disabled={running}\n onInput={(e) => setInput((e.target as HTMLTextAreaElement).value)}\n onKeyDown={onKeyDown}\n />\n <button type=\"button\" class=\"surg-send\" aria-label=\"Send\" disabled={running || !input.trim()} onClick={send}>\n {ICON_SEND}\n </button>\n </div>\n <div class=\"surg-powered\">Powered by Surogate</div>\n </div>\n );\n}\n","/**\n * ``mount()`` — the drop-in entry point.\n *\n * A single call stands up a complete chat widget: it creates a host\n * element, isolates the UI in a Shadow DOM (so neither the host page's\n * CSS nor the widget's styles can bleed across), instantiates the\n * headless :class:`WebsiteAgent`, and renders the Preact UI into the\n * shadow root. Returns an imperative :type:`WidgetHandle` for hosts\n * that want to open/close/destroy the widget programmatically.\n *\n * This is what the ``<script>``-tag embed and the Studio live preview\n * both call. Everything cosmetic is optional; ``apiUrl`` and\n * ``publishableKey`` are the only required fields.\n */\nimport { render } from 'preact';\n\nimport { WebsiteAgent } from '../agent.js';\nimport { WIDGET_STYLES } from './styles.js';\nimport type { MountConfig, WidgetHandle } from './types.js';\nimport { Widget } from './widget.js';\n\nexport type { MountConfig, WidgetHandle, WidgetAppearance } from './types.js';\n\nconst DEFAULT_ACCENT = '#4f46e5';\n\n/**\n * Pick a readable foreground (near-black vs white) for text/icons that\n * sit on the accent colour, using an sRGB luminance approximation. A\n * pale accent gets dark text; a saturated one gets white.\n */\nfunction accentForeground(hex: string): string {\n const v = hex.replace('#', '');\n const full = v.length === 3 ? v.split('').map((c) => c + c).join('') : v;\n if (full.length !== 6 || /[^0-9a-f]/i.test(full)) return '#ffffff';\n const r = parseInt(full.slice(0, 2), 16);\n const g = parseInt(full.slice(2, 4), 16);\n const b = parseInt(full.slice(4, 6), 16);\n const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n return luminance > 0.62 ? '#0f172a' : '#ffffff';\n}\n\nexport function mount(config: MountConfig): WidgetHandle {\n if (!config || !config.apiUrl || !config.publishableKey) {\n throw new Error(\n 'SurogatesWidget.mount: \"apiUrl\" and \"publishableKey\" are required.',\n );\n }\n\n const accent = config.accentColor ?? DEFAULT_ACCENT;\n let destroyed = false;\n\n // Host element + Shadow DOM: total style isolation in both directions.\n const host = document.createElement('div');\n host.setAttribute('data-surogates-widget', '');\n const shadow = host.attachShadow({ mode: 'open' });\n\n const style = document.createElement('style');\n style.textContent = WIDGET_STYLES;\n shadow.appendChild(style);\n\n const root = document.createElement('div');\n root.className = 'surg-root';\n shadow.appendChild(root);\n\n // Accent drives CSS custom properties on the root, so it can be re-applied\n // on update() without re-rendering anything else.\n const applyAccent = (accentColor?: string) => {\n const a = accentColor ?? DEFAULT_ACCENT;\n root.style.setProperty('--surg-accent', a);\n root.style.setProperty('--surg-accent-fg', accentForeground(a));\n };\n applyAccent(accent);\n\n // Attach to the page. Drop-in embeds are often placed in <head>, where\n // document.body doesn't exist yet — defer the append until the DOM is ready.\n const attach = () => (config.target ?? document.body)?.appendChild(host);\n if (config.target || document.body) {\n attach();\n } else {\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n if (!destroyed) attach();\n },\n { once: true },\n );\n }\n\n const agent = new WebsiteAgent(config);\n\n let setOpen: ((open: boolean) => void) | undefined;\n let isOpen = config.inline || !!config.openByDefault;\n\n // Stable callbacks so re-rendering on update() doesn't re-fire the\n // component's open-control / open-change effects.\n const registerOpenControl = (fn: (open: boolean) => void) => {\n setOpen = fn;\n };\n const onOpenChange = (o: boolean) => {\n isOpen = o;\n };\n\n // Current config is mutable so update() can re-render appearance without\n // tearing down the agent (no re-bootstrap, no lost conversation).\n let current: MountConfig = config;\n const renderWidget = () => {\n render(\n <Widget\n agent={agent}\n config={current}\n registerOpenControl={registerOpenControl}\n onOpenChange={onOpenChange}\n />,\n root,\n );\n };\n renderWidget();\n\n // Best-effort session close when the visitor navigates away.\n const endOnUnload = () => {\n void agent.end();\n };\n window.addEventListener('beforeunload', endOnUnload);\n\n return {\n agent,\n open: () => setOpen?.(true),\n close: () => setOpen?.(false),\n toggle: () => setOpen?.(!isOpen),\n update: (partial) => {\n if (destroyed) return;\n current = { ...current, ...partial };\n if ('accentColor' in partial) applyAccent(current.accentColor);\n renderWidget();\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n window.removeEventListener('beforeunload', endOnUnload);\n void agent.end();\n render(null, root);\n host.remove();\n },\n };\n}\n"]}
|