diversion-crumb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/CrumbProvider.d.ts +12 -0
- package/dist/CrumbProvider.d.ts.map +1 -0
- package/dist/CrumbProvider.js +394 -0
- package/dist/CrumbWidget.d.ts +12 -0
- package/dist/CrumbWidget.d.ts.map +1 -0
- package/dist/CrumbWidget.js +248 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @diversion/crumb
|
|
2
|
+
|
|
3
|
+
Crumb voice assistant SDK. Install in your app, set your API key and **adapters**, and you get a full bakery-style experience: voice + chat widget, cart proposals, navigation, and discounts. The SDK fetches assistant config from the Crumb IaaS backend; later you can customise tools and system prompts via the provider.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @diversion/crumb @vapi-ai/web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Env
|
|
12
|
+
|
|
13
|
+
- `NEXT_PUBLIC_CRUMB_API_KEY` – Your Crumb API key (from the IaaS provider).
|
|
14
|
+
- `NEXT_PUBLIC_CRUMB_BACKEND_URL` – Crumb backend base URL (e.g. `https://api.crumb.example.com` or `http://localhost:3001` for dev).
|
|
15
|
+
- `NEXT_PUBLIC_VAPI_PUBLIC_KEY` – Your VAPI public key (from the same provider or your own VAPI project).
|
|
16
|
+
|
|
17
|
+
## Quick start (full UI)
|
|
18
|
+
|
|
19
|
+
Wrap your app with `CrumbProvider`, pass adapters, and render `<CrumbWidget />`. You get floating voice + chat buttons, sidebar with messages and cart proposal cards (Yes/No), mic selector, and call controls. No extra UI code.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { CrumbProvider, CrumbWidget } from "@diversion/crumb";
|
|
23
|
+
import { useRouter } from "next/navigation";
|
|
24
|
+
|
|
25
|
+
function App({ children }) {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const adapters = {
|
|
28
|
+
navigate: (path) => router.push(path),
|
|
29
|
+
applyDiscount: (percent) => { /* update your cart */ },
|
|
30
|
+
removeFromCart: async (cartItemId) => { /* call your API */ },
|
|
31
|
+
onCartUpdated: () => { /* reload cart */ },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<CrumbProvider
|
|
36
|
+
apiKey={process.env.NEXT_PUBLIC_CRUMB_API_KEY!}
|
|
37
|
+
backendUrl={process.env.NEXT_PUBLIC_CRUMB_BACKEND_URL!}
|
|
38
|
+
vapiPublicKey={process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY!}
|
|
39
|
+
adapters={adapters}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
<CrumbWidget assistantName="Crumb" />
|
|
43
|
+
</CrumbProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When the assistant calls `proposeCartUpdate`, the widget shows the proposal cards and Yes/No automatically. Optional: pass `onCartProposal` if you want to show your own UI and call `approveProposal()` / `rejectProposal()` from `useCrumb()`.
|
|
49
|
+
|
|
50
|
+
## Headless (custom UI)
|
|
51
|
+
|
|
52
|
+
Use `useCrumb()` and build your own UI:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { CrumbProvider, useCrumb } from "@diversion/crumb";
|
|
56
|
+
|
|
57
|
+
// Wrap with CrumbProvider and adapters, then:
|
|
58
|
+
const { status, startCall, endCall, messages, pendingProposal, approveProposal, rejectProposal } = useCrumb();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Adapters
|
|
62
|
+
|
|
63
|
+
| Adapter | Required | Description |
|
|
64
|
+
|--------|----------|-------------|
|
|
65
|
+
| `navigate` | Yes | Navigate to a path (e.g. `router.push(path)`). |
|
|
66
|
+
| `onCartProposal` | No | Called when the assistant shows a cart update proposal; show your UI and use `approveProposal()` / `rejectProposal()` from `useCrumb()`. |
|
|
67
|
+
| `applyDiscount` | No | Apply a discount percentage to the cart. |
|
|
68
|
+
| `removeFromCart` | No | Remove a cart item by ID (async). |
|
|
69
|
+
| `onCartUpdated` | No | Called after server-side cart tools run; reload your cart. |
|
|
70
|
+
|
|
71
|
+
## Provider selection and env
|
|
72
|
+
|
|
73
|
+
Your IaaS provider may give you an env template based on selected providers (STT, TTS, LLM). Set the variables they provide; the SDK only needs `NEXT_PUBLIC_CRUMB_API_KEY`, `NEXT_PUBLIC_CRUMB_BACKEND_URL`, and `NEXT_PUBLIC_VAPI_PUBLIC_KEY`.
|
|
74
|
+
|
|
75
|
+
## Scaffolding a new project
|
|
76
|
+
|
|
77
|
+
For a new Next.js app from scratch, you can use **create-vapi-agent** (`packages/create-agent` in this repo) to scaffold a project with Vapi, shadcn/ui, and a voice agent. For the Crumb IaaS flow (our backend + tools + system prompts), use this SDK instead: install `@diversion/crumb`, add `CrumbProvider` + `CrumbWidget` and adapters, and point env at the Crumb backend.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CrumbAdapters, type CrumbContextValue, type CrumbIdentity } from "./types";
|
|
2
|
+
export interface CrumbProviderProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
backendUrl: string;
|
|
6
|
+
vapiPublicKey: string;
|
|
7
|
+
adapters: CrumbAdapters;
|
|
8
|
+
identity?: CrumbIdentity;
|
|
9
|
+
}
|
|
10
|
+
export declare function CrumbProvider({ children, apiKey, backendUrl, vapiPublicKey, adapters, identity }: CrumbProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function useCrumb(): CrumbContextValue;
|
|
12
|
+
//# sourceMappingURL=CrumbProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrumbProvider.d.ts","sourceRoot":"","sources":["../src/CrumbProvider.tsx"],"names":[],"mappings":"AAYA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAMnB,MAAM,SAAS,CAAC;AA8CjB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CAgXpH;AAED,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C"}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react";
|
|
4
|
+
import { fetchAssistantConfig } from "./config";
|
|
5
|
+
import { CLIENT_TOOL_NAMES, } from "./types";
|
|
6
|
+
const SESSION_STORAGE_KEY = "crumb_session_id";
|
|
7
|
+
const MAX_POLICY_DISCOUNT = 20;
|
|
8
|
+
function parseToolArgs(raw) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return {};
|
|
11
|
+
if (typeof raw === "object")
|
|
12
|
+
return raw;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizePath(raw) {
|
|
21
|
+
const value = String(raw ?? "/").trim();
|
|
22
|
+
const lower = value.toLowerCase();
|
|
23
|
+
const aliasMap = {
|
|
24
|
+
"/home": "/", "/homepage": "/", "home": "/",
|
|
25
|
+
"/product": "/products", "products": "/products",
|
|
26
|
+
"/my-orders": "/account", "/orders": "/account", "orders": "/account", "/order-history": "/account",
|
|
27
|
+
};
|
|
28
|
+
const aliased = aliasMap[lower] ?? value;
|
|
29
|
+
const path = aliased.startsWith("/") ? aliased : `/${aliased}`;
|
|
30
|
+
const blocked = path === "/admin" || path.startsWith("/admin/");
|
|
31
|
+
return { path, blocked };
|
|
32
|
+
}
|
|
33
|
+
const CrumbContext = createContext(undefined);
|
|
34
|
+
export function CrumbProvider({ children, apiKey, backendUrl, vapiPublicKey, adapters, identity }) {
|
|
35
|
+
const vapiRef = useRef(null);
|
|
36
|
+
const isStartingRef = useRef(false);
|
|
37
|
+
const [status, setStatus] = useState("idle");
|
|
38
|
+
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
39
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
40
|
+
const [messages, setMessages] = useState([]);
|
|
41
|
+
const [liveTranscript, setLiveTranscript] = useState(null);
|
|
42
|
+
const [pendingProposal, setPendingProposal] = useState(null);
|
|
43
|
+
const [conversationId, setConversationId] = useState(null);
|
|
44
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
45
|
+
const [selectedMicrophoneId, setSelectedMicrophoneId] = useState("");
|
|
46
|
+
const pendingUserMessageRef = useRef(null);
|
|
47
|
+
const pendingMicrophoneIdRef = useRef("");
|
|
48
|
+
const sessionIdRef = useRef(null);
|
|
49
|
+
const MIC_STORAGE_KEY = "crumb_selected_mic_id";
|
|
50
|
+
const handleClientToolRef = useRef(async () => { });
|
|
51
|
+
const onMessageRef = useRef(() => { });
|
|
52
|
+
const shouldPersistEscalatedRef = useRef(false);
|
|
53
|
+
const autoMutedForTextRef = useRef(false);
|
|
54
|
+
const mutedBeforeTextRef = useRef(false);
|
|
55
|
+
const handleClientTool = useCallback(async (vapi, call) => {
|
|
56
|
+
const { name } = call.function;
|
|
57
|
+
const args = parseToolArgs(call.function.arguments);
|
|
58
|
+
let result = "done";
|
|
59
|
+
try {
|
|
60
|
+
switch (name) {
|
|
61
|
+
case "navigateTo": {
|
|
62
|
+
const { path, blocked } = normalizePath(args.path);
|
|
63
|
+
if (blocked) {
|
|
64
|
+
result = "Admin pages are restricted. I can navigate to public pages only.";
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
adapters.navigate(path);
|
|
68
|
+
result = `Navigated to ${path}`;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "openCartDrawer": {
|
|
72
|
+
adapters.navigate("/cart");
|
|
73
|
+
result = "Navigated to /cart";
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "removeFromCart": {
|
|
77
|
+
const cartItemId = String(args.cartItemId ?? "");
|
|
78
|
+
if (adapters.removeFromCart) {
|
|
79
|
+
await adapters.removeFromCart(cartItemId);
|
|
80
|
+
result = "Removed item from cart";
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
result = "removeFromCart adapter not provided";
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "proposeCartUpdate": {
|
|
88
|
+
const items = args.items ?? [];
|
|
89
|
+
const message = String(args.message ?? "Do you want me to update your cart?");
|
|
90
|
+
setPendingProposal({ toolCallId: call.id, message, items });
|
|
91
|
+
setIsSidebarOpen(true);
|
|
92
|
+
if (adapters.onCartProposal) {
|
|
93
|
+
adapters.onCartProposal({ toolCallId: call.id, message, items });
|
|
94
|
+
}
|
|
95
|
+
result = "Confirmation cards displayed. Ask the customer to confirm or decline.";
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "applyDiscount": {
|
|
99
|
+
const percent = Number(args.discountPercent ?? 0);
|
|
100
|
+
if (!Number.isFinite(percent) || percent < 0) {
|
|
101
|
+
result = "Invalid discount percentage.";
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const capped = Math.min(percent, MAX_POLICY_DISCOUNT);
|
|
105
|
+
if (adapters.applyDiscount) {
|
|
106
|
+
adapters.applyDiscount(capped);
|
|
107
|
+
}
|
|
108
|
+
result = capped > 0 ? `${capped}% discount applied.` : "Discount cleared.";
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
result = `Unknown client tool: ${name}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
result = `Error: ${err instanceof Error ? err.message : "Tool failed"}`;
|
|
117
|
+
}
|
|
118
|
+
vapi.send({
|
|
119
|
+
type: "add-message",
|
|
120
|
+
message: { role: "tool", tool_call_id: call.id, content: result },
|
|
121
|
+
triggerResponseEnabled: true,
|
|
122
|
+
});
|
|
123
|
+
}, [adapters]);
|
|
124
|
+
const handleMessage = useCallback((msg) => {
|
|
125
|
+
if (msg.type === "transcript") {
|
|
126
|
+
const role = (msg.role ?? "assistant");
|
|
127
|
+
const content = msg.transcript ?? "";
|
|
128
|
+
if (msg.transcriptType === "partial") {
|
|
129
|
+
setLiveTranscript({ role, content });
|
|
130
|
+
}
|
|
131
|
+
else if (msg.transcriptType === "final" && content.trim()) {
|
|
132
|
+
setLiveTranscript(null);
|
|
133
|
+
setMessages((prev) => [...prev, { id: crypto.randomUUID(), role, content, timestamp: Date.now() }]);
|
|
134
|
+
if (role === "assistant" && autoMutedForTextRef.current && vapiRef.current) {
|
|
135
|
+
vapiRef.current.setMuted(mutedBeforeTextRef.current);
|
|
136
|
+
setIsMuted(mutedBeforeTextRef.current);
|
|
137
|
+
autoMutedForTextRef.current = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const vapi = vapiRef.current;
|
|
143
|
+
if (!vapi)
|
|
144
|
+
return;
|
|
145
|
+
let callList = [];
|
|
146
|
+
if (msg.type === "tool-calls" && Array.isArray(msg.toolCallList)) {
|
|
147
|
+
callList = msg.toolCallList.filter((c) => CLIENT_TOOL_NAMES.has(c.function.name));
|
|
148
|
+
const overPolicy = msg.toolCallList.some((tc) => {
|
|
149
|
+
if (tc.function.name !== "requestSupervisorApproval")
|
|
150
|
+
return false;
|
|
151
|
+
const a = parseToolArgs(tc.function.arguments);
|
|
152
|
+
return Number(a.requestedDiscountPercent) > MAX_POLICY_DISCOUNT;
|
|
153
|
+
});
|
|
154
|
+
if (overPolicy && status === "active") {
|
|
155
|
+
shouldPersistEscalatedRef.current = true;
|
|
156
|
+
setStatus("escalated");
|
|
157
|
+
setIsSidebarOpen(true);
|
|
158
|
+
setLiveTranscript(null);
|
|
159
|
+
setPendingProposal(null);
|
|
160
|
+
vapi.stop();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (msg.type === "function-call" && msg.functionCall && CLIENT_TOOL_NAMES.has(msg.functionCall.name)) {
|
|
164
|
+
callList.push({
|
|
165
|
+
id: msg.functionCall.id ?? crypto.randomUUID(),
|
|
166
|
+
type: "function",
|
|
167
|
+
function: { name: msg.functionCall.name, arguments: JSON.stringify(msg.functionCall.parameters) },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const cartTools = new Set(["addToCart", "updateCartItemQuantity"]);
|
|
171
|
+
const allCalls = msg.toolCallList ?? (msg.functionCall ? [{ function: { name: msg.functionCall.name } }] : []);
|
|
172
|
+
if (allCalls.some((c) => cartTools.has(c.function.name)) && adapters.onCartUpdated) {
|
|
173
|
+
setTimeout(() => adapters.onCartUpdated(), 2000);
|
|
174
|
+
}
|
|
175
|
+
for (const call of callList) {
|
|
176
|
+
void handleClientToolRef.current(vapi, call);
|
|
177
|
+
}
|
|
178
|
+
}, [status, adapters]);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
handleClientToolRef.current = handleClientTool;
|
|
181
|
+
}, [handleClientTool]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
onMessageRef.current = handleMessage;
|
|
184
|
+
}, [handleMessage]);
|
|
185
|
+
const getVapi = useCallback(async () => {
|
|
186
|
+
if (vapiRef.current)
|
|
187
|
+
return vapiRef.current;
|
|
188
|
+
const { default: VapiSDK } = await import("@vapi-ai/web");
|
|
189
|
+
const instance = new VapiSDK(vapiPublicKey);
|
|
190
|
+
instance.on("call-start", () => {
|
|
191
|
+
setStatus("active");
|
|
192
|
+
if (!pendingUserMessageRef.current)
|
|
193
|
+
setMessages([]);
|
|
194
|
+
setLiveTranscript(null);
|
|
195
|
+
setPendingProposal(null);
|
|
196
|
+
setIsSidebarOpen(true);
|
|
197
|
+
if (autoMutedForTextRef.current) {
|
|
198
|
+
instance.setMuted(true);
|
|
199
|
+
setIsMuted(true);
|
|
200
|
+
}
|
|
201
|
+
if (pendingMicrophoneIdRef.current) {
|
|
202
|
+
try {
|
|
203
|
+
instance.setInputDevicesAsync?.({ audioSource: pendingMicrophoneIdRef.current });
|
|
204
|
+
}
|
|
205
|
+
catch { /* ignore */ }
|
|
206
|
+
}
|
|
207
|
+
if (pendingUserMessageRef.current) {
|
|
208
|
+
instance.send({ type: "add-message", message: { role: "user", content: pendingUserMessageRef.current }, triggerResponseEnabled: true });
|
|
209
|
+
pendingUserMessageRef.current = null;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
instance.on("call-end", () => {
|
|
213
|
+
if (shouldPersistEscalatedRef.current) {
|
|
214
|
+
setStatus("escalated");
|
|
215
|
+
setIsSidebarOpen(true);
|
|
216
|
+
shouldPersistEscalatedRef.current = false;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
setStatus("idle");
|
|
220
|
+
setIsSidebarOpen(false);
|
|
221
|
+
}
|
|
222
|
+
setIsSpeaking(false);
|
|
223
|
+
setIsMuted(false);
|
|
224
|
+
setLiveTranscript(null);
|
|
225
|
+
setPendingProposal(null);
|
|
226
|
+
isStartingRef.current = false;
|
|
227
|
+
});
|
|
228
|
+
instance.on("speech-start", () => setIsSpeaking(true));
|
|
229
|
+
instance.on("speech-end", () => setIsSpeaking(false));
|
|
230
|
+
instance.on("message", (m) => onMessageRef.current(m));
|
|
231
|
+
instance.on("error", (err) => {
|
|
232
|
+
console.error("[Crumb] VAPI error:", err);
|
|
233
|
+
setStatus("idle");
|
|
234
|
+
isStartingRef.current = false;
|
|
235
|
+
});
|
|
236
|
+
vapiRef.current = instance;
|
|
237
|
+
return instance;
|
|
238
|
+
}, [vapiPublicKey]);
|
|
239
|
+
const startCall = useCallback(async (userName, options) => {
|
|
240
|
+
if ((!options?.force && status !== "idle") || isStartingRef.current)
|
|
241
|
+
return;
|
|
242
|
+
isStartingRef.current = true;
|
|
243
|
+
setStatus("connecting");
|
|
244
|
+
try {
|
|
245
|
+
const vapi = await getVapi();
|
|
246
|
+
const sessionId = (() => {
|
|
247
|
+
try {
|
|
248
|
+
let id = typeof window !== "undefined" ? localStorage.getItem(SESSION_STORAGE_KEY) : null;
|
|
249
|
+
if (!id) {
|
|
250
|
+
id = crypto.randomUUID();
|
|
251
|
+
if (typeof window !== "undefined")
|
|
252
|
+
localStorage.setItem(SESSION_STORAGE_KEY, id);
|
|
253
|
+
}
|
|
254
|
+
return id;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return crypto.randomUUID();
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
sessionIdRef.current = sessionId;
|
|
261
|
+
setConversationId(sessionId);
|
|
262
|
+
const identityForConfig = {
|
|
263
|
+
userId: identity?.userId,
|
|
264
|
+
sessionId: identity?.sessionId ?? sessionId,
|
|
265
|
+
userName: userName ?? identity?.userName,
|
|
266
|
+
};
|
|
267
|
+
const config = await fetchAssistantConfig({
|
|
268
|
+
apiKey,
|
|
269
|
+
backendUrl,
|
|
270
|
+
identity: identityForConfig,
|
|
271
|
+
});
|
|
272
|
+
await vapi.start(config);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
console.error("[Crumb] startCall error:", err);
|
|
276
|
+
setStatus("idle");
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
isStartingRef.current = false;
|
|
280
|
+
}
|
|
281
|
+
}, [apiKey, backendUrl, identity, status, getVapi]);
|
|
282
|
+
const sendTextMessage = useCallback(async (text, options) => {
|
|
283
|
+
const message = text.trim();
|
|
284
|
+
if (!message)
|
|
285
|
+
return;
|
|
286
|
+
const append = () => setMessages((prev) => [...prev, { id: crypto.randomUUID(), role: "user", content: message, timestamp: Date.now() }]);
|
|
287
|
+
if (options?.textOnly) {
|
|
288
|
+
mutedBeforeTextRef.current = isMuted;
|
|
289
|
+
autoMutedForTextRef.current = true;
|
|
290
|
+
if (vapiRef.current)
|
|
291
|
+
vapiRef.current.setMuted(true);
|
|
292
|
+
setIsMuted(true);
|
|
293
|
+
}
|
|
294
|
+
if (status === "idle") {
|
|
295
|
+
append();
|
|
296
|
+
pendingUserMessageRef.current = message;
|
|
297
|
+
await startCall();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (status === "connecting") {
|
|
301
|
+
append();
|
|
302
|
+
pendingUserMessageRef.current = message;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (status === "active" && vapiRef.current) {
|
|
306
|
+
append();
|
|
307
|
+
vapiRef.current.send({ type: "add-message", message: { role: "user", content: message }, triggerResponseEnabled: true });
|
|
308
|
+
}
|
|
309
|
+
}, [status, isMuted, startCall]);
|
|
310
|
+
const approveProposal = useCallback(() => {
|
|
311
|
+
setPendingProposal(null);
|
|
312
|
+
vapiRef.current?.send({
|
|
313
|
+
type: "add-message",
|
|
314
|
+
message: { role: "user", content: "Yes, please make those changes to my cart." },
|
|
315
|
+
triggerResponseEnabled: true,
|
|
316
|
+
});
|
|
317
|
+
}, []);
|
|
318
|
+
const rejectProposal = useCallback(() => {
|
|
319
|
+
setPendingProposal(null);
|
|
320
|
+
vapiRef.current?.send({
|
|
321
|
+
type: "add-message",
|
|
322
|
+
message: { role: "user", content: "No, keep my cart as it is." },
|
|
323
|
+
triggerResponseEnabled: true,
|
|
324
|
+
});
|
|
325
|
+
}, []);
|
|
326
|
+
const endCall = useCallback(() => {
|
|
327
|
+
vapiRef.current?.stop();
|
|
328
|
+
setStatus("idle");
|
|
329
|
+
}, []);
|
|
330
|
+
const toggleMute = useCallback(() => {
|
|
331
|
+
if (!vapiRef.current)
|
|
332
|
+
return;
|
|
333
|
+
const next = !isMuted;
|
|
334
|
+
vapiRef.current.setMuted(next);
|
|
335
|
+
setIsMuted(next);
|
|
336
|
+
}, [isMuted]);
|
|
337
|
+
const setMicrophoneDevice = useCallback(async (deviceId) => {
|
|
338
|
+
const id = deviceId.trim();
|
|
339
|
+
setSelectedMicrophoneId(id);
|
|
340
|
+
pendingMicrophoneIdRef.current = id;
|
|
341
|
+
try {
|
|
342
|
+
if (typeof window !== "undefined")
|
|
343
|
+
localStorage.setItem(MIC_STORAGE_KEY, id);
|
|
344
|
+
}
|
|
345
|
+
catch { /* ignore */ }
|
|
346
|
+
if (vapiRef.current && status === "active") {
|
|
347
|
+
try {
|
|
348
|
+
vapiRef.current.setInputDevicesAsync?.({ audioSource: id });
|
|
349
|
+
}
|
|
350
|
+
catch { /* ignore */ }
|
|
351
|
+
}
|
|
352
|
+
}, [status]);
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
try {
|
|
355
|
+
if (typeof window !== "undefined") {
|
|
356
|
+
const saved = localStorage.getItem(MIC_STORAGE_KEY) ?? "";
|
|
357
|
+
if (saved) {
|
|
358
|
+
setSelectedMicrophoneId(saved);
|
|
359
|
+
pendingMicrophoneIdRef.current = saved;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch { /* ignore */ }
|
|
364
|
+
}, []);
|
|
365
|
+
useEffect(() => () => { vapiRef.current?.stop(); }, []);
|
|
366
|
+
const value = {
|
|
367
|
+
status,
|
|
368
|
+
isSpeaking,
|
|
369
|
+
isMuted,
|
|
370
|
+
messages,
|
|
371
|
+
liveTranscript,
|
|
372
|
+
pendingProposal,
|
|
373
|
+
conversationId,
|
|
374
|
+
isSidebarOpen,
|
|
375
|
+
startCall,
|
|
376
|
+
endCall,
|
|
377
|
+
toggleMute,
|
|
378
|
+
sendTextMessage,
|
|
379
|
+
approveProposal,
|
|
380
|
+
rejectProposal,
|
|
381
|
+
selectedMicrophoneId,
|
|
382
|
+
setMicrophoneDevice,
|
|
383
|
+
openSidebar: () => setIsSidebarOpen(true),
|
|
384
|
+
closeSidebar: () => setIsSidebarOpen(false),
|
|
385
|
+
toggleSidebar: () => setIsSidebarOpen((o) => !o),
|
|
386
|
+
};
|
|
387
|
+
return _jsx(CrumbContext.Provider, { value: value, children: children });
|
|
388
|
+
}
|
|
389
|
+
export function useCrumb() {
|
|
390
|
+
const ctx = useContext(CrumbContext);
|
|
391
|
+
if (!ctx)
|
|
392
|
+
throw new Error("useCrumb must be used within CrumbProvider");
|
|
393
|
+
return ctx;
|
|
394
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CrumbWidgetProps {
|
|
2
|
+
/** Assistant name shown in the sidebar header */
|
|
3
|
+
assistantName?: string;
|
|
4
|
+
/** Optional custom styles merged over defaults */
|
|
5
|
+
styles?: Partial<{
|
|
6
|
+
widget: React.CSSProperties;
|
|
7
|
+
fab: React.CSSProperties;
|
|
8
|
+
sidebar: React.CSSProperties;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function CrumbWidget({ assistantName, styles: customStyles }: CrumbWidgetProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=CrumbWidget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrumbWidget.d.ts","sourceRoot":"","sources":["../src/CrumbWidget.tsx"],"names":[],"mappings":"AAmMA,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;QAAE,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;QAAC,GAAG,EAAE,KAAK,CAAC,aAAa,CAAC;QAAC,OAAO,EAAE,KAAK,CAAC,aAAa,CAAA;KAAE,CAAC,CAAC;CAC3G;AAED,wBAAgB,WAAW,CAAC,EAAE,aAA2B,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,gBAAgB,2CAoTlG"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useCrumb } from "./CrumbProvider";
|
|
5
|
+
const defaultStyles = {
|
|
6
|
+
widget: {
|
|
7
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
8
|
+
fontSize: "14px",
|
|
9
|
+
zIndex: 9998,
|
|
10
|
+
},
|
|
11
|
+
fab: {
|
|
12
|
+
position: "fixed",
|
|
13
|
+
bottom: 24,
|
|
14
|
+
right: 24,
|
|
15
|
+
width: 56,
|
|
16
|
+
height: 56,
|
|
17
|
+
borderRadius: "50%",
|
|
18
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
19
|
+
background: "rgba(24,24,27,0.95)",
|
|
20
|
+
color: "#fff",
|
|
21
|
+
display: "flex",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
justifyContent: "center",
|
|
24
|
+
cursor: "pointer",
|
|
25
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
|
26
|
+
transition: "transform 0.2s, opacity 0.3s",
|
|
27
|
+
},
|
|
28
|
+
sidebar: {
|
|
29
|
+
position: "fixed",
|
|
30
|
+
top: 0,
|
|
31
|
+
right: 0,
|
|
32
|
+
width: "min(100vw, 26rem)",
|
|
33
|
+
height: "100vh",
|
|
34
|
+
background: "rgba(24,24,27,0.97)",
|
|
35
|
+
borderLeft: "1px solid rgba(255,255,255,0.08)",
|
|
36
|
+
boxShadow: "-4px 0 24px rgba(0,0,0,0.2)",
|
|
37
|
+
display: "flex",
|
|
38
|
+
flexDirection: "column",
|
|
39
|
+
transition: "transform 0.3s ease, opacity 0.3s ease",
|
|
40
|
+
},
|
|
41
|
+
header: {
|
|
42
|
+
padding: "12px 14px",
|
|
43
|
+
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
justifyContent: "space-between",
|
|
47
|
+
gap: 8,
|
|
48
|
+
},
|
|
49
|
+
btnIcon: {
|
|
50
|
+
width: 32,
|
|
51
|
+
height: 32,
|
|
52
|
+
borderRadius: "50%",
|
|
53
|
+
border: "none",
|
|
54
|
+
background: "transparent",
|
|
55
|
+
color: "rgba(255,255,255,0.7)",
|
|
56
|
+
cursor: "pointer",
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
justifyContent: "center",
|
|
60
|
+
},
|
|
61
|
+
msgBubble: (role) => ({
|
|
62
|
+
padding: "6px 10px",
|
|
63
|
+
borderRadius: 6,
|
|
64
|
+
fontSize: 12,
|
|
65
|
+
marginBottom: 6,
|
|
66
|
+
maxWidth: "95%",
|
|
67
|
+
alignSelf: role === "user" ? "flex-end" : "flex-start",
|
|
68
|
+
background: role === "user" ? "rgba(255,255,255,0.12)" : "rgba(59,130,246,0.2)",
|
|
69
|
+
color: role === "user" ? "#fff" : "rgb(191,219,254)",
|
|
70
|
+
}),
|
|
71
|
+
input: {
|
|
72
|
+
width: "100%",
|
|
73
|
+
padding: "8px 12px",
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
76
|
+
background: "rgba(0,0,0,0.2)",
|
|
77
|
+
color: "#fff",
|
|
78
|
+
fontSize: 13,
|
|
79
|
+
outline: "none",
|
|
80
|
+
},
|
|
81
|
+
proposalCard: {
|
|
82
|
+
padding: 10,
|
|
83
|
+
borderRadius: 8,
|
|
84
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
85
|
+
background: "rgba(0,0,0,0.15)",
|
|
86
|
+
marginBottom: 8,
|
|
87
|
+
},
|
|
88
|
+
proposalActions: {
|
|
89
|
+
display: "flex",
|
|
90
|
+
gap: 8,
|
|
91
|
+
marginTop: 10,
|
|
92
|
+
},
|
|
93
|
+
btn: (primary) => ({
|
|
94
|
+
padding: "8px 14px",
|
|
95
|
+
borderRadius: 6,
|
|
96
|
+
border: "none",
|
|
97
|
+
fontSize: 12,
|
|
98
|
+
fontWeight: 600,
|
|
99
|
+
cursor: "pointer",
|
|
100
|
+
background: primary ? "rgba(34,197,94,0.9)" : "rgba(255,255,255,0.1)",
|
|
101
|
+
color: primary ? "#fff" : "rgba(255,255,255,0.9)",
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
function MicIcon() {
|
|
105
|
+
return (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 1 3 3v7a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", y1: "19", x2: "12", y2: "22" })] }));
|
|
106
|
+
}
|
|
107
|
+
function MicOffIcon() {
|
|
108
|
+
return (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "2", y1: "2", x2: "22", y2: "22" }), _jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }), _jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }), _jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }), _jsx("path", { d: "M12 19v3" }), _jsx("path", { d: "M8 23h8" })] }));
|
|
109
|
+
}
|
|
110
|
+
function PhoneIcon() {
|
|
111
|
+
return (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }));
|
|
112
|
+
}
|
|
113
|
+
function PhoneOffIcon() {
|
|
114
|
+
return (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }), _jsx("line", { x1: "2", y1: "2", x2: "22", y2: "22" })] }));
|
|
115
|
+
}
|
|
116
|
+
function MessageCircleIcon() {
|
|
117
|
+
return (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
|
|
118
|
+
}
|
|
119
|
+
function SendIcon() {
|
|
120
|
+
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), _jsx("path", { d: "M22 2 11 13" })] }));
|
|
121
|
+
}
|
|
122
|
+
function XIcon() {
|
|
123
|
+
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }));
|
|
124
|
+
}
|
|
125
|
+
function LoaderIcon() {
|
|
126
|
+
return (_jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: "crumb-spin 0.8s linear infinite" }, children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
|
|
127
|
+
}
|
|
128
|
+
function PersonaPlaceholder({ state }) {
|
|
129
|
+
const pulse = state === "active" || state === "speaking";
|
|
130
|
+
return (_jsx("div", { style: {
|
|
131
|
+
width: 64,
|
|
132
|
+
height: 64,
|
|
133
|
+
borderRadius: "50%",
|
|
134
|
+
background: state === "connecting" ? "rgba(59,130,246,0.3)" : "rgba(255,255,255,0.08)",
|
|
135
|
+
display: "flex",
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
justifyContent: "center",
|
|
138
|
+
animation: pulse ? "crumb-pulse 1.5s ease-in-out infinite" : undefined,
|
|
139
|
+
}, children: _jsx(MicIcon, {}) }));
|
|
140
|
+
}
|
|
141
|
+
export function CrumbWidget({ assistantName = "Assistant", styles: customStyles }) {
|
|
142
|
+
const { status, isSpeaking, isMuted, messages, liveTranscript, pendingProposal, startCall, endCall, toggleMute, sendTextMessage, approveProposal, rejectProposal, selectedMicrophoneId, setMicrophoneDevice, openSidebar, closeSidebar, isSidebarOpen, } = useCrumb();
|
|
143
|
+
const [isVoiceExpanded, setIsVoiceExpanded] = useState(false);
|
|
144
|
+
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
|
145
|
+
const [chatTab, setChatTab] = useState("chat");
|
|
146
|
+
const [chatDraft, setChatDraft] = useState("");
|
|
147
|
+
const [micDevices, setMicDevices] = useState([]);
|
|
148
|
+
const prevStatusRef = useRef(status);
|
|
149
|
+
const isConnecting = status === "connecting";
|
|
150
|
+
const isActive = status === "active";
|
|
151
|
+
const isEscalated = status === "escalated";
|
|
152
|
+
const convoLine = useMemo(() => {
|
|
153
|
+
if (liveTranscript?.content.trim()) {
|
|
154
|
+
const who = liveTranscript.role === "user" ? "You" : assistantName;
|
|
155
|
+
return `${who}: ${liveTranscript.content.trim()}`;
|
|
156
|
+
}
|
|
157
|
+
if (messages.length > 0) {
|
|
158
|
+
const last = messages[messages.length - 1];
|
|
159
|
+
const who = last.role === "user" ? "You" : assistantName;
|
|
160
|
+
return `${who}: ${last.content}`;
|
|
161
|
+
}
|
|
162
|
+
if (status === "connecting")
|
|
163
|
+
return "Connecting…";
|
|
164
|
+
if (status === "active")
|
|
165
|
+
return isSpeaking ? "Speaking…" : "Listening…";
|
|
166
|
+
if (status === "escalated")
|
|
167
|
+
return "Human agent joining…";
|
|
168
|
+
return `Hi, I'm ${assistantName} — ask me anything.`;
|
|
169
|
+
}, [liveTranscript, messages, status, isSpeaking, assistantName]);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (typeof navigator !== "undefined" && navigator.mediaDevices?.enumerateDevices) {
|
|
172
|
+
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
173
|
+
const mics = devices.filter((d) => d.kind === "audioinput").map((d) => ({ deviceId: d.deviceId, label: d.label || `Microphone ${d.deviceId.slice(0, 8)}` }));
|
|
174
|
+
setMicDevices(mics);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}, [isChatExpanded || isVoiceExpanded]);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (prevStatusRef.current !== "idle" && status === "idle")
|
|
180
|
+
setIsVoiceExpanded(false);
|
|
181
|
+
if (status === "escalated") {
|
|
182
|
+
setIsVoiceExpanded(false);
|
|
183
|
+
setIsChatExpanded(true);
|
|
184
|
+
setChatTab("chat");
|
|
185
|
+
}
|
|
186
|
+
prevStatusRef.current = status;
|
|
187
|
+
}, [status]);
|
|
188
|
+
const handleOpenVoice = useCallback(async () => {
|
|
189
|
+
setIsChatExpanded(false);
|
|
190
|
+
setIsVoiceExpanded(true);
|
|
191
|
+
if (!isActive && !isConnecting && !isEscalated)
|
|
192
|
+
await startCall();
|
|
193
|
+
}, [isActive, isConnecting, isEscalated, startCall]);
|
|
194
|
+
const handleOpenChat = useCallback(() => {
|
|
195
|
+
setIsVoiceExpanded(false);
|
|
196
|
+
setIsChatExpanded(true);
|
|
197
|
+
setChatTab("chat");
|
|
198
|
+
}, []);
|
|
199
|
+
const handleSendChat = useCallback(async () => {
|
|
200
|
+
const t = chatDraft.trim();
|
|
201
|
+
if (!t)
|
|
202
|
+
return;
|
|
203
|
+
setChatDraft("");
|
|
204
|
+
await sendTextMessage(t, { textOnly: true });
|
|
205
|
+
}, [chatDraft, sendTextMessage]);
|
|
206
|
+
const styles = useMemo(() => ({
|
|
207
|
+
widget: { ...defaultStyles.widget, ...customStyles?.widget },
|
|
208
|
+
fab: { ...defaultStyles.fab, ...customStyles?.fab },
|
|
209
|
+
sidebar: { ...defaultStyles.sidebar, ...customStyles?.sidebar },
|
|
210
|
+
}), [customStyles]);
|
|
211
|
+
const sidebarVisible = isChatExpanded || isVoiceExpanded;
|
|
212
|
+
const personaState = isConnecting ? "connecting" : isActive ? (isSpeaking ? "speaking" : "active") : "idle";
|
|
213
|
+
return (_jsxs("div", { className: "crumb-widget", style: styles.widget, "data-crumb-widget": true, children: [_jsx("style", { children: `
|
|
214
|
+
@keyframes crumb-spin { to { transform: rotate(360deg); } }
|
|
215
|
+
@keyframes crumb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
216
|
+
` }), _jsx("button", { type: "button", onClick: handleOpenVoice, style: {
|
|
217
|
+
...styles.fab,
|
|
218
|
+
opacity: isVoiceExpanded ? 0 : 1,
|
|
219
|
+
pointerEvents: isVoiceExpanded ? "none" : "auto",
|
|
220
|
+
transform: isVoiceExpanded ? "scale(0.95)" : "scale(1)",
|
|
221
|
+
}, "aria-label": "Open voice assistant", children: _jsx(MicIcon, {}) }), _jsx("button", { type: "button", onClick: handleOpenChat, style: {
|
|
222
|
+
...styles.fab,
|
|
223
|
+
bottom: isVoiceExpanded ? 24 : 96,
|
|
224
|
+
opacity: isChatExpanded ? 0 : 1,
|
|
225
|
+
pointerEvents: isChatExpanded ? "none" : "auto",
|
|
226
|
+
transform: isChatExpanded ? "scale(0.95)" : "scale(1)",
|
|
227
|
+
}, "aria-label": "Open chat", children: _jsx(MessageCircleIcon, {}) }), _jsxs("div", { style: {
|
|
228
|
+
...styles.sidebar,
|
|
229
|
+
transform: sidebarVisible ? "translateX(0)" : "translateX(100%)",
|
|
230
|
+
opacity: sidebarVisible ? 1 : 0,
|
|
231
|
+
pointerEvents: sidebarVisible ? "auto" : "none",
|
|
232
|
+
}, children: [_jsxs("div", { style: defaultStyles.header, children: [_jsx("span", { style: { color: "rgba(255,255,255,0.85)", fontWeight: 600, fontSize: 13 }, children: assistantName }), _jsxs("div", { style: { display: "flex", gap: 4 }, children: [_jsx("button", { type: "button", onClick: () => setChatTab((t) => (t === "chat" ? "voice" : "chat")), style: defaultStyles.btnIcon, "aria-label": chatTab === "chat" ? "Switch to voice" : "Switch to chat", children: chatTab === "chat" ? _jsx(MicIcon, {}) : _jsx(MessageCircleIcon, {}) }), _jsx("button", { type: "button", onClick: () => { setIsChatExpanded(false); setIsVoiceExpanded(false); closeSidebar(); }, style: defaultStyles.btnIcon, "aria-label": "Close", children: _jsx(XIcon, {}) })] })] }), pendingProposal && (_jsxs("div", { style: { padding: 12, borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [_jsx("p", { style: { color: "rgba(255,255,255,0.9)", fontSize: 12, marginBottom: 8 }, children: pendingProposal.message }), _jsx("div", { style: { marginBottom: 8 }, children: pendingProposal.items.map((item, i) => (_jsxs("div", { style: defaultStyles.proposalCard, children: [item.imageUrl && _jsx("img", { src: item.imageUrl, alt: "", style: { width: 40, height: 40, borderRadius: 4, objectFit: "cover", marginRight: 8 } }), _jsx("span", { style: { color: "rgba(255,255,255,0.9)", fontSize: 12 }, children: item.name }), _jsxs("span", { style: { color: "rgba(255,255,255,0.6)", fontSize: 12, marginLeft: 8 }, children: ["$", Number(item.price).toFixed(2)] })] }, i))) }), _jsxs("div", { style: defaultStyles.proposalActions, children: [_jsx("button", { type: "button", onClick: approveProposal, style: defaultStyles.btn(true), children: "Yes" }), _jsx("button", { type: "button", onClick: rejectProposal, style: defaultStyles.btn(false), children: "No" })] })] })), chatTab === "chat" ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { flex: 1, overflow: "auto", padding: 12, display: "flex", flexDirection: "column", minHeight: 0 }, children: messages.length === 0 && !liveTranscript?.content ? (_jsx("p", { style: { color: "rgba(255,255,255,0.5)", fontSize: 11 }, children: "No conversation yet. Type below or open voice." })) : (_jsxs(_Fragment, { children: [messages.map((m) => (_jsxs("div", { style: { ...defaultStyles.msgBubble(m.role), display: "flex", flexDirection: "column", alignItems: m.role === "user" ? "flex-end" : "flex-start" }, children: [_jsxs("span", { style: { opacity: 0.8, marginRight: 4 }, children: [m.role === "user" ? "You" : assistantName, ":"] }), m.content] }, m.id))), liveTranscript?.content?.trim() && (_jsxs("div", { style: { ...defaultStyles.msgBubble(liveTranscript.role), opacity: 0.9 }, children: [_jsxs("span", { style: { opacity: 0.8 }, children: [liveTranscript.role === "user" ? "You" : assistantName, ":"] }), " ", liveTranscript.content] }))] })) }), _jsxs("div", { style: { padding: 12, display: "flex", gap: 8, borderTop: "1px solid rgba(255,255,255,0.08)" }, children: [_jsx("input", { value: chatDraft, onChange: (e) => setChatDraft(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
handleSendChat();
|
|
235
|
+
} }, placeholder: "Type a message...", style: { ...defaultStyles.input, flex: 1 } }), _jsx("button", { type: "button", onClick: () => handleSendChat(), style: defaultStyles.btnIcon, "aria-label": "Send", children: _jsx(SendIcon, {}) })] })] })) : (_jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }, children: [_jsx("p", { style: { padding: "12px 12px 8px", color: "rgba(255,255,255,0.6)", fontSize: 11, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, title: convoLine, children: convoLine }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: 8, padding: "8px 12px 12px" }, children: [_jsx("button", { type: "button", onClick: isConnecting || isEscalated ? undefined : isActive ? endCall : () => startCall(), disabled: isConnecting || isEscalated, style: { ...defaultStyles.btnIcon, ...(isActive ? { background: "rgba(255,255,255,0.1)", color: "#fff" } : {}) }, "aria-label": isActive ? "End call" : "Start call", children: isConnecting ? _jsx(LoaderIcon, {}) : isActive ? _jsx(PhoneOffIcon, {}) : _jsx(PhoneIcon, {}) }), micDevices.length > 0 && (_jsx("select", { value: selectedMicrophoneId || micDevices[0]?.deviceId, onChange: (e) => setMicrophoneDevice(e.target.value), style: { ...defaultStyles.input, width: 140, padding: "6px 8px" }, children: micDevices.map((d) => (_jsx("option", { value: d.deviceId, children: d.label }, d.deviceId))) })), _jsx(PersonaPlaceholder, { state: personaState }), isActive && (_jsx("button", { type: "button", onClick: toggleMute, style: defaultStyles.btnIcon, "aria-label": isMuted ? "Unmute" : "Mute", children: isMuted ? _jsx(MicOffIcon, {}) : _jsx(MicIcon, {}) }))] }), _jsx("div", { style: { flex: 1, overflow: "auto", padding: 12 }, children: messages.length === 0 && !liveTranscript?.content ? (_jsx("p", { style: { color: "rgba(255,255,255,0.5)", fontSize: 11 }, children: "No voice conversation yet." })) : (messages.map((m) => (_jsxs("div", { style: { ...defaultStyles.msgBubble(m.role) }, children: [_jsxs("span", { style: { opacity: 0.8 }, children: [m.role === "user" ? "You" : assistantName, ":"] }), " ", m.content] }, m.id)))) })] }))] }), isVoiceExpanded && (_jsxs("div", { style: {
|
|
236
|
+
position: "fixed",
|
|
237
|
+
bottom: 12,
|
|
238
|
+
left: "50%",
|
|
239
|
+
transform: "translateX(-50%)",
|
|
240
|
+
width: "min(100vw - 16px, 272px)",
|
|
241
|
+
padding: "6px 10px 10px",
|
|
242
|
+
borderRadius: 12,
|
|
243
|
+
border: "1px solid rgba(255,255,255,0.06)",
|
|
244
|
+
background: "rgba(24,24,27,0.95)",
|
|
245
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
|
|
246
|
+
zIndex: 9998,
|
|
247
|
+
}, children: [_jsx("p", { style: { textAlign: "center", color: "rgba(255,255,255,0.6)", fontSize: 11, marginBottom: 6 }, title: convoLine, children: convoLine }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: 8 }, children: [_jsx("button", { type: "button", onClick: isConnecting || isEscalated ? undefined : isActive ? endCall : () => startCall(), disabled: isConnecting || isEscalated, style: { ...defaultStyles.btnIcon, ...(isActive ? { background: "rgba(255,255,255,0.1)" } : {}) }, children: isConnecting ? _jsx(LoaderIcon, {}) : isActive ? _jsx(PhoneOffIcon, {}) : _jsx(PhoneIcon, {}) }), micDevices.length > 0 && (_jsx("select", { value: selectedMicrophoneId || micDevices[0]?.deviceId, onChange: (e) => setMicrophoneDevice(e.target.value), style: { ...defaultStyles.input, width: 120, padding: "4px 6px" }, children: micDevices.map((d) => (_jsx("option", { value: d.deviceId, children: d.label }, d.deviceId))) })), _jsx(PersonaPlaceholder, { state: personaState }), isActive && (_jsx("button", { type: "button", onClick: toggleMute, style: defaultStyles.btnIcon, children: isMuted ? _jsx(MicOffIcon, {}) : _jsx(MicIcon, {}) }))] })] }))] }));
|
|
248
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch assistant config from the Crumb IaaS backend.
|
|
3
|
+
*/
|
|
4
|
+
export interface FetchConfigParams {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
backendUrl: string;
|
|
7
|
+
identity?: {
|
|
8
|
+
userId?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
userName?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function fetchAssistantConfig(params: FetchConfigParams): Promise<Record<string, unknown>>;
|
|
14
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAetG"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch assistant config from the Crumb IaaS backend.
|
|
3
|
+
*/
|
|
4
|
+
export async function fetchAssistantConfig(params) {
|
|
5
|
+
const { apiKey, backendUrl, identity } = params;
|
|
6
|
+
const base = backendUrl.replace(/\/$/, "");
|
|
7
|
+
const url = new URL("/api/vapi/config", base);
|
|
8
|
+
url.searchParams.set("apiKey", apiKey);
|
|
9
|
+
if (identity?.userId)
|
|
10
|
+
url.searchParams.set("userId", identity.userId);
|
|
11
|
+
if (identity?.sessionId)
|
|
12
|
+
url.searchParams.set("sessionId", identity.sessionId);
|
|
13
|
+
if (identity?.userName)
|
|
14
|
+
url.searchParams.set("userName", identity.userName);
|
|
15
|
+
const res = await fetch(url.toString());
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const err = await res.json().catch(() => ({}));
|
|
18
|
+
throw new Error(err?.error ?? `Config fetch failed: ${res.status}`);
|
|
19
|
+
}
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CrumbProvider, useCrumb } from "./CrumbProvider";
|
|
2
|
+
export { CrumbWidget } from "./CrumbWidget";
|
|
3
|
+
export type { CrumbWidgetProps } from "./CrumbWidget";
|
|
4
|
+
export { fetchAssistantConfig } from "./config";
|
|
5
|
+
export type { CrumbAdapters, CrumbContextValue, CrumbIdentity, CrumbStatus, CartProposalPayload, CartProposalItem, ChatMessage, } from "./types";
|
|
6
|
+
export { CLIENT_TOOL_NAMES } from "./types";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter types: the host app implements these so the SDK can manipulate the site
|
|
3
|
+
* without assuming routes or cart implementation.
|
|
4
|
+
*/
|
|
5
|
+
export interface CrumbAdapters {
|
|
6
|
+
/** Navigate to a path (e.g. router.push(path) or window.location) */
|
|
7
|
+
navigate: (path: string) => void;
|
|
8
|
+
/** Optional: show a cart update proposal; host calls approveProposal() / rejectProposal() from context */
|
|
9
|
+
onCartProposal?: (payload: CartProposalPayload) => void;
|
|
10
|
+
/** Optional: apply a discount percentage to the cart */
|
|
11
|
+
applyDiscount?: (percent: number) => void;
|
|
12
|
+
/** Optional: remove a cart item by ID; return when done so SDK can send result to VAPI */
|
|
13
|
+
removeFromCart?: (cartItemId: string) => Promise<void>;
|
|
14
|
+
/** Optional: called when server-side cart tools (addToCart, updateCartItemQuantity) ran; host can reload cart */
|
|
15
|
+
onCartUpdated?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface CartProposalPayload {
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
message: string;
|
|
20
|
+
items: CartProposalItem[];
|
|
21
|
+
}
|
|
22
|
+
export interface CartProposalItem {
|
|
23
|
+
variantId?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
price: number;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
action?: "add" | "remove" | "replace";
|
|
28
|
+
}
|
|
29
|
+
export interface CrumbIdentity {
|
|
30
|
+
userId?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
userName?: string;
|
|
33
|
+
}
|
|
34
|
+
export type CrumbStatus = "idle" | "connecting" | "active" | "escalated";
|
|
35
|
+
export interface CrumbContextValue {
|
|
36
|
+
status: CrumbStatus;
|
|
37
|
+
isSpeaking: boolean;
|
|
38
|
+
isMuted: boolean;
|
|
39
|
+
messages: ChatMessage[];
|
|
40
|
+
liveTranscript: {
|
|
41
|
+
role: "user" | "assistant";
|
|
42
|
+
content: string;
|
|
43
|
+
} | null;
|
|
44
|
+
pendingProposal: CartProposalPayload | null;
|
|
45
|
+
conversationId: string | null;
|
|
46
|
+
isSidebarOpen: boolean;
|
|
47
|
+
startCall: (userName?: string, options?: {
|
|
48
|
+
force?: boolean;
|
|
49
|
+
}) => Promise<void>;
|
|
50
|
+
endCall: () => void;
|
|
51
|
+
toggleMute: () => void;
|
|
52
|
+
sendTextMessage: (text: string, options?: {
|
|
53
|
+
textOnly?: boolean;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
approveProposal: () => void;
|
|
56
|
+
rejectProposal: () => void;
|
|
57
|
+
selectedMicrophoneId: string;
|
|
58
|
+
setMicrophoneDevice: (deviceId: string) => Promise<void>;
|
|
59
|
+
openSidebar: () => void;
|
|
60
|
+
closeSidebar: () => void;
|
|
61
|
+
toggleSidebar: () => void;
|
|
62
|
+
}
|
|
63
|
+
export interface ChatMessage {
|
|
64
|
+
id: string;
|
|
65
|
+
role: "user" | "assistant";
|
|
66
|
+
content: string;
|
|
67
|
+
timestamp: number;
|
|
68
|
+
}
|
|
69
|
+
/** Client tool names the SDK handles locally (no server URL in config) */
|
|
70
|
+
export declare const CLIENT_TOOL_NAMES: Set<string>;
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,0GAA0G;IAC1G,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,wDAAwD;IACxD,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,0FAA0F;IAC1F,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,iHAAiH;IACjH,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvE,eAAe,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,aAM5B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter types: the host app implements these so the SDK can manipulate the site
|
|
3
|
+
* without assuming routes or cart implementation.
|
|
4
|
+
*/
|
|
5
|
+
/** Client tool names the SDK handles locally (no server URL in config) */
|
|
6
|
+
export const CLIENT_TOOL_NAMES = new Set([
|
|
7
|
+
"navigateTo",
|
|
8
|
+
"openCartDrawer",
|
|
9
|
+
"removeFromCart",
|
|
10
|
+
"proposeCartUpdate",
|
|
11
|
+
"applyDiscount",
|
|
12
|
+
]);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diversion-crumb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Crumb voice assistant SDK – connect your site to the Crumb IaaS backend via adapters",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "pnpm run build"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": ">=18.0.0",
|
|
22
|
+
"@vapi-ai/web": ">=2.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19",
|
|
26
|
+
"@vapi-ai/web": "^2.5.2",
|
|
27
|
+
"react": "19.2.3",
|
|
28
|
+
"tsup": "^8.3.5",
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
}
|
|
31
|
+
}
|