@pinecall/skills 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 +65 -0
- package/build.mjs +204 -0
- package/package.json +29 -0
- package/skills/pinecall-concepts/SKILL.md +41 -0
- package/skills/pinecall-concepts/references/concepts/agents-and-channels.md +155 -0
- package/skills/pinecall-concepts/references/concepts/deployment-topologies.md +120 -0
- package/skills/pinecall-concepts/references/concepts/hot-reload.md +119 -0
- package/skills/pinecall-concepts/references/concepts/philosophy.md +100 -0
- package/skills/pinecall-concepts/references/concepts/server-vs-client-llm.md +119 -0
- package/skills/pinecall-examples/SKILL.md +59 -0
- package/skills/pinecall-examples/references/examples/browser-widget.md +206 -0
- package/skills/pinecall-examples/references/examples/chat-bot.md +184 -0
- package/skills/pinecall-examples/references/examples/headless-agent.md +121 -0
- package/skills/pinecall-examples/references/examples/index.md +183 -0
- package/skills/pinecall-examples/references/examples/multi-channel-bot.md +173 -0
- package/skills/pinecall-examples/references/examples/outbound-dispatch.md +109 -0
- package/skills/pinecall-examples/references/examples/turn-detection.md +150 -0
- package/skills/pinecall-guides/SKILL.md +68 -0
- package/skills/pinecall-guides/references/guides/call-ringing.md +149 -0
- package/skills/pinecall-guides/references/guides/conversation-history.md +377 -0
- package/skills/pinecall-guides/references/guides/dev-mode.md +130 -0
- package/skills/pinecall-guides/references/guides/events.md +677 -0
- package/skills/pinecall-guides/references/guides/human-takeover.md +184 -0
- package/skills/pinecall-guides/references/guides/inbound-voice.md +201 -0
- package/skills/pinecall-guides/references/guides/knowledge-bases.md +166 -0
- package/skills/pinecall-guides/references/guides/live-listening.md +199 -0
- package/skills/pinecall-guides/references/guides/multi-tenant.md +158 -0
- package/skills/pinecall-guides/references/guides/outbound-calls.md +279 -0
- package/skills/pinecall-guides/references/guides/sse-streaming.md +207 -0
- package/skills/pinecall-guides/references/guides/testing-agents.md +272 -0
- package/skills/pinecall-guides/references/guides/tools-and-functions.md +254 -0
- package/skills/pinecall-guides/references/guides/webrtc-browser.md +200 -0
- package/skills/pinecall-guides/references/guides/whatsapp.md +370 -0
- package/skills/pinecall-guides/references/guides/ws-streaming.md +235 -0
- package/skills/pinecall-quickstart/SKILL.md +54 -0
- package/skills/pinecall-quickstart/references/index.md +123 -0
- package/skills/pinecall-quickstart/references/quickstart.md +185 -0
- package/skills/pinecall-reference/SKILL.md +43 -0
- package/skills/pinecall-reference/references/reference/cli.md +578 -0
- package/skills/pinecall-reference/references/reference/events.md +366 -0
- package/skills/pinecall-reference/references/reference/llm-providers.md +263 -0
- package/skills/pinecall-reference/references/reference/rest-api.md +122 -0
- package/skills/pinecall-reference/references/reference/session-limits.md +119 -0
- package/skills/pinecall-reference/references/reference/stt-providers.md +174 -0
- package/skills/pinecall-reference/references/reference/tts-providers.md +149 -0
- package/skills/pinecall-sdk-api/SKILL.md +56 -0
- package/skills/pinecall-sdk-api/references/api/agent.md +328 -0
- package/skills/pinecall-sdk-api/references/api/call.md +324 -0
- package/skills/pinecall-sdk-api/references/api/pinecall.md +186 -0
- package/skills/pinecall-sdk-api/references/api/reply-stream.md +148 -0
- package/skills/pinecall-security/SKILL.md +37 -0
- package/skills/pinecall-security/references/security.md +138 -0
- package/skills/pinecall-web-chat/SKILL.md +38 -0
- package/skills/pinecall-web-chat/references/web/chat/chat-session.md +178 -0
- package/skills/pinecall-web-chat/references/web/chat/overview.md +98 -0
- package/skills/pinecall-web-components/SKILL.md +37 -0
- package/skills/pinecall-web-components/references/web/components/overview.md +128 -0
- package/skills/pinecall-web-voice/SKILL.md +40 -0
- package/skills/pinecall-web-voice/references/web/core/datachannel-protocol.md +149 -0
- package/skills/pinecall-web-voice/references/web/core/overview.md +70 -0
- package/skills/pinecall-web-voice/references/web/core/state-and-phases.md +153 -0
- package/skills/pinecall-web-voice/references/web/core/voice-session.md +279 -0
- package/skills/pinecall-web-widget/SKILL.md +41 -0
- package/skills/pinecall-web-widget/references/web/widget/overview.md +67 -0
- package/skills/pinecall-web-widget/references/web/widget/props.md +291 -0
- package/skills/pinecall-web-widget/references/web/widget/theming.md +131 -0
- package/skills/pinecall-web-widget/references/web/widget/tools-api.md +381 -0
- package/skills/pinecall-web-widget/references/web/widget/use-voice-session-hook.md +130 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Tools API"
|
|
3
|
+
description: "Render interactive UI in response to LLM tool calls. Buttons, forms, pickers — all synced to the conversation."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tools API
|
|
7
|
+
|
|
8
|
+
The widget can render interactive UI in response to server-side LLM tool calls. The agent decides when to surface a UI (slot picker, form, confirmation), the widget renders your component, the user interacts, and the agent sees the result through the same conversation.
|
|
9
|
+
|
|
10
|
+
## The flow
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Enabling tool tracking
|
|
15
|
+
|
|
16
|
+
Tell the widget which tool names to track via the `trackedTools` prop. Untracked tools are handled silently by the agent — only tracked ones expose their state to the UI.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<VoiceWidget
|
|
20
|
+
agent="booking-demo"
|
|
21
|
+
trackedTools={["getAvailableSlots", "showContactForm", "fillField"]}
|
|
22
|
+
>
|
|
23
|
+
<ToolPanel />
|
|
24
|
+
</VoiceWidget>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Children of `<VoiceWidget>` (like `<ToolPanel />`) can read the tool state through the `useVoice()` context hook.
|
|
28
|
+
|
|
29
|
+
## `ToolUI` shape
|
|
30
|
+
|
|
31
|
+
Each tracked tool call is stored in `state.toolCalls` as:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
interface ToolUI {
|
|
35
|
+
toolCallId: string; // correlation ID — pass to dismissTool()
|
|
36
|
+
name: string; // tool function name
|
|
37
|
+
arguments: Record<string, unknown>; // parsed LLM arguments
|
|
38
|
+
result?: unknown; // populated when the tool result arrives
|
|
39
|
+
timestamp: number;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`result` is `undefined` between the call and the result — render a loading state if needed.
|
|
44
|
+
|
|
45
|
+
## `useVoice()` — the context hook
|
|
46
|
+
|
|
47
|
+
Reads from `<VoiceWidget>` context. Use it in any component that lives inside the widget.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useVoice } from "@pinecall/web";
|
|
51
|
+
|
|
52
|
+
function SlotPicker() {
|
|
53
|
+
const { toolCalls, sendText, dismissTool } = useVoice();
|
|
54
|
+
|
|
55
|
+
const tool = toolCalls.find(
|
|
56
|
+
(tc) => tc.name === "getAvailableSlots" && tc.result !== undefined,
|
|
57
|
+
);
|
|
58
|
+
if (!tool) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="slot-picker">
|
|
62
|
+
{tool.result.slots.map((slot: string) => (
|
|
63
|
+
<button
|
|
64
|
+
key={slot}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
sendText(`I'd like the ${slot} slot`);
|
|
67
|
+
dismissTool(tool.toolCallId);
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{slot}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### What `useVoice()` exposes
|
|
79
|
+
|
|
80
|
+
| Field | Type | What it does |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `toolCalls` | `ToolUI[]` | Active tracked tool calls |
|
|
83
|
+
| `messages` | `TranscriptMessage[]` | Full transcript |
|
|
84
|
+
| `status` | `SessionStatus` | Connection status |
|
|
85
|
+
| `phase` | `CallPhase` | Current phase |
|
|
86
|
+
| `sendText` | `(text: string) => void` | Inject text as if the user spoke it |
|
|
87
|
+
| `setContext` | `(key: string, value: string \| null) => void` | Inject keyed context into the LLM system prompt |
|
|
88
|
+
| `dismissTool` | `(toolCallId: string) => void` | Remove a tool from state (hides the UI) |
|
|
89
|
+
|
|
90
|
+
> **`useVoice()` vs `useVoiceSession()`.** `useVoice()` reads from the widget's context — only works inside `<VoiceWidget>` children. `useVoiceSession()` creates its own standalone session. Use `useVoice()` when building tool renderers.
|
|
91
|
+
|
|
92
|
+
## The three primitives
|
|
93
|
+
|
|
94
|
+
### `sendText(text)`
|
|
95
|
+
|
|
96
|
+
Inject text into the conversation as if the user spoke it. It routes through the server's LLM pipeline so the agent processes it normally.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// User clicks a slot button
|
|
100
|
+
sendText("I'd like to book the 10:00 AM slot");
|
|
101
|
+
|
|
102
|
+
// User submits a form
|
|
103
|
+
sendText("Form submitted: name=John, email=john@example.com, phone=+1555000");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use this for click-based interactions where you want the agent to react conversationally.
|
|
107
|
+
|
|
108
|
+
### `setContext(key, value)`
|
|
109
|
+
|
|
110
|
+
Inject dynamic context into the agent's LLM system prompt. Keyed — setting the same key replaces its value. Pass `null` to clear.
|
|
111
|
+
|
|
112
|
+
This is the magic for syncing UI state (form inputs, selections, page content) into the agent's awareness:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// Sync form state on every keystroke
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
setContext("contact_form", JSON.stringify({
|
|
118
|
+
name: formData.name || "(empty)",
|
|
119
|
+
email: formData.email || "(empty)",
|
|
120
|
+
phone: formData.phone || "(empty)",
|
|
121
|
+
}));
|
|
122
|
+
}, [formData, setContext]);
|
|
123
|
+
|
|
124
|
+
// Clear when done
|
|
125
|
+
setContext("contact_form", null);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
On the server, this appears in the LLM's system prompt as:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
## UI Context
|
|
132
|
+
### contact_form
|
|
133
|
+
{"name":"John","email":"john@example.com","phone":"(empty)"}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The agent can now reason about UI state without you having to explicitly tell it.
|
|
137
|
+
|
|
138
|
+
### `dismissTool(toolCallId)`
|
|
139
|
+
|
|
140
|
+
Remove a tool from `state.toolCalls`. Hides the rendered UI. Call this after the user interacts (selects a slot, submits a form, etc.).
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
dismissTool(tool.toolCallId);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Full example — booking with auto-fill
|
|
147
|
+
|
|
148
|
+
This shows the full pattern: slot picker, contact form, agent-driven auto-fill, and live context sync.
|
|
149
|
+
|
|
150
|
+
### Server-side agent
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { tool } from "@pinecall/sdk";
|
|
154
|
+
import { z } from "zod";
|
|
155
|
+
|
|
156
|
+
const getAvailableSlots = tool({
|
|
157
|
+
name: "getAvailableSlots",
|
|
158
|
+
description: "Get available slots for a date.",
|
|
159
|
+
schema: z.object({ date: z.string() }),
|
|
160
|
+
execute: async ({ date }) => ({
|
|
161
|
+
slots: ["9:00 AM", "10:00 AM", "2:00 PM", "4:00 PM"],
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const showContactForm = tool({
|
|
166
|
+
name: "showContactForm",
|
|
167
|
+
description: "Show a contact form on screen.",
|
|
168
|
+
schema: z.object({}),
|
|
169
|
+
execute: async () => ({ shown: true }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const fillField = tool({
|
|
173
|
+
name: "fillField",
|
|
174
|
+
description: "Auto-fill a form field with a value extracted from the conversation.",
|
|
175
|
+
schema: z.object({
|
|
176
|
+
field: z.enum(["name", "email", "phone"]),
|
|
177
|
+
value: z.string(),
|
|
178
|
+
}),
|
|
179
|
+
execute: async ({ field, value }) => ({ field, value }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const confirmBooking = tool({
|
|
183
|
+
name: "confirmBooking",
|
|
184
|
+
description: "Confirm the booking.",
|
|
185
|
+
schema: z.object({ date: z.string(), time: z.string(), clientName: z.string() }),
|
|
186
|
+
execute: async ({ date, time, clientName }) => ({
|
|
187
|
+
confirmationId: "BK-" + Math.random().toString(36).slice(2, 8),
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const agent = pc.agent("booking-demo", {
|
|
192
|
+
prompt: `You are a booking assistant.
|
|
193
|
+
- Call getAvailableSlots when the user wants to book.
|
|
194
|
+
- After they pick a slot, call showContactForm.
|
|
195
|
+
- If they say their name/email/phone, call fillField to auto-fill.
|
|
196
|
+
- The form state is in "## UI Context" — you can see what they've typed.
|
|
197
|
+
- When the form is submitted, call confirmBooking.`,
|
|
198
|
+
llm: "openai/gpt-5-chat-latest",
|
|
199
|
+
voice: "elevenlabs/sarah",
|
|
200
|
+
tools: [getAvailableSlots, showContactForm, fillField, confirmBooking],
|
|
201
|
+
greeting: "Hi! Want to book an appointment?",
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Browser — contact form with auto-fill
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useState, useEffect } from "react";
|
|
209
|
+
import { VoiceWidget, useVoice } from "@pinecall/web";
|
|
210
|
+
|
|
211
|
+
function ContactForm({ tool }) {
|
|
212
|
+
const { sendText, dismissTool, setContext, toolCalls } = useVoice();
|
|
213
|
+
const [form, setForm] = useState({ name: "", email: "", phone: "" });
|
|
214
|
+
|
|
215
|
+
// Agent calls fillField → auto-fill the form
|
|
216
|
+
const fillTool = toolCalls.find((tc) => tc.name === "fillField" && tc.result);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (fillTool?.result) {
|
|
219
|
+
const { field, value } = fillTool.result as { field: string; value: string };
|
|
220
|
+
setForm((prev) => ({ ...prev, [field]: value }));
|
|
221
|
+
dismissTool(fillTool.toolCallId);
|
|
222
|
+
}
|
|
223
|
+
}, [fillTool, dismissTool]);
|
|
224
|
+
|
|
225
|
+
// Sync form state → LLM system prompt
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
setContext("contact_form", JSON.stringify(form));
|
|
228
|
+
}, [form, setContext]);
|
|
229
|
+
|
|
230
|
+
const submit = (e: React.FormEvent) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
sendText(`Form submitted: ${JSON.stringify(form)}`);
|
|
233
|
+
setContext("contact_form", null);
|
|
234
|
+
dismissTool(tool.toolCallId);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<form onSubmit={submit}>
|
|
239
|
+
<input
|
|
240
|
+
placeholder="Name"
|
|
241
|
+
value={form.name}
|
|
242
|
+
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
|
243
|
+
/>
|
|
244
|
+
<input
|
|
245
|
+
placeholder="Email"
|
|
246
|
+
value={form.email}
|
|
247
|
+
onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
|
|
248
|
+
/>
|
|
249
|
+
<input
|
|
250
|
+
placeholder="Phone"
|
|
251
|
+
value={form.phone}
|
|
252
|
+
onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))}
|
|
253
|
+
/>
|
|
254
|
+
<button type="submit">Confirm</button>
|
|
255
|
+
</form>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function SlotPicker({ tool }) {
|
|
260
|
+
const { sendText, dismissTool } = useVoice();
|
|
261
|
+
return (
|
|
262
|
+
<div>
|
|
263
|
+
{(tool.result as any).slots.map((slot: string) => (
|
|
264
|
+
<button
|
|
265
|
+
key={slot}
|
|
266
|
+
onClick={() => {
|
|
267
|
+
sendText(`I'd like the ${slot} slot`);
|
|
268
|
+
dismissTool(tool.toolCallId);
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
{slot}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ToolPanel() {
|
|
279
|
+
const { toolCalls } = useVoice();
|
|
280
|
+
return (
|
|
281
|
+
<>
|
|
282
|
+
{toolCalls.map((tool) => {
|
|
283
|
+
if (tool.name === "getAvailableSlots" && tool.result) {
|
|
284
|
+
return <SlotPicker key={tool.toolCallId} tool={tool} />;
|
|
285
|
+
}
|
|
286
|
+
if (tool.name === "showContactForm" && tool.result) {
|
|
287
|
+
return <ContactForm key={tool.toolCallId} tool={tool} />;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
})}
|
|
291
|
+
</>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default function App() {
|
|
296
|
+
return (
|
|
297
|
+
<VoiceWidget
|
|
298
|
+
agent="booking-demo"
|
|
299
|
+
trackedTools={["getAvailableSlots", "showContactForm", "fillField", "confirmBooking"]}
|
|
300
|
+
>
|
|
301
|
+
<ToolPanel />
|
|
302
|
+
</VoiceWidget>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Alternative: `tools` prop (render functions)
|
|
308
|
+
|
|
309
|
+
If you don't need the flexibility of `trackedTools` + `useVoice()`, use the `tools` prop for a simpler inline approach. Each tool name maps to a render function that receives the result:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
<VoiceWidget
|
|
313
|
+
agent="booking-demo"
|
|
314
|
+
tools={{
|
|
315
|
+
getAvailableSlots: (result, { respond, dismiss }) => (
|
|
316
|
+
<div className="slot-picker">
|
|
317
|
+
{result.slots.map((slot: string) => (
|
|
318
|
+
<button
|
|
319
|
+
key={slot}
|
|
320
|
+
onClick={() => {
|
|
321
|
+
respond(`I'd like the ${slot} slot`);
|
|
322
|
+
dismiss();
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
{slot}
|
|
326
|
+
</button>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
),
|
|
330
|
+
confirmBooking: (result, { dismiss }) => (
|
|
331
|
+
<div className="confirmation">
|
|
332
|
+
<p>✅ Booked for {result.time}</p>
|
|
333
|
+
<button onClick={dismiss}>Done</button>
|
|
334
|
+
</div>
|
|
335
|
+
),
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Render function signature
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
type ToolRenderer = (
|
|
344
|
+
result: any, // parsed tool result
|
|
345
|
+
context: ToolRenderContext, // { respond, dismiss }
|
|
346
|
+
toolCall: ToolUI, // full tool call metadata
|
|
347
|
+
) => React.ReactNode;
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| Parameter | Type | Description |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| `result` | `any` | The parsed JSON result from your backend tool |
|
|
353
|
+
| `context.respond` | `(text: string) => void` | Inject text as if the user spoke it |
|
|
354
|
+
| `context.dismiss` | `() => void` | Remove the tool UI from the transcript |
|
|
355
|
+
| `toolCall` | `ToolUI` | Full tool call metadata (name, arguments, ID) |
|
|
356
|
+
|
|
357
|
+
### `tools` vs `trackedTools` — when to use which
|
|
358
|
+
|
|
359
|
+
| Scenario | Use |
|
|
360
|
+
|---|---|
|
|
361
|
+
| Simple inline renderers | `tools` prop |
|
|
362
|
+
| Complex components needing React state | `trackedTools` + `useVoice()` |
|
|
363
|
+
| Multiple components sharing tool state | `trackedTools` + `useVoice()` |
|
|
364
|
+
| Context injection via `setContext` | `trackedTools` + `useVoice()` |
|
|
365
|
+
|
|
366
|
+
## Why this pattern is powerful
|
|
367
|
+
|
|
368
|
+
What this enables is multimodal interaction:
|
|
369
|
+
|
|
370
|
+
- **Voice for natural language** — "I want to book an appointment for next Tuesday morning"
|
|
371
|
+
- **UI for precise input** — pick the exact slot, type your email, submit a form
|
|
372
|
+
- **Sync via setContext** — the agent always knows what's on screen
|
|
373
|
+
- **sendText for confirmation** — the user's UI action becomes part of the conversation
|
|
374
|
+
|
|
375
|
+
The agent doesn't need different code paths for "voice user" vs "GUI user" — it just sees a conversation with rich context.
|
|
376
|
+
|
|
377
|
+
## What's next
|
|
378
|
+
|
|
379
|
+
- [Props reference](/web/widget/props) — `tools`, `trackedTools`, `tokenProvider`, and more
|
|
380
|
+
- [`useVoiceSession` hook](/web/widget/use-voice-session-hook) — for non-tool custom UIs
|
|
381
|
+
- [Tools and functions guide](/guides/tools-and-functions) — server-side tool definition
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "useVoiceSession hook"
|
|
3
|
+
description: "Build a fully custom voice UI without giving up the widget's session management."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `useVoiceSession` hook
|
|
7
|
+
|
|
8
|
+
If the orb UI doesn't fit your design, use the `useVoiceSession()` hook directly. It gives you the same reactive state and actions, with no orb, no transcript bubbles, no styling — you bring the UI.
|
|
9
|
+
|
|
10
|
+
## When to use this vs `<VoiceWidget />`
|
|
11
|
+
|
|
12
|
+
| Use `<VoiceWidget />` | Use `useVoiceSession()` |
|
|
13
|
+
|---|---|
|
|
14
|
+
| You want a floating orb in the corner | You want voice as part of your app's existing UI |
|
|
15
|
+
| You need theming via presets | You're styling everything yourself anyway |
|
|
16
|
+
| You want the Tools API context (`useVoice()`) | You're building a transcript-first interface |
|
|
17
|
+
|
|
18
|
+
The hook wraps `VoiceSession` from `@pinecall/web/core` with `useSyncExternalStore` for efficient React rendering. The session is created once on mount and destroyed on unmount.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { useVoiceSession } from "@pinecall/web";
|
|
24
|
+
|
|
25
|
+
function CustomVoice() {
|
|
26
|
+
const {
|
|
27
|
+
status, error, isMuted, phase,
|
|
28
|
+
userSpeaking, agentSpeaking, duration,
|
|
29
|
+
messages, idleWarning,
|
|
30
|
+
connect, disconnect, toggleMute, setMuted,
|
|
31
|
+
} = useVoiceSession({ agent: "mara" });
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<p>Status: {status} · Phase: {phase} · {duration}s</p>
|
|
36
|
+
|
|
37
|
+
{status === "idle" && <button onClick={connect}>Start call</button>}
|
|
38
|
+
{status === "connected" && (
|
|
39
|
+
<>
|
|
40
|
+
<button onClick={disconnect}>End call</button>
|
|
41
|
+
<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
<div>
|
|
46
|
+
{messages.map((m) => (
|
|
47
|
+
<div key={m.id} className={m.role}>
|
|
48
|
+
<strong>{m.role}:</strong> {m.text}
|
|
49
|
+
{m.isInterim && " (typing...)"}
|
|
50
|
+
{m.speaking && " 🔊"}
|
|
51
|
+
{m.interrupted && " ⚡ interrupted"}
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Return shape
|
|
61
|
+
|
|
62
|
+
The hook returns the full session state **plus** action methods:
|
|
63
|
+
|
|
64
|
+
| Field | Type | What it is |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `status` | `SessionStatus` | `"idle" \| "connecting" \| "connected" \| "error"` |
|
|
67
|
+
| `error` | `string \| null` | Error message when `status === "error"` |
|
|
68
|
+
| `isMuted` | `boolean` | Mic state |
|
|
69
|
+
| `phase` | `CallPhase` | `"idle" \| "listening" \| "speaking" \| "pause" \| "thinking"` |
|
|
70
|
+
| `userSpeaking` | `boolean` | User is physically talking (VAD-level) |
|
|
71
|
+
| `agentSpeaking` | `boolean` | TTS is currently playing |
|
|
72
|
+
| `duration` | `number` | Seconds since connected, updates every second |
|
|
73
|
+
| `messages` | `TranscriptMessage[]` | Full transcript — see [State and Phases](/web/core/state-and-phases) |
|
|
74
|
+
| `idleWarning` | `number \| null` | Seconds until idle timeout (null = no warning) |
|
|
75
|
+
| `connect` | `() => Promise<void>` | Start the call |
|
|
76
|
+
| `disconnect` | `() => void` | End the call |
|
|
77
|
+
| `toggleMute` | `() => void` | Toggle mic |
|
|
78
|
+
| `setMuted` | `(muted: boolean) => void` | Explicit mute control |
|
|
79
|
+
|
|
80
|
+
## Accessing raw events
|
|
81
|
+
|
|
82
|
+
For tool calls or other low-level events the state machine doesn't expose, drop down to `@pinecall/web/core` directly and listen to the `event` listener:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { useState, useEffect } from "react";
|
|
86
|
+
import { VoiceSession } from "@pinecall/web/core";
|
|
87
|
+
|
|
88
|
+
function AdvancedVoice() {
|
|
89
|
+
const [session] = useState(() => new VoiceSession({ agent: "mara" }));
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const onEvent = (e: CustomEvent) => {
|
|
93
|
+
const { event, tool_calls } = e.detail;
|
|
94
|
+
|
|
95
|
+
if (event === "llm.tool_call" && tool_calls) {
|
|
96
|
+
for (const tc of tool_calls) {
|
|
97
|
+
console.log(`Tool call: ${tc.name}`, tc.arguments);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
session.addEventListener("event", onEvent);
|
|
103
|
+
return () => {
|
|
104
|
+
session.removeEventListener("event", onEvent);
|
|
105
|
+
session.destroy();
|
|
106
|
+
};
|
|
107
|
+
}, [session]);
|
|
108
|
+
|
|
109
|
+
// ... render UI using session.getState()
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If you specifically want to render interactive UI for tool calls, stick with `<VoiceWidget>` and use the [Tools API](/web/widget/tools-api) — it handles the correlation between calls and results for you.
|
|
114
|
+
|
|
115
|
+
## `useVoice()` vs `useVoiceSession()`
|
|
116
|
+
|
|
117
|
+
There are two hooks in this package and the names are easy to confuse:
|
|
118
|
+
|
|
119
|
+
| Hook | Purpose | Where to use |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `useVoiceSession()` | Creates its own session | Anywhere — standalone |
|
|
122
|
+
| `useVoice()` | Reads from `<VoiceWidget>` context | Inside `<VoiceWidget>` children only |
|
|
123
|
+
|
|
124
|
+
Use `useVoiceSession()` for fully custom UIs that replace the widget entirely. Use `useVoice()` when you're building tool renderers as children of `<VoiceWidget>` — see [Tools API](/web/widget/tools-api).
|
|
125
|
+
|
|
126
|
+
## What's next
|
|
127
|
+
|
|
128
|
+
- [Props reference](/web/widget/props) — if you want the orb after all
|
|
129
|
+
- [Tools API](/web/widget/tools-api) — interactive UI for tool calls
|
|
130
|
+
- [`@pinecall/web/core`](/web/core/overview) — for non-React frameworks
|