@skippr/live-agent-sdk 0.5.0 → 0.7.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/dist/esm/lib-exports.js +709 -0
- package/dist/types/lib-exports.d.ts +2 -2
- package/package.json +1 -1
package/dist/esm/lib-exports.js
CHANGED
|
@@ -1,3 +1,712 @@
|
|
|
1
|
+
// src/components/LiveAgent.tsx
|
|
2
|
+
import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
|
|
3
|
+
import { useCallback as useCallback6, useMemo as useMemo2, useState as useState4 } from "react";
|
|
4
|
+
|
|
5
|
+
// src/context/LiveAgentContext.tsx
|
|
6
|
+
import { createContext } from "react";
|
|
7
|
+
var LiveAgentContext = createContext(null);
|
|
8
|
+
|
|
9
|
+
// src/hooks/useSession.ts
|
|
10
|
+
import { useCallback, useState } from "react";
|
|
11
|
+
var API_URL = "https://skipprapi-production.up.railway.app";
|
|
12
|
+
function useSession({ organizationId, agentId }) {
|
|
13
|
+
const [connection, setConnection] = useState(null);
|
|
14
|
+
const [shouldConnect, setShouldConnect] = useState(false);
|
|
15
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
16
|
+
const [error, setError] = useState("");
|
|
17
|
+
const [sessionId, setSessionId] = useState(null);
|
|
18
|
+
const startSession = useCallback(async () => {
|
|
19
|
+
setIsStarting(true);
|
|
20
|
+
setError("");
|
|
21
|
+
try {
|
|
22
|
+
const createResp = await fetch(`${API_URL}/v1/sessions`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify({ organizationId, agentId })
|
|
26
|
+
});
|
|
27
|
+
if (!createResp.ok) {
|
|
28
|
+
throw new Error(`Failed to create session: ${createResp.status}`);
|
|
29
|
+
}
|
|
30
|
+
const { session } = await createResp.json();
|
|
31
|
+
const startResp = await fetch(`${API_URL}/v1/sessions/${session.id}/start`, {
|
|
32
|
+
method: "POST"
|
|
33
|
+
});
|
|
34
|
+
if (!startResp.ok) {
|
|
35
|
+
throw new Error(`Failed to start session: ${startResp.status}`);
|
|
36
|
+
}
|
|
37
|
+
const { connection: conn } = await startResp.json();
|
|
38
|
+
setSessionId(session.id);
|
|
39
|
+
setConnection({
|
|
40
|
+
livekitUrl: conn.livekitUrl,
|
|
41
|
+
token: conn.token
|
|
42
|
+
});
|
|
43
|
+
setShouldConnect(true);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
setError(e instanceof Error ? e.message : "Failed to start session");
|
|
46
|
+
} finally {
|
|
47
|
+
setIsStarting(false);
|
|
48
|
+
}
|
|
49
|
+
}, [organizationId, agentId]);
|
|
50
|
+
const disconnect = useCallback(async () => {
|
|
51
|
+
if (sessionId) {
|
|
52
|
+
try {
|
|
53
|
+
await fetch(`${API_URL}/v1/sessions/${sessionId}/complete`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({})
|
|
57
|
+
});
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
setError("");
|
|
61
|
+
setShouldConnect(false);
|
|
62
|
+
setConnection(null);
|
|
63
|
+
setSessionId(null);
|
|
64
|
+
}, [sessionId]);
|
|
65
|
+
return { connection, shouldConnect, isStarting, error, startSession, disconnect };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/components/Sidebar.tsx
|
|
69
|
+
import { useConnectionState } from "@livekit/components-react";
|
|
70
|
+
import { ConnectionState } from "livekit-client";
|
|
71
|
+
import { useEffect as useEffect3 } from "react";
|
|
72
|
+
|
|
73
|
+
// src/hooks/useLiveAgent.ts
|
|
74
|
+
import { use } from "react";
|
|
75
|
+
function useLiveAgent() {
|
|
76
|
+
const ctx = use(LiveAgentContext);
|
|
77
|
+
if (!ctx) {
|
|
78
|
+
throw new Error("useLiveAgent must be used within a <LiveAgent> provider");
|
|
79
|
+
}
|
|
80
|
+
const { connection, shouldConnect, ...publicValue } = ctx;
|
|
81
|
+
return publicValue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/hooks/usePhaseUpdates.ts
|
|
85
|
+
import { useCallback as useCallback2 } from "react";
|
|
86
|
+
|
|
87
|
+
// src/hooks/useAgentState.ts
|
|
88
|
+
import { useRemoteParticipants } from "@livekit/components-react";
|
|
89
|
+
import { useEffect, useState as useState2 } from "react";
|
|
90
|
+
function useAgentState(attributeKey, parse, initial) {
|
|
91
|
+
const [value, setValue] = useState2(initial);
|
|
92
|
+
const remoteParticipants = useRemoteParticipants();
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const agentParticipant = remoteParticipants.find((p) => p.attributes?.[attributeKey]);
|
|
95
|
+
if (agentParticipant) {
|
|
96
|
+
const attr = agentParticipant.attributes?.[attributeKey];
|
|
97
|
+
if (attr) {
|
|
98
|
+
const parsed = parse(attr);
|
|
99
|
+
if (parsed)
|
|
100
|
+
setValue(parsed);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const handlers = new Map;
|
|
104
|
+
for (const p of remoteParticipants) {
|
|
105
|
+
const handler = (changedAttributes) => {
|
|
106
|
+
if (changedAttributes[attributeKey]) {
|
|
107
|
+
const parsed = parse(changedAttributes[attributeKey]);
|
|
108
|
+
if (parsed)
|
|
109
|
+
setValue(parsed);
|
|
110
|
+
} else if (p.attributes?.[attributeKey]) {
|
|
111
|
+
const parsed = parse(p.attributes[attributeKey]);
|
|
112
|
+
if (parsed)
|
|
113
|
+
setValue(parsed);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
handlers.set(p, handler);
|
|
117
|
+
p.on("attributesChanged", handler);
|
|
118
|
+
}
|
|
119
|
+
return () => {
|
|
120
|
+
for (const [p, handler] of handlers) {
|
|
121
|
+
p.off("attributesChanged", handler);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}, [remoteParticipants, attributeKey, parse]);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/hooks/usePhaseUpdates.ts
|
|
129
|
+
function parsePhases(json) {
|
|
130
|
+
try {
|
|
131
|
+
const data = JSON.parse(json);
|
|
132
|
+
if (data.type === "phase_update" && Array.isArray(data.phases)) {
|
|
133
|
+
return data.phases;
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function usePhaseUpdates() {
|
|
139
|
+
const parse = useCallback2(parsePhases, []);
|
|
140
|
+
const phases = useAgentState("phases", parse, []);
|
|
141
|
+
return { phases };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/hooks/useQuestionUpdates.ts
|
|
145
|
+
import { useCallback as useCallback3 } from "react";
|
|
146
|
+
function parseQuestions(json) {
|
|
147
|
+
try {
|
|
148
|
+
const data = JSON.parse(json);
|
|
149
|
+
if (data.type === "question_update" && Array.isArray(data.questions)) {
|
|
150
|
+
return data.questions;
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function useQuestionUpdates() {
|
|
156
|
+
const parse = useCallback3(parseQuestions, []);
|
|
157
|
+
const questions = useAgentState("questions", parse, []);
|
|
158
|
+
return { questions };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/hooks/useTranscriptMessages.ts
|
|
162
|
+
import {
|
|
163
|
+
useLocalParticipant,
|
|
164
|
+
useTrackTranscription,
|
|
165
|
+
useVoiceAssistant
|
|
166
|
+
} from "@livekit/components-react";
|
|
167
|
+
import { Track } from "livekit-client";
|
|
168
|
+
import { useMemo } from "react";
|
|
169
|
+
function segmentsToMessages(segments, role) {
|
|
170
|
+
return segments.filter((s) => s.final && s.text.trim().length > 0).map((s) => ({
|
|
171
|
+
id: s.id,
|
|
172
|
+
role,
|
|
173
|
+
content: s.text
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
function useTranscriptMessages() {
|
|
177
|
+
const { agentTranscriptions, state } = useVoiceAssistant();
|
|
178
|
+
const { localParticipant } = useLocalParticipant();
|
|
179
|
+
const localMicTrack = useMemo(() => localParticipant.getTrackPublication(Track.Source.Microphone) ? {
|
|
180
|
+
participant: localParticipant,
|
|
181
|
+
source: Track.Source.Microphone,
|
|
182
|
+
publication: localParticipant.getTrackPublication(Track.Source.Microphone)
|
|
183
|
+
} : undefined, [localParticipant]);
|
|
184
|
+
const { segments: userSegments } = useTrackTranscription(localMicTrack);
|
|
185
|
+
const messages = useMemo(() => {
|
|
186
|
+
const agent = segmentsToMessages(agentTranscriptions, "assistant");
|
|
187
|
+
const user = segmentsToMessages(userSegments, "user");
|
|
188
|
+
const timeMap = new Map;
|
|
189
|
+
for (const s of agentTranscriptions)
|
|
190
|
+
timeMap.set(s.id, s.firstReceivedTime);
|
|
191
|
+
for (const s of userSegments)
|
|
192
|
+
timeMap.set(s.id, s.firstReceivedTime);
|
|
193
|
+
return [...agent, ...user].sort((a, b) => (timeMap.get(a.id) ?? 0) - (timeMap.get(b.id) ?? 0));
|
|
194
|
+
}, [agentTranscriptions, userSegments]);
|
|
195
|
+
return { messages, agentState: state };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/lib/constants.ts
|
|
199
|
+
var SIDEBAR_WIDTH = 600;
|
|
200
|
+
|
|
201
|
+
// src/lib/utils.ts
|
|
202
|
+
import { clsx } from "clsx";
|
|
203
|
+
import { twMerge } from "tailwind-merge";
|
|
204
|
+
function cn(...inputs) {
|
|
205
|
+
return twMerge(clsx(inputs));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/components/ChatHeader.tsx
|
|
209
|
+
import { Bot, X } from "lucide-react";
|
|
210
|
+
|
|
211
|
+
// src/components/ui/button.tsx
|
|
212
|
+
import { forwardRef } from "react";
|
|
213
|
+
import { jsx } from "react/jsx-runtime";
|
|
214
|
+
var variantClasses = {
|
|
215
|
+
default: "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90",
|
|
216
|
+
destructive: "skippr:bg-destructive skippr:text-white skippr:hover:bg-destructive/90",
|
|
217
|
+
outline: "skippr:border skippr:border-input skippr:bg-background skippr:shadow-xs skippr:hover:bg-accent skippr:hover:text-accent-foreground",
|
|
218
|
+
secondary: "skippr:bg-secondary skippr:text-secondary-foreground skippr:hover:bg-secondary/80",
|
|
219
|
+
ghost: "skippr:hover:bg-accent skippr:hover:text-accent-foreground"
|
|
220
|
+
};
|
|
221
|
+
var sizeClasses = {
|
|
222
|
+
default: "skippr:h-9 skippr:px-4 skippr:py-2",
|
|
223
|
+
xs: "skippr:h-6 skippr:gap-1 skippr:rounded-md skippr:px-2 skippr:text-xs",
|
|
224
|
+
sm: "skippr:h-8 skippr:gap-1.5 skippr:rounded-md skippr:px-3",
|
|
225
|
+
lg: "skippr:h-10 skippr:rounded-md skippr:px-6",
|
|
226
|
+
icon: "skippr:size-9",
|
|
227
|
+
"icon-xs": "skippr:size-6",
|
|
228
|
+
"icon-sm": "skippr:size-8",
|
|
229
|
+
"icon-lg": "skippr:size-10"
|
|
230
|
+
};
|
|
231
|
+
var Button = forwardRef(({ className, variant = "default", size = "default", ...props }, ref) => {
|
|
232
|
+
return /* @__PURE__ */ jsx("button", {
|
|
233
|
+
className: cn("skippr:inline-flex skippr:items-center skippr:justify-center skippr:gap-2 skippr:whitespace-nowrap skippr:rounded-md skippr:text-sm skippr:font-medium skippr:ring-offset-background skippr:transition-all skippr:focus-visible:outline-none skippr:focus-visible:ring-2 skippr:focus-visible:ring-ring skippr:focus-visible:ring-offset-2 skippr:disabled:pointer-events-none skippr:disabled:opacity-50 skippr:shrink-0 skippr:[&_svg]:pointer-events-none skippr:[&_svg:not([class*='size-'])]:size-4 skippr:[&_svg]:shrink-0", variantClasses[variant], sizeClasses[size], className),
|
|
234
|
+
ref,
|
|
235
|
+
...props
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
Button.displayName = "Button";
|
|
239
|
+
|
|
240
|
+
// src/components/ChatHeader.tsx
|
|
241
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
242
|
+
function ChatHeader({ onClose }) {
|
|
243
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
244
|
+
className: "skippr:flex skippr:items-center skippr:gap-3 skippr:bg-primary skippr:px-4 skippr:py-3 skippr:text-primary-foreground",
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ jsx2("div", {
|
|
247
|
+
className: "skippr:flex skippr:size-6 skippr:items-center skippr:justify-center skippr:rounded-full skippr:bg-primary-foreground/20",
|
|
248
|
+
children: /* @__PURE__ */ jsx2(Bot, {
|
|
249
|
+
className: "skippr:size-3.5 skippr:text-primary-foreground"
|
|
250
|
+
})
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ jsx2("div", {
|
|
253
|
+
className: "skippr:flex-1",
|
|
254
|
+
children: /* @__PURE__ */ jsx2("p", {
|
|
255
|
+
className: "skippr:text-sm skippr:font-semibold skippr:leading-none",
|
|
256
|
+
children: "AI Agent"
|
|
257
|
+
})
|
|
258
|
+
}),
|
|
259
|
+
/* @__PURE__ */ jsx2(Button, {
|
|
260
|
+
variant: "ghost",
|
|
261
|
+
size: "icon-xs",
|
|
262
|
+
onClick: onClose,
|
|
263
|
+
className: "skippr:text-primary-foreground skippr:hover:bg-primary-foreground/20 skippr:hover:text-primary-foreground",
|
|
264
|
+
children: /* @__PURE__ */ jsx2(X, {
|
|
265
|
+
className: "skippr:size-4"
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
]
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/components/MeetingControls.tsx
|
|
273
|
+
import { useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react";
|
|
274
|
+
import { ScreenSharePresets } from "livekit-client";
|
|
275
|
+
import { Mic, MicOff, Monitor, MonitorOff, PhoneOff } from "lucide-react";
|
|
276
|
+
import { useCallback as useCallback4, useEffect as useEffect2, useState as useState3 } from "react";
|
|
277
|
+
|
|
278
|
+
// src/lib/format.ts
|
|
279
|
+
function formatTime(seconds) {
|
|
280
|
+
const m = Math.floor(seconds / 60).toString().padStart(2, "0");
|
|
281
|
+
const s = (seconds % 60).toString().padStart(2, "0");
|
|
282
|
+
return `${m}:${s}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/components/MeetingControls.tsx
|
|
286
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
287
|
+
function MeetingControls({ onHangUp }) {
|
|
288
|
+
const { localParticipant } = useLocalParticipant2();
|
|
289
|
+
const isMuted = !localParticipant.isMicrophoneEnabled;
|
|
290
|
+
const isScreenSharing = localParticipant.isScreenShareEnabled;
|
|
291
|
+
const [elapsed, setElapsed] = useState3(0);
|
|
292
|
+
useEffect2(() => {
|
|
293
|
+
const id = setInterval(() => setElapsed((t) => t + 1), 1000);
|
|
294
|
+
return () => clearInterval(id);
|
|
295
|
+
}, []);
|
|
296
|
+
const toggleMute = useCallback4(async () => {
|
|
297
|
+
try {
|
|
298
|
+
await localParticipant.setMicrophoneEnabled(isMuted);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error("Failed to toggle microphone:", e);
|
|
301
|
+
}
|
|
302
|
+
}, [localParticipant, isMuted]);
|
|
303
|
+
const toggleScreenShare = useCallback4(async () => {
|
|
304
|
+
try {
|
|
305
|
+
await localParticipant.setScreenShareEnabled(!isScreenSharing, {
|
|
306
|
+
video: { displaySurface: "browser" },
|
|
307
|
+
resolution: ScreenSharePresets.h720fps30.resolution,
|
|
308
|
+
contentHint: "detail"
|
|
309
|
+
});
|
|
310
|
+
} catch (e) {
|
|
311
|
+
console.error("Failed to toggle screen share:", e);
|
|
312
|
+
}
|
|
313
|
+
}, [localParticipant, isScreenSharing]);
|
|
314
|
+
useEffect2(() => {
|
|
315
|
+
toggleMute().then(() => toggleScreenShare());
|
|
316
|
+
}, []);
|
|
317
|
+
return /* @__PURE__ */ jsxs2("div", {
|
|
318
|
+
className: "skippr:flex skippr:items-center skippr:justify-between skippr:border-b skippr:px-4 skippr:py-3",
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
321
|
+
className: "skippr:flex skippr:items-center skippr:gap-2",
|
|
322
|
+
children: [
|
|
323
|
+
/* @__PURE__ */ jsx3(Button, {
|
|
324
|
+
size: "icon-sm",
|
|
325
|
+
variant: isMuted ? "destructive" : "outline",
|
|
326
|
+
onClick: toggleMute,
|
|
327
|
+
"aria-label": isMuted ? "Unmute" : "Mute",
|
|
328
|
+
children: isMuted ? /* @__PURE__ */ jsx3(MicOff, {
|
|
329
|
+
className: "skippr:size-4"
|
|
330
|
+
}) : /* @__PURE__ */ jsx3(Mic, {
|
|
331
|
+
className: "skippr:size-4"
|
|
332
|
+
})
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ jsx3(Button, {
|
|
335
|
+
size: "icon-sm",
|
|
336
|
+
variant: isScreenSharing ? "default" : "outline",
|
|
337
|
+
onClick: toggleScreenShare,
|
|
338
|
+
"aria-label": isScreenSharing ? "Stop sharing" : "Share screen",
|
|
339
|
+
children: isScreenSharing ? /* @__PURE__ */ jsx3(MonitorOff, {
|
|
340
|
+
className: "skippr:size-4"
|
|
341
|
+
}) : /* @__PURE__ */ jsx3(Monitor, {
|
|
342
|
+
className: "skippr:size-4"
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
]
|
|
346
|
+
}),
|
|
347
|
+
/* @__PURE__ */ jsx3("span", {
|
|
348
|
+
className: "skippr:text-sm skippr:font-medium skippr:tabular-nums skippr:text-muted-foreground",
|
|
349
|
+
children: formatTime(elapsed)
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ jsx3(Button, {
|
|
352
|
+
size: "icon-sm",
|
|
353
|
+
variant: "destructive",
|
|
354
|
+
onClick: onHangUp,
|
|
355
|
+
"aria-label": "Hang up",
|
|
356
|
+
children: /* @__PURE__ */ jsx3(PhoneOff, {
|
|
357
|
+
className: "skippr:size-4"
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
]
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/components/MessageList.tsx
|
|
365
|
+
import { useCallback as useCallback5 } from "react";
|
|
366
|
+
|
|
367
|
+
// src/components/ChatMessage.tsx
|
|
368
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
369
|
+
function ChatMessage({ message }) {
|
|
370
|
+
const isUser = message.role === "user";
|
|
371
|
+
return /* @__PURE__ */ jsx4("div", {
|
|
372
|
+
className: cn("skippr:flex skippr:w-full skippr:px-4 skippr:py-1", isUser ? "skippr:justify-end" : "skippr:justify-start"),
|
|
373
|
+
children: /* @__PURE__ */ jsx4("div", {
|
|
374
|
+
className: cn("skippr:max-w-[85%] skippr:whitespace-pre-wrap skippr:rounded-2xl skippr:px-4 skippr:py-2.5 skippr:text-sm skippr:leading-relaxed", isUser ? "skippr:rounded-br-sm skippr:bg-primary skippr:text-primary-foreground" : "skippr:rounded-bl-sm skippr:bg-muted skippr:text-foreground"),
|
|
375
|
+
children: message.content
|
|
376
|
+
})
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/components/TypingIndicator.tsx
|
|
381
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
382
|
+
function TypingIndicator() {
|
|
383
|
+
return /* @__PURE__ */ jsx5("div", {
|
|
384
|
+
className: "skippr:flex skippr:items-center skippr:gap-1 skippr:px-4 skippr:py-3",
|
|
385
|
+
children: /* @__PURE__ */ jsxs3("div", {
|
|
386
|
+
className: "skippr:flex skippr:items-center skippr:gap-1 skippr:rounded-2xl skippr:rounded-bl-sm skippr:bg-muted skippr:px-4 skippr:py-2.5",
|
|
387
|
+
children: [
|
|
388
|
+
/* @__PURE__ */ jsx5("span", {
|
|
389
|
+
className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:0ms]"
|
|
390
|
+
}),
|
|
391
|
+
/* @__PURE__ */ jsx5("span", {
|
|
392
|
+
className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:150ms]"
|
|
393
|
+
}),
|
|
394
|
+
/* @__PURE__ */ jsx5("span", {
|
|
395
|
+
className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:300ms]"
|
|
396
|
+
})
|
|
397
|
+
]
|
|
398
|
+
})
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/components/MessageList.tsx
|
|
403
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
404
|
+
function MessageList({ messages, isStreaming }) {
|
|
405
|
+
const scrollRef = useCallback5((node) => {
|
|
406
|
+
node?.scrollIntoView({ behavior: "smooth" });
|
|
407
|
+
}, []);
|
|
408
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
|
|
409
|
+
const showTyping = isStreaming && lastMessage?.role === "assistant" && lastMessage.content === "";
|
|
410
|
+
return /* @__PURE__ */ jsx6("div", {
|
|
411
|
+
className: "skippr:flex-1 skippr:overflow-y-auto",
|
|
412
|
+
children: /* @__PURE__ */ jsxs4("div", {
|
|
413
|
+
className: "skippr:flex skippr:flex-col skippr:gap-1 skippr:py-3",
|
|
414
|
+
children: [
|
|
415
|
+
messages.map((message) => /* @__PURE__ */ jsx6(ChatMessage, {
|
|
416
|
+
message
|
|
417
|
+
}, message.id)),
|
|
418
|
+
showTyping && /* @__PURE__ */ jsx6(TypingIndicator, {}),
|
|
419
|
+
/* @__PURE__ */ jsx6("div", {
|
|
420
|
+
ref: scrollRef
|
|
421
|
+
}, `scroll-${messages.length}`)
|
|
422
|
+
]
|
|
423
|
+
})
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/components/QuickActions.tsx
|
|
428
|
+
import { Loader2, MessageCircleQuestion } from "lucide-react";
|
|
429
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
430
|
+
function QuickActions({ onStartSession, isStarting, error }) {
|
|
431
|
+
return /* @__PURE__ */ jsxs5("div", {
|
|
432
|
+
className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:items-center skippr:gap-6 skippr:overflow-y-auto skippr:px-4 skippr:py-4",
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ jsx7("p", {
|
|
435
|
+
className: "skippr:mb-1 skippr:text-sm skippr:text-muted-foreground",
|
|
436
|
+
children: "How can I help you today?"
|
|
437
|
+
}),
|
|
438
|
+
/* @__PURE__ */ jsxs5(Button, {
|
|
439
|
+
variant: "outline",
|
|
440
|
+
className: "skippr:h-auto skippr:flex-col skippr:gap-1.5 skippr:whitespace-normal skippr:py-3 skippr:text-xs",
|
|
441
|
+
onClick: onStartSession,
|
|
442
|
+
disabled: isStarting,
|
|
443
|
+
children: [
|
|
444
|
+
isStarting ? /* @__PURE__ */ jsx7(Loader2, {
|
|
445
|
+
className: "skippr:size-4 skippr:animate-spin skippr:text-primary"
|
|
446
|
+
}) : /* @__PURE__ */ jsx7(MessageCircleQuestion, {
|
|
447
|
+
className: "skippr:size-4 skippr:text-primary"
|
|
448
|
+
}),
|
|
449
|
+
isStarting ? "Starting..." : "Start Session"
|
|
450
|
+
]
|
|
451
|
+
}),
|
|
452
|
+
error && /* @__PURE__ */ jsx7("p", {
|
|
453
|
+
className: "skippr:text-xs skippr:text-destructive",
|
|
454
|
+
children: error
|
|
455
|
+
})
|
|
456
|
+
]
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/components/SessionAgenda.tsx
|
|
461
|
+
import { Check, Circle, Loader2 as Loader22 } from "lucide-react";
|
|
462
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
463
|
+
function SessionAgenda({ phases, questions = [] }) {
|
|
464
|
+
if (phases.length === 0) {
|
|
465
|
+
return /* @__PURE__ */ jsxs6("div", {
|
|
466
|
+
className: "skippr:flex skippr:flex-col skippr:gap-3 skippr:p-4",
|
|
467
|
+
children: [
|
|
468
|
+
/* @__PURE__ */ jsx8("h3", {
|
|
469
|
+
className: "skippr:text-sm skippr:font-semibold",
|
|
470
|
+
children: "Agenda"
|
|
471
|
+
}),
|
|
472
|
+
/* @__PURE__ */ jsx8("p", {
|
|
473
|
+
className: "skippr:text-xs skippr:text-muted-foreground",
|
|
474
|
+
children: "Waiting for session..."
|
|
475
|
+
})
|
|
476
|
+
]
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return /* @__PURE__ */ jsxs6("div", {
|
|
480
|
+
className: "skippr:flex skippr:flex-col skippr:gap-3 skippr:p-4",
|
|
481
|
+
children: [
|
|
482
|
+
/* @__PURE__ */ jsx8("h3", {
|
|
483
|
+
className: "skippr:text-sm skippr:font-semibold",
|
|
484
|
+
children: "Agenda"
|
|
485
|
+
}),
|
|
486
|
+
/* @__PURE__ */ jsx8("ul", {
|
|
487
|
+
className: "skippr:flex skippr:flex-col skippr:gap-2",
|
|
488
|
+
children: phases.map((phase) => {
|
|
489
|
+
const phaseQuestions = questions.filter((q) => q.phaseName === phase.name);
|
|
490
|
+
const answeredCount = phaseQuestions.filter((q) => q.status === "answered").length;
|
|
491
|
+
const totalCount = phaseQuestions.length;
|
|
492
|
+
return /* @__PURE__ */ jsxs6("li", {
|
|
493
|
+
className: "skippr:flex skippr:flex-col skippr:gap-0.5",
|
|
494
|
+
children: [
|
|
495
|
+
/* @__PURE__ */ jsxs6("div", {
|
|
496
|
+
className: "skippr:flex skippr:items-center skippr:gap-2 skippr:text-sm",
|
|
497
|
+
children: [
|
|
498
|
+
/* @__PURE__ */ jsx8(PhaseIcon, {
|
|
499
|
+
status: phase.status
|
|
500
|
+
}),
|
|
501
|
+
/* @__PURE__ */ jsx8("span", {
|
|
502
|
+
className: cn(phase.status === "completed" && "skippr:text-muted-foreground skippr:line-through", phase.status === "active" && "skippr:font-medium skippr:text-primary"),
|
|
503
|
+
children: phase.name
|
|
504
|
+
})
|
|
505
|
+
]
|
|
506
|
+
}),
|
|
507
|
+
totalCount > 0 && /* @__PURE__ */ jsxs6("span", {
|
|
508
|
+
className: "skippr:ml-6 skippr:text-xs skippr:text-muted-foreground",
|
|
509
|
+
children: [
|
|
510
|
+
answeredCount,
|
|
511
|
+
"/",
|
|
512
|
+
totalCount,
|
|
513
|
+
" answered"
|
|
514
|
+
]
|
|
515
|
+
})
|
|
516
|
+
]
|
|
517
|
+
}, phase.name);
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function PhaseIcon({ status }) {
|
|
524
|
+
if (status === "completed") {
|
|
525
|
+
return /* @__PURE__ */ jsx8("div", {
|
|
526
|
+
className: "skippr:flex skippr:size-4 skippr:shrink-0 skippr:items-center skippr:justify-center skippr:rounded-full skippr:bg-primary",
|
|
527
|
+
children: /* @__PURE__ */ jsx8(Check, {
|
|
528
|
+
className: "skippr:size-2.5 skippr:text-primary-foreground",
|
|
529
|
+
strokeWidth: 3
|
|
530
|
+
})
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (status === "active") {
|
|
534
|
+
return /* @__PURE__ */ jsx8(Loader22, {
|
|
535
|
+
className: "skippr:size-4 skippr:shrink-0 skippr:text-primary skippr:animate-spin"
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return /* @__PURE__ */ jsx8(Circle, {
|
|
539
|
+
className: "skippr:size-4 skippr:shrink-0 skippr:text-muted-foreground"
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/components/Sidebar.tsx
|
|
544
|
+
import { jsx as jsx9, jsxs as jsxs7, Fragment } from "react/jsx-runtime";
|
|
545
|
+
function Sidebar() {
|
|
546
|
+
const { isConnected, isStarting, error, startSession, disconnect, isPanelOpen, closePanel } = useLiveAgent();
|
|
547
|
+
useEffect3(() => {
|
|
548
|
+
document.body.style.transition = "margin-right 300ms ease-in-out";
|
|
549
|
+
document.body.style.marginRight = isPanelOpen ? `${SIDEBAR_WIDTH}px` : "0px";
|
|
550
|
+
}, [isPanelOpen]);
|
|
551
|
+
useEffect3(() => {
|
|
552
|
+
return () => {
|
|
553
|
+
document.body.style.marginRight = "";
|
|
554
|
+
document.body.style.transition = "";
|
|
555
|
+
};
|
|
556
|
+
}, []);
|
|
557
|
+
return /* @__PURE__ */ jsx9("div", {
|
|
558
|
+
className: cn("skippr:fixed skippr:top-0 skippr:right-0 skippr:h-full skippr:z-[9999]", "skippr:bg-background skippr:border-l skippr:border-border", "skippr:flex skippr:flex-col", "skippr:transition-all skippr:duration-300 skippr:ease-in-out skippr:overflow-hidden", !isPanelOpen && "skippr:w-0 skippr:border-l-0"),
|
|
559
|
+
style: { width: isPanelOpen ? SIDEBAR_WIDTH : undefined },
|
|
560
|
+
children: isConnected ? /* @__PURE__ */ jsxs7(Fragment, {
|
|
561
|
+
children: [
|
|
562
|
+
/* @__PURE__ */ jsx9(ChatHeader, {
|
|
563
|
+
onClose: closePanel
|
|
564
|
+
}),
|
|
565
|
+
/* @__PURE__ */ jsx9(ConnectedContent, {
|
|
566
|
+
onDisconnect: disconnect
|
|
567
|
+
})
|
|
568
|
+
]
|
|
569
|
+
}) : /* @__PURE__ */ jsxs7(Fragment, {
|
|
570
|
+
children: [
|
|
571
|
+
/* @__PURE__ */ jsx9(ChatHeader, {
|
|
572
|
+
onClose: closePanel
|
|
573
|
+
}),
|
|
574
|
+
/* @__PURE__ */ jsx9(QuickActions, {
|
|
575
|
+
onStartSession: startSession,
|
|
576
|
+
isStarting,
|
|
577
|
+
error
|
|
578
|
+
})
|
|
579
|
+
]
|
|
580
|
+
})
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function ConnectedContent({ onDisconnect }) {
|
|
584
|
+
const connectionState = useConnectionState();
|
|
585
|
+
const isConnected = connectionState === ConnectionState.Connected;
|
|
586
|
+
const { messages, agentState } = useTranscriptMessages();
|
|
587
|
+
const { phases } = usePhaseUpdates();
|
|
588
|
+
const { questions } = useQuestionUpdates();
|
|
589
|
+
if (!isConnected) {
|
|
590
|
+
return /* @__PURE__ */ jsx9("div", {
|
|
591
|
+
className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
|
|
592
|
+
children: /* @__PURE__ */ jsx9("p", {
|
|
593
|
+
className: "skippr:text-sm skippr:text-muted-foreground",
|
|
594
|
+
children: "Connecting..."
|
|
595
|
+
})
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const isAgentSpeaking = agentState === "speaking";
|
|
599
|
+
return /* @__PURE__ */ jsxs7(Fragment, {
|
|
600
|
+
children: [
|
|
601
|
+
/* @__PURE__ */ jsx9(MeetingControls, {
|
|
602
|
+
onHangUp: onDisconnect
|
|
603
|
+
}),
|
|
604
|
+
/* @__PURE__ */ jsxs7("div", {
|
|
605
|
+
className: "skippr:flex skippr:min-h-0 skippr:flex-1",
|
|
606
|
+
children: [
|
|
607
|
+
/* @__PURE__ */ jsx9("div", {
|
|
608
|
+
className: "skippr:w-[260px] skippr:shrink-0 skippr:overflow-y-auto skippr:border-r",
|
|
609
|
+
children: /* @__PURE__ */ jsx9(SessionAgenda, {
|
|
610
|
+
phases,
|
|
611
|
+
questions
|
|
612
|
+
})
|
|
613
|
+
}),
|
|
614
|
+
/* @__PURE__ */ jsx9("div", {
|
|
615
|
+
className: "skippr:flex skippr:min-w-0 skippr:flex-1 skippr:flex-col",
|
|
616
|
+
children: /* @__PURE__ */ jsx9(MessageList, {
|
|
617
|
+
messages,
|
|
618
|
+
isStreaming: isAgentSpeaking
|
|
619
|
+
})
|
|
620
|
+
})
|
|
621
|
+
]
|
|
622
|
+
})
|
|
623
|
+
]
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/components/SidebarTrigger.tsx
|
|
628
|
+
import { MessageCircle, X as X2 } from "lucide-react";
|
|
629
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
630
|
+
var TRIGGER_GAP = 16;
|
|
631
|
+
var TRIGGER_DEFAULT_RIGHT = 24;
|
|
632
|
+
function SidebarTrigger() {
|
|
633
|
+
const { isPanelOpen, togglePanel } = useLiveAgent();
|
|
634
|
+
return /* @__PURE__ */ jsx10(Button, {
|
|
635
|
+
size: "icon-lg",
|
|
636
|
+
onClick: togglePanel,
|
|
637
|
+
className: "skippr:fixed skippr:bottom-6 skippr:z-[9998] skippr:size-14 skippr:rounded-full skippr:shadow-lg skippr:transition-all skippr:duration-300",
|
|
638
|
+
style: { right: isPanelOpen ? SIDEBAR_WIDTH + TRIGGER_GAP : TRIGGER_DEFAULT_RIGHT },
|
|
639
|
+
title: isPanelOpen ? "Close chat" : "Chat with us",
|
|
640
|
+
children: isPanelOpen ? /* @__PURE__ */ jsx10(X2, {
|
|
641
|
+
className: "skippr:size-6"
|
|
642
|
+
}) : /* @__PURE__ */ jsx10(MessageCircle, {
|
|
643
|
+
className: "skippr:size-6"
|
|
644
|
+
})
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/components/LiveAgent.tsx
|
|
649
|
+
import { jsx as jsx11, jsxs as jsxs8, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
650
|
+
function LiveAgent({
|
|
651
|
+
organizationId,
|
|
652
|
+
agentId,
|
|
653
|
+
defaultOpen = false,
|
|
654
|
+
children
|
|
655
|
+
}) {
|
|
656
|
+
const { connection, shouldConnect, isStarting, error, startSession, disconnect } = useSession({
|
|
657
|
+
organizationId,
|
|
658
|
+
agentId
|
|
659
|
+
});
|
|
660
|
+
const [isPanelOpen, setIsPanelOpen] = useState4(defaultOpen);
|
|
661
|
+
const openPanel = useCallback6(() => setIsPanelOpen(true), []);
|
|
662
|
+
const closePanel = useCallback6(() => setIsPanelOpen(false), []);
|
|
663
|
+
const togglePanel = useCallback6(() => setIsPanelOpen((prev) => !prev), []);
|
|
664
|
+
const isConnected = connection !== null;
|
|
665
|
+
const ctx = useMemo2(() => ({
|
|
666
|
+
connection,
|
|
667
|
+
shouldConnect,
|
|
668
|
+
isConnected,
|
|
669
|
+
isStarting,
|
|
670
|
+
error,
|
|
671
|
+
startSession,
|
|
672
|
+
disconnect,
|
|
673
|
+
isPanelOpen,
|
|
674
|
+
openPanel,
|
|
675
|
+
closePanel,
|
|
676
|
+
togglePanel
|
|
677
|
+
}), [
|
|
678
|
+
connection,
|
|
679
|
+
shouldConnect,
|
|
680
|
+
isConnected,
|
|
681
|
+
isStarting,
|
|
682
|
+
error,
|
|
683
|
+
startSession,
|
|
684
|
+
disconnect,
|
|
685
|
+
isPanelOpen,
|
|
686
|
+
openPanel,
|
|
687
|
+
closePanel,
|
|
688
|
+
togglePanel
|
|
689
|
+
]);
|
|
690
|
+
const widgetContent = /* @__PURE__ */ jsxs8(Fragment2, {
|
|
691
|
+
children: [
|
|
692
|
+
connection && /* @__PURE__ */ jsx11(RoomAudioRenderer, {}),
|
|
693
|
+
/* @__PURE__ */ jsx11(SidebarTrigger, {}),
|
|
694
|
+
/* @__PURE__ */ jsx11(Sidebar, {}),
|
|
695
|
+
children
|
|
696
|
+
]
|
|
697
|
+
});
|
|
698
|
+
return /* @__PURE__ */ jsx11(LiveAgentContext.Provider, {
|
|
699
|
+
value: ctx,
|
|
700
|
+
children: connection ? /* @__PURE__ */ jsx11(LiveKitRoom, {
|
|
701
|
+
serverUrl: connection.livekitUrl,
|
|
702
|
+
token: connection.token,
|
|
703
|
+
connect: shouldConnect,
|
|
704
|
+
audio: true,
|
|
705
|
+
onDisconnected: disconnect,
|
|
706
|
+
children: widgetContent
|
|
707
|
+
}) : widgetContent
|
|
708
|
+
});
|
|
709
|
+
}
|
|
1
710
|
export {
|
|
2
711
|
useLiveAgent,
|
|
3
712
|
LiveAgent
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export * from './components/LiveAgent';
|
|
2
|
+
export * from './hooks/useLiveAgent';
|