@pinecall/chat-core 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 +240 -0
- package/dist/chunk-ST5DVE5W.js +237 -0
- package/dist/chunk-ST5DVE5W.js.map +1 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +279 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +35 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +41 -0
- package/dist/react.js.map +1 -0
- package/dist/types-CGPqse91.d.cts +45 -0
- package/dist/types-CGPqse91.d.ts +45 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<h1 align="center">@pinecall/chat-core</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Text chat client for Pinecall voice agents.</strong><br/>
|
|
5
|
+
Framework-agnostic core + React hook. Zero dependencies.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="#install">Install</a> ·
|
|
10
|
+
<a href="#vanilla-js">Vanilla JS</a> ·
|
|
11
|
+
<a href="#react">React</a> ·
|
|
12
|
+
<a href="#api-reference">API</a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @pinecall/chat-core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> **Browser-only.** Uses the native `WebSocket` and `EventTarget` APIs. Works in any modern browser, bundler, or SSR-hydrated app.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Vanilla JS
|
|
28
|
+
|
|
29
|
+
`ChatSession` is framework-agnostic — no React, no dependencies. Works in vanilla JS, Vue, Svelte, Angular, or any framework.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
import { ChatSession } from "@pinecall/chat-core";
|
|
33
|
+
|
|
34
|
+
const chat = new ChatSession({ agent: "florencia" });
|
|
35
|
+
|
|
36
|
+
// Listen to events via standard EventTarget
|
|
37
|
+
chat.addEventListener("message", (e) => {
|
|
38
|
+
const msg = e.detail.message;
|
|
39
|
+
console.log(`${msg.role}: ${msg.text}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
chat.addEventListener("status", (e) => {
|
|
43
|
+
console.log("Status:", e.detail.status);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Connect and send
|
|
47
|
+
await chat.connect();
|
|
48
|
+
chat.send("Hola, quiero reservar un turno");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Subscribe pattern (for reactive UI)
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
// Works with any reactive system (MobX, signals, stores)
|
|
55
|
+
const unsubscribe = chat.subscribe(() => {
|
|
56
|
+
const state = chat.getState();
|
|
57
|
+
console.log("Messages:", state.messages.length);
|
|
58
|
+
console.log("Typing:", state.typing);
|
|
59
|
+
console.log("Status:", state.status);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Clean up
|
|
63
|
+
unsubscribe();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Dynamic context injection
|
|
67
|
+
|
|
68
|
+
Inject live context into the LLM system prompt — form state, user selections, page data:
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
chat.setContext("cart", JSON.stringify({
|
|
72
|
+
items: ["Corte de cabello", "Tinte"],
|
|
73
|
+
total: 85.00,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// Clear a context key
|
|
77
|
+
chat.setContext("cart", null);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## React
|
|
83
|
+
|
|
84
|
+
The `@pinecall/chat-core/react` subpath export provides a `usePinecallChat` hook. React is an **optional** peer dependency.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { usePinecallChat } from "@pinecall/chat-core/react";
|
|
88
|
+
|
|
89
|
+
function Chat() {
|
|
90
|
+
const { messages, send, connected, typing, streamingText } = usePinecallChat({
|
|
91
|
+
agent: "florencia",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!connected) return <p>Connecting...</p>;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
{messages.map((m) => (
|
|
99
|
+
<p key={m.id}>
|
|
100
|
+
<strong>{m.role}:</strong> {m.text}
|
|
101
|
+
{m.isStreaming && "▊"}
|
|
102
|
+
</p>
|
|
103
|
+
))}
|
|
104
|
+
{typing && <p>Bot is typing: {streamingText}▊</p>}
|
|
105
|
+
<input
|
|
106
|
+
placeholder="Type a message..."
|
|
107
|
+
onKeyDown={(e) => {
|
|
108
|
+
if (e.key === "Enter") {
|
|
109
|
+
send(e.currentTarget.value);
|
|
110
|
+
e.currentTarget.value = "";
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Hook options
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
usePinecallChat({
|
|
123
|
+
agent: "florencia", // Agent ID (required)
|
|
124
|
+
server: "https://voice.pinecall.io", // Voice server URL (default)
|
|
125
|
+
autoConnect: true, // Connect on mount (default: true)
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Hook return
|
|
130
|
+
|
|
131
|
+
| Field | Type | Description |
|
|
132
|
+
|-------|------|-------------|
|
|
133
|
+
| `messages` | `ChatMessage[]` | All messages in the conversation |
|
|
134
|
+
| `send` | `(text: string) => void` | Send a text message |
|
|
135
|
+
| `connected` | `boolean` | `true` when connected to the server |
|
|
136
|
+
| `typing` | `boolean` | `true` while the bot is streaming |
|
|
137
|
+
| `streamingText` | `string` | Partial text of the current bot response |
|
|
138
|
+
| `error` | `string \| null` | Current error, if any |
|
|
139
|
+
| `setContext` | `(key, value) => void` | Inject dynamic context into the LLM prompt |
|
|
140
|
+
| `connect` | `() => void` | Manually connect |
|
|
141
|
+
| `disconnect` | `() => void` | Manually disconnect |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### `ChatSession`
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { ChatSession } from "@pinecall/chat-core";
|
|
151
|
+
|
|
152
|
+
const chat = new ChatSession(options);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Options
|
|
156
|
+
|
|
157
|
+
| Field | Type | Required | Description |
|
|
158
|
+
|-------|------|----------|-------------|
|
|
159
|
+
| `agent` | `string` | ✅ | Agent slug (e.g. `"florencia"`, `"dev-berna-florencia"`) |
|
|
160
|
+
| `server` | `string` | — | Voice server URL (default: `https://voice.pinecall.io`) |
|
|
161
|
+
|
|
162
|
+
#### Methods
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
|--------|-------------|
|
|
166
|
+
| `connect()` | Connect to the chat server (fetches token → opens WebSocket) |
|
|
167
|
+
| `disconnect()` | Close the WebSocket connection |
|
|
168
|
+
| `destroy()` | Disconnect + clear all subscribers. Do not reuse. |
|
|
169
|
+
| `send(text)` | Send a text message to the agent |
|
|
170
|
+
| `setContext(key, value)` | Inject/update/clear keyed context in the LLM prompt |
|
|
171
|
+
| `getState()` | Read-only snapshot of current state |
|
|
172
|
+
| `subscribe(cb)` | Subscribe to state changes (for React `useSyncExternalStore`) |
|
|
173
|
+
|
|
174
|
+
#### Events (via `EventTarget`)
|
|
175
|
+
|
|
176
|
+
| Event | `detail` | When |
|
|
177
|
+
|-------|----------|------|
|
|
178
|
+
| `status` | `{ status }` | Connection status changed |
|
|
179
|
+
| `message` | `{ message }` | New or updated message |
|
|
180
|
+
| `error` | `{ error }` | Error occurred |
|
|
181
|
+
| `change` | `{ state }` | Any state mutation (most general) |
|
|
182
|
+
| `event` | raw server payload | Every raw server event |
|
|
183
|
+
|
|
184
|
+
#### State shape
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
interface ChatSessionState {
|
|
188
|
+
status: "idle" | "connecting" | "connected" | "error";
|
|
189
|
+
error: string | null;
|
|
190
|
+
messages: ChatMessage[];
|
|
191
|
+
typing: boolean;
|
|
192
|
+
streamingText: string;
|
|
193
|
+
sessionId: string | null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface ChatMessage {
|
|
197
|
+
id: number;
|
|
198
|
+
role: "user" | "bot";
|
|
199
|
+
text: string;
|
|
200
|
+
messageId?: string; // server-assigned ID (bot messages)
|
|
201
|
+
isStreaming?: boolean; // true while bot is still streaming
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Protocol
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Browser Voice Server
|
|
211
|
+
│ │
|
|
212
|
+
├─ GET /chat/token?agent_id=X ──→│ (fetch short-lived token)
|
|
213
|
+
│←── { token: "cht_xxx" } ──────│
|
|
214
|
+
│ │
|
|
215
|
+
├─ WS /chat/ws?token=cht_xxx ──→│ (open WebSocket)
|
|
216
|
+
│←── { event: "chat.connected" }│
|
|
217
|
+
│ │
|
|
218
|
+
├─→ { event: "message", text } │ (user sends message)
|
|
219
|
+
│←── { event: "chat.token", … } │ (streaming tokens)
|
|
220
|
+
│←── { event: "chat.token", … } │
|
|
221
|
+
│←── { event: "chat.done", … } │ (stream complete)
|
|
222
|
+
│ │
|
|
223
|
+
├─→ { event: "set_context", … } │ (inject LLM context)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Related Packages
|
|
229
|
+
|
|
230
|
+
| Package | Description |
|
|
231
|
+
|---------|-------------|
|
|
232
|
+
| [`@pinecall/sdk`](https://npmjs.com/package/@pinecall/sdk) | Server-side SDK — agent, call, tools, channels |
|
|
233
|
+
| [`@pinecall/voice-core`](https://npmjs.com/package/@pinecall/voice-core) | WebRTC voice session (framework-agnostic) |
|
|
234
|
+
| [`@pinecall/voice-widget`](https://npmjs.com/package/@pinecall/voice-widget) | React voice widget with animated orb UI |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/ChatSession.ts
|
|
2
|
+
var INITIAL_STATE = {
|
|
3
|
+
status: "idle",
|
|
4
|
+
error: null,
|
|
5
|
+
messages: [],
|
|
6
|
+
typing: false,
|
|
7
|
+
streamingText: "",
|
|
8
|
+
sessionId: null
|
|
9
|
+
};
|
|
10
|
+
var ChatSession = class extends EventTarget {
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
super();
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
}
|
|
15
|
+
opts;
|
|
16
|
+
state = { ...INITIAL_STATE };
|
|
17
|
+
listeners = /* @__PURE__ */ new Set();
|
|
18
|
+
ws = null;
|
|
19
|
+
reconnectTimer = null;
|
|
20
|
+
msgCounter = 0;
|
|
21
|
+
/** Read-only snapshot of current state (stable ref until next mutation). */
|
|
22
|
+
getState() {
|
|
23
|
+
return this.state;
|
|
24
|
+
}
|
|
25
|
+
/** Subscribe to ANY state change (for React useSyncExternalStore). */
|
|
26
|
+
subscribe(listener) {
|
|
27
|
+
this.listeners.add(listener);
|
|
28
|
+
return () => {
|
|
29
|
+
this.listeners.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
setState(patch) {
|
|
33
|
+
const prev = this.state;
|
|
34
|
+
this.state = { ...prev, ...patch };
|
|
35
|
+
for (const l of this.listeners) l();
|
|
36
|
+
if (patch.status !== void 0 && patch.status !== prev.status) {
|
|
37
|
+
this.dispatchEvent(
|
|
38
|
+
new CustomEvent("status", { detail: { status: this.state.status } })
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
|
|
42
|
+
this.dispatchEvent(
|
|
43
|
+
new CustomEvent("error", { detail: { error: this.state.error } })
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
this.dispatchEvent(
|
|
47
|
+
new CustomEvent("change", { detail: { state: this.state } })
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
setMessages(updater) {
|
|
51
|
+
const next = updater(this.state.messages);
|
|
52
|
+
this.setState({ messages: next });
|
|
53
|
+
const last = next[next.length - 1];
|
|
54
|
+
if (last) {
|
|
55
|
+
this.dispatchEvent(
|
|
56
|
+
new CustomEvent("message", { detail: { message: last } })
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ── Connection ──────────────────────────────────────────────────────
|
|
61
|
+
async connect() {
|
|
62
|
+
if (this.ws) return;
|
|
63
|
+
try {
|
|
64
|
+
this.setState({
|
|
65
|
+
status: "connecting",
|
|
66
|
+
error: null
|
|
67
|
+
});
|
|
68
|
+
const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
|
|
69
|
+
/\/$/,
|
|
70
|
+
""
|
|
71
|
+
);
|
|
72
|
+
const tRes = await fetch(
|
|
73
|
+
`${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`
|
|
74
|
+
);
|
|
75
|
+
if (!tRes.ok) {
|
|
76
|
+
const body = await tRes.text();
|
|
77
|
+
throw new Error(`Token: ${tRes.status} ${body}`);
|
|
78
|
+
}
|
|
79
|
+
const { token, server: chatServer } = await tRes.json();
|
|
80
|
+
const wsBase = (chatServer || base).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
81
|
+
const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);
|
|
82
|
+
this.ws = ws;
|
|
83
|
+
ws.onopen = () => {
|
|
84
|
+
};
|
|
85
|
+
ws.onmessage = (evt) => this.handleMessage(evt);
|
|
86
|
+
ws.onerror = () => {
|
|
87
|
+
this.setState({ error: "WebSocket error", status: "error" });
|
|
88
|
+
};
|
|
89
|
+
ws.onclose = (evt) => {
|
|
90
|
+
this.ws = null;
|
|
91
|
+
if (this.state.status === "connected") {
|
|
92
|
+
this.setState({ status: "idle" });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.setState({
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
status: "error"
|
|
99
|
+
});
|
|
100
|
+
this.ws = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
handleMessage(evt) {
|
|
104
|
+
let d;
|
|
105
|
+
try {
|
|
106
|
+
d = JSON.parse(evt.data);
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
switch (d.event) {
|
|
111
|
+
case "chat.connected":
|
|
112
|
+
this.setState({
|
|
113
|
+
status: "connected",
|
|
114
|
+
sessionId: d.session_id ?? null
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
case "chat.token":
|
|
118
|
+
case "llm.chat.token":
|
|
119
|
+
this.setState({
|
|
120
|
+
typing: true,
|
|
121
|
+
streamingText: d.text ?? ""
|
|
122
|
+
});
|
|
123
|
+
this.setMessages((prev) => {
|
|
124
|
+
const idx = prev.findIndex(
|
|
125
|
+
(m) => m.messageId === d.message_id && m.isStreaming
|
|
126
|
+
);
|
|
127
|
+
if (idx >= 0) {
|
|
128
|
+
return prev.map(
|
|
129
|
+
(m, i) => i === idx ? { ...m, text: d.text ?? "" } : m
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return [
|
|
133
|
+
...prev,
|
|
134
|
+
{
|
|
135
|
+
id: ++this.msgCounter,
|
|
136
|
+
role: "bot",
|
|
137
|
+
text: d.text ?? "",
|
|
138
|
+
messageId: d.message_id,
|
|
139
|
+
isStreaming: true
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
break;
|
|
144
|
+
case "chat.done":
|
|
145
|
+
case "llm.chat.done":
|
|
146
|
+
this.setState({
|
|
147
|
+
typing: false,
|
|
148
|
+
streamingText: ""
|
|
149
|
+
});
|
|
150
|
+
this.setMessages((prev) => {
|
|
151
|
+
const idx = prev.findIndex(
|
|
152
|
+
(m) => m.messageId === d.message_id && m.isStreaming
|
|
153
|
+
);
|
|
154
|
+
if (idx >= 0) {
|
|
155
|
+
return prev.map(
|
|
156
|
+
(m, i) => i === idx ? { ...m, text: d.text ?? m.text, isStreaming: false } : m
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return [
|
|
160
|
+
...prev,
|
|
161
|
+
{
|
|
162
|
+
id: ++this.msgCounter,
|
|
163
|
+
role: "bot",
|
|
164
|
+
text: d.text ?? "",
|
|
165
|
+
messageId: d.message_id,
|
|
166
|
+
isStreaming: false
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
case "chat.error":
|
|
172
|
+
case "llm.chat.error":
|
|
173
|
+
this.setState({
|
|
174
|
+
typing: false,
|
|
175
|
+
error: d.error ?? "Unknown error"
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
case "error":
|
|
179
|
+
this.setState({
|
|
180
|
+
error: d.error ?? "Unknown error",
|
|
181
|
+
status: "error"
|
|
182
|
+
});
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
this.dispatchEvent(new CustomEvent("event", { detail: d }));
|
|
186
|
+
}
|
|
187
|
+
// ── Actions ─────────────────────────────────────────────────────────
|
|
188
|
+
/** Send a text message to the agent. */
|
|
189
|
+
send(text) {
|
|
190
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
191
|
+
const trimmed = text.trim();
|
|
192
|
+
if (!trimmed) return;
|
|
193
|
+
this.setMessages((prev) => [
|
|
194
|
+
...prev,
|
|
195
|
+
{
|
|
196
|
+
id: ++this.msgCounter,
|
|
197
|
+
role: "user",
|
|
198
|
+
text: trimmed
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
this.ws.send(JSON.stringify({ event: "message", text: trimmed }));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Set or clear a keyed context block in the LLM system prompt.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* session.setContext("form", JSON.stringify({ name: "Juan" }));
|
|
209
|
+
* session.setContext("form", null); // clear
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
setContext(key, value) {
|
|
213
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
214
|
+
this.ws.send(JSON.stringify({ event: "set_context", key, value }));
|
|
215
|
+
}
|
|
216
|
+
/** Disconnect the chat session. */
|
|
217
|
+
disconnect() {
|
|
218
|
+
if (this.reconnectTimer) {
|
|
219
|
+
clearTimeout(this.reconnectTimer);
|
|
220
|
+
this.reconnectTimer = null;
|
|
221
|
+
}
|
|
222
|
+
if (this.ws) {
|
|
223
|
+
this.ws.close();
|
|
224
|
+
this.ws = null;
|
|
225
|
+
}
|
|
226
|
+
this.setState({ status: "idle", typing: false, streamingText: "" });
|
|
227
|
+
}
|
|
228
|
+
/** Tear down the session and clear subscribers. Do not reuse after this. */
|
|
229
|
+
destroy() {
|
|
230
|
+
this.disconnect();
|
|
231
|
+
this.listeners.clear();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { ChatSession };
|
|
236
|
+
//# sourceMappingURL=chunk-ST5DVE5W.js.map
|
|
237
|
+
//# sourceMappingURL=chunk-ST5DVE5W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ChatSession.ts"],"names":[],"mappings":"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","file":"chunk-ST5DVE5W.js","sourcesContent":["/**\n * ChatSession — Framework-agnostic text chat client for Pinecall agents.\n *\n * Mirrors VoiceSession's API patterns:\n * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore\n * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)\n *\n * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat\n */\nimport type {\n ChatSessionState,\n ChatSessionOptions,\n ChatMessage,\n} from \"./types\";\n\nconst INITIAL_STATE: ChatSessionState = {\n status: \"idle\",\n error: null,\n messages: [],\n typing: false,\n streamingText: \"\",\n sessionId: null,\n};\n\nexport class ChatSession extends EventTarget {\n private state: ChatSessionState = { ...INITIAL_STATE };\n private listeners = new Set<() => void>();\n\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private msgCounter = 0;\n\n constructor(private opts: ChatSessionOptions) {\n super();\n }\n\n /** Read-only snapshot of current state (stable ref until next mutation). */\n getState(): Readonly<ChatSessionState> {\n return this.state;\n }\n\n /** Subscribe to ANY state change (for React useSyncExternalStore). */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private setState(patch: Partial<ChatSessionState>): void {\n const prev = this.state;\n this.state = { ...prev, ...patch };\n for (const l of this.listeners) l();\n\n if (patch.status !== undefined && patch.status !== prev.status) {\n this.dispatchEvent(\n new CustomEvent(\"status\", { detail: { status: this.state.status } }),\n );\n }\n if (\n patch.error !== undefined &&\n patch.error !== null &&\n patch.error !== prev.error\n ) {\n this.dispatchEvent(\n new CustomEvent(\"error\", { detail: { error: this.state.error } }),\n );\n }\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { state: this.state } }),\n );\n }\n\n private setMessages(\n updater: (prev: ChatMessage[]) => ChatMessage[],\n ): void {\n const next = updater(this.state.messages);\n this.setState({ messages: next });\n const last = next[next.length - 1];\n if (last) {\n this.dispatchEvent(\n new CustomEvent(\"message\", { detail: { message: last } }),\n );\n }\n }\n\n // ── Connection ──────────────────────────────────────────────────────\n\n async connect(): Promise<void> {\n if (this.ws) return;\n\n try {\n this.setState({\n status: \"connecting\",\n error: null,\n });\n\n const base = (this.opts.server ?? \"https://voice.pinecall.io\").replace(\n /\\/$/,\n \"\",\n );\n\n // 1. Fetch chat token (public, no API key)\n const tRes = await fetch(\n `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`,\n );\n if (!tRes.ok) {\n const body = await tRes.text();\n throw new Error(`Token: ${tRes.status} ${body}`);\n }\n const { token, server: chatServer } = await tRes.json();\n const wsBase = (chatServer || base)\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\");\n\n // 2. Open WebSocket\n const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);\n this.ws = ws;\n\n ws.onopen = () => {\n // Wait for chat.connected event before setting status\n };\n\n ws.onmessage = (evt) => this.handleMessage(evt);\n\n ws.onerror = () => {\n this.setState({ error: \"WebSocket error\", status: \"error\" });\n };\n\n ws.onclose = (evt) => {\n this.ws = null;\n if (this.state.status === \"connected\") {\n // Unexpected disconnect\n this.setState({ status: \"idle\" });\n }\n };\n } catch (err) {\n this.setState({\n error: err instanceof Error ? err.message : String(err),\n status: \"error\",\n });\n this.ws = null;\n }\n }\n\n private handleMessage(evt: MessageEvent): void {\n let d: any;\n try {\n d = JSON.parse(evt.data);\n } catch {\n return;\n }\n\n switch (d.event) {\n case \"chat.connected\":\n this.setState({\n status: \"connected\",\n sessionId: d.session_id ?? null,\n });\n break;\n\n case \"chat.token\":\n case \"llm.chat.token\":\n // Streaming token from LLM\n this.setState({\n typing: true,\n streamingText: d.text ?? \"\",\n });\n\n // Update or create bot message\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx ? { ...m, text: d.text ?? \"\" } : m,\n );\n }\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: true,\n },\n ];\n });\n break;\n\n case \"chat.done\":\n case \"llm.chat.done\":\n // LLM finished streaming\n this.setState({\n typing: false,\n streamingText: \"\",\n });\n\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx\n ? { ...m, text: d.text ?? m.text, isStreaming: false }\n : m,\n );\n }\n // If we missed the streaming, add the final message\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: false,\n },\n ];\n });\n break;\n\n case \"chat.error\":\n case \"llm.chat.error\":\n this.setState({\n typing: false,\n error: d.error ?? \"Unknown error\",\n });\n break;\n\n case \"error\":\n this.setState({\n error: d.error ?? \"Unknown error\",\n status: \"error\",\n });\n break;\n }\n\n // Emit raw event for power users\n this.dispatchEvent(new CustomEvent(\"event\", { detail: d }));\n }\n\n // ── Actions ─────────────────────────────────────────────────────────\n\n /** Send a text message to the agent. */\n send(text: string): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n\n const trimmed = text.trim();\n if (!trimmed) return;\n\n // Add user message to local state immediately\n this.setMessages((prev) => [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"user\",\n text: trimmed,\n },\n ]);\n\n // Send to server\n this.ws.send(JSON.stringify({ event: \"message\", text: trimmed }));\n }\n\n /**\n * Set or clear a keyed context block in the LLM system prompt.\n *\n * @example\n * ```ts\n * session.setContext(\"form\", JSON.stringify({ name: \"Juan\" }));\n * session.setContext(\"form\", null); // clear\n * ```\n */\n setContext(key: string, value: string | null): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n this.ws.send(JSON.stringify({ event: \"set_context\", key, value }));\n }\n\n /** Disconnect the chat session. */\n disconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n this.setState({ status: \"idle\", typing: false, streamingText: \"\" });\n }\n\n /** Tear down the session and clear subscribers. Do not reuse after this. */\n destroy(): void {\n this.disconnect();\n this.listeners.clear();\n }\n}\n"]}
|