@aikaara/chat-sdk 0.1.4 → 0.2.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 ADDED
@@ -0,0 +1,435 @@
1
+ # @aikaara/chat-sdk
2
+
3
+ Embeddable chat widget and headless client for the Aikaara AI agent platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @aikaara/chat-sdk
9
+ ```
10
+
11
+ ## Three Entry Points
12
+
13
+ | Import Path | Use Case |
14
+ |-------------|----------|
15
+ | `@aikaara/chat-sdk` | Full bundle: widget + headless + `mount()`/`unmount()` |
16
+ | `@aikaara/chat-sdk/headless` | Headless only: no DOM dependency. For React Native, custom UI, server-side |
17
+ | `@aikaara/chat-sdk/ui` | Web Components only |
18
+
19
+ ## Quick Start — Chat Widget (CDN)
20
+
21
+ ```html
22
+ <script src="https://cdn.aikaara.com/chat-sdk/latest/aikaara-chat.iife.js"></script>
23
+ <script>
24
+ AikaaraChat.mount({
25
+ baseUrl: 'https://api.aikaara.com',
26
+ userToken: 'ut_...',
27
+ title: 'Support',
28
+ primaryColor: '#6366f1',
29
+ });
30
+ </script>
31
+ ```
32
+
33
+ ## Quick Start — Headless Client
34
+
35
+ ```typescript
36
+ import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
37
+
38
+ const client = new AikaaraChatClient({
39
+ baseUrl: 'https://api.aikaara.com',
40
+ userToken: 'ut_...',
41
+ apiKey: 'ak_...',
42
+ });
43
+
44
+ await client.connect();
45
+ client.on('message:received', (msg) => console.log(msg.content));
46
+ client.on('stream:update', ({ content }) => updateUI(content));
47
+
48
+ await client.sendMessage('Hello!');
49
+ ```
50
+
51
+ ## Quick Start — Headless with JWT Auth (Dashboard / Internal Apps)
52
+
53
+ ```typescript
54
+ import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
55
+
56
+ const client = new AikaaraChatClient({
57
+ baseUrl: 'https://api.aikaara.com',
58
+ userToken: 'ut_...',
59
+ authToken: jwtToken, // Bearer token for REST calls
60
+ channel: 'sidekick',
61
+ });
62
+
63
+ await client.connect();
64
+ ```
65
+
66
+ ## App Context — Connecting the Agent to Your App
67
+
68
+ The SDK doesn't know about your app's routes, pages, or entities. You provide that context at runtime via `setContext()`, and the agent uses it to navigate, edit forms, and stay aware of what the user is doing.
69
+
70
+ ### Why This Matters
71
+
72
+ Without context, the agent doesn't know:
73
+ - What page the user is on (`/products/42`? `/orders`?)
74
+ - What routes exist in your app (so it can navigate)
75
+ - What entity the user is viewing (so it can edit the right form)
76
+
77
+ ### Providing Context on Route Changes
78
+
79
+ Call `setContext()` whenever the user navigates in your app:
80
+
81
+ ```typescript
82
+ import { AikaaraChatClient } from '@aikaara/chat-sdk/headless';
83
+
84
+ const client = new AikaaraChatClient({
85
+ baseUrl: 'https://api.aikaara.com',
86
+ userToken: 'ut_...',
87
+ authToken: jwt,
88
+ channel: 'sidekick',
89
+ });
90
+
91
+ await client.connect();
92
+
93
+ // Call on every route change (e.g., in a React useEffect, Vue watch, etc.)
94
+ client.setContext({
95
+ currentPage: '/products/42',
96
+ entityType: 'product',
97
+ entityId: '42',
98
+ availableRoutes: {
99
+ 'Product list': '/products',
100
+ 'Order list': '/orders',
101
+ 'Customer list': '/customers',
102
+ 'Settings': '/settings',
103
+ 'Analytics': '/analytics',
104
+ },
105
+ custom: {
106
+ formFields: ['name', 'price', 'description', 'category'],
107
+ userName: 'Jane',
108
+ },
109
+ });
110
+ ```
111
+
112
+ ### What Happens Under the Hood
113
+
114
+ 1. `setContext()` sends a PATCH to the backend, storing the context in conversation metadata
115
+ 2. The backend interpolates `{{current_page}}`, `{{available_routes}}`, and `{{custom_context}}` into the agent's system prompt
116
+ 3. The agent now knows your app's routes and can call `navigate_to` with them
117
+ 4. The agent knows the current entity and can call `edit_current_entity` for it
118
+
119
+ ### AppContext Interface
120
+
121
+ ```typescript
122
+ interface AppContext {
123
+ /** Current page/route path in your app (e.g., '/products/42') */
124
+ currentPage: string;
125
+ /** Entity type on the current page (e.g., 'product', 'order') */
126
+ entityType?: string;
127
+ /** Entity ID on the current page */
128
+ entityId?: string | number;
129
+ /** Project/workspace ID if applicable */
130
+ projectId?: string | number;
131
+ /** Routes the agent can navigate to — map of label to path */
132
+ availableRoutes?: Record<string, string>;
133
+ /** Any additional context for the agent (form fields, user info, etc.) */
134
+ custom?: Record<string, unknown>;
135
+ }
136
+ ```
137
+
138
+ ### Handling Navigation
139
+
140
+ The agent doesn't control your router — it returns a path, and you handle it:
141
+
142
+ ```typescript
143
+ client.on('action:navigate', ({ navigate_to }) => {
144
+ // Your router (React Router, Vue Router, Next.js, etc.)
145
+ router.push(navigate_to);
146
+ });
147
+ ```
148
+
149
+ The `navigate_to` value will be one of the paths from your `availableRoutes`, or a path the agent constructs from context (e.g., `/products/42`).
150
+
151
+ ### Framework Examples
152
+
153
+ **React (with React Router)**
154
+
155
+ ```tsx
156
+ import { useEffect } from 'react';
157
+ import { useLocation, useNavigate } from 'react-router';
158
+
159
+ function useSidekickContext(client: AikaaraChatClient) {
160
+ const location = useLocation();
161
+ const navigate = useNavigate();
162
+
163
+ // Update context on route change
164
+ useEffect(() => {
165
+ const match = location.pathname.match(/^\/(products|orders|customers)\/(\d+)/);
166
+ client.setContext({
167
+ currentPage: location.pathname,
168
+ entityType: match?.[1],
169
+ entityId: match?.[2],
170
+ availableRoutes: {
171
+ 'Products': '/products',
172
+ 'Orders': '/orders',
173
+ 'Customers': '/customers',
174
+ 'Analytics': '/analytics',
175
+ },
176
+ });
177
+ }, [location.pathname]);
178
+
179
+ // Handle navigation from agent
180
+ useEffect(() => {
181
+ const off = client.on('action:navigate', ({ navigate_to }) => {
182
+ navigate(navigate_to);
183
+ });
184
+ return off;
185
+ }, [navigate]);
186
+ }
187
+ ```
188
+
189
+ **Vue 3 (with Vue Router)**
190
+
191
+ ```typescript
192
+ import { watch } from 'vue';
193
+ import { useRoute, useRouter } from 'vue-router';
194
+
195
+ function useSidekickContext(client: AikaaraChatClient) {
196
+ const route = useRoute();
197
+ const router = useRouter();
198
+
199
+ watch(() => route.fullPath, (path) => {
200
+ client.setContext({
201
+ currentPage: path,
202
+ entityType: route.params.type as string,
203
+ entityId: route.params.id as string,
204
+ availableRoutes: { Products: '/products', Orders: '/orders' },
205
+ });
206
+ }, { immediate: true });
207
+
208
+ client.on('action:navigate', ({ navigate_to }) => {
209
+ router.push(navigate_to);
210
+ });
211
+ }
212
+ ```
213
+
214
+ **Vanilla JS / Any Framework**
215
+
216
+ ```typescript
217
+ // On route change (however your app detects it)
218
+ window.addEventListener('popstate', () => {
219
+ client.setContext({ currentPage: window.location.pathname });
220
+ });
221
+
222
+ // On navigation action from agent
223
+ client.on('action:navigate', ({ navigate_to }) => {
224
+ window.history.pushState(null, '', navigate_to);
225
+ // Trigger your app's route handler
226
+ });
227
+ ```
228
+
229
+ ## FormBridge — AI-Driven Form Editing
230
+
231
+ The `FormBridge` lets AI agents visually edit forms in your application. When the agent calls tools like `edit_current_entity`, the bridge pushes field updates to your registered form — the user sees changes live and can confirm or reject them.
232
+
233
+ ### How It Works
234
+
235
+ 1. Your agent has tools like `edit_current_entity`, `save_current_entity`, and `test_tool_by_id`
236
+ 2. When the agent calls these tools, the result flows through the WebSocket as a `tool_execution_end` event
237
+ 3. The SDK's `AikaaraChatClient` parses the result and emits typed action events
238
+ 4. `FormBridge` listens for these events and pushes updates to your registered form
239
+
240
+ ### Integration Steps
241
+
242
+ #### 1. Import FormBridge from the SDK
243
+
244
+ ```typescript
245
+ import { AikaaraChatClient, FormBridge } from '@aikaara/chat-sdk/headless';
246
+ ```
247
+
248
+ #### 2. Pass it your AikaaraChatClient
249
+
250
+ ```typescript
251
+ const client = new AikaaraChatClient({
252
+ baseUrl: 'https://api.aikaara.com',
253
+ userToken: 'ut_...',
254
+ authToken: jwtToken,
255
+ channel: 'sidekick',
256
+ });
257
+
258
+ const bridge = new FormBridge(client);
259
+ await client.connect();
260
+ ```
261
+
262
+ #### 3. Register your forms with `registerForm()`
263
+
264
+ Call this when your form component mounts. Only one form can be active at a time (the current page).
265
+
266
+ ```typescript
267
+ bridge.registerForm({
268
+ entityType: 'product', // matches what the agent sends
269
+ entityId: 42, // the entity being edited
270
+ onFieldUpdate: (fields) => {
271
+ // Update your form UI — fields is FieldUpdate[]
272
+ for (const { field, value } of fields) {
273
+ formState[field] = value;
274
+ rerenderField(field); // your UI framework's update mechanism
275
+ }
276
+ },
277
+ onSave: async () => {
278
+ // Called when the agent triggers save_current_entity
279
+ await api.updateProduct(42, formState);
280
+ },
281
+ onTest: async (params) => {
282
+ // Optional: called when the agent triggers test_tool_by_id
283
+ await api.testProduct(42, params);
284
+ },
285
+ getCurrentValues: () => ({ ...formState }),
286
+ });
287
+
288
+ // Unregister when the form unmounts
289
+ bridge.unregisterForm('product', 42);
290
+ ```
291
+
292
+ #### 4. The bridge automatically handles tool results
293
+
294
+ When the agent calls these backend tools, the bridge takes action automatically:
295
+
296
+ | Agent Tool | Bridge Action |
297
+ |-----------|---------------|
298
+ | `edit_current_entity` | Calls `onFieldUpdate()` with the field changes |
299
+ | `save_current_entity` | Calls `onSave()` on the registered form |
300
+ | `test_tool_by_id` | Calls `onTest()` with parameters |
301
+
302
+ If no form is registered when an `edit_current_entity` result arrives, the bridge queues it. When a matching form later registers, the queued edits are applied automatically.
303
+
304
+ #### 5. Build your own confirmation UI using bridge events
305
+
306
+ ```typescript
307
+ bridge.on('edit:applied', ({ entityType, entityId, fields }) => {
308
+ // Show a confirmation banner: "AI edited: name, description"
309
+ showBanner({
310
+ message: `AI edited: ${fields.map(f => f.field).join(', ')}`,
311
+ onConfirm: () => dismissBanner(),
312
+ onReject: () => {
313
+ // Revert fields using previousValue
314
+ revertFields(fields);
315
+ dismissBanner();
316
+ },
317
+ onSave: async () => {
318
+ await bridge.requestSave();
319
+ dismissBanner();
320
+ },
321
+ });
322
+ });
323
+
324
+ bridge.on('edit:pending', ({ entityType, entityId, fields }) => {
325
+ // Edits queued — no matching form registered yet
326
+ // Navigate the user to the right page, then the form will pick them up
327
+ });
328
+
329
+ bridge.on('save:success', () => toast.success('Saved by AI'));
330
+ bridge.on('save:error', ({ error }) => toast.error(`Save failed: ${error}`));
331
+ bridge.on('test:triggered', ({ toolId, parameters }) => {
332
+ // Show test UI if needed
333
+ });
334
+ ```
335
+
336
+ ### Lower-Level: Action Events on AikaaraChatClient
337
+
338
+ If you don't need `FormBridge` and want to handle actions yourself:
339
+
340
+ ```typescript
341
+ client.on('action:edit_entity', (action) => {
342
+ // action: { entity_type, entity_id, fields: FieldUpdate[] }
343
+ });
344
+
345
+ client.on('action:save_entity', () => {
346
+ // Trigger save
347
+ });
348
+
349
+ client.on('action:navigate', ({ navigate_to }) => {
350
+ // Route change: e.g., router.push(navigate_to)
351
+ });
352
+
353
+ client.on('action:test_tool', ({ tool_id, parameters }) => {
354
+ // Run a tool test
355
+ });
356
+
357
+ // Raw tool lifecycle events
358
+ client.on('tool:start', ({ toolName, args }) => showSpinner(toolName));
359
+ client.on('tool:end', ({ toolName, result, isError }) => hideSpinner(toolName));
360
+ ```
361
+
362
+ ### React Hook Example
363
+
364
+ ```tsx
365
+ import { useEffect, useRef } from 'react';
366
+ import type { FormBridge, FieldUpdate } from '@aikaara/chat-sdk/headless';
367
+
368
+ function useFormBridge(
369
+ bridge: FormBridge,
370
+ options: {
371
+ entityType: string;
372
+ entityId: string | number | undefined;
373
+ onFieldUpdate: (fields: FieldUpdate[]) => void;
374
+ onSave: () => Promise<void>;
375
+ onTest?: (params?: Record<string, unknown>) => Promise<void>;
376
+ getCurrentValues: () => Record<string, unknown>;
377
+ }
378
+ ) {
379
+ const refs = useRef(options);
380
+ refs.current = options;
381
+
382
+ useEffect(() => {
383
+ if (!options.entityId) return;
384
+ bridge.registerForm({
385
+ entityType: options.entityType,
386
+ entityId: options.entityId,
387
+ onFieldUpdate: (f) => refs.current.onFieldUpdate(f),
388
+ onSave: () => refs.current.onSave(),
389
+ onTest: refs.current.onTest ? (p) => refs.current.onTest!(p) : undefined,
390
+ getCurrentValues: () => refs.current.getCurrentValues(),
391
+ });
392
+ return () => bridge.unregisterForm(options.entityType, options.entityId!);
393
+ }, [bridge, options.entityType, options.entityId]);
394
+ }
395
+ ```
396
+
397
+ ## Agent Events
398
+
399
+ Events broadcast over ActionCable from the Aikaara Rails backend:
400
+
401
+ | Event | Description |
402
+ |-------|-------------|
403
+ | `status` | Processing state (processing/completed) |
404
+ | `message_start/update/end` | Streaming response lifecycle |
405
+ | `message_queued` | User message acknowledged |
406
+ | `tool_execution_start/update/end` | Tool call lifecycle |
407
+ | `agent_start/end` | Agent session lifecycle |
408
+ | `turn_start/end` | Conversation turn lifecycle |
409
+ | `auto_retry_start/end` | Auto-retry on failure |
410
+ | `cancelled` | Processing cancelled |
411
+
412
+ ## TypeScript Types
413
+
414
+ All types are exported from `@aikaara/chat-sdk/headless`:
415
+
416
+ ```typescript
417
+ import type {
418
+ // Connection
419
+ ConnectionConfig, ConnectionState, ChatClientConfig,
420
+ // Messages
421
+ Message, ToolCall, ToolCallResult,
422
+ // Events
423
+ AgentEvent, AgentEventType, ChatEvents,
424
+ // App Context
425
+ AppContext,
426
+ // Form Bridge
427
+ FieldUpdate, FormRegistration, FormBridgeEvents,
428
+ // Actions
429
+ EditEntityAction, SaveEntityAction, TestToolAction, NavigateAction, AgentAction,
430
+ } from '@aikaara/chat-sdk/headless';
431
+ ```
432
+
433
+ ## License
434
+
435
+ MIT
@@ -163,8 +163,8 @@ class m {
163
163
  this.handlers.clear();
164
164
  }
165
165
  }
166
- const f = 1e3, b = 10, y = 400, I = 600, k = "#6366f1", E = 12, T = "system-ui, -apple-system, sans-serif", C = "Type a message...", M = "bottom-right", A = "light", O = { x: 20, y: 20 }, d = "aikaara_conversation_id";
167
- class _ extends m {
166
+ const f = 1e3, _ = 10, w = 400, k = 600, T = "#6366f1", I = 12, E = "system-ui, -apple-system, sans-serif", C = "Type a message...", A = "bottom-right", M = "light", O = { x: 20, y: 20 }, d = "aikaara_conversation_id";
167
+ class b extends m {
168
168
  client;
169
169
  config;
170
170
  state = "disconnected";
@@ -220,7 +220,7 @@ class _ extends m {
220
220
  this.state !== t && (this.state = t, this.emit("connection:state", t));
221
221
  }
222
222
  scheduleReconnect() {
223
- const t = this.config.maxReconnectAttempts ?? b;
223
+ const t = this.config.maxReconnectAttempts ?? _;
224
224
  if (this.reconnectAttempt >= t) {
225
225
  this.emit("error", new Error("Max reconnection attempts reached"));
226
226
  return;
@@ -246,12 +246,13 @@ class _ extends m {
246
246
  return `${t.replace(/^http/, "ws")}/cable?token=${e}`;
247
247
  }
248
248
  }
249
- class S {
249
+ class y {
250
250
  baseUrl;
251
251
  apiKey;
252
+ authToken;
252
253
  userToken;
253
- constructor(t, e, s) {
254
- this.baseUrl = t, this.userToken = e, this.apiKey = s;
254
+ constructor(t, e, s, n) {
255
+ this.baseUrl = t, this.userToken = e, this.apiKey = s, this.authToken = n;
255
256
  }
256
257
  async createConversation(t) {
257
258
  const e = {
@@ -264,6 +265,10 @@ class S {
264
265
  };
265
266
  return this.request("POST", "/api/v1/conversations", e);
266
267
  }
268
+ async updateContext(t, e) {
269
+ const s = this.authToken ? `/dashboard/sidekick_conversations/${t}` : `/api/v1/conversations/${t}`;
270
+ await this.request("PATCH", s, e);
271
+ }
267
272
  async getMessages(t) {
268
273
  return (await this.request(
269
274
  "GET",
@@ -294,22 +299,22 @@ class S {
294
299
  "Content-Type": "application/json",
295
300
  Accept: "application/json"
296
301
  };
297
- this.apiKey && (n["X-Api-Key"] = this.apiKey);
302
+ this.apiKey && (n["X-Api-Key"] = this.apiKey), this.authToken && (n.Authorization = `Bearer ${this.authToken}`);
298
303
  const i = `${this.baseUrl}${e}`, l = { method: t, headers: n };
299
304
  s && (l.body = JSON.stringify(s));
300
- const c = await fetch(i, l);
301
- if (!c.ok) {
302
- const a = await c.text();
305
+ const a = await fetch(i, l);
306
+ if (!a.ok) {
307
+ const c = await a.text();
303
308
  let h;
304
309
  try {
305
- const u = JSON.parse(a);
306
- h = u.error || u.message || a;
310
+ const u = JSON.parse(c);
311
+ h = u.error || u.message || c;
307
312
  } catch {
308
- h = a;
313
+ h = c;
309
314
  }
310
- throw new Error(`API error ${c.status}: ${h}`);
315
+ throw new Error(`API error ${a.status}: ${h}`);
311
316
  }
312
- const r = await c.json();
317
+ const r = await a.json();
313
318
  if (r && typeof r == "object" && "success" in r) {
314
319
  if (!r.success)
315
320
  throw new Error(`API error: ${r.message || "Request failed"}`);
@@ -375,7 +380,7 @@ class v {
375
380
  this._messages = [];
376
381
  }
377
382
  }
378
- class w {
383
+ class S {
379
384
  _conversationId;
380
385
  persist;
381
386
  constructor(t, e = !0) {
@@ -417,7 +422,7 @@ class U extends m {
417
422
  subscription = null;
418
423
  config;
419
424
  constructor(t) {
420
- super(), this.config = t, this.connection = new _(t), this.api = new S(t.baseUrl, t.userToken, t.apiKey), this.messageStore = new v(), this.conversationManager = new w(t.conversationId), this.connection.on("connection:state", (e) => {
425
+ super(), this.config = t, this.connection = new b(t), this.api = new y(t.baseUrl, t.userToken, t.apiKey, t.authToken), this.messageStore = new v(), this.conversationManager = new S(t.conversationId), this.connection.on("connection:state", (e) => {
421
426
  this.emit("connection:state", e), this.config.onConnectionStateChange?.(e);
422
427
  }), this.connection.on("error", (e) => {
423
428
  this.emit("error", e), this.config.onError?.(e);
@@ -470,9 +475,51 @@ class U extends m {
470
475
  get isConnected() {
471
476
  return this.connection.connectionState === "connected";
472
477
  }
478
+ /**
479
+ * Update the agent's context with information about the host app's current state.
480
+ * Call this on route changes so the agent knows what page/entity the user is viewing.
481
+ *
482
+ * The context is stored in conversation metadata and interpolated into the system prompt.
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * // On route change
487
+ * client.setContext({
488
+ * currentPage: '/products/42',
489
+ * entityType: 'product',
490
+ * entityId: '42',
491
+ * availableRoutes: { products: '/products', orders: '/orders' },
492
+ * });
493
+ * ```
494
+ */
495
+ async setContext(t) {
496
+ const e = this.conversationManager.conversationId;
497
+ e && await this.api.updateContext(e, {
498
+ current_page: t.currentPage,
499
+ entity_type: t.entityType,
500
+ entity_id: t.entityId,
501
+ project_id: t.projectId,
502
+ available_routes: t.availableRoutes,
503
+ custom_context: t.custom
504
+ });
505
+ }
473
506
  async disconnect() {
474
507
  this.subscription && (this.subscription = null), await this.connection.disconnect();
475
508
  }
