@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/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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|