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