@qoretechnologies/qorus-chat 0.1.0-beta.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.
@@ -0,0 +1,721 @@
1
+ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import styled, { keyframes } from "styled-components";
3
+ import { ReqoreBubble, ReqoreBubbleGroup, ReqoreButton, ReqoreControlGroup, ReqoreMessage, ReqoreP, ReqorePanel, ReqoreSpinner, ReqoreTextarea, ReqoreUIProvider } from "@qoretechnologies/reqore";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/Sources.tsx
6
+ var StyledSourcesWrap = styled.div`
7
+ margin-top: 10px;
8
+ padding-top: 8px;
9
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
10
+ font-size: 11px;
11
+ opacity: 0.8;
12
+ `;
13
+ var StyledSourceItem = styled.div`
14
+ display: flex;
15
+ align-items: flex-start;
16
+ gap: 6px;
17
+ padding: 4px 0;
18
+ &:not(:last-child) { border-bottom: 1px dashed rgba(255, 255, 255, 0.05); }
19
+ `;
20
+ var StyledSourceLabel = styled.span`
21
+ font-weight: 600;
22
+ font-size: 11px;
23
+ `;
24
+ var StyledSourceExcerpt = styled.span`
25
+ font-size: 11px;
26
+ opacity: 0.75;
27
+ font-style: italic;
28
+ `;
29
+ var Sources = memo(({ sources }) => {
30
+ const [open, setOpen] = useState(false);
31
+ return /* @__PURE__ */ jsxs(StyledSourcesWrap, { children: [/* @__PURE__ */ jsxs(ReqoreButton, {
32
+ flat: true,
33
+ minimal: true,
34
+ size: "small",
35
+ icon: open ? "ArrowDownSLine" : "ArrowRightSLine",
36
+ onClick: () => setOpen((v) => !v),
37
+ children: [
38
+ sources.length,
39
+ " ",
40
+ sources.length === 1 ? "source" : "sources"
41
+ ]
42
+ }), open && /* @__PURE__ */ jsx("div", {
43
+ style: { marginTop: 6 },
44
+ children: sources.map((s, i) => /* @__PURE__ */ jsxs(StyledSourceItem, { children: [
45
+ /* @__PURE__ */ jsx(StyledSourceLabel, { children: s.collection }),
46
+ s.excerpt && /* @__PURE__ */ jsx(StyledSourceExcerpt, { children: s.excerpt }),
47
+ typeof s.score === "number" && /* @__PURE__ */ jsx(ReqoreP, {
48
+ style: {
49
+ fontSize: 10,
50
+ opacity: .6
51
+ },
52
+ children: s.score.toFixed(2)
53
+ })
54
+ ] }, `${s.collection}-${s.chunk_id ?? i}`))
55
+ })] });
56
+ });
57
+ Sources.displayName = "Sources";
58
+ //#endregion
59
+ //#region src/components/theme.ts
60
+ var DEFAULT_THEME = "qorus";
61
+ var DEFAULT_ACCENT = "#7b68ee";
62
+ var PRESETS = {
63
+ qorus: {
64
+ main: "#1b1226",
65
+ text: "#ece9f5"
66
+ },
67
+ dark: {
68
+ main: "#17181c",
69
+ text: "#e7e7ea"
70
+ },
71
+ light: {
72
+ main: "#f4f4f7",
73
+ text: "#1d1d20"
74
+ }
75
+ };
76
+ /** Build the Reqore theme for a preset — fed to `ReqoreUIProvider`. */
77
+ function buildWidgetTheme(theme = DEFAULT_THEME) {
78
+ const preset = PRESETS[theme] ?? PRESETS["qorus"];
79
+ return {
80
+ main: preset.main,
81
+ text: { color: preset.text }
82
+ };
83
+ }
84
+ /** Darken (`percent < 0`) or lighten (`percent > 0`) a `#rrggbb` hex. */
85
+ function shade(hex, percent) {
86
+ const n = parseInt(hex.replace("#", ""), 16);
87
+ const clamp = (c) => Math.max(0, Math.min(255, Math.round(c + (percent < 0 ? c : 255 - c) * percent)));
88
+ return `#${[
89
+ clamp(n >> 16 & 255),
90
+ clamp(n >> 8 & 255),
91
+ clamp(n & 255)
92
+ ].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
93
+ }
94
+ /** Append a 2-digit alpha to a `#rrggbb` hex (`alpha` 0..1). */
95
+ function withAlpha(hex, alpha) {
96
+ return `${hex}${Math.max(0, Math.min(255, Math.round(alpha * 255))).toString(16).padStart(2, "0")}`;
97
+ }
98
+ /** Solid accent gradient — send button. */
99
+ function accentGradient(accent) {
100
+ return { gradient: {
101
+ colors: {
102
+ 0: accent,
103
+ 100: shade(accent, -.32)
104
+ },
105
+ direction: "to bottom right"
106
+ } };
107
+ }
108
+ /** Accent gradient with a glow — the floating launcher button. */
109
+ function fabEffect(accent) {
110
+ return {
111
+ gradient: {
112
+ colors: {
113
+ 0: accent,
114
+ 100: shade(accent, -.42)
115
+ },
116
+ direction: "to bottom right"
117
+ },
118
+ glow: {
119
+ color: accent,
120
+ size: 2,
121
+ blur: 10
122
+ }
123
+ };
124
+ }
125
+ /** Translucent accent gradient — the visitor's own message bubble. */
126
+ function accentBubbleEffect(accent) {
127
+ return { gradient: {
128
+ colors: {
129
+ 0: withAlpha(accent, .16),
130
+ 100: withAlpha(shade(accent, -.32), .16)
131
+ },
132
+ direction: "to bottom right"
133
+ } };
134
+ }
135
+ //#endregion
136
+ //#region src/components/MessageList.tsx
137
+ var StyledScroll = styled.div`
138
+ flex: 1;
139
+ min-height: 0;
140
+ overflow-y: auto;
141
+ padding: 14px;
142
+ display: flex;
143
+ flex-direction: column;
144
+
145
+ scrollbar-width: thin;
146
+ scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
147
+ &::-webkit-scrollbar {
148
+ width: 6px;
149
+ }
150
+ &::-webkit-scrollbar-thumb {
151
+ background: rgba(255, 255, 255, 0.16);
152
+ border-radius: 3px;
153
+ }
154
+ `;
155
+ var StyledEmpty = styled.div`
156
+ flex: 1;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ padding: 24px;
161
+ `;
162
+ var StyledCursor = styled.span`
163
+ display: inline-block;
164
+ width: 2px;
165
+ height: 1em;
166
+ margin-left: 1px;
167
+ background: currentColor;
168
+ vertical-align: text-bottom;
169
+ animation: qorus-chat-blink 1s steps(1) infinite;
170
+
171
+ @keyframes qorus-chat-blink {
172
+ 50% {
173
+ opacity: 0;
174
+ }
175
+ }
176
+ `;
177
+ function renderText(content, streaming) {
178
+ const lines = content.split("\n");
179
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [lines.map((line, i) => /* @__PURE__ */ jsxs(Fragment, { children: [line, i < lines.length - 1 && /* @__PURE__ */ jsx("br", {})] }, i)), streaming && /* @__PURE__ */ jsx(StyledCursor, {})] });
180
+ }
181
+ function formatTime(ts) {
182
+ return new Date(ts).toLocaleTimeString([], {
183
+ hour: "2-digit",
184
+ minute: "2-digit"
185
+ });
186
+ }
187
+ var MessageList = memo(({ messages, isStreaming, emptyText, accent }) => {
188
+ const bottomRef = useRef(null);
189
+ const userEffect = useMemo(() => accentBubbleEffect(accent), [accent]);
190
+ const spinnerColor = useMemo(() => shade(accent, .35), [accent]);
191
+ useEffect(() => {
192
+ bottomRef.current?.scrollIntoView({
193
+ behavior: "smooth",
194
+ block: "end"
195
+ });
196
+ }, [messages, isStreaming]);
197
+ if (messages.length === 0) return /* @__PURE__ */ jsx(StyledScroll, { children: /* @__PURE__ */ jsx(StyledEmpty, { children: /* @__PURE__ */ jsx(ReqoreMessage, {
198
+ flat: true,
199
+ minimal: true,
200
+ icon: "ChatSmile2Line",
201
+ size: "small",
202
+ children: emptyText ?? "Ask me anything to get started."
203
+ }) }) });
204
+ return /* @__PURE__ */ jsxs(StyledScroll, { children: [/* @__PURE__ */ jsx(ReqoreBubbleGroup, { children: messages.map((m, i) => {
205
+ const isUser = m.role === "user";
206
+ const isStreamingMsg = m.status === "streaming";
207
+ const isWaiting = isStreamingMsg && !m.content;
208
+ const isError = m.status === "error";
209
+ const isEmptyReply = !isUser && m.status === "complete" && !m.content;
210
+ const isLastOfRun = i === messages.length - 1 || messages[i + 1].role !== m.role;
211
+ return /* @__PURE__ */ jsxs(ReqoreBubble, {
212
+ align: isUser ? "right" : "left",
213
+ size: "small",
214
+ minimal: !isUser && !isError,
215
+ raised: true,
216
+ intent: isError ? "danger" : void 0,
217
+ effect: isUser ? userEffect : void 0,
218
+ timestamp: !isWaiting && isLastOfRun ? formatTime(m.createdAt) : void 0,
219
+ children: [isError ? m.error ?? "Something went wrong." : isWaiting ? /* @__PURE__ */ jsx(ReqoreSpinner, {
220
+ size: "small",
221
+ iconColor: spinnerColor
222
+ }) : isEmptyReply ? /* @__PURE__ */ jsx(ReqoreP, {
223
+ effect: {
224
+ opacity: .6,
225
+ italic: true
226
+ },
227
+ children: "No response received."
228
+ }) : renderText(m.content, isStreamingMsg), m.sources && m.sources.length > 0 && /* @__PURE__ */ jsx(Sources, { sources: m.sources })]
229
+ }, m.id);
230
+ }) }), /* @__PURE__ */ jsx("div", { ref: bottomRef })] });
231
+ });
232
+ MessageList.displayName = "MessageList";
233
+ //#endregion
234
+ //#region src/components/MessageInput.tsx
235
+ var StyledInputWrap = styled.div`
236
+ padding: 10px 12px 10px;
237
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
238
+ flex-shrink: 0;
239
+ `;
240
+ var StyledPoweredBy = styled.div`
241
+ margin-top: 8px;
242
+ text-align: center;
243
+ font-size: 10px;
244
+ letter-spacing: 0.2px;
245
+ opacity: 0.45;
246
+
247
+ a {
248
+ color: inherit;
249
+ text-decoration: none;
250
+ font-weight: 600;
251
+ }
252
+ a:hover {
253
+ text-decoration: underline;
254
+ }
255
+ `;
256
+ var MessageInput = memo(({ isStreaming, onSend, onCancel, placeholder, disabled, accent }) => {
257
+ const [value, setValue] = useState("");
258
+ const sendEffect = useMemo(() => accentGradient(accent), [accent]);
259
+ const submit = useCallback(() => {
260
+ const trimmed = value.trim();
261
+ if (!trimmed || isStreaming || disabled) return;
262
+ onSend(trimmed);
263
+ setValue("");
264
+ }, [
265
+ value,
266
+ isStreaming,
267
+ disabled,
268
+ onSend
269
+ ]);
270
+ return /* @__PURE__ */ jsxs(StyledInputWrap, { children: [/* @__PURE__ */ jsxs(ReqoreControlGroup, {
271
+ fluid: true,
272
+ verticalAlign: "flex-end",
273
+ children: [/* @__PURE__ */ jsx(ReqoreTextarea, {
274
+ fluid: true,
275
+ scaleWithContent: true,
276
+ rows: 1,
277
+ value,
278
+ onChange: (e) => setValue(e.target.value),
279
+ onKeyDown: useCallback((e) => {
280
+ if (e.key === "Enter" && !e.shiftKey) {
281
+ e.preventDefault();
282
+ submit();
283
+ }
284
+ }, [submit]),
285
+ placeholder: placeholder ?? "Ask anything…",
286
+ disabled
287
+ }), /* @__PURE__ */ jsx(ReqoreButton, {
288
+ fixed: true,
289
+ icon: isStreaming ? "StopCircleLine" : "SendPlane2Fill",
290
+ intent: isStreaming ? "danger" : void 0,
291
+ effect: isStreaming ? void 0 : sendEffect,
292
+ onClick: isStreaming ? onCancel : submit,
293
+ disabled: !isStreaming && (!value.trim() || disabled),
294
+ tooltip: isStreaming ? "Stop" : "Send"
295
+ })]
296
+ }), /* @__PURE__ */ jsxs(StyledPoweredBy, { children: [
297
+ "Powered by",
298
+ " ",
299
+ /* @__PURE__ */ jsx("a", {
300
+ href: "https://qorus.cloud",
301
+ target: "_blank",
302
+ rel: "noopener noreferrer",
303
+ children: "qorus.cloud"
304
+ })
305
+ ] })] });
306
+ });
307
+ MessageInput.displayName = "MessageInput";
308
+ //#endregion
309
+ //#region src/client/sseParser.ts
310
+ async function* parseSseStream(stream) {
311
+ const reader = stream.getReader();
312
+ const decoder = new TextDecoder("utf-8");
313
+ let buffer = "";
314
+ try {
315
+ while (true) {
316
+ const { value, done } = await reader.read();
317
+ if (done) break;
318
+ buffer += decoder.decode(value, { stream: true });
319
+ let nlIdx;
320
+ while ((nlIdx = buffer.indexOf("\n\n")) !== -1) {
321
+ const rawEvent = buffer.slice(0, nlIdx);
322
+ buffer = buffer.slice(nlIdx + 2);
323
+ const line = rawEvent.replace(/^data:\s?/, "").trim();
324
+ if (!line) continue;
325
+ if (line === "[DONE]") {
326
+ yield { type: "done" };
327
+ continue;
328
+ }
329
+ try {
330
+ const payload = JSON.parse(line);
331
+ if (typeof payload.delta === "string") yield {
332
+ type: "delta",
333
+ delta: payload.delta
334
+ };
335
+ if (Array.isArray(payload.sources)) yield {
336
+ type: "sources",
337
+ sources: payload.sources
338
+ };
339
+ if (payload.usage && typeof payload.usage === "object") yield {
340
+ type: "usage",
341
+ usage: payload.usage
342
+ };
343
+ } catch (err) {
344
+ yield {
345
+ type: "error",
346
+ error: `parse-error: ${err.message}`
347
+ };
348
+ }
349
+ }
350
+ }
351
+ } finally {
352
+ reader.releaseLock();
353
+ }
354
+ }
355
+ //#endregion
356
+ //#region src/client/chatClient.ts
357
+ async function readErrorMessage(res) {
358
+ let detail = "";
359
+ try {
360
+ const text = await res.text();
361
+ try {
362
+ const json = JSON.parse(text);
363
+ detail = String(json.desc ?? json.err ?? json.message ?? text);
364
+ } catch {
365
+ detail = text;
366
+ }
367
+ } catch {}
368
+ if (res.status === 409 || /guardrail/i.test(detail)) return "Your message was blocked by a content guardrail.";
369
+ if (res.status === 401 || res.status === 403) return "This chat is not authorized. Please contact the site owner.";
370
+ if (res.status === 429) return "Too many messages right now — please wait a moment and try again.";
371
+ return detail.trim() ? `Something went wrong: ${detail.trim()}` : "Something went wrong.";
372
+ }
373
+ var QorusChatClient = class {
374
+ constructor(opts) {
375
+ this.baseUrl = (opts.baseUrl ?? "").replace(/\/+$/, "");
376
+ this.endpoint = opts.endpoint;
377
+ this.apiKey = opts.apiKey;
378
+ this.visitorId = opts.visitorId;
379
+ }
380
+ async *streamStateless(message) {
381
+ const url = `${this.baseUrl}/api/latest/ai-endpoints/${encodeURIComponent(this.endpoint)}/chat`;
382
+ const res = await this.postSse(url, {
383
+ message,
384
+ stream: true
385
+ });
386
+ if (!res.ok) {
387
+ yield {
388
+ type: "error",
389
+ error: await readErrorMessage(res)
390
+ };
391
+ return;
392
+ }
393
+ if (!res.body) {
394
+ yield {
395
+ type: "error",
396
+ error: "No response received."
397
+ };
398
+ return;
399
+ }
400
+ yield* parseSseStream(res.body);
401
+ }
402
+ async createConversation() {
403
+ const url = `${this.baseUrl}/api/latest/ai-endpoints/${encodeURIComponent(this.endpoint)}/conversations`;
404
+ const res = await fetch(url, {
405
+ method: "POST",
406
+ headers: this.headers("application/json"),
407
+ body: JSON.stringify({})
408
+ });
409
+ if (!res.ok) throw new Error(`createConversation: ${res.status}`);
410
+ return { uuid: (await res.json()).conversation_uuid };
411
+ }
412
+ async *streamMessage(conversationUuid, message, history) {
413
+ const url = `${this.baseUrl}/api/latest/ai-endpoints/${encodeURIComponent(this.endpoint)}/conversations/${encodeURIComponent(conversationUuid)}/messages`;
414
+ const res = await this.postSse(url, {
415
+ message,
416
+ stream: true
417
+ });
418
+ if (!res.ok) {
419
+ yield {
420
+ type: "error",
421
+ error: await readErrorMessage(res)
422
+ };
423
+ return;
424
+ }
425
+ if (!res.body) {
426
+ yield {
427
+ type: "error",
428
+ error: "No response received."
429
+ };
430
+ return;
431
+ }
432
+ yield* parseSseStream(res.body);
433
+ }
434
+ postSse(url, body) {
435
+ return fetch(url, {
436
+ method: "POST",
437
+ headers: this.headers("text/event-stream"),
438
+ body: JSON.stringify(body)
439
+ });
440
+ }
441
+ headers(accept) {
442
+ const h = {
443
+ "Content-Type": "application/json",
444
+ Accept: `${accept}, application/json;q=0.5`,
445
+ Authorization: `Bearer ${this.apiKey}`
446
+ };
447
+ if (this.visitorId) h["qorus-visitor-id"] = this.visitorId;
448
+ return h;
449
+ }
450
+ };
451
+ //#endregion
452
+ //#region src/components/useChatSession.ts
453
+ var VISITOR_ID_KEY_PREFIX = "qorus-chat-visitor-";
454
+ function loadOrCreateVisitorId(endpoint) {
455
+ try {
456
+ const key = `${VISITOR_ID_KEY_PREFIX}${endpoint}`;
457
+ const existing = window.localStorage.getItem(key);
458
+ if (existing) return existing;
459
+ const fresh = `v_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
460
+ window.localStorage.setItem(key, fresh);
461
+ return fresh;
462
+ } catch {
463
+ return `v_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
464
+ }
465
+ }
466
+ function genMessageId() {
467
+ return `m_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
468
+ }
469
+ function useChatSession(opts) {
470
+ const [messages, setMessages] = useState([]);
471
+ const [isStreaming, setIsStreaming] = useState(false);
472
+ const conversationRef = useRef(null);
473
+ const abortRef = useRef(null);
474
+ const visitorId = useMemo(() => opts.visitorId ?? loadOrCreateVisitorId(opts.endpoint), [opts.endpoint, opts.visitorId]);
475
+ const client = useMemo(() => new QorusChatClient({
476
+ endpoint: opts.endpoint,
477
+ apiKey: opts.apiKey,
478
+ baseUrl: opts.baseUrl,
479
+ visitorId
480
+ }), [
481
+ opts.endpoint,
482
+ opts.apiKey,
483
+ opts.baseUrl,
484
+ visitorId
485
+ ]);
486
+ const mode = opts.mode ?? "stateless";
487
+ const updateMessage = useCallback((id, patch) => {
488
+ setMessages((prev) => prev.map((m) => m.id === id ? {
489
+ ...m,
490
+ ...patch
491
+ } : m));
492
+ }, []);
493
+ const send = useCallback(async (text) => {
494
+ const trimmed = text.trim();
495
+ if (!trimmed || isStreaming) return;
496
+ const userMsg = {
497
+ id: genMessageId(),
498
+ role: "user",
499
+ content: trimmed,
500
+ status: "complete",
501
+ createdAt: Date.now()
502
+ };
503
+ const assistantMsg = {
504
+ id: genMessageId(),
505
+ role: "assistant",
506
+ content: "",
507
+ status: "streaming",
508
+ createdAt: Date.now()
509
+ };
510
+ setMessages((prev) => [
511
+ ...prev,
512
+ userMsg,
513
+ assistantMsg
514
+ ]);
515
+ setIsStreaming(true);
516
+ abortRef.current = new AbortController();
517
+ try {
518
+ let convUuid = conversationRef.current;
519
+ if (mode === "conversation" && !convUuid) {
520
+ convUuid = (await client.createConversation()).uuid;
521
+ conversationRef.current = convUuid;
522
+ }
523
+ const stream = mode === "conversation" && convUuid ? client.streamMessage(convUuid, trimmed) : client.streamStateless(trimmed);
524
+ let accumulated = "";
525
+ for await (const event of stream) {
526
+ if (abortRef.current?.signal.aborted) break;
527
+ switch (event.type) {
528
+ case "delta":
529
+ accumulated += event.delta;
530
+ updateMessage(assistantMsg.id, { content: accumulated });
531
+ break;
532
+ case "sources":
533
+ updateMessage(assistantMsg.id, { sources: event.sources });
534
+ break;
535
+ case "usage":
536
+ updateMessage(assistantMsg.id, { usage: event.usage });
537
+ break;
538
+ case "error":
539
+ updateMessage(assistantMsg.id, {
540
+ status: "error",
541
+ error: event.error
542
+ });
543
+ break;
544
+ case "done": break;
545
+ }
546
+ }
547
+ updateMessage(assistantMsg.id, { status: "complete" });
548
+ } catch (err) {
549
+ const message = err instanceof Error ? err.message : "unknown error";
550
+ updateMessage(assistantMsg.id, {
551
+ status: "error",
552
+ error: message
553
+ });
554
+ } finally {
555
+ setIsStreaming(false);
556
+ abortRef.current = null;
557
+ }
558
+ }, [
559
+ client,
560
+ isStreaming,
561
+ mode,
562
+ updateMessage
563
+ ]);
564
+ const cancel = useCallback(() => {
565
+ abortRef.current?.abort();
566
+ setIsStreaming(false);
567
+ }, []);
568
+ const reset = useCallback(async () => {
569
+ cancel();
570
+ conversationRef.current = null;
571
+ setMessages([]);
572
+ }, [cancel]);
573
+ useEffect(() => () => abortRef.current?.abort(), []);
574
+ return {
575
+ messages,
576
+ isStreaming,
577
+ send,
578
+ cancel,
579
+ reset
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/components/ChatShell.tsx
584
+ var ChatShell = memo(({ endpoint, apiKey, baseUrl, visitorId, title = "Assistant", subtitle, placeholder, emptyText, accent, showClose, onClose, extraActions }) => {
585
+ const { messages, isStreaming, send, cancel, reset } = useChatSession({
586
+ endpoint,
587
+ apiKey,
588
+ baseUrl,
589
+ visitorId
590
+ });
591
+ const handleSend = useCallback((text) => {
592
+ send(text);
593
+ }, [send]);
594
+ const actions = [...extraActions ?? [], {
595
+ icon: "RefreshLine",
596
+ tooltip: "New conversation",
597
+ onClick: () => void reset(),
598
+ disabled: messages.length === 0
599
+ }];
600
+ if (showClose) actions.push({
601
+ icon: "CloseLine",
602
+ tooltip: "Close",
603
+ onClick: onClose
604
+ });
605
+ return /* @__PURE__ */ jsxs(ReqorePanel, {
606
+ fill: true,
607
+ rounded: true,
608
+ responsiveTitle: false,
609
+ responsiveActions: false,
610
+ label: title,
611
+ icon: "ChatSmile2Line",
612
+ badge: {
613
+ label: isStreaming ? "Typing…" : "Online",
614
+ intent: "success",
615
+ minimal: true
616
+ },
617
+ description: subtitle,
618
+ actions,
619
+ contentStyle: {
620
+ padding: 0,
621
+ display: "flex",
622
+ flexDirection: "column",
623
+ overflow: "hidden"
624
+ },
625
+ children: [/* @__PURE__ */ jsx(MessageList, {
626
+ messages,
627
+ isStreaming,
628
+ emptyText,
629
+ accent
630
+ }), /* @__PURE__ */ jsx(MessageInput, {
631
+ isStreaming,
632
+ onSend: handleSend,
633
+ onCancel: cancel,
634
+ placeholder,
635
+ accent
636
+ })]
637
+ });
638
+ });
639
+ ChatShell.displayName = "ChatShell";
640
+ //#endregion
641
+ //#region src/components/BubbleLauncher.tsx
642
+ var slideUp = keyframes`
643
+ from { transform: translateY(16px); opacity: 0; }
644
+ to { transform: translateY(0); opacity: 1; }
645
+ `;
646
+ var StyledLauncher = styled.div`
647
+ position: fixed;
648
+ bottom: 20px;
649
+ right: 20px;
650
+ z-index: 2147483600;
651
+ display: flex;
652
+ flex-direction: column;
653
+ align-items: flex-end;
654
+ gap: 14px;
655
+ `;
656
+ var StyledPanel = styled.div`
657
+ width: min(400px, calc(100vw - 32px));
658
+ height: min(600px, calc(100vh - 116px));
659
+ animation: ${slideUp} 0.2s cubic-bezier(0.16, 1, 0.3, 1);
660
+ /* Layered drop-shadows follow the panel's real rounded shape and lift it
661
+ clearly off a dark host page. */
662
+ filter: drop-shadow(0 26px 64px rgba(0, 0, 0, 0.72))
663
+ drop-shadow(0 8px 22px rgba(0, 0, 0, 0.55));
664
+ `;
665
+ var BubbleLauncher = memo(({ defaultOpen, ...shellProps }) => {
666
+ const [open, setOpen] = useState(!!defaultOpen);
667
+ const effect = useMemo(() => fabEffect(shellProps.accent), [shellProps.accent]);
668
+ return /* @__PURE__ */ jsxs(StyledLauncher, { children: [open && /* @__PURE__ */ jsx(StyledPanel, { children: /* @__PURE__ */ jsx(ChatShell, {
669
+ ...shellProps,
670
+ showClose: true,
671
+ onClose: () => setOpen(false)
672
+ }) }), /* @__PURE__ */ jsx(ReqoreButton, {
673
+ circle: true,
674
+ compact: true,
675
+ size: "huge",
676
+ icon: open ? "CloseLine" : "ChatSmile2Fill",
677
+ effect,
678
+ onClick: () => setOpen((v) => !v),
679
+ tooltip: open ? "Close chat" : "Open chat",
680
+ style: {
681
+ width: "58px",
682
+ height: "58px",
683
+ padding: 0
684
+ }
685
+ })] });
686
+ });
687
+ BubbleLauncher.displayName = "BubbleLauncher";
688
+ //#endregion
689
+ //#region src/components/ChatWidget.tsx
690
+ var StyledInlineFrame = styled.div`
691
+ width: 100%;
692
+ height: 100%;
693
+ min-height: 320px;
694
+ display: flex;
695
+ `;
696
+ var ChatWidget = memo(({ endpoint, apiKey, baseUrl, mode = "bubble", visitorId, title, subtitle, placeholder, defaultOpen, theme, accent = DEFAULT_ACCENT, extraActions }) => {
697
+ const reqoreTheme = buildWidgetTheme(theme);
698
+ const sharedShellProps = {
699
+ endpoint,
700
+ apiKey,
701
+ baseUrl,
702
+ visitorId,
703
+ title,
704
+ subtitle,
705
+ placeholder,
706
+ accent,
707
+ extraActions
708
+ };
709
+ return /* @__PURE__ */ jsx(ReqoreUIProvider, {
710
+ theme: reqoreTheme,
711
+ children: mode === "bubble" ? /* @__PURE__ */ jsx(BubbleLauncher, {
712
+ ...sharedShellProps,
713
+ defaultOpen
714
+ }) : /* @__PURE__ */ jsx(StyledInlineFrame, { children: /* @__PURE__ */ jsx(ChatShell, { ...sharedShellProps }) })
715
+ });
716
+ });
717
+ ChatWidget.displayName = "ChatWidget";
718
+ //#endregion
719
+ export { ChatShell, ChatWidget };
720
+
721
+ //# sourceMappingURL=qorus-chat.js.map