@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/dist/index.cjs ADDED
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // src/ChatSession.ts
4
+ var INITIAL_STATE = {
5
+ status: "idle",
6
+ error: null,
7
+ messages: [],
8
+ typing: false,
9
+ streamingText: "",
10
+ sessionId: null
11
+ };
12
+ var ChatSession = class extends EventTarget {
13
+ constructor(opts) {
14
+ super();
15
+ this.opts = opts;
16
+ }
17
+ opts;
18
+ state = { ...INITIAL_STATE };
19
+ listeners = /* @__PURE__ */ new Set();
20
+ ws = null;
21
+ reconnectTimer = null;
22
+ msgCounter = 0;
23
+ /** Read-only snapshot of current state (stable ref until next mutation). */
24
+ getState() {
25
+ return this.state;
26
+ }
27
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
28
+ subscribe(listener) {
29
+ this.listeners.add(listener);
30
+ return () => {
31
+ this.listeners.delete(listener);
32
+ };
33
+ }
34
+ setState(patch) {
35
+ const prev = this.state;
36
+ this.state = { ...prev, ...patch };
37
+ for (const l of this.listeners) l();
38
+ if (patch.status !== void 0 && patch.status !== prev.status) {
39
+ this.dispatchEvent(
40
+ new CustomEvent("status", { detail: { status: this.state.status } })
41
+ );
42
+ }
43
+ if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
44
+ this.dispatchEvent(
45
+ new CustomEvent("error", { detail: { error: this.state.error } })
46
+ );
47
+ }
48
+ this.dispatchEvent(
49
+ new CustomEvent("change", { detail: { state: this.state } })
50
+ );
51
+ }
52
+ setMessages(updater) {
53
+ const next = updater(this.state.messages);
54
+ this.setState({ messages: next });
55
+ const last = next[next.length - 1];
56
+ if (last) {
57
+ this.dispatchEvent(
58
+ new CustomEvent("message", { detail: { message: last } })
59
+ );
60
+ }
61
+ }
62
+ // ── Connection ──────────────────────────────────────────────────────
63
+ async connect() {
64
+ if (this.ws) return;
65
+ try {
66
+ this.setState({
67
+ status: "connecting",
68
+ error: null
69
+ });
70
+ const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
71
+ /\/$/,
72
+ ""
73
+ );
74
+ const tRes = await fetch(
75
+ `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`
76
+ );
77
+ if (!tRes.ok) {
78
+ const body = await tRes.text();
79
+ throw new Error(`Token: ${tRes.status} ${body}`);
80
+ }
81
+ const { token, server: chatServer } = await tRes.json();
82
+ const wsBase = (chatServer || base).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
83
+ const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);
84
+ this.ws = ws;
85
+ ws.onopen = () => {
86
+ };
87
+ ws.onmessage = (evt) => this.handleMessage(evt);
88
+ ws.onerror = () => {
89
+ this.setState({ error: "WebSocket error", status: "error" });
90
+ };
91
+ ws.onclose = (evt) => {
92
+ this.ws = null;
93
+ if (this.state.status === "connected") {
94
+ this.setState({ status: "idle" });
95
+ }
96
+ };
97
+ } catch (err) {
98
+ this.setState({
99
+ error: err instanceof Error ? err.message : String(err),
100
+ status: "error"
101
+ });
102
+ this.ws = null;
103
+ }
104
+ }
105
+ handleMessage(evt) {
106
+ let d;
107
+ try {
108
+ d = JSON.parse(evt.data);
109
+ } catch {
110
+ return;
111
+ }
112
+ switch (d.event) {
113
+ case "chat.connected":
114
+ this.setState({
115
+ status: "connected",
116
+ sessionId: d.session_id ?? null
117
+ });
118
+ break;
119
+ case "chat.token":
120
+ case "llm.chat.token":
121
+ this.setState({
122
+ typing: true,
123
+ streamingText: d.text ?? ""
124
+ });
125
+ this.setMessages((prev) => {
126
+ const idx = prev.findIndex(
127
+ (m) => m.messageId === d.message_id && m.isStreaming
128
+ );
129
+ if (idx >= 0) {
130
+ return prev.map(
131
+ (m, i) => i === idx ? { ...m, text: d.text ?? "" } : m
132
+ );
133
+ }
134
+ return [
135
+ ...prev,
136
+ {
137
+ id: ++this.msgCounter,
138
+ role: "bot",
139
+ text: d.text ?? "",
140
+ messageId: d.message_id,
141
+ isStreaming: true
142
+ }
143
+ ];
144
+ });
145
+ break;
146
+ case "chat.done":
147
+ case "llm.chat.done":
148
+ this.setState({
149
+ typing: false,
150
+ streamingText: ""
151
+ });
152
+ this.setMessages((prev) => {
153
+ const idx = prev.findIndex(
154
+ (m) => m.messageId === d.message_id && m.isStreaming
155
+ );
156
+ if (idx >= 0) {
157
+ return prev.map(
158
+ (m, i) => i === idx ? { ...m, text: d.text ?? m.text, isStreaming: false } : m
159
+ );
160
+ }
161
+ return [
162
+ ...prev,
163
+ {
164
+ id: ++this.msgCounter,
165
+ role: "bot",
166
+ text: d.text ?? "",
167
+ messageId: d.message_id,
168
+ isStreaming: false
169
+ }
170
+ ];
171
+ });
172
+ break;
173
+ case "chat.error":
174
+ case "llm.chat.error":
175
+ this.setState({
176
+ typing: false,
177
+ error: d.error ?? "Unknown error"
178
+ });
179
+ break;
180
+ case "error":
181
+ this.setState({
182
+ error: d.error ?? "Unknown error",
183
+ status: "error"
184
+ });
185
+ break;
186
+ }
187
+ this.dispatchEvent(new CustomEvent("event", { detail: d }));
188
+ }
189
+ // ── Actions ─────────────────────────────────────────────────────────
190
+ /** Send a text message to the agent. */
191
+ send(text) {
192
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
193
+ const trimmed = text.trim();
194
+ if (!trimmed) return;
195
+ this.setMessages((prev) => [
196
+ ...prev,
197
+ {
198
+ id: ++this.msgCounter,
199
+ role: "user",
200
+ text: trimmed
201
+ }
202
+ ]);
203
+ this.ws.send(JSON.stringify({ event: "message", text: trimmed }));
204
+ }
205
+ /**
206
+ * Set or clear a keyed context block in the LLM system prompt.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
211
+ * session.setContext("form", null); // clear
212
+ * ```
213
+ */
214
+ setContext(key, value) {
215
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
216
+ this.ws.send(JSON.stringify({ event: "set_context", key, value }));
217
+ }
218
+ /** Disconnect the chat session. */
219
+ disconnect() {
220
+ if (this.reconnectTimer) {
221
+ clearTimeout(this.reconnectTimer);
222
+ this.reconnectTimer = null;
223
+ }
224
+ if (this.ws) {
225
+ this.ws.close();
226
+ this.ws = null;
227
+ }
228
+ this.setState({ status: "idle", typing: false, streamingText: "" });
229
+ }
230
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
231
+ destroy() {
232
+ this.disconnect();
233
+ this.listeners.clear();
234
+ }
235
+ };
236
+
237
+ exports.ChatSession = ChatSession;
238
+ //# sourceMappingURL=index.cjs.map
239
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ChatSession.ts"],"names":[],"mappings":"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","file":"index.cjs","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"]}
@@ -0,0 +1,48 @@
1
+ import { b as ChatSessionOptions, c as ChatSessionState } from './types-CGPqse91.cjs';
2
+ export { C as ChatEventType, a as ChatMessage, d as ChatStatus } from './types-CGPqse91.cjs';
3
+
4
+ /**
5
+ * ChatSession — Framework-agnostic text chat client for Pinecall agents.
6
+ *
7
+ * Mirrors VoiceSession's API patterns:
8
+ * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore
9
+ * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)
10
+ *
11
+ * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat
12
+ */
13
+
14
+ declare class ChatSession extends EventTarget {
15
+ private opts;
16
+ private state;
17
+ private listeners;
18
+ private ws;
19
+ private reconnectTimer;
20
+ private msgCounter;
21
+ constructor(opts: ChatSessionOptions);
22
+ /** Read-only snapshot of current state (stable ref until next mutation). */
23
+ getState(): Readonly<ChatSessionState>;
24
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
25
+ subscribe(listener: () => void): () => void;
26
+ private setState;
27
+ private setMessages;
28
+ connect(): Promise<void>;
29
+ private handleMessage;
30
+ /** Send a text message to the agent. */
31
+ send(text: string): void;
32
+ /**
33
+ * Set or clear a keyed context block in the LLM system prompt.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
38
+ * session.setContext("form", null); // clear
39
+ * ```
40
+ */
41
+ setContext(key: string, value: string | null): void;
42
+ /** Disconnect the chat session. */
43
+ disconnect(): void;
44
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
45
+ destroy(): void;
46
+ }
47
+
48
+ export { ChatSession, ChatSessionOptions, ChatSessionState };
@@ -0,0 +1,48 @@
1
+ import { b as ChatSessionOptions, c as ChatSessionState } from './types-CGPqse91.js';
2
+ export { C as ChatEventType, a as ChatMessage, d as ChatStatus } from './types-CGPqse91.js';
3
+
4
+ /**
5
+ * ChatSession — Framework-agnostic text chat client for Pinecall agents.
6
+ *
7
+ * Mirrors VoiceSession's API patterns:
8
+ * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore
9
+ * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)
10
+ *
11
+ * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat
12
+ */
13
+
14
+ declare class ChatSession extends EventTarget {
15
+ private opts;
16
+ private state;
17
+ private listeners;
18
+ private ws;
19
+ private reconnectTimer;
20
+ private msgCounter;
21
+ constructor(opts: ChatSessionOptions);
22
+ /** Read-only snapshot of current state (stable ref until next mutation). */
23
+ getState(): Readonly<ChatSessionState>;
24
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
25
+ subscribe(listener: () => void): () => void;
26
+ private setState;
27
+ private setMessages;
28
+ connect(): Promise<void>;
29
+ private handleMessage;
30
+ /** Send a text message to the agent. */
31
+ send(text: string): void;
32
+ /**
33
+ * Set or clear a keyed context block in the LLM system prompt.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
38
+ * session.setContext("form", null); // clear
39
+ * ```
40
+ */
41
+ setContext(key: string, value: string | null): void;
42
+ /** Disconnect the chat session. */
43
+ disconnect(): void;
44
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
45
+ destroy(): void;
46
+ }
47
+
48
+ export { ChatSession, ChatSessionOptions, ChatSessionState };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ChatSession } from './chunk-ST5DVE5W.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/dist/react.cjs ADDED
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/react.tsx
6
+
7
+ // src/ChatSession.ts
8
+ var INITIAL_STATE = {
9
+ status: "idle",
10
+ error: null,
11
+ messages: [],
12
+ typing: false,
13
+ streamingText: "",
14
+ sessionId: null
15
+ };
16
+ var ChatSession = class extends EventTarget {
17
+ constructor(opts) {
18
+ super();
19
+ this.opts = opts;
20
+ }
21
+ opts;
22
+ state = { ...INITIAL_STATE };
23
+ listeners = /* @__PURE__ */ new Set();
24
+ ws = null;
25
+ reconnectTimer = null;
26
+ msgCounter = 0;
27
+ /** Read-only snapshot of current state (stable ref until next mutation). */
28
+ getState() {
29
+ return this.state;
30
+ }
31
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
32
+ subscribe(listener) {
33
+ this.listeners.add(listener);
34
+ return () => {
35
+ this.listeners.delete(listener);
36
+ };
37
+ }
38
+ setState(patch) {
39
+ const prev = this.state;
40
+ this.state = { ...prev, ...patch };
41
+ for (const l of this.listeners) l();
42
+ if (patch.status !== void 0 && patch.status !== prev.status) {
43
+ this.dispatchEvent(
44
+ new CustomEvent("status", { detail: { status: this.state.status } })
45
+ );
46
+ }
47
+ if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
48
+ this.dispatchEvent(
49
+ new CustomEvent("error", { detail: { error: this.state.error } })
50
+ );
51
+ }
52
+ this.dispatchEvent(
53
+ new CustomEvent("change", { detail: { state: this.state } })
54
+ );
55
+ }
56
+ setMessages(updater) {
57
+ const next = updater(this.state.messages);
58
+ this.setState({ messages: next });
59
+ const last = next[next.length - 1];
60
+ if (last) {
61
+ this.dispatchEvent(
62
+ new CustomEvent("message", { detail: { message: last } })
63
+ );
64
+ }
65
+ }
66
+ // ── Connection ──────────────────────────────────────────────────────
67
+ async connect() {
68
+ if (this.ws) return;
69
+ try {
70
+ this.setState({
71
+ status: "connecting",
72
+ error: null
73
+ });
74
+ const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
75
+ /\/$/,
76
+ ""
77
+ );
78
+ const tRes = await fetch(
79
+ `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`
80
+ );
81
+ if (!tRes.ok) {
82
+ const body = await tRes.text();
83
+ throw new Error(`Token: ${tRes.status} ${body}`);
84
+ }
85
+ const { token, server: chatServer } = await tRes.json();
86
+ const wsBase = (chatServer || base).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
87
+ const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);
88
+ this.ws = ws;
89
+ ws.onopen = () => {
90
+ };
91
+ ws.onmessage = (evt) => this.handleMessage(evt);
92
+ ws.onerror = () => {
93
+ this.setState({ error: "WebSocket error", status: "error" });
94
+ };
95
+ ws.onclose = (evt) => {
96
+ this.ws = null;
97
+ if (this.state.status === "connected") {
98
+ this.setState({ status: "idle" });
99
+ }
100
+ };
101
+ } catch (err) {
102
+ this.setState({
103
+ error: err instanceof Error ? err.message : String(err),
104
+ status: "error"
105
+ });
106
+ this.ws = null;
107
+ }
108
+ }
109
+ handleMessage(evt) {
110
+ let d;
111
+ try {
112
+ d = JSON.parse(evt.data);
113
+ } catch {
114
+ return;
115
+ }
116
+ switch (d.event) {
117
+ case "chat.connected":
118
+ this.setState({
119
+ status: "connected",
120
+ sessionId: d.session_id ?? null
121
+ });
122
+ break;
123
+ case "chat.token":
124
+ case "llm.chat.token":
125
+ this.setState({
126
+ typing: true,
127
+ streamingText: d.text ?? ""
128
+ });
129
+ this.setMessages((prev) => {
130
+ const idx = prev.findIndex(
131
+ (m) => m.messageId === d.message_id && m.isStreaming
132
+ );
133
+ if (idx >= 0) {
134
+ return prev.map(
135
+ (m, i) => i === idx ? { ...m, text: d.text ?? "" } : m
136
+ );
137
+ }
138
+ return [
139
+ ...prev,
140
+ {
141
+ id: ++this.msgCounter,
142
+ role: "bot",
143
+ text: d.text ?? "",
144
+ messageId: d.message_id,
145
+ isStreaming: true
146
+ }
147
+ ];
148
+ });
149
+ break;
150
+ case "chat.done":
151
+ case "llm.chat.done":
152
+ this.setState({
153
+ typing: false,
154
+ streamingText: ""
155
+ });
156
+ this.setMessages((prev) => {
157
+ const idx = prev.findIndex(
158
+ (m) => m.messageId === d.message_id && m.isStreaming
159
+ );
160
+ if (idx >= 0) {
161
+ return prev.map(
162
+ (m, i) => i === idx ? { ...m, text: d.text ?? m.text, isStreaming: false } : m
163
+ );
164
+ }
165
+ return [
166
+ ...prev,
167
+ {
168
+ id: ++this.msgCounter,
169
+ role: "bot",
170
+ text: d.text ?? "",
171
+ messageId: d.message_id,
172
+ isStreaming: false
173
+ }
174
+ ];
175
+ });
176
+ break;
177
+ case "chat.error":
178
+ case "llm.chat.error":
179
+ this.setState({
180
+ typing: false,
181
+ error: d.error ?? "Unknown error"
182
+ });
183
+ break;
184
+ case "error":
185
+ this.setState({
186
+ error: d.error ?? "Unknown error",
187
+ status: "error"
188
+ });
189
+ break;
190
+ }
191
+ this.dispatchEvent(new CustomEvent("event", { detail: d }));
192
+ }
193
+ // ── Actions ─────────────────────────────────────────────────────────
194
+ /** Send a text message to the agent. */
195
+ send(text) {
196
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
197
+ const trimmed = text.trim();
198
+ if (!trimmed) return;
199
+ this.setMessages((prev) => [
200
+ ...prev,
201
+ {
202
+ id: ++this.msgCounter,
203
+ role: "user",
204
+ text: trimmed
205
+ }
206
+ ]);
207
+ this.ws.send(JSON.stringify({ event: "message", text: trimmed }));
208
+ }
209
+ /**
210
+ * Set or clear a keyed context block in the LLM system prompt.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
215
+ * session.setContext("form", null); // clear
216
+ * ```
217
+ */
218
+ setContext(key, value) {
219
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
220
+ this.ws.send(JSON.stringify({ event: "set_context", key, value }));
221
+ }
222
+ /** Disconnect the chat session. */
223
+ disconnect() {
224
+ if (this.reconnectTimer) {
225
+ clearTimeout(this.reconnectTimer);
226
+ this.reconnectTimer = null;
227
+ }
228
+ if (this.ws) {
229
+ this.ws.close();
230
+ this.ws = null;
231
+ }
232
+ this.setState({ status: "idle", typing: false, streamingText: "" });
233
+ }
234
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
235
+ destroy() {
236
+ this.disconnect();
237
+ this.listeners.clear();
238
+ }
239
+ };
240
+
241
+ // src/react.tsx
242
+ function usePinecallChat(opts) {
243
+ const sessionRef = react.useRef(null);
244
+ if (!sessionRef.current) {
245
+ sessionRef.current = new ChatSession(opts);
246
+ }
247
+ const session = sessionRef.current;
248
+ const state = react.useSyncExternalStore(
249
+ react.useCallback((cb) => session.subscribe(cb), [session]),
250
+ react.useCallback(() => session.getState(), [session])
251
+ );
252
+ react.useEffect(() => {
253
+ if (opts.autoConnect !== false) {
254
+ session.connect();
255
+ }
256
+ return () => {
257
+ session.destroy();
258
+ sessionRef.current = null;
259
+ };
260
+ }, []);
261
+ return {
262
+ messages: state.messages,
263
+ send: react.useCallback((text) => session.send(text), [session]),
264
+ connected: state.status === "connected",
265
+ typing: state.typing,
266
+ streamingText: state.streamingText,
267
+ error: state.error,
268
+ setContext: react.useCallback(
269
+ (key, value) => session.setContext(key, value),
270
+ [session]
271
+ ),
272
+ connect: react.useCallback(() => session.connect(), [session]),
273
+ disconnect: react.useCallback(() => session.disconnect(), [session])
274
+ };
275
+ }
276
+
277
+ exports.usePinecallChat = usePinecallChat;
278
+ //# sourceMappingURL=react.cjs.map
279
+ //# sourceMappingURL=react.cjs.map