@applica-software-guru/persona-chat-sdk 0.1.102

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.
Files changed (132) hide show
  1. package/.eslintrc.cjs +11 -0
  2. package/.github/copilot-instructions.md +3 -0
  3. package/.nvmrc +1 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +8 -0
  6. package/CLAUDE.md +3 -0
  7. package/GEMINI.md +3 -0
  8. package/README.md +33 -0
  9. package/bitbucket-pipelines.yml +19 -0
  10. package/components.json +24 -0
  11. package/dist/bundle.cjs.js +28 -0
  12. package/dist/bundle.cjs.js.map +1 -0
  13. package/dist/bundle.es.js +5173 -0
  14. package/dist/bundle.es.js.map +1 -0
  15. package/dist/bundle.iife.js +28 -0
  16. package/dist/bundle.iife.js.map +1 -0
  17. package/dist/bundle.umd.js +28 -0
  18. package/dist/bundle.umd.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/logging.d.ts +18 -0
  22. package/dist/logging.d.ts.map +1 -0
  23. package/dist/messages.d.ts +9 -0
  24. package/dist/messages.d.ts.map +1 -0
  25. package/dist/projects.d.ts +17 -0
  26. package/dist/projects.d.ts.map +1 -0
  27. package/dist/protocol/base.d.ts +25 -0
  28. package/dist/protocol/base.d.ts.map +1 -0
  29. package/dist/protocol/index.d.ts +6 -0
  30. package/dist/protocol/index.d.ts.map +1 -0
  31. package/dist/protocol/rest.d.ts +25 -0
  32. package/dist/protocol/rest.d.ts.map +1 -0
  33. package/dist/protocol/transaction.d.ts +56 -0
  34. package/dist/protocol/transaction.d.ts.map +1 -0
  35. package/dist/protocol/webrtc.d.ts +60 -0
  36. package/dist/protocol/webrtc.d.ts.map +1 -0
  37. package/dist/protocol/websocket.d.ts +22 -0
  38. package/dist/protocol/websocket.d.ts.map +1 -0
  39. package/dist/runtime/context.d.ts +34 -0
  40. package/dist/runtime/context.d.ts.map +1 -0
  41. package/dist/runtime/file-attachment-adapter.d.ts +15 -0
  42. package/dist/runtime/file-attachment-adapter.d.ts.map +1 -0
  43. package/dist/runtime/handlers.d.ts +21 -0
  44. package/dist/runtime/handlers.d.ts.map +1 -0
  45. package/dist/runtime/listeners.d.ts +6 -0
  46. package/dist/runtime/listeners.d.ts.map +1 -0
  47. package/dist/runtime/protocols.d.ts +17 -0
  48. package/dist/runtime/protocols.d.ts.map +1 -0
  49. package/dist/runtime/threads.d.ts +34 -0
  50. package/dist/runtime/threads.d.ts.map +1 -0
  51. package/dist/runtime/utils.d.ts +10 -0
  52. package/dist/runtime/utils.d.ts.map +1 -0
  53. package/dist/runtime.d.ts +6 -0
  54. package/dist/runtime.d.ts.map +1 -0
  55. package/dist/storage/base.d.ts +9 -0
  56. package/dist/storage/base.d.ts.map +1 -0
  57. package/dist/storage/index.d.ts +3 -0
  58. package/dist/storage/index.d.ts.map +1 -0
  59. package/dist/storage/persona.d.ts +29 -0
  60. package/dist/storage/persona.d.ts.map +1 -0
  61. package/dist/tools.d.ts +72 -0
  62. package/dist/tools.d.ts.map +1 -0
  63. package/dist/types.d.ts +237 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/docs/README.md +13 -0
  66. package/docs/api-reference.md +16 -0
  67. package/docs/contributing.md +21 -0
  68. package/docs/customization.md +74 -0
  69. package/docs/features.md +7 -0
  70. package/docs/installation.md +9 -0
  71. package/docs/messages.md +214 -0
  72. package/docs/protocols.md +39 -0
  73. package/docs/transactions.md +121 -0
  74. package/docs/usage.md +40 -0
  75. package/jsconfig.node.json +10 -0
  76. package/package.json +82 -0
  77. package/playground/index.html +14 -0
  78. package/playground/src/app.tsx +10 -0
  79. package/playground/src/chat.tsx +117 -0
  80. package/playground/src/components/assistant-ui/assistant-modal.tsx +57 -0
  81. package/playground/src/components/assistant-ui/attachment.tsx +197 -0
  82. package/playground/src/components/assistant-ui/markdown-text.tsx +228 -0
  83. package/playground/src/components/assistant-ui/thread-list.tsx +91 -0
  84. package/playground/src/components/assistant-ui/thread.tsx +302 -0
  85. package/playground/src/components/assistant-ui/threadlist-sidebar.tsx +59 -0
  86. package/playground/src/components/assistant-ui/tool-fallback.tsx +93 -0
  87. package/playground/src/components/assistant-ui/tooltip-icon-button.tsx +42 -0
  88. package/playground/src/components/chat/logging.tsx +53 -0
  89. package/playground/src/components/ui/avatar.tsx +51 -0
  90. package/playground/src/components/ui/button.tsx +60 -0
  91. package/playground/src/components/ui/dialog.tsx +141 -0
  92. package/playground/src/components/ui/input.tsx +21 -0
  93. package/playground/src/components/ui/separator.tsx +26 -0
  94. package/playground/src/components/ui/sheet.tsx +139 -0
  95. package/playground/src/components/ui/sidebar.tsx +619 -0
  96. package/playground/src/components/ui/skeleton.tsx +13 -0
  97. package/playground/src/components/ui/tooltip.tsx +59 -0
  98. package/playground/src/hooks/theme.ts +70 -0
  99. package/playground/src/hooks/use-mobile.ts +19 -0
  100. package/playground/src/lib/utils.ts +6 -0
  101. package/playground/src/main.tsx +10 -0
  102. package/playground/src/styles.css +120 -0
  103. package/playground/src/tools.ts +149 -0
  104. package/playground/src/vite-env.d.ts +1 -0
  105. package/preview-build.sh +23 -0
  106. package/src/index.ts +7 -0
  107. package/src/logging.ts +34 -0
  108. package/src/messages.ts +202 -0
  109. package/src/projects.ts +57 -0
  110. package/src/protocol/base.ts +73 -0
  111. package/src/protocol/index.ts +5 -0
  112. package/src/protocol/rest.ts +107 -0
  113. package/src/protocol/transaction.ts +182 -0
  114. package/src/protocol/webrtc.ts +379 -0
  115. package/src/protocol/websocket.ts +111 -0
  116. package/src/runtime/context.ts +88 -0
  117. package/src/runtime/file-attachment-adapter.ts +48 -0
  118. package/src/runtime/handlers.ts +322 -0
  119. package/src/runtime/index.ts +6 -0
  120. package/src/runtime/listeners.ts +79 -0
  121. package/src/runtime/protocols.ts +169 -0
  122. package/src/runtime/threads.ts +105 -0
  123. package/src/runtime/utils.ts +46 -0
  124. package/src/runtime.tsx +334 -0
  125. package/src/storage/base.ts +13 -0
  126. package/src/storage/index.ts +2 -0
  127. package/src/storage/persona.ts +138 -0
  128. package/src/tools.ts +211 -0
  129. package/src/types.ts +284 -0
  130. package/tsconfig.json +36 -0
  131. package/tsconfig.node.json +15 -0
  132. package/vite.config.ts +74 -0