509
+ /**
510
+ * Parse structured action results from tool execution output.
511
+ * When the agent calls tools like `edit_current_entity`, `save_current_entity`,
512
+ * `navigate_to`, or `test_tool_by_id`, the result contains an action payload
513
+ * that the SDK emits as a typed event for the host app to handle.
514
+ */
515
+ parseActionResult(t) {
516
+ try {
517
+ const e = typeof t == "string" ? JSON.parse(t) : t;
518
+ if (!e || typeof e != "object") return;
519
+ e.navigate_to ? this.emit("action:navigate", e) : e.action === "edit_entity" ? this.emit("action:edit_entity", e) : e.action === "save_entity" ? this.emit("action:save_entity", e) : e.action === "test_tool" && this.emit("action:test_tool", e);
520
+ } catch {
521
+ }
522
+ }
476
523
  handleBroadcast(t) {
477
524
  const e = this.conversationManager.conversationId;
478
525
  switch (t.type) {
@@ -515,9 +562,22 @@ class U extends m {
515
562
  s && this.messageStore.confirmOptimistic(s.id);
516
563
  break;
517
564
  }
518
- case "tool_execution_start":
565
+ case "tool_execution_start": {
566
+ this.emit("tool:start", {
567
+ toolName: t.tool_name || "",
568
+ args: t.args || {}
569
+ }), this.emit("status", t.type);
570
+ break;
571
+ }
572
+ case "tool_execution_end": {
573
+ this.emit("tool:end", {
574
+ toolName: t.tool_name || "",
575
+ result: t.result,
576
+ isError: !!t.is_error
577
+ }), this.emit("status", t.type), this.parseActionResult(t.result);
578
+ break;
579
+ }
519
580
  case "tool_execution_update":
520
- case "tool_execution_end":
521
581
  case "agent_start":
522
582
  case "agent_end":
523
583
  case "turn_start":
@@ -537,15 +597,15 @@ export {
537
597
  m as E,
538
598
  v as M,
539
599
  U as a,
540
- S as b,
541
- _ as c,
542
- w as d,
600
+ y as b,
601
+ b as c,
602
+ S as d,
543
603
  C as e,
544
- E as f,
545
- T as g,
546
- I as h,
547
- y as i,
548
- M as j,
549
- k,
550
- A as l
604
+ I as f,
605
+ E as g,
606
+ k as h,
607
+ w as i,
608
+ A as j,
609
+ T as k,
610
+ M as l
551
611
  };
@@ -0,0 +1 @@
1
+ "use strict";class p{identifier;callbacks={};sendFn;constructor(t,e){this.identifier=t,this.sendFn=e}onReceived(t){return this.callbacks.received=t,this}onConnected(t){return this.callbacks.connected=t,this}onDisconnected(t){return this.callbacks.disconnected=t,this}onRejected(t){return this.callbacks.rejected=t,this}perform(t,e={}){this.sendFn({action:t,...e})}_notifyReceived(t){this.callbacks.received?.(t)}_notifyConnected(){this.callbacks.connected?.()}_notifyDisconnected(){this.callbacks.disconnected?.()}_notifyRejected(){this.callbacks.rejected?.()}}class l{ws=null;url;subscriptions=new Map;welcomePromise=null;pendingSubscriptions=new Map;constructor(t){this.url=t}connect(){return new Promise((t,e)=>{this.welcomePromise={resolve:t,reject:e},this.ws=new WebSocket(this.url),this.ws.onopen=()=>{},this.ws.onmessage=s=>{this.handleMessage(s)},this.ws.onerror=()=>{const s=new Error("WebSocket connection error");this.welcomePromise?.reject(s),this.welcomePromise=null},this.ws.onclose=()=>{this.subscriptions.forEach(s=>s._notifyDisconnected())}})}disconnect(){this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null),this.subscriptions.forEach(t=>t._notifyDisconnected()),this.subscriptions.clear()}subscribe(t){const e=JSON.stringify(t),s=new p(e,n=>{this.send({command:"message",identifier:e,data:JSON.stringify(n)})});return this.subscriptions.set(e,s),this.send({command:"subscribe",identifier:e}),s}subscribeAsync(t){const e=this.subscribe(t),s=e.identifier;return new Promise((n,i)=>{this.pendingSubscriptions.set(s,{resolve:()=>n(e),reject:i}),setTimeout(()=>{this.pendingSubscriptions.has(s)&&(this.pendingSubscriptions.delete(s),i(new Error(`Subscription timeout for ${s}`)))},1e4)})}unsubscribe(t){this.send({command:"unsubscribe",identifier:t}),this.subscriptions.delete(t)}perform(t,e,s={}){this.send({command:"message",identifier:t,data:JSON.stringify({action:e,...s})})}get isConnected(){return this.ws?.readyState===WebSocket.OPEN}send(t){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(t))}handleMessage(t){let e;try{e=JSON.parse(t.data)}catch{return}switch(e.type){case"welcome":this.welcomePromise?.resolve(),this.welcomePromise=null;break;case"ping":break;case"confirm_subscription":{const s=e.identifier;this.subscriptions.get(s)?._notifyConnected();const i=this.pendingSubscriptions.get(s);i&&(i.resolve(),this.pendingSubscriptions.delete(s));break}case"reject_subscription":{const s=e.identifier;this.subscriptions.get(s)?._notifyRejected(),this.subscriptions.delete(s);const i=this.pendingSubscriptions.get(s);i&&(i.reject(new Error(`Subscription rejected: ${s}`)),this.pendingSubscriptions.delete(s));break}case"disconnect":this.subscriptions.forEach(s=>s._notifyDisconnected());break;default:{e.identifier&&e.message!==void 0&&this.subscriptions.get(e.identifier)?._notifyReceived(e.message);break}}}}class u{handlers=new Map;on(t,e){return this.handlers.has(t)||this.handlers.set(t,new Set),this.handlers.get(t).add(e),()=>this.off(t,e)}off(t,e){this.handlers.get(t)?.delete(e)}emit(t,e){this.handlers.get(t)?.forEach(s=>{try{s(e)}catch(n){console.error(`Error in event handler for "${t}":`,n)}})}removeAllListeners(){this.handlers.clear()}}const y=1e3,S=10,w=400,T=600,E="#6366f1",I=12,k="system-ui, -apple-system, sans-serif",A="Type a message...",C="bottom-right",M="light",O={x:20,y:20},d="aikaara_conversation_id";class f extends u{client;config;state="disconnected";reconnectAttempt=0;reconnectTimer=null;constructor(t){super(),this.config=t;const e=this.buildWsUrl(t.baseUrl,t.userToken);this.client=new l(e)}async connect(){this.setState("connecting");try{await this.client.connect(),this.setState("connected"),this.reconnectAttempt=0}catch(t){if(this.setState("disconnected"),this.config.reconnect!==!1)this.scheduleReconnect();else throw t}}async disconnect(){this.clearReconnectTimer(),this.client.disconnect(),this.setState("disconnected")}subscribeToConversation(t){return this.client.subscribeAsync({channel:"ConversationChannel",conversation_id:t})}sendMessage(t,e){const s=JSON.stringify({channel:"ConversationChannel",conversation_id:t});this.client.perform(s,"send_message",{content:e})}sendUserEvent(t,e,s,n){const i=JSON.stringify({channel:"ConversationChannel",conversation_id:t});this.client.perform(i,"send_user_event",{event_key:e,...s&&{value:s},...n&&{source:n}})}get connectionState(){return this.state}setState(t){this.state!==t&&(this.state=t,this.emit("connection:state",t))}scheduleReconnect(){const t=this.config.maxReconnectAttempts??S;if(this.reconnectAttempt>=t){this.emit("error",new Error("Max reconnection attempts reached"));return}this.setState("reconnecting");const s=(this.config.reconnectInterval??y)*Math.pow(2,this.reconnectAttempt);this.reconnectAttempt++,this.reconnectTimer=setTimeout(async()=>{try{const n=this.buildWsUrl(this.config.baseUrl,this.config.userToken);this.client=new l(n),await this.connect()}catch{}},s)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}buildWsUrl(t,e){if(this.config.wsUrl){const n=this.config.wsUrl.includes("?")?"&":"?";return`${this.config.wsUrl}${n}token=${e}`}return`${t.replace(/^http/,"ws")}/cable?token=${e}`}}class _{baseUrl;apiKey;authToken;userToken;constructor(t,e,s,n){this.baseUrl=t,this.userToken=e,this.apiKey=s,this.authToken=n}async createConversation(t){const e={conversation:{...t.extUid&&{ext_uid:t.extUid},...t.systemPromptId&&{system_prompt_id:t.systemPromptId},...t.channel&&{channel:t.channel},...t.title&&{title:t.title}}};return this.request("POST","/api/v1/conversations",e)}async updateContext(t,e){const s=this.authToken?`/dashboard/sidekick_conversations/${t}`:`/api/v1/conversations/${t}`;await this.request("PATCH",s,e)}async getMessages(t){return(await this.request("GET",`/api/v1/conversations/${t}/messages`)).map(this.mapMessage)}mapMessage(t){return{id:String(t.id),conversationId:String(t.conversation_id),role:t.role,content:t.content||"",toolCalls:t.tool_calls?.map(e=>({id:e.id,type:e.type,function:e.function})),toolCallResults:t.tool_call_results,tokensInput:t.tokens_input,tokensOutput:t.tokens_output,metadata:t.metadata,createdAt:t.created_at,status:"complete"}}async request(t,e,s){const n={"Content-Type":"application/json",Accept:"application/json"};this.apiKey&&(n["X-Api-Key"]=this.apiKey),this.authToken&&(n.Authorization=`Bearer ${this.authToken}`);const i=`${this.baseUrl}${e}`,g={method:t,headers:n};s&&(g.body=JSON.stringify(s));const a=await fetch(i,g);if(!a.ok){const c=await a.text();let h;try{const m=JSON.parse(c);h=m.error||m.message||c}catch{h=c}throw new Error(`API error ${a.status}: ${h}`)}const r=await a.json();if(r&&typeof r=="object"&&"success"in r){if(!r.success)throw new Error(`API error: ${r.message||"Request failed"}`);return r.data}return r}}class b{_messages=[];optimisticCounter=0;get messages(){return[...this._messages]}addOptimistic(t,e,s){const n={id:`optimistic_${++this.optimisticCounter}`,conversationId:s,role:t,content:e,createdAt:new Date().toISOString(),status:"sending"};return this._messages.push(n),n}confirmOptimistic(t){const e=this._messages.find(s=>s.id===t);e&&(e.status="sent")}addStreamingMessage(t){const e={id:`streaming_${Date.now()}`,conversationId:t,role:"assistant",content:"",createdAt:new Date().toISOString(),status:"streaming"};return this._messages.push(e),e}updateStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");e&&(e.content=t)}appendToStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");e&&(e.content+=t)}get streamingContent(){return this._messages.findLast(e=>e.status==="streaming")?.content||""}finalizeStreaming(t){const e=this._messages.findLast(s=>s.status==="streaming");return e&&(e.status="complete",t&&(e.tokensInput=t.tokensInput,e.tokensOutput=t.tokensOutput)),e}addMessage(t){this._messages.push(t)}setMessages(t){this._messages=[...t]}clear(){this._messages=[]}}class v{_conversationId;persist;constructor(t,e=!0){this.persist=e,this._conversationId=t||this.loadFromStorage()}get conversationId(){return this._conversationId}set conversationId(t){this._conversationId=t,this.persist&&t&&this.saveToStorage(t)}clear(){if(this._conversationId=null,this.persist)try{localStorage.removeItem(d)}catch{}}loadFromStorage(){if(!this.persist)return null;try{return localStorage.getItem(d)}catch{return null}}saveToStorage(t){try{localStorage.setItem(d,t)}catch{}}}class U extends u{connection;api;messageStore;conversationManager;subscription=null;config;constructor(t){super(),this.config=t,this.connection=new f(t),this.api=new _(t.baseUrl,t.userToken,t.apiKey,t.authToken),this.messageStore=new b,this.conversationManager=new v(t.conversationId),this.connection.on("connection:state",e=>{this.emit("connection:state",e),this.config.onConnectionStateChange?.(e)}),this.connection.on("error",e=>{this.emit("error",e),this.config.onError?.(e)})}async connect(){if(await this.connection.connect(),!this.conversationManager.conversationId){const t=await this.api.createConversation({systemPromptId:this.config.systemPromptId,channel:this.config.channel||"widget",extUid:this.config.extUid});this.conversationManager.conversationId=String(t.id)}this.subscription=await this.connection.subscribeToConversation(this.conversationManager.conversationId),this.subscription.onReceived(t=>{this.handleBroadcast(t)}),await this.loadHistory()}async sendMessage(t){const e=this.conversationManager.conversationId;if(!e)throw new Error("No active conversation");const s=this.messageStore.addOptimistic("user",t,e);this.emit("message:sent",s),this.config.onMessage?.(s),this.connection.sendMessage(e,t)}async sendUserEvent(t,e,s){const n=this.conversationManager.conversationId;if(!n)throw new Error("No active conversation");this.connection.sendUserEvent(n,t,e,s)}async loadHistory(){const t=this.conversationManager.conversationId;if(!t)return[];try{const e=await this.api.getMessages(t);return this.messageStore.setMessages(e),e}catch{return[]}}get messages(){return this.messageStore.messages}get conversationId(){return this.conversationManager.conversationId}get isConnected(){return this.connection.connectionState==="connected"}async setContext(t){const e=this.conversationManager.conversationId;e&&await this.api.updateContext(e,{current_page:t.currentPage,entity_type:t.entityType,entity_id:t.entityId,project_id:t.projectId,available_routes:t.availableRoutes,custom_context:t.custom})}async disconnect(){this.subscription&&(this.subscription=null),await this.connection.disconnect()}parseActionResult(t){try{const e=typeof t=="string"?JSON.parse(t):t;if(!e||typeof e!="object")return;e.navigate_to?this.emit("action:navigate",e):e.action==="edit_entity"?this.emit("action:edit_entity",e):e.action==="save_entity"?this.emit("action:save_entity",e):e.action==="test_tool"&&this.emit("action:test_tool",e)}catch{}}handleBroadcast(t){const e=this.conversationManager.conversationId;switch(t.type){case"status":{const s=t.status;this.emit("status",s),this.config.onStatusChange?.(s),s==="processing"&&this.emit("typing:start",void 0);break}case"error":{const s=new Error(t.message||"Unknown error");this.emit("error",s),this.config.onError?.(s);break}case"message_start":{if(t.role==="assistant"){const s=this.messageStore.addStreamingMessage(e);this.emit("stream:start",{messageId:s.id}),this.emit("typing:start",void 0)}break}case"message_update":{const s=t.delta||"",n=t.content||"";n?this.messageStore.updateStreaming(n):s&&this.messageStore.appendToStreaming(s);const i=this.messageStore.streamingContent;this.emit("stream:update",{delta:s,content:i}),this.config.onStreamUpdate?.(s,i);break}case"message_end":{const s=t.usage,n=this.messageStore.finalizeStreaming(s?{tokensInput:s.tokens_input||0,tokensOutput:s.tokens_output||0}:void 0);this.emit("typing:stop",void 0),n&&(this.emit("stream:end",{messageId:n.id,usage:s?{tokensInput:s.tokens_input||0,tokensOutput:s.tokens_output||0}:void 0}),this.emit("message:received",n),this.config.onMessage?.(n));break}case"message_queued":{const s=this.messageStore.messages.findLast(n=>n.status==="sending");s&&this.messageStore.confirmOptimistic(s.id);break}case"tool_execution_start":{this.emit("tool:start",{toolName:t.tool_name||"",args:t.args||{}}),this.emit("status",t.type);break}case"tool_execution_end":{this.emit("tool:end",{toolName:t.tool_name||"",result:t.result,isError:!!t.is_error}),this.emit("status",t.type),this.parseActionResult(t.result);break}case"tool_execution_update":case"agent_start":case"agent_end":case"turn_start":case"turn_end":case"auto_retry_start":case"auto_retry_end":case"cancelled":this.emit("status",t.type);break}}}exports.ActionCableClient=l;exports.AikaaraChatClient=U;exports.ApiClient=_;exports.ChannelSubscription=p;exports.ConnectionManager=f;exports.ConversationManager=v;exports.DEFAULT_BORDER_RADIUS=I;exports.DEFAULT_FONT_FAMILY=k;exports.DEFAULT_OFFSET=O;exports.DEFAULT_PLACEHOLDER=A;exports.DEFAULT_POSITION=C;exports.DEFAULT_PRIMARY_COLOR=E;exports.DEFAULT_THEME=M;exports.DEFAULT_WIDGET_HEIGHT=T;exports.DEFAULT_WIDGET_WIDTH=w;exports.EventEmitter=u;exports.MessageStore=b;