@usepanacea/react 0.1.0 → 0.1.3
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/dist/index.cjs +242 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +242 -82
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useMemo, useState, useRef,
|
|
1
|
+
import { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext } from 'react';
|
|
2
2
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
3
|
import { cva } from 'class-variance-authority';
|
|
4
4
|
import { clsx } from 'clsx';
|
|
@@ -80,6 +80,42 @@ function PanaceaProvider({
|
|
|
80
80
|
const [liveEscalationId, setLiveEscalationId] = useState(null);
|
|
81
81
|
const [liveMessages, setLiveMessages] = useState([]);
|
|
82
82
|
const [isOpen, setIsOpen] = useState(false);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!customerToken) return;
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
async function tryResume() {
|
|
87
|
+
try {
|
|
88
|
+
const token = await tokenMgr.getToken();
|
|
89
|
+
const url = new URL(`${apiBase}/api/v1/sessions/resume`);
|
|
90
|
+
url.searchParams.set("customerToken", customerToken);
|
|
91
|
+
const res = await fetch(url.toString(), {
|
|
92
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok || cancelled) return;
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
const resumedId = data?.session?.sessionId ?? null;
|
|
97
|
+
if (!resumedId || cancelled) return;
|
|
98
|
+
sessionId.current = resumedId;
|
|
99
|
+
const turnsRes = await fetch(`${apiBase}/api/v1/sessions/${resumedId}/turns`, {
|
|
100
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
101
|
+
});
|
|
102
|
+
if (!turnsRes.ok || cancelled) return;
|
|
103
|
+
const turnsData = await turnsRes.json();
|
|
104
|
+
const rawTurns = turnsData?.data?.turns ?? turnsData?.turns ?? [];
|
|
105
|
+
if (cancelled) return;
|
|
106
|
+
const hydrated = rawTurns.flatMap((t) => [
|
|
107
|
+
{ role: "user", content: t.query },
|
|
108
|
+
{ role: "assistant", content: t.response }
|
|
109
|
+
]);
|
|
110
|
+
if (hydrated.length > 0) setTurns(hydrated);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
void tryResume();
|
|
115
|
+
return () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
};
|
|
118
|
+
}, [customerToken, apiBase]);
|
|
83
119
|
const value = useMemo(
|
|
84
120
|
() => ({
|
|
85
121
|
config,
|
|
@@ -99,23 +135,12 @@ function PanaceaProvider({
|
|
|
99
135
|
isOpen,
|
|
100
136
|
setIsOpen
|
|
101
137
|
}),
|
|
102
|
-
[
|
|
103
|
-
config,
|
|
104
|
-
apiBase,
|
|
105
|
-
tokenMgr,
|
|
106
|
-
turns,
|
|
107
|
-
loading,
|
|
108
|
-
streaming,
|
|
109
|
-
liveEscalationId,
|
|
110
|
-
liveMessages,
|
|
111
|
-
isOpen
|
|
112
|
-
]
|
|
138
|
+
[config, apiBase, tokenMgr, turns, loading, streaming, liveEscalationId, liveMessages, isOpen]
|
|
113
139
|
);
|
|
114
140
|
return /* @__PURE__ */ jsx(PanaceaContext.Provider, { value, children });
|
|
115
141
|
}
|
|
116
142
|
function useChat() {
|
|
117
143
|
const {
|
|
118
|
-
config,
|
|
119
144
|
apiBase,
|
|
120
145
|
getToken,
|
|
121
146
|
turns,
|
|
@@ -134,17 +159,14 @@ function useChat() {
|
|
|
134
159
|
setTurns((prev) => [...prev, { role: "user", content: message }]);
|
|
135
160
|
try {
|
|
136
161
|
const token = await getToken();
|
|
137
|
-
await fetch(
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
body: JSON.stringify({ content: message })
|
|
146
|
-
}
|
|
147
|
-
);
|
|
162
|
+
await fetch(`${apiBase}/api/v1/inbox/${liveEscalationId}/customer-message`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
Authorization: `Bearer ${token}`
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({ content: message })
|
|
169
|
+
});
|
|
148
170
|
} catch {
|
|
149
171
|
}
|
|
150
172
|
return;
|
|
@@ -230,32 +252,21 @@ function useChat() {
|
|
|
230
252
|
setStreaming("");
|
|
231
253
|
}
|
|
232
254
|
},
|
|
233
|
-
[
|
|
234
|
-
apiBase,
|
|
235
|
-
getToken,
|
|
236
|
-
liveEscalationId,
|
|
237
|
-
sessionId,
|
|
238
|
-
setLoading,
|
|
239
|
-
setStreaming,
|
|
240
|
-
setTurns
|
|
241
|
-
]
|
|
255
|
+
[apiBase, getToken, liveEscalationId, sessionId, setLoading, setStreaming, setTurns]
|
|
242
256
|
);
|
|
243
257
|
const escalate = useCallback(
|
|
244
258
|
async (reason = "Customer requested human support") => {
|
|
245
259
|
if (!sessionId.current) return;
|
|
246
260
|
try {
|
|
247
261
|
const token = await getToken();
|
|
248
|
-
const res = await fetch(
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
body: JSON.stringify({ reason })
|
|
257
|
-
}
|
|
258
|
-
);
|
|
262
|
+
const res = await fetch(`${apiBase}/api/v1/sessions/${sessionId.current}/escalate`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
Authorization: `Bearer ${token}`
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({ reason })
|
|
269
|
+
});
|
|
259
270
|
if (res.ok) {
|
|
260
271
|
const data = await res.json();
|
|
261
272
|
const id = data?.data?.escalationId ?? data?.escalationId;
|
|
@@ -269,22 +280,17 @@ function useChat() {
|
|
|
269
280
|
const react = useCallback(
|
|
270
281
|
async (turnIndex, reaction) => {
|
|
271
282
|
if (!sessionId.current) return;
|
|
272
|
-
setTurns(
|
|
273
|
-
(prev) => prev.map((t, i) => i === turnIndex ? { ...t, reaction } : t)
|
|
274
|
-
);
|
|
283
|
+
setTurns((prev) => prev.map((t, i) => i === turnIndex ? { ...t, reaction } : t));
|
|
275
284
|
try {
|
|
276
285
|
const token = await getToken();
|
|
277
|
-
await fetch(
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
body: JSON.stringify({ reaction, turnIndex })
|
|
286
|
-
}
|
|
287
|
-
);
|
|
286
|
+
await fetch(`${apiBase}/api/v1/sessions/${sessionId.current}/reaction`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
Authorization: `Bearer ${token}`
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({ reaction, turnIndex })
|
|
293
|
+
});
|
|
288
294
|
} catch {
|
|
289
295
|
}
|
|
290
296
|
},
|
|
@@ -295,6 +301,24 @@ function useChat() {
|
|
|
295
301
|
setStreaming("");
|
|
296
302
|
sessionId.current = null;
|
|
297
303
|
}, [sessionId, setStreaming, setTurns]);
|
|
304
|
+
const setPageContext = useCallback(
|
|
305
|
+
async (ctx) => {
|
|
306
|
+
if (!sessionId.current) return;
|
|
307
|
+
try {
|
|
308
|
+
const token = await getToken();
|
|
309
|
+
await fetch(`${apiBase}/api/v1/sessions/${sessionId.current}`, {
|
|
310
|
+
method: "PATCH",
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "application/json",
|
|
313
|
+
Authorization: `Bearer ${token}`
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({ pageContext: ctx })
|
|
316
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
[apiBase, getToken, sessionId]
|
|
321
|
+
);
|
|
298
322
|
return {
|
|
299
323
|
turns,
|
|
300
324
|
loading,
|
|
@@ -304,27 +328,21 @@ function useChat() {
|
|
|
304
328
|
send,
|
|
305
329
|
escalate,
|
|
306
330
|
react,
|
|
331
|
+
setPageContext,
|
|
307
332
|
reset
|
|
308
333
|
};
|
|
309
334
|
}
|
|
310
335
|
function useLiveSession() {
|
|
311
|
-
const {
|
|
312
|
-
apiBase,
|
|
313
|
-
getToken,
|
|
314
|
-
liveEscalationId,
|
|
315
|
-
liveMessages,
|
|
316
|
-
setLiveMessages
|
|
317
|
-
} = usePanaceaContext();
|
|
336
|
+
const { apiBase, getToken, liveEscalationId, liveMessages, setLiveMessages } = usePanaceaContext();
|
|
318
337
|
const seenIds = useRef(/* @__PURE__ */ new Set());
|
|
319
338
|
useEffect(() => {
|
|
320
339
|
if (!liveEscalationId) return;
|
|
321
340
|
const poll = async () => {
|
|
322
341
|
try {
|
|
323
342
|
const token = await getToken();
|
|
324
|
-
const res = await fetch(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
);
|
|
343
|
+
const res = await fetch(`${apiBase}/api/v1/inbox/${liveEscalationId}/messages`, {
|
|
344
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
345
|
+
});
|
|
328
346
|
if (!res.ok) return;
|
|
329
347
|
const data = await res.json();
|
|
330
348
|
const all = data?.data?.messages ?? data?.messages ?? [];
|
|
@@ -345,17 +363,14 @@ function useLiveSession() {
|
|
|
345
363
|
if (!liveEscalationId) return;
|
|
346
364
|
try {
|
|
347
365
|
const token = await getToken();
|
|
348
|
-
await fetch(
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
body: JSON.stringify({ content })
|
|
357
|
-
}
|
|
358
|
-
);
|
|
366
|
+
await fetch(`${apiBase}/api/v1/inbox/${liveEscalationId}/customer-message`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: {
|
|
369
|
+
"Content-Type": "application/json",
|
|
370
|
+
Authorization: `Bearer ${token}`
|
|
371
|
+
},
|
|
372
|
+
body: JSON.stringify({ content })
|
|
373
|
+
});
|
|
359
374
|
} catch {
|
|
360
375
|
}
|
|
361
376
|
},
|
|
@@ -376,6 +391,55 @@ function useWidget() {
|
|
|
376
391
|
const toggle = useCallback(() => setIsOpen((v) => !v), [setIsOpen]);
|
|
377
392
|
return { isOpen, open, close, toggle };
|
|
378
393
|
}
|
|
394
|
+
function useSessionHistory() {
|
|
395
|
+
const { apiBase, getToken, config, setTurns, sessionId: activeSessionIdRef } = usePanaceaContext();
|
|
396
|
+
const [sessions, setSessions] = useState([]);
|
|
397
|
+
const [loading, setLoading] = useState(false);
|
|
398
|
+
const fetchSessions = useCallback(async () => {
|
|
399
|
+
if (!config.customerToken) return;
|
|
400
|
+
setLoading(true);
|
|
401
|
+
try {
|
|
402
|
+
const token = await getToken();
|
|
403
|
+
const url = new URL(`${apiBase}/api/v1/sessions`);
|
|
404
|
+
url.searchParams.set("customerToken", config.customerToken);
|
|
405
|
+
const res = await fetch(url.toString(), {
|
|
406
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
407
|
+
});
|
|
408
|
+
if (!res.ok) return;
|
|
409
|
+
const data = await res.json();
|
|
410
|
+
const list = Array.isArray(data) ? data : data?.data ?? data?.sessions ?? [];
|
|
411
|
+
setSessions(list);
|
|
412
|
+
} catch {
|
|
413
|
+
} finally {
|
|
414
|
+
setLoading(false);
|
|
415
|
+
}
|
|
416
|
+
}, [apiBase, getToken, config.customerToken]);
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
void fetchSessions();
|
|
419
|
+
}, [fetchSessions]);
|
|
420
|
+
const loadSession = useCallback(
|
|
421
|
+
async (sessionId) => {
|
|
422
|
+
try {
|
|
423
|
+
const token = await getToken();
|
|
424
|
+
const res = await fetch(`${apiBase}/api/v1/sessions/${sessionId}/turns`, {
|
|
425
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
426
|
+
});
|
|
427
|
+
if (!res.ok) return;
|
|
428
|
+
const data = await res.json();
|
|
429
|
+
const raw = data?.data?.turns ?? data?.turns ?? [];
|
|
430
|
+
const turns = raw.flatMap((t) => [
|
|
431
|
+
{ role: "user", content: t.query },
|
|
432
|
+
{ role: "assistant", content: t.response }
|
|
433
|
+
]);
|
|
434
|
+
activeSessionIdRef.current = sessionId;
|
|
435
|
+
setTurns(turns);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
[apiBase, getToken, setTurns, activeSessionIdRef]
|
|
440
|
+
);
|
|
441
|
+
return { sessions, loading, loadSession, refresh: fetchSessions };
|
|
442
|
+
}
|
|
379
443
|
function cn(...inputs) {
|
|
380
444
|
return twMerge(clsx(inputs));
|
|
381
445
|
}
|
|
@@ -415,6 +479,28 @@ function PanaceaFAB({ className }) {
|
|
|
415
479
|
}
|
|
416
480
|
);
|
|
417
481
|
}
|
|
482
|
+
function renderContent(text) {
|
|
483
|
+
const parts = text.split(/(\[\[[^\]]+\]\])/g);
|
|
484
|
+
return parts.map((part, i) => {
|
|
485
|
+
const m = part.match(/^\[\[(?:([^|\]]+)\|)?([^\]]+)\]\]$/);
|
|
486
|
+
if (m) {
|
|
487
|
+
const label = (m[1] ?? m[2] ?? part).trim();
|
|
488
|
+
return /* @__PURE__ */ jsxs(
|
|
489
|
+
"span",
|
|
490
|
+
{
|
|
491
|
+
className: "inline-flex items-center gap-0.5 rounded bg-blue-50 px-1 py-0.5 text-xs font-medium text-blue-700",
|
|
492
|
+
children: [
|
|
493
|
+
/* @__PURE__ */ jsx("svg", { className: "inline size-3 shrink-0", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M2 2h5v2H4v8h8v-3h2v5H2V2zm7 0h5v5h-2V4.414L6.707 9.707 5.293 8.293 10.586 3H9V2z" }) }),
|
|
494
|
+
label
|
|
495
|
+
]
|
|
496
|
+
},
|
|
497
|
+
i
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (!part) return null;
|
|
501
|
+
return /* @__PURE__ */ jsx("span", { children: part }, i);
|
|
502
|
+
}).filter(Boolean);
|
|
503
|
+
}
|
|
418
504
|
function Bubble({ turn }) {
|
|
419
505
|
const isUser = turn.role === "user";
|
|
420
506
|
const isAgent = turn.isLiveAgent;
|
|
@@ -427,7 +513,7 @@ function Bubble({ turn }) {
|
|
|
427
513
|
"max-w-[82%] rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
|
|
428
514
|
isUser ? "rounded-tr-sm bg-primary text-primary-foreground" : isAgent ? "rounded-tl-sm bg-blue-100 text-blue-900" : "rounded-tl-sm bg-muted text-foreground"
|
|
429
515
|
),
|
|
430
|
-
children: turn.content
|
|
516
|
+
children: isUser ? turn.content : renderContent(turn.content)
|
|
431
517
|
}
|
|
432
518
|
)
|
|
433
519
|
] });
|
|
@@ -535,7 +621,81 @@ function PanaceaChat({
|
|
|
535
621
|
)
|
|
536
622
|
] });
|
|
537
623
|
}
|
|
624
|
+
function formatDate(iso) {
|
|
625
|
+
try {
|
|
626
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
627
|
+
month: "short",
|
|
628
|
+
day: "numeric",
|
|
629
|
+
hour: "2-digit",
|
|
630
|
+
minute: "2-digit"
|
|
631
|
+
}).format(new Date(iso));
|
|
632
|
+
} catch {
|
|
633
|
+
return iso;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function PanaceaHistory({ onSessionLoaded, className }) {
|
|
637
|
+
const { config } = usePanaceaContext();
|
|
638
|
+
const { sessions, loading, loadSession } = useSessionHistory();
|
|
639
|
+
const [active, setActive] = useState(null);
|
|
640
|
+
const [loadingId, setLoadingId] = useState(null);
|
|
641
|
+
if (!config.customerToken) {
|
|
642
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col items-center justify-center gap-2 py-8 text-center text-sm text-muted-foreground", className), children: /* @__PURE__ */ jsx("span", { children: "Sign in to see your conversation history." }) });
|
|
643
|
+
}
|
|
644
|
+
if (loading) {
|
|
645
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2 p-3", className), children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx(
|
|
646
|
+
"div",
|
|
647
|
+
{
|
|
648
|
+
className: "h-14 animate-pulse rounded-lg bg-muted"
|
|
649
|
+
},
|
|
650
|
+
i
|
|
651
|
+
)) });
|
|
652
|
+
}
|
|
653
|
+
if (sessions.length === 0) {
|
|
654
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col items-center justify-center gap-2 py-8 text-center text-sm text-muted-foreground", className), children: /* @__PURE__ */ jsx("span", { children: "No previous conversations found." }) });
|
|
655
|
+
}
|
|
656
|
+
async function handleLoad(session) {
|
|
657
|
+
setLoadingId(session.sessionId);
|
|
658
|
+
await loadSession(session.sessionId);
|
|
659
|
+
setActive(session.sessionId);
|
|
660
|
+
setLoadingId(null);
|
|
661
|
+
onSessionLoaded?.(session.sessionId);
|
|
662
|
+
}
|
|
663
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-1 overflow-y-auto p-2", className), children: sessions.map((session) => {
|
|
664
|
+
const isActive = active === session.sessionId;
|
|
665
|
+
const isLoading = loadingId === session.sessionId;
|
|
666
|
+
return /* @__PURE__ */ jsxs(
|
|
667
|
+
"button",
|
|
668
|
+
{
|
|
669
|
+
onClick: () => void handleLoad(session),
|
|
670
|
+
disabled: isLoading,
|
|
671
|
+
className: cn(
|
|
672
|
+
"flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent disabled:cursor-wait",
|
|
673
|
+
isActive && "bg-accent"
|
|
674
|
+
),
|
|
675
|
+
children: [
|
|
676
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium leading-snug text-foreground", children: session.preview ?? "Conversation" }),
|
|
677
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
678
|
+
/* @__PURE__ */ jsx("span", { children: formatDate(session.createdAt) }),
|
|
679
|
+
session.turnCount !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
680
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\xB7" }),
|
|
681
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
682
|
+
session.turnCount,
|
|
683
|
+
" ",
|
|
684
|
+
session.turnCount === 1 ? "message" : "messages"
|
|
685
|
+
] })
|
|
686
|
+
] }),
|
|
687
|
+
isLoading && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
688
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\xB7" }),
|
|
689
|
+
/* @__PURE__ */ jsx("span", { children: "Loading\u2026" })
|
|
690
|
+
] })
|
|
691
|
+
] })
|
|
692
|
+
]
|
|
693
|
+
},
|
|
694
|
+
session.sessionId
|
|
695
|
+
);
|
|
696
|
+
}) });
|
|
697
|
+
}
|
|
538
698
|
|
|
539
|
-
export { PanaceaChat, PanaceaFAB, PanaceaInput, PanaceaMessages, PanaceaProvider, useChat, useLiveSession, useWidget };
|
|
699
|
+
export { PanaceaChat, PanaceaFAB, PanaceaHistory, PanaceaInput, PanaceaMessages, PanaceaProvider, useChat, useLiveSession, useSessionHistory, useWidget };
|
|
540
700
|
//# sourceMappingURL=index.js.map
|
|
541
701
|
//# sourceMappingURL=index.js.map
|