@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.
- package/README.md +82 -0
- package/dist/client/chatClient.d.ts +16 -0
- package/dist/client/chatClient.d.ts.map +1 -0
- package/dist/client/sseParser.d.ts +3 -0
- package/dist/client/sseParser.d.ts.map +1 -0
- package/dist/client/types.d.ts +38 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/components/BubbleLauncher.d.ts +6 -0
- package/dist/components/BubbleLauncher.d.ts.map +1 -0
- package/dist/components/ChatShell.d.ts +24 -0
- package/dist/components/ChatShell.d.ts.map +1 -0
- package/dist/components/ChatWidget.d.ts +26 -0
- package/dist/components/ChatWidget.d.ts.map +1 -0
- package/dist/components/MessageInput.d.ts +11 -0
- package/dist/components/MessageInput.d.ts.map +1 -0
- package/dist/components/MessageList.d.ts +9 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/Sources.d.ts +6 -0
- package/dist/components/Sources.d.ts.map +1 -0
- package/dist/components/theme.d.ts +20 -0
- package/dist/components/theme.d.ts.map +1 -0
- package/dist/components/types.d.ts +15 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/useChatSession.d.ts +17 -0
- package/dist/components/useChatSession.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/qorus-chat.js +721 -0
- package/dist/qorus-chat.js.map +1 -0
- package/dist/widget.js +6550 -0
- package/dist/widget.js.map +1 -0
- package/package.json +57 -0
|
@@ -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
|