@skippr/live-agent-sdk 0.5.0 → 0.6.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.
@@ -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 { jsxDEV } from "react/jsx-dev-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__ */ jsxDEV("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
+ }, undefined, false, undefined, this);
237
+ });
238
+ Button.displayName = "Button";
239
+
240
+ // src/components/ChatHeader.tsx
241
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
242
+ function ChatHeader({ onClose }) {
243
+ return /* @__PURE__ */ jsxDEV2("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__ */ jsxDEV2("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__ */ jsxDEV2(Bot, {
249
+ className: "skippr:size-3.5 skippr:text-primary-foreground"
250
+ }, undefined, false, undefined, this)
251
+ }, undefined, false, undefined, this),
252
+ /* @__PURE__ */ jsxDEV2("div", {
253
+ className: "skippr:flex-1",
254
+ children: /* @__PURE__ */ jsxDEV2("p", {
255
+ className: "skippr:text-sm skippr:font-semibold skippr:leading-none",
256
+ children: "AI Agent"
257
+ }, undefined, false, undefined, this)
258
+ }, undefined, false, undefined, this),
259
+ /* @__PURE__ */ jsxDEV2(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__ */ jsxDEV2(X, {
265
+ className: "skippr:size-4"
266
+ }, undefined, false, undefined, this)
267
+ }, undefined, false, undefined, this)
268
+ ]
269
+ }, undefined, true, undefined, this);
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 { jsxDEV as jsxDEV3 } from "react/jsx-dev-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__ */ jsxDEV3("div", {
318
+ className: "skippr:flex skippr:items-center skippr:justify-between skippr:border-b skippr:px-4 skippr:py-3",
319
+ children: [
320
+ /* @__PURE__ */ jsxDEV3("div", {
321
+ className: "skippr:flex skippr:items-center skippr:gap-2",
322
+ children: [
323
+ /* @__PURE__ */ jsxDEV3(Button, {
324
+ size: "icon-sm",
325
+ variant: isMuted ? "destructive" : "outline",
326
+ onClick: toggleMute,
327
+ "aria-label": isMuted ? "Unmute" : "Mute",
328
+ children: isMuted ? /* @__PURE__ */ jsxDEV3(MicOff, {
329
+ className: "skippr:size-4"
330
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Mic, {
331
+ className: "skippr:size-4"
332
+ }, undefined, false, undefined, this)
333
+ }, undefined, false, undefined, this),
334
+ /* @__PURE__ */ jsxDEV3(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__ */ jsxDEV3(MonitorOff, {
340
+ className: "skippr:size-4"
341
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Monitor, {
342
+ className: "skippr:size-4"
343
+ }, undefined, false, undefined, this)
344
+ }, undefined, false, undefined, this)
345
+ ]
346
+ }, undefined, true, undefined, this),
347
+ /* @__PURE__ */ jsxDEV3("span", {
348
+ className: "skippr:text-sm skippr:font-medium skippr:tabular-nums skippr:text-muted-foreground",
349
+ children: formatTime(elapsed)
350
+ }, undefined, false, undefined, this),
351
+ /* @__PURE__ */ jsxDEV3(Button, {
352
+ size: "icon-sm",
353
+ variant: "destructive",
354
+ onClick: onHangUp,
355
+ "aria-label": "Hang up",
356
+ children: /* @__PURE__ */ jsxDEV3(PhoneOff, {
357
+ className: "skippr:size-4"
358
+ }, undefined, false, undefined, this)
359
+ }, undefined, false, undefined, this)
360
+ ]
361
+ }, undefined, true, undefined, this);
362
+ }
363
+
364
+ // src/components/MessageList.tsx
365
+ import { useCallback as useCallback5 } from "react";
366
+
367
+ // src/components/ChatMessage.tsx
368
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
369
+ function ChatMessage({ message }) {
370
+ const isUser = message.role === "user";
371
+ return /* @__PURE__ */ jsxDEV4("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__ */ jsxDEV4("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
+ }, undefined, false, undefined, this)
377
+ }, undefined, false, undefined, this);
378
+ }
379
+
380
+ // src/components/TypingIndicator.tsx
381
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
382
+ function TypingIndicator() {
383
+ return /* @__PURE__ */ jsxDEV5("div", {
384
+ className: "skippr:flex skippr:items-center skippr:gap-1 skippr:px-4 skippr:py-3",
385
+ children: /* @__PURE__ */ jsxDEV5("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__ */ jsxDEV5("span", {
389
+ className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:0ms]"
390
+ }, undefined, false, undefined, this),
391
+ /* @__PURE__ */ jsxDEV5("span", {
392
+ className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:150ms]"
393
+ }, undefined, false, undefined, this),
394
+ /* @__PURE__ */ jsxDEV5("span", {
395
+ className: "skippr:size-1.5 skippr:animate-bounce skippr:rounded-full skippr:bg-muted-foreground/60 skippr:[animation-delay:300ms]"
396
+ }, undefined, false, undefined, this)
397
+ ]
398
+ }, undefined, true, undefined, this)
399
+ }, undefined, false, undefined, this);
400
+ }
401
+
402
+ // src/components/MessageList.tsx
403
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-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__ */ jsxDEV6("div", {
411
+ className: "skippr:flex-1 skippr:overflow-y-auto",
412
+ children: /* @__PURE__ */ jsxDEV6("div", {
413
+ className: "skippr:flex skippr:flex-col skippr:gap-1 skippr:py-3",
414
+ children: [
415
+ messages.map((message) => /* @__PURE__ */ jsxDEV6(ChatMessage, {
416
+ message
417
+ }, message.id, false, undefined, this)),
418
+ showTyping && /* @__PURE__ */ jsxDEV6(TypingIndicator, {}, undefined, false, undefined, this),
419
+ /* @__PURE__ */ jsxDEV6("div", {
420
+ ref: scrollRef
421
+ }, `scroll-${messages.length}`, false, undefined, this)
422
+ ]
423
+ }, undefined, true, undefined, this)
424
+ }, undefined, false, undefined, this);
425
+ }
426
+
427
+ // src/components/QuickActions.tsx
428
+ import { Loader2, MessageCircleQuestion } from "lucide-react";
429
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
430
+ function QuickActions({ onStartSession, isStarting, error }) {
431
+ return /* @__PURE__ */ jsxDEV7("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__ */ jsxDEV7("p", {
435
+ className: "skippr:mb-1 skippr:text-sm skippr:text-muted-foreground",
436
+ children: "How can I help you today?"
437
+ }, undefined, false, undefined, this),
438
+ /* @__PURE__ */ jsxDEV7(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__ */ jsxDEV7(Loader2, {
445
+ className: "skippr:size-4 skippr:animate-spin skippr:text-primary"
446
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(MessageCircleQuestion, {
447
+ className: "skippr:size-4 skippr:text-primary"
448
+ }, undefined, false, undefined, this),
449
+ isStarting ? "Starting..." : "Start Session"
450
+ ]
451
+ }, undefined, true, undefined, this),
452
+ error && /* @__PURE__ */ jsxDEV7("p", {
453
+ className: "skippr:text-xs skippr:text-destructive",
454
+ children: error
455
+ }, undefined, false, undefined, this)
456
+ ]
457
+ }, undefined, true, undefined, this);
458
+ }
459
+
460
+ // src/components/SessionAgenda.tsx
461
+ import { Check, Circle, Loader2 as Loader22 } from "lucide-react";
462
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
463
+ function SessionAgenda({ phases, questions = [] }) {
464
+ if (phases.length === 0) {
465
+ return /* @__PURE__ */ jsxDEV8("div", {
466
+ className: "skippr:flex skippr:flex-col skippr:gap-3 skippr:p-4",
467
+ children: [
468
+ /* @__PURE__ */ jsxDEV8("h3", {
469
+ className: "skippr:text-sm skippr:font-semibold",
470
+ children: "Agenda"
471
+ }, undefined, false, undefined, this),
472
+ /* @__PURE__ */ jsxDEV8("p", {
473
+ className: "skippr:text-xs skippr:text-muted-foreground",
474
+ children: "Waiting for session..."
475
+ }, undefined, false, undefined, this)
476
+ ]
477
+ }, undefined, true, undefined, this);
478
+ }
479
+ return /* @__PURE__ */ jsxDEV8("div", {
480
+ className: "skippr:flex skippr:flex-col skippr:gap-3 skippr:p-4",
481
+ children: [
482
+ /* @__PURE__ */ jsxDEV8("h3", {
483
+ className: "skippr:text-sm skippr:font-semibold",
484
+ children: "Agenda"
485
+ }, undefined, false, undefined, this),
486
+ /* @__PURE__ */ jsxDEV8("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__ */ jsxDEV8("li", {
493
+ className: "skippr:flex skippr:flex-col skippr:gap-0.5",
494
+ children: [
495
+ /* @__PURE__ */ jsxDEV8("div", {
496
+ className: "skippr:flex skippr:items-center skippr:gap-2 skippr:text-sm",
497
+ children: [
498
+ /* @__PURE__ */ jsxDEV8(PhaseIcon, {
499
+ status: phase.status
500
+ }, undefined, false, undefined, this),
501
+ /* @__PURE__ */ jsxDEV8("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
+ }, undefined, false, undefined, this)
505
+ ]
506
+ }, undefined, true, undefined, this),
507
+ totalCount > 0 && /* @__PURE__ */ jsxDEV8("span", {
508
+ className: "skippr:ml-6 skippr:text-xs skippr:text-muted-foreground",
509
+ children: [
510
+ answeredCount,
511
+ "/",
512
+ totalCount,
513
+ " answered"
514
+ ]
515
+ }, undefined, true, undefined, this)
516
+ ]
517
+ }, phase.name, true, undefined, this);
518
+ })
519
+ }, undefined, false, undefined, this)
520
+ ]
521
+ }, undefined, true, undefined, this);
522
+ }
523
+ function PhaseIcon({ status }) {
524
+ if (status === "completed") {
525
+ return /* @__PURE__ */ jsxDEV8("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__ */ jsxDEV8(Check, {
528
+ className: "skippr:size-2.5 skippr:text-primary-foreground",
529
+ strokeWidth: 3
530
+ }, undefined, false, undefined, this)
531
+ }, undefined, false, undefined, this);
532
+ }
533
+ if (status === "active") {
534
+ return /* @__PURE__ */ jsxDEV8(Loader22, {
535
+ className: "skippr:size-4 skippr:shrink-0 skippr:text-primary skippr:animate-spin"
536
+ }, undefined, false, undefined, this);
537
+ }
538
+ return /* @__PURE__ */ jsxDEV8(Circle, {
539
+ className: "skippr:size-4 skippr:shrink-0 skippr:text-muted-foreground"
540
+ }, undefined, false, undefined, this);
541
+ }
542
+
543
+ // src/components/Sidebar.tsx
544
+ import { jsxDEV as jsxDEV9, Fragment } from "react/jsx-dev-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__ */ jsxDEV9("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__ */ jsxDEV9(Fragment, {
561
+ children: [
562
+ /* @__PURE__ */ jsxDEV9(ChatHeader, {
563
+ onClose: closePanel
564
+ }, undefined, false, undefined, this),
565
+ /* @__PURE__ */ jsxDEV9(ConnectedContent, {
566
+ onDisconnect: disconnect
567
+ }, undefined, false, undefined, this)
568
+ ]
569
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV9(Fragment, {
570
+ children: [
571
+ /* @__PURE__ */ jsxDEV9(ChatHeader, {
572
+ onClose: closePanel
573
+ }, undefined, false, undefined, this),
574
+ /* @__PURE__ */ jsxDEV9(QuickActions, {
575
+ onStartSession: startSession,
576
+ isStarting,
577
+ error
578
+ }, undefined, false, undefined, this)
579
+ ]
580
+ }, undefined, true, undefined, this)
581
+ }, undefined, false, undefined, this);
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__ */ jsxDEV9("div", {
591
+ className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
592
+ children: /* @__PURE__ */ jsxDEV9("p", {
593
+ className: "skippr:text-sm skippr:text-muted-foreground",
594
+ children: "Connecting..."
595
+ }, undefined, false, undefined, this)
596
+ }, undefined, false, undefined, this);
597
+ }
598
+ const isAgentSpeaking = agentState === "speaking";
599
+ return /* @__PURE__ */ jsxDEV9(Fragment, {
600
+ children: [
601
+ /* @__PURE__ */ jsxDEV9(MeetingControls, {
602
+ onHangUp: onDisconnect
603
+ }, undefined, false, undefined, this),
604
+ /* @__PURE__ */ jsxDEV9("div", {
605
+ className: "skippr:flex skippr:min-h-0 skippr:flex-1",
606
+ children: [
607
+ /* @__PURE__ */ jsxDEV9("div", {
608
+ className: "skippr:w-[260px] skippr:shrink-0 skippr:overflow-y-auto skippr:border-r",
609
+ children: /* @__PURE__ */ jsxDEV9(SessionAgenda, {
610
+ phases,
611
+ questions
612
+ }, undefined, false, undefined, this)
613
+ }, undefined, false, undefined, this),
614
+ /* @__PURE__ */ jsxDEV9("div", {
615
+ className: "skippr:flex skippr:min-w-0 skippr:flex-1 skippr:flex-col",
616
+ children: /* @__PURE__ */ jsxDEV9(MessageList, {
617
+ messages,
618
+ isStreaming: isAgentSpeaking
619
+ }, undefined, false, undefined, this)
620
+ }, undefined, false, undefined, this)
621
+ ]
622
+ }, undefined, true, undefined, this)
623
+ ]
624
+ }, undefined, true, undefined, this);
625
+ }
626
+
627
+ // src/components/SidebarTrigger.tsx
628
+ import { MessageCircle, X as X2 } from "lucide-react";
629
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
630
+ var TRIGGER_GAP = 16;
631
+ var TRIGGER_DEFAULT_RIGHT = 24;
632
+ function SidebarTrigger() {
633
+ const { isPanelOpen, togglePanel } = useLiveAgent();
634
+ return /* @__PURE__ */ jsxDEV10(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__ */ jsxDEV10(X2, {
641
+ className: "skippr:size-6"
642
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV10(MessageCircle, {
643
+ className: "skippr:size-6"
644
+ }, undefined, false, undefined, this)
645
+ }, undefined, false, undefined, this);
646
+ }
647
+
648
+ // src/components/LiveAgent.tsx
649
+ import { jsxDEV as jsxDEV11, Fragment as Fragment2 } from "react/jsx-dev-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__ */ jsxDEV11(Fragment2, {
691
+ children: [
692
+ connection && /* @__PURE__ */ jsxDEV11(RoomAudioRenderer, {}, undefined, false, undefined, this),
693
+ /* @__PURE__ */ jsxDEV11(SidebarTrigger, {}, undefined, false, undefined, this),
694
+ /* @__PURE__ */ jsxDEV11(Sidebar, {}, undefined, false, undefined, this),
695
+ children
696
+ ]
697
+ }, undefined, true, undefined, this);
698
+ return /* @__PURE__ */ jsxDEV11(LiveAgentContext.Provider, {
699
+ value: ctx,
700
+ children: connection ? /* @__PURE__ */ jsxDEV11(LiveKitRoom, {
701
+ serverUrl: connection.livekitUrl,
702
+ token: connection.token,
703
+ connect: shouldConnect,
704
+ audio: true,
705
+ onDisconnected: disconnect,
706
+ children: widgetContent
707
+ }, undefined, false, undefined, this) : widgetContent
708
+ }, undefined, false, undefined, this);
709
+ }
1
710
  export {
2
711
  useLiveAgent,
3
712
  LiveAgent
@@ -1,2 +1,2 @@
1
- export { LiveAgent } from './components/LiveAgent';
2
- export { useLiveAgent } from './hooks/useLiveAgent';
1
+ export * from './components/LiveAgent';
2
+ export * from './hooks/useLiveAgent';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skippr/live-agent-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/esm/lib-exports.js",
6
6
  "module": "dist/esm/lib-exports.js",