@@ -0,0 +1,379 @@
1
+ import { PersonaProtocolBase } from './base';
2
+ import { PersonaPacket, PersonaProtocolBaseConfig, ProtocolStatus, Session } from '../types';
3
+
4
+ type AudioAnalysisData = {
5
+ localAmplitude: number;
6
+ remoteAmplitude: number;
7
+ };
8
+
9
+ type AudioVisualizerCallback = (data: AudioAnalysisData) => void;
10
+
11
+ type PersonaWebRTCMessageCallback = (data: MessageEvent) => void;
12
+
13
+ type PersonaWebRTCErrorCallback = (error: string) => void;
14
+
15
+ type PersonaWebRTCConfig = PersonaProtocolBaseConfig & {
16
+ webrtcUrl: string;
17
+ iceServers?: RTCIceServer[];
18
+ };
19
+
20
+ class PersonaWebRTCClient {
21
+ private config: PersonaWebRTCConfig;
22
+ private pc: RTCPeerConnection | null = null;
23
+ private ws: WebSocket | null = null;
24
+ private localStream: MediaStream | null = null;
25
+ private remoteStream: MediaStream = new MediaStream();
26
+ private audioCtx: AudioContext | null = null;
27
+
28
+ private localAnalyser: AnalyserNode | null = null;
29
+ private remoteAnalyser: AnalyserNode | null = null;
30
+ private analyzerFrame: number | null = null;
31
+ private dataChannel: RTCDataChannel | null = null;
32
+
33
+ private isConnected: boolean = false;
34
+ private visualizerCallbacks: AudioVisualizerCallback[] = [];
35
+ private messageCallbacks: PersonaWebRTCMessageCallback[] = [];
36
+ private errorCallbacks: PersonaWebRTCErrorCallback[] = [];
37
+ private queuedMessages: PersonaPacket[] = [];
38
+
39
+ constructor(config: PersonaWebRTCConfig) {
40
+ this.config = config;
41
+ }
42
+
43
+ public async connect(session: Session): Promise<Session> {
44
+ if (this.isConnected) return;
45
+
46
+ this.isConnected = true;
47
+
48
+ try {
49
+ this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
50
+ } catch (err) {
51
+ this.config.logger?.error('Error accessing microphone:', err);
52
+ return;
53
+ }
54
+
55
+ this.pc = new RTCPeerConnection({
56
+ iceServers: this.config.iceServers || [
57
+ {
58
+ urls: 'stun:34.38.108.251:3478',
59
+ },
60
+ {
61
+ urls: 'turn:34.38.108.251:3478',
62
+ username: 'webrtc',
63
+ credential: 'webrtc',
64
+ },
65
+ ],
66
+ });
67
+
68
+ this.localStream.getTracks().forEach((track) => {
69
+ this.pc!.addTrack(track, this.localStream!);
70
+ });
71
+
72
+ this.pc.ontrack = (event) => {
73
+ event.streams[0].getTracks().forEach((track) => {
74
+ this.remoteStream.addTrack(track);
75
+ });
76
+
77
+ if (!this.audioCtx) {
78
+ this._startAnalyzers();
79
+ }
80
+
81
+ const remoteAudio = new Audio();
82
+ remoteAudio.srcObject = this.remoteStream;
83
+ remoteAudio.play().catch((e) => {
84
+ this.config.logger?.error('Error playing remote audio:', e);
85
+ });
86
+ };
87
+
88
+ this.pc.onicecandidate = (event) => {
89
+ if (event.candidate && this.ws?.readyState === WebSocket.OPEN) {
90
+ this.ws.send(
91
+ JSON.stringify({
92
+ type: 'CANDIDATE',
93
+ src: 'client',
94
+ payload: { candidate: event.candidate },
95
+ }),
96
+ );
97
+ }
98
+ };
99
+
100
+ this.pc.ondatachannel = (event) => {
101
+ const channel = event.channel;
102
+ channel.onmessage = (msg) => {
103
+ this.messageCallbacks.forEach((callback) => {
104
+ callback(msg);
105
+ });
106
+ };
107
+ channel.onopen = () => {
108
+ while (this.queuedMessages.length > 0) {
109
+ const packet = this.queuedMessages.shift();
110
+ if (packet) {
111
+ channel.send(JSON.stringify(packet));
112
+ this.config.logger?.info('Sent queued message:', packet);
113
+ }
114
+ }
115
+ };
116
+ };
117
+
118
+ const url = this.config.webrtcUrl || 'wss://persona.applica.guru/api/webrtc';
119
+ this.ws = new WebSocket(`${url}?apiKey=${encodeURIComponent(this.config.apiKey)}`);
120
+ this.ws.onopen = async () => {
121
+ const offer = await this.pc!.createOffer();
122
+ await this.pc!.setLocalDescription(offer);
123
+
124
+ const metadata = {
125
+ apiKey: this.config.apiKey,
126
+ agentId: this.config.agentId,
127
+ userId: this.config.userId || 'anonymous',
128
+ sessionCode: session as string,
129
+ };
130
+ this.config.logger?.debug('Opening connection to WebRTC server: ', metadata);
131
+
132
+ const offerMessage = {
133
+ type: 'OFFER',
134
+ src: crypto.randomUUID?.() || 'client_' + Date.now(),
135
+ payload: {
136
+ sdp: {
137
+ sdp: offer.sdp,
138
+ type: offer.type,
139
+ },
140
+ connectionId: (Date.now() % 1000000).toString(),
141
+ metadata,
142
+ },
143
+ };
144
+
145
+ this.ws!.send(JSON.stringify(offerMessage));
146
+ };
147
+
148
+ this.ws.onmessage = async (event) => {
149
+ const data = JSON.parse(event.data);
150
+ if (data.type === 'ANSWER') {
151
+ await this.pc!.setRemoteDescription(new RTCSessionDescription(data.payload.sdp));
152
+ } else if (data.type === 'CANDIDATE') {
153
+ try {
154
+ await this.pc!.addIceCandidate(new RTCIceCandidate(data.payload.candidate));
155
+ } catch (err) {
156
+ this.config.logger?.error('Error adding ICE candidate:', err);
157
+ }
158
+ }
159
+ };
160
+
161
+ this.ws.onclose = (event: CloseEvent) => {
162
+ if (event.code !== 1000) {
163
+ this.errorCallbacks.forEach((callback) => {
164
+ callback('Oops! The connection to the server was lost. Please try again later.');
165
+ });
166
+ }
167
+ this._stopAnalyzers();
168
+ };
169
+ }
170
+
171
+ public async disconnect(): Promise<void> {
172
+ if (!this.isConnected) return;
173
+
174
+ this.isConnected = false;
175
+
176
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.close();
177
+ if (this.pc) this.pc.close();
178
+ if (this.localStream) {
179
+ this.localStream.getTracks().forEach((track) => track.stop());
180
+ }
181
+
182
+ this.remoteStream = new MediaStream();
183
+ if (this.audioCtx) {
184
+ await this.audioCtx.close();
185
+ this.audioCtx = null;
186
+ }
187
+
188
+ this._stopAnalyzers();
189
+ }
190
+
191
+ public addVisualizerCallback(callback: AudioVisualizerCallback): void {
192
+ this.visualizerCallbacks.push(callback);
193
+ }
194
+ public addMessageCallback(callback: PersonaWebRTCMessageCallback): void {
195
+ this.messageCallbacks.push(callback);
196
+ }
197
+
198
+ public addErrorCallback(callback: PersonaWebRTCErrorCallback): void {
199
+ this.errorCallbacks.push(callback);
200
+ }
201
+
202
+ public createDataChannel(label = 'messages'): void {
203
+ if (!this.pc) return;
204
+ this.dataChannel = this.pc.createDataChannel(label);
205
+ this.dataChannel.onopen = () => {
206
+ this.config.logger?.info('Data channel opened');
207
+ while (this.queuedMessages.length > 0) {
208
+ const packet = this.queuedMessages.shift();
209
+ if (packet) {
210
+ this.dataChannel!.send(JSON.stringify(packet));
211
+ this.config.logger?.info('Sent queued message:', packet);
212
+ }
213
+ }
214
+ };
215
+ this.dataChannel.onmessage = (msg: MessageEvent) => {
216
+ this.messageCallbacks.forEach((callback) => {
217
+ callback(msg);
218
+ });
219
+ };
220
+ }
221
+
222
+ public sendPacket(packet: PersonaPacket): void {
223
+ if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
224
+ this.queuedMessages.push(packet);
225
+ return;
226
+ }
227
+
228
+ this.dataChannel.send(JSON.stringify(packet));
229
+ this.config.logger?.info('Sent message:', packet);
230
+ }
231
+
232
+ private _startAnalyzers(): void {
233
+ if (!this.localStream || !this.remoteStream || this.visualizerCallbacks.length === 0) {
234
+ return;
235
+ }
236
+
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
239
+
240
+ const localSource = this.audioCtx.createMediaStreamSource(this.localStream);
241
+ const remoteSource = this.audioCtx.createMediaStreamSource(this.remoteStream);
242
+
243
+ this.localAnalyser = this.audioCtx.createAnalyser();
244
+ this.remoteAnalyser = this.audioCtx.createAnalyser();
245
+ this.localAnalyser.fftSize = 256;
246
+ this.remoteAnalyser.fftSize = 256;
247
+
248
+ localSource.connect(this.localAnalyser);
249
+ remoteSource.connect(this.remoteAnalyser);
250
+
251
+ const loop = () => {
252
+ if (!this.localAnalyser || !this.remoteAnalyser || this.visualizerCallbacks.length === 0) {
253
+ return;
254
+ }
255
+
256
+ const localArray = new Uint8Array(this.localAnalyser.frequencyBinCount);
257
+ const remoteArray = new Uint8Array(this.remoteAnalyser.frequencyBinCount);
258
+
259
+ this.localAnalyser.getByteFrequencyData(localArray);
260
+ this.remoteAnalyser.getByteFrequencyData(remoteArray);
261
+
262
+ const localAmp = localArray.reduce((a, b) => a + b, 0) / localArray.length;
263
+ const remoteAmp = remoteArray.reduce((a, b) => a + b, 0) / remoteArray.length;
264
+
265
+ if (this.visualizerCallbacks.length > 0) {
266
+ this.visualizerCallbacks.forEach((callback) => {
267
+ callback({
268
+ localAmplitude: localAmp,
269
+ remoteAmplitude: remoteAmp,
270
+ });
271
+ });
272
+ }
273
+
274
+ this.analyzerFrame = requestAnimationFrame(loop);
275
+ };
276
+
277
+ this.analyzerFrame = requestAnimationFrame(loop);
278
+ }
279
+
280
+ private _stopAnalyzers(): void {
281
+ if (this.analyzerFrame) {
282
+ cancelAnimationFrame(this.analyzerFrame);
283
+ this.analyzerFrame = null;
284
+ }
285
+ this.localAnalyser = null;
286
+ this.remoteAnalyser = null;
287
+ }
288
+ }
289
+
290
+ type PersonaWebRTCProtocolConfig = PersonaWebRTCConfig & {
291
+ autostart?: boolean;
292
+ };
293
+
294
+ class PersonaWebRTCProtocol extends PersonaProtocolBase {
295
+ status: ProtocolStatus;
296
+ session: Session;
297
+ autostart: boolean;
298
+ config: PersonaWebRTCProtocolConfig;
299
+ webRTCClient: PersonaWebRTCClient;
300
+
301
+ constructor(config: PersonaWebRTCProtocolConfig) {
302
+ super();
303
+ this.config = config;
304
+ this.status = 'disconnected';
305
+ this.session = null;
306
+ this.autostart = config?.autostart ?? false;
307
+ this.webRTCClient = new PersonaWebRTCClient(config);
308
+ this.webRTCClient.addMessageCallback((msg: MessageEvent) => {
309
+ const data = JSON.parse(msg.data) as PersonaPacket;
310
+ this.notifyPacket(data);
311
+ });
312
+ this.webRTCClient.addErrorCallback((error: string) => {
313
+ this.config.logger?.error('WebRTC error:', error);
314
+ this.notifyPacket({
315
+ type: 'message',
316
+ payload: {
317
+ type: 'text',
318
+ role: 'assistant',
319
+ text: error,
320
+ },
321
+ });
322
+ });
323
+ }
324
+
325
+ public getName(): string {
326
+ return 'webrtc';
327
+ }
328
+ public getPriority(): number {
329
+ return 1000;
330
+ }
331
+
332
+ public async syncSession(session: Session): Promise<void> {
333
+ super.syncSession(session);
334
+ if (this.status === 'connected') {
335
+ await this.disconnect();
336
+ await this.connect(session);
337
+ }
338
+ }
339
+
340
+ public async connect(session?: Session): Promise<Session> {
341
+ if (this.status === 'connected') {
342
+ return Promise.resolve(this.session);
343
+ }
344
+ this.session = session || this.session || 'new';
345
+ this.setStatus('connecting');
346
+
347
+ this.config.logger?.debug('Connecting to WebRTC with sessionId:', this.session);
348
+ await this.webRTCClient.connect(this.session);
349
+ this.setStatus('connected');
350
+
351
+ await this.webRTCClient.createDataChannel();
352
+
353
+ return this.session;
354
+ }
355
+
356
+ public async disconnect(): Promise<void> {
357
+ if (this.status === 'disconnected') {
358
+ this.config.logger?.warn('Already disconnected');
359
+ return Promise.resolve();
360
+ }
361
+
362
+ await this.webRTCClient.disconnect();
363
+
364
+ this.setStatus('disconnected');
365
+ this.config?.logger?.debug('Disconnected from WebRTC');
366
+ }
367
+
368
+ public sendPacket(packet: PersonaPacket): Promise<void> {
369
+ if (this.status !== 'connected') {
370
+ return Promise.reject(new Error('Not connected'));
371
+ }
372
+
373
+ this.webRTCClient.sendPacket(packet);
374
+ return Promise.resolve();
375
+ }
376
+ }
377
+
378
+ export { PersonaWebRTCProtocol };
379
+ export type { PersonaWebRTCProtocolConfig, AudioVisualizerCallback, AudioAnalysisData };
@@ -0,0 +1,111 @@
1
+ import { PersonaPacket, PersonaProtocolBaseConfig, ProtocolStatus, Session } from '../types';
2
+ import { PersonaProtocolBase } from './base';
3
+
4
+ type PersonaWebSocketProtocolConfig = PersonaProtocolBaseConfig & {
5
+ webSocketUrl: string;
6
+ };
7
+
8
+ class PersonaWebSocketProtocol extends PersonaProtocolBase {
9
+ status: ProtocolStatus;
10
+ autostart: boolean;
11
+ session: Session;
12
+ config: PersonaWebSocketProtocolConfig;
13
+ webSocket: WebSocket | null;
14
+
15
+ constructor(config: PersonaWebSocketProtocolConfig) {
16
+ super();
17
+ this.config = config;
18
+ this.status = 'disconnected';
19
+ this.autostart = true;
20
+ this.session = null;
21
+ this.webSocket = null;
22
+ }
23
+
24
+ public getName(): string {
25
+ return 'websocket';
26
+ }
27
+
28
+ public getPriority(): number {
29
+ return 500;
30
+ }
31
+
32
+ public async syncSession(session: Session): Promise<void> {
33
+ this.config.logger?.debug('Syncing session with WebSocket protocol:', session);
34
+ this.session = session;
35
+ if (this.webSocket && this.status === 'connected') {
36
+ this.disconnect();
37
+ this.connect(session);
38
+ }
39
+ }
40
+
41
+ public connect(session?: Session): Promise<Session> {
42
+ if (this.webSocket !== null && this.status === 'connected') {
43
+ return Promise.resolve(this.session);
44
+ }
45
+
46
+ const sid = session || this.session || 'new';
47
+
48
+ this.config.logger?.debug('Connecting to WebSocket with sessionId:', sid);
49
+
50
+ const apiKey = encodeURIComponent(this.config.apiKey);
51
+ const agentId = this.config.agentId;
52
+ const userId = this.config.userId || 'anonymous';
53
+ const webSocketUrl = `${this.config.webSocketUrl}?sessionCode=${sid}&agentId=${agentId}&apiKey=${apiKey}&userId=${encodeURIComponent(
54
+ userId,
55
+ )}`;
56
+ this.setStatus('connecting');
57
+ this.webSocket = new WebSocket(webSocketUrl);
58
+ this.webSocket.addEventListener('open', () => {
59
+ this.setStatus('connected');
60
+ });
61
+ this.webSocket.addEventListener('message', (event) => {
62
+ const data = JSON.parse(event.data) as PersonaPacket;
63
+ this.notifyPacket(data);
64
+ });
65
+ this.webSocket.addEventListener('close', (event: CloseEvent) => {
66
+ this.setStatus('disconnected');
67
+ this.webSocket = null;
68
+ if (event.code !== 1000) {
69
+ this.notifyPacket({
70
+ type: 'message',
71
+ payload: {
72
+ role: 'assistant',
73
+ type: 'text',
74
+ text: 'Oops! The connection to the server was lost. Please try again later.',
75
+ },
76
+ });
77
+ this.config.logger?.warn('WebSocket connection closed');
78
+ }
79
+ });
80
+
81
+ this.webSocket.addEventListener('error', () => {
82
+ this.setStatus('disconnected');
83
+ this.webSocket = null;
84
+ this.config.logger?.error('WebSocket connection error');
85
+ });
86
+
87
+ return Promise.resolve(sid);
88
+ }
89
+
90
+ public disconnect(): Promise<void> {
91
+ this.config.logger?.debug('Disconnecting WebSocket');
92
+ if (this.webSocket && this.status === 'connected') {
93
+ this.webSocket.close(1000, 'Normal closure');
94
+ this.setStatus('disconnected');
95
+ this.webSocket = null;
96
+ }
97
+ return Promise.resolve();
98
+ }
99
+
100
+ public sendPacket(packet: PersonaPacket): Promise<void> {
101
+ if (this.webSocket && this.status === 'connected') {
102
+ this.webSocket.send(JSON.stringify(packet));
103
+ return Promise.resolve();
104
+ } else {
105
+ return Promise.reject(new Error('WebSocket is not connected'));
106
+ }
107
+ }
108
+ }
109
+
110
+ export { PersonaWebSocketProtocol };
111
+ export type { PersonaWebSocketProtocolConfig };
@@ -0,0 +1,88 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { PersonaProtocol, ProtocolStatus } from '../types';
3
+ import { PersonaMessage } from '../types';
4
+ import { PersonaWebRTCProtocol } from '../protocol';
5
+ import { getFirstAvailableProtocolEndpoint } from './utils';
6
+
7
+ export type PersonaRuntimeContextType = {
8
+ protocols: PersonaProtocol[];
9
+ protocolsStatus: Map<string, ProtocolStatus>;
10
+ getMessages: () => PersonaMessage[];
11
+ };
12
+
13
+ export const PersonaRuntimeContext = createContext<PersonaRuntimeContextType | undefined>(undefined);
14
+
15
+ /**
16
+ * Hook to access the PersonaRuntime context
17
+ */
18
+ export function usePersonaRuntime(): PersonaRuntimeContextType {
19
+ const context = useContext(PersonaRuntimeContext);
20
+ if (!context) {
21
+ throw new Error('usePersonaRuntime must be used within a PersonaRuntimeProvider');
22
+ }
23
+ return context;
24
+ }
25
+
26
+ /**
27
+ * Retrieves a specific protocol instance from the PersonaRuntimeContext.
28
+ *
29
+ * @param protocol - The name of the protocol to use.
30
+ * @returns {PersonaProtocol | null} - The protocol instance or null if not found.
31
+ * @throws {Error} - If the hook is used outside of a PersonaRuntimeProvider.
32
+ */
33
+ export function usePersonaRuntimeProtocol(protocol: string): PersonaProtocol | null {
34
+ const context = useContext(PersonaRuntimeContext);
35
+ if (!context) {
36
+ throw new Error('usePersonaRuntimeProtocol must be used within a PersonaRuntimeProvider');
37
+ }
38
+
39
+ const protocolInstance = context.protocols.find((p) => p.getName() === protocol);
40
+ if (!protocolInstance) {
41
+ return null;
42
+ }
43
+
44
+ const status = context.protocolsStatus.get(protocolInstance.getName());
45
+
46
+ return {
47
+ ...protocolInstance,
48
+ connect: protocolInstance.connect.bind(protocolInstance),
49
+ disconnect: protocolInstance.disconnect.bind(protocolInstance),
50
+ sendPacket: protocolInstance.sendPacket.bind(protocolInstance),
51
+ setSession: protocolInstance.setSession.bind(protocolInstance),
52
+ addStatusChangeListener: protocolInstance.addStatusChangeListener.bind(protocolInstance),
53
+ addPacketListener: protocolInstance.addPacketListener.bind(protocolInstance),
54
+ getName: protocolInstance.getName.bind(protocolInstance),
55
+ getPriority: protocolInstance.getPriority.bind(protocolInstance),
56
+ status: status || protocolInstance.status,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Gets the endpoint URL of the first available protocol
62
+ */
63
+ export function usePersonaRuntimeEndpoint(): string | null {
64
+ const context = useContext(PersonaRuntimeContext);
65
+ if (!context) {
66
+ throw new Error('usePersonaRuntimeEndpoint must be used within a PersonaRuntimeProvider');
67
+ }
68
+
69
+ return getFirstAvailableProtocolEndpoint(context.protocols);
70
+ }
71
+
72
+ /**
73
+ * Gets the WebRTC protocol instance
74
+ */
75
+ export function usePersonaRuntimeWebRTCProtocol(): PersonaWebRTCProtocol | null {
76
+ return usePersonaRuntimeProtocol('webrtc') as PersonaWebRTCProtocol;
77
+ }
78
+
79
+ /**
80
+ * Gets all messages from the current conversation
81
+ */
82
+ export function usePersonaRuntimeMessages(): PersonaMessage[] {
83
+ const context = useContext(PersonaRuntimeContext);
84
+ if (!context) {
85
+ throw new Error('usePersonaRuntimeMessages must be used within a PersonaRuntimeProvider');
86
+ }
87
+ return context.getMessages();
88
+ }
@@ -0,0 +1,48 @@
1
+ import type { AttachmentAdapter } from '@assistant-ui/react';
2
+ import type { PendingAttachment, CompleteAttachment, Attachment } from '@assistant-ui/react';
3
+
4
+ function getFileDataURL(file: File): Promise<string> {
5
+ return new Promise((resolve, reject) => {
6
+ const reader = new FileReader();
7
+ reader.onload = () => resolve(reader.result as string);
8
+ reader.onerror = (error) => reject(error);
9
+ reader.readAsDataURL(file);
10
+ });
11
+ }
12
+
13
+ /**
14
+ * Generic file attachment adapter that accepts any file type.
15
+ * Files are read as base64 data URLs and sent with their original MIME type.
16
+ */
17
+ export class SimpleFileAttachmentAdapter implements AttachmentAdapter {
18
+ accept = '*';
19
+
20
+ async add(state: { file: File }): Promise<PendingAttachment> {
21
+ return {
22
+ id: state.file.name,
23
+ type: 'file',
24
+ name: state.file.name,
25
+ contentType: state.file.type || 'application/octet-stream',
26
+ file: state.file,
27
+ status: { type: 'requires-action', reason: 'composer-send' },
28
+ };
29
+ }
30
+
31
+ async send(attachment: PendingAttachment): Promise<CompleteAttachment> {
32
+ const dataUrl = await getFileDataURL(attachment.file);
33
+ const data = dataUrl.split(';base64,')[1];
34
+ return {
35
+ ...attachment,
36
+ status: { type: 'complete' },
37
+ content: [
38
+ {
39
+ type: 'file' as const,
40
+ data,
41
+ mimeType: attachment.contentType || 'application/octet-stream',
42
+ },
43
+ ],
44
+ };
45
+ }
46
+
47
+ async remove(_attachment: Attachment): Promise<void> {}
48
+ }