@lantos1618/better-ui 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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/lib/aui/README.md +136 -0
  4. package/lib/aui/__tests__/aui-complete.test.ts +251 -0
  5. package/lib/aui/__tests__/aui-comprehensive.test.ts +376 -0
  6. package/lib/aui/__tests__/aui-concise.test.ts +278 -0
  7. package/lib/aui/__tests__/aui-integration.test.ts +309 -0
  8. package/lib/aui/__tests__/aui-simple.test.ts +116 -0
  9. package/lib/aui/__tests__/aui.test.ts +269 -0
  10. package/lib/aui/__tests__/concise-api.test.ts +165 -0
  11. package/lib/aui/__tests__/core.test.ts +265 -0
  12. package/lib/aui/__tests__/simple-api.test.ts +200 -0
  13. package/lib/aui/ai-assistant.ts +408 -0
  14. package/lib/aui/ai-control.ts +353 -0
  15. package/lib/aui/client/use-aui.ts +55 -0
  16. package/lib/aui/client-control.ts +551 -0
  17. package/lib/aui/client-executor.ts +417 -0
  18. package/lib/aui/components/ToolRenderer.tsx +22 -0
  19. package/lib/aui/core.ts +137 -0
  20. package/lib/aui/demo.tsx +89 -0
  21. package/lib/aui/examples/ai-complete-demo.tsx +359 -0
  22. package/lib/aui/examples/ai-control-demo.tsx +356 -0
  23. package/lib/aui/examples/ai-control-tools.ts +308 -0
  24. package/lib/aui/examples/concise-api.tsx +153 -0
  25. package/lib/aui/examples/index.tsx +163 -0
  26. package/lib/aui/examples/quick-demo.tsx +91 -0
  27. package/lib/aui/examples/simple-demo.tsx +71 -0
  28. package/lib/aui/examples/simple-tools.tsx +160 -0
  29. package/lib/aui/examples/user-api.tsx +208 -0
  30. package/lib/aui/examples/user-requested.tsx +174 -0
  31. package/lib/aui/examples/weather-search-tools.tsx +119 -0
  32. package/lib/aui/examples.tsx +367 -0
  33. package/lib/aui/hooks/useAUITool.ts +142 -0
  34. package/lib/aui/hooks/useAUIToolEnhanced.ts +343 -0
  35. package/lib/aui/hooks/useAUITools.ts +195 -0
  36. package/lib/aui/index.ts +156 -0
  37. package/lib/aui/provider.tsx +45 -0
  38. package/lib/aui/server-control.ts +386 -0
  39. package/lib/aui/server-executor.ts +165 -0
  40. package/lib/aui/server.ts +167 -0
  41. package/lib/aui/tool-registry.ts +380 -0
  42. package/lib/aui/tools/advanced-examples.tsx +86 -0
  43. package/lib/aui/tools/ai-complete.ts +375 -0
  44. package/lib/aui/tools/api-tools.tsx +230 -0
  45. package/lib/aui/tools/data-tools.tsx +232 -0
  46. package/lib/aui/tools/dom-tools.tsx +202 -0
  47. package/lib/aui/tools/examples.ts +43 -0
  48. package/lib/aui/tools/file-tools.tsx +202 -0
  49. package/lib/aui/tools/form-tools.tsx +233 -0
  50. package/lib/aui/tools/index.ts +8 -0
  51. package/lib/aui/tools/navigation-tools.tsx +172 -0
  52. package/lib/aui/tools/notification-tools.ts +213 -0
  53. package/lib/aui/tools/state-tools.tsx +209 -0
  54. package/lib/aui/types.ts +47 -0
  55. package/lib/aui/vercel-ai.ts +100 -0
  56. package/package.json +51 -0
@@ -0,0 +1,375 @@
1
+ import aui from '../index';
2
+ import { z } from 'zod';
3
+
4
+ // Navigation control - AI can navigate the app
5
+ export const navigationTool = aui
6
+ .tool('navigate')
7
+ .input(z.object({
8
+ action: z.enum(['goto', 'back', 'forward', 'reload', 'open']),
9
+ url: z.string().optional(),
10
+ target: z.enum(['_self', '_blank', '_parent', '_top']).optional()
11
+ }))
12
+ .clientExecute(async ({ input }) => {
13
+ switch (input.action) {
14
+ case 'goto':
15
+ if (!input.url) throw new Error('URL required for goto action');
16
+ window.location.href = input.url;
17
+ break;
18
+ case 'back':
19
+ window.history.back();
20
+ break;
21
+ case 'forward':
22
+ window.history.forward();
23
+ break;
24
+ case 'reload':
25
+ window.location.reload();
26
+ break;
27
+ case 'open':
28
+ if (!input.url) throw new Error('URL required for open action');
29
+ window.open(input.url, input.target || '_blank');
30
+ break;
31
+ }
32
+ return { success: true, action: input.action, url: input.url };
33
+ })
34
+ .describe('Control browser navigation')
35
+ .tag('ai', 'navigation', 'client');
36
+
37
+ // State management - AI can manage application state
38
+ export const stateTool = aui
39
+ .tool('state-manager')
40
+ .input(z.object({
41
+ action: z.enum(['set', 'get', 'update', 'delete', 'clear']),
42
+ key: z.string().optional(),
43
+ value: z.any().optional(),
44
+ namespace: z.string().optional()
45
+ }))
46
+ .clientExecute(async ({ input }) => {
47
+ const storage = input.namespace === 'session' ? sessionStorage : localStorage;
48
+
49
+ switch (input.action) {
50
+ case 'set':
51
+ if (!input.key) throw new Error('Key required for set action');
52
+ storage.setItem(input.key, JSON.stringify(input.value));
53
+ return { success: true, key: input.key, value: input.value };
54
+
55
+ case 'get':
56
+ if (!input.key) throw new Error('Key required for get action');
57
+ const value = storage.getItem(input.key);
58
+ return { success: true, key: input.key, value: value ? JSON.parse(value) : null };
59
+
60
+ case 'update':
61
+ if (!input.key) throw new Error('Key required for update action');
62
+ const existing = storage.getItem(input.key);
63
+ const current = existing ? JSON.parse(existing) : {};
64
+ const updated = { ...current, ...input.value };
65
+ storage.setItem(input.key, JSON.stringify(updated));
66
+ return { success: true, key: input.key, value: updated };
67
+
68
+ case 'delete':
69
+ if (!input.key) throw new Error('Key required for delete action');
70
+ storage.removeItem(input.key);
71
+ return { success: true, key: input.key, deleted: true };
72
+
73
+ case 'clear':
74
+ storage.clear();
75
+ return { success: true, cleared: true };
76
+
77
+ default:
78
+ throw new Error('Unknown action');
79
+ }
80
+ })
81
+ .describe('Manage client-side state storage')
82
+ .tag('ai', 'state', 'client');
83
+
84
+ // API calls - AI can make API requests
85
+ export const apiTool = aui
86
+ .tool('api-call')
87
+ .input(z.object({
88
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']),
89
+ url: z.string(),
90
+ headers: z.record(z.string()).optional(),
91
+ body: z.any().optional(),
92
+ queryParams: z.record(z.string()).optional()
93
+ }))
94
+ .execute(async ({ input }) => {
95
+ const url = new URL(input.url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000');
96
+
97
+ if (input.queryParams) {
98
+ Object.entries(input.queryParams).forEach(([key, value]) => {
99
+ url.searchParams.append(key, value);
100
+ });
101
+ }
102
+
103
+ const options: RequestInit = {
104
+ method: input.method,
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ ...input.headers
108
+ }
109
+ };
110
+
111
+ if (input.body && input.method !== 'GET') {
112
+ options.body = JSON.stringify(input.body);
113
+ }
114
+
115
+ const response = await fetch(url.toString(), options);
116
+ const data = await response.json();
117
+
118
+ return {
119
+ status: response.status,
120
+ statusText: response.statusText,
121
+ data,
122
+ headers: Object.fromEntries(response.headers.entries())
123
+ };
124
+ })
125
+ .describe('Make HTTP API requests')
126
+ .tag('ai', 'api', 'network');
127
+
128
+ // File operations - AI can handle files
129
+ export const fileTool = aui
130
+ .tool('file-handler')
131
+ .input(z.object({
132
+ action: z.enum(['read', 'download', 'upload']),
133
+ file: z.any().optional(),
134
+ url: z.string().optional(),
135
+ filename: z.string().optional(),
136
+ content: z.string().optional(),
137
+ mimeType: z.string().optional()
138
+ }))
139
+ .clientExecute(async ({ input }) => {
140
+ switch (input.action) {
141
+ case 'download':
142
+ if (!input.content || !input.filename) {
143
+ throw new Error('Content and filename required for download');
144
+ }
145
+ const blob = new Blob([input.content], {
146
+ type: input.mimeType || 'text/plain'
147
+ });
148
+ const url = URL.createObjectURL(blob);
149
+ const a = document.createElement('a');
150
+ a.href = url;
151
+ a.download = input.filename;
152
+ a.click();
153
+ URL.revokeObjectURL(url);
154
+ return { success: true, filename: input.filename };
155
+
156
+ case 'read':
157
+ if (!input.file) throw new Error('File required for read action');
158
+ return new Promise((resolve) => {
159
+ const reader = new FileReader();
160
+ reader.onload = (e) => {
161
+ resolve({
162
+ success: true,
163
+ content: e.target?.result,
164
+ filename: input.file.name,
165
+ size: input.file.size,
166
+ type: input.file.type
167
+ });
168
+ };
169
+ reader.readAsText(input.file);
170
+ });
171
+
172
+ case 'upload':
173
+ // This would typically upload to a server
174
+ return { success: true, message: 'Upload handler not implemented' };
175
+
176
+ default:
177
+ throw new Error('Unknown action');
178
+ }
179
+ })
180
+ .describe('Handle file operations')
181
+ .tag('ai', 'file', 'client');
182
+
183
+ // Notification system - AI can send notifications
184
+ export const notificationTool = aui
185
+ .tool('notification')
186
+ .input(z.object({
187
+ type: z.enum(['info', 'success', 'warning', 'error', 'toast']),
188
+ title: z.string(),
189
+ message: z.string().optional(),
190
+ duration: z.number().optional(),
191
+ position: z.enum(['top', 'bottom', 'top-right', 'top-left', 'bottom-right', 'bottom-left']).optional()
192
+ }))
193
+ .clientExecute(async ({ input }) => {
194
+ // Check if browser supports notifications
195
+ if (input.type !== 'toast' && 'Notification' in window) {
196
+ if (Notification.permission === 'default') {
197
+ await Notification.requestPermission();
198
+ }
199
+
200
+ if (Notification.permission === 'granted') {
201
+ new Notification(input.title, {
202
+ body: input.message,
203
+ icon: `/icon-${input.type}.png`,
204
+ tag: input.type,
205
+ });
206
+ return { success: true, type: 'notification' };
207
+ }
208
+ }
209
+
210
+ // Fallback to toast
211
+ const toast = document.createElement('div');
212
+ toast.className = `fixed z-50 p-4 rounded-lg shadow-lg ${
213
+ input.type === 'error' ? 'bg-red-500' :
214
+ input.type === 'warning' ? 'bg-yellow-500' :
215
+ input.type === 'success' ? 'bg-green-500' :
216
+ 'bg-blue-500'
217
+ } text-white`;
218
+
219
+ // Position the toast
220
+ const position = input.position || 'top-right';
221
+ if (position.includes('top')) toast.style.top = '20px';
222
+ if (position.includes('bottom')) toast.style.bottom = '20px';
223
+ if (position.includes('right')) toast.style.right = '20px';
224
+ if (position.includes('left')) toast.style.left = '20px';
225
+ if (!position.includes('left') && !position.includes('right')) {
226
+ toast.style.left = '50%';
227
+ toast.style.transform = 'translateX(-50%)';
228
+ }
229
+
230
+ toast.innerHTML = `
231
+ <div class="font-semibold">${input.title}</div>
232
+ ${input.message ? `<div class="text-sm mt-1">${input.message}</div>` : ''}
233
+ `;
234
+
235
+ document.body.appendChild(toast);
236
+
237
+ setTimeout(() => {
238
+ toast.remove();
239
+ }, input.duration || 3000);
240
+
241
+ return { success: true, type: 'toast' };
242
+ })
243
+ .describe('Show notifications to the user')
244
+ .tag('ai', 'notification', 'ui');
245
+
246
+ // Analytics tracking - AI can track events
247
+ export const analyticsTool = aui
248
+ .tool('analytics')
249
+ .input(z.object({
250
+ action: z.enum(['track', 'identify', 'page', 'group']),
251
+ event: z.string().optional(),
252
+ properties: z.record(z.any()).optional(),
253
+ userId: z.string().optional(),
254
+ traits: z.record(z.any()).optional()
255
+ }))
256
+ .execute(async ({ input }) => {
257
+ // This would integrate with your analytics provider
258
+ console.log('Analytics event:', input);
259
+
260
+ // Simulate sending to analytics service
261
+ const analyticsData = {
262
+ timestamp: new Date().toISOString(),
263
+ ...input
264
+ };
265
+
266
+ // In production, this would send to Google Analytics, Segment, etc.
267
+ return {
268
+ success: true,
269
+ tracked: analyticsData
270
+ };
271
+ })
272
+ .describe('Track analytics events')
273
+ .tag('ai', 'analytics', 'tracking');
274
+
275
+ // Database query builder - AI can build and execute queries
276
+ export const queryBuilderTool = aui
277
+ .tool('query-builder')
278
+ .input(z.object({
279
+ table: z.string(),
280
+ operation: z.enum(['select', 'insert', 'update', 'delete']),
281
+ columns: z.array(z.string()).optional(),
282
+ where: z.record(z.any()).optional(),
283
+ data: z.record(z.any()).optional(),
284
+ orderBy: z.object({
285
+ column: z.string(),
286
+ direction: z.enum(['asc', 'desc'])
287
+ }).optional(),
288
+ limit: z.number().optional(),
289
+ offset: z.number().optional()
290
+ }))
291
+ .execute(async ({ input }) => {
292
+ // Build SQL-like query (this is a simulation)
293
+ let query = '';
294
+
295
+ switch (input.operation) {
296
+ case 'select':
297
+ query = `SELECT ${input.columns?.join(', ') || '*'} FROM ${input.table}`;
298
+ if (input.where) {
299
+ const conditions = Object.entries(input.where)
300
+ .map(([key, value]) => `${key} = '${value}'`)
301
+ .join(' AND ');
302
+ query += ` WHERE ${conditions}`;
303
+ }
304
+ if (input.orderBy) {
305
+ query += ` ORDER BY ${input.orderBy.column} ${input.orderBy.direction.toUpperCase()}`;
306
+ }
307
+ if (input.limit) query += ` LIMIT ${input.limit}`;
308
+ if (input.offset) query += ` OFFSET ${input.offset}`;
309
+ break;
310
+
311
+ case 'insert':
312
+ if (!input.data) throw new Error('Data required for insert');
313
+ const columns = Object.keys(input.data);
314
+ const values = Object.values(input.data).map(v => `'${v}'`);
315
+ query = `INSERT INTO ${input.table} (${columns.join(', ')}) VALUES (${values.join(', ')})`;
316
+ break;
317
+
318
+ case 'update':
319
+ if (!input.data) throw new Error('Data required for update');
320
+ const updates = Object.entries(input.data)
321
+ .map(([key, value]) => `${key} = '${value}'`)
322
+ .join(', ');
323
+ query = `UPDATE ${input.table} SET ${updates}`;
324
+ if (input.where) {
325
+ const conditions = Object.entries(input.where)
326
+ .map(([key, value]) => `${key} = '${value}'`)
327
+ .join(' AND ');
328
+ query += ` WHERE ${conditions}`;
329
+ }
330
+ break;
331
+
332
+ case 'delete':
333
+ query = `DELETE FROM ${input.table}`;
334
+ if (input.where) {
335
+ const conditions = Object.entries(input.where)
336
+ .map(([key, value]) => `${key} = '${value}'`)
337
+ .join(' AND ');
338
+ query += ` WHERE ${conditions}`;
339
+ }
340
+ break;
341
+ }
342
+
343
+ // In production, this would execute against a real database
344
+ return {
345
+ success: true,
346
+ query,
347
+ operation: input.operation,
348
+ table: input.table,
349
+ // Simulated results
350
+ results: input.operation === 'select' ? [
351
+ { id: 1, name: 'Sample 1' },
352
+ { id: 2, name: 'Sample 2' }
353
+ ] : { affectedRows: 1 }
354
+ };
355
+ })
356
+ .describe('Build and execute database queries')
357
+ .tag('ai', 'database', 'query');
358
+
359
+ // Export all AI control tools
360
+ export const aiCompleteTools = {
361
+ navigationTool,
362
+ stateTool,
363
+ apiTool,
364
+ fileTool,
365
+ notificationTool,
366
+ analyticsTool,
367
+ queryBuilderTool
368
+ };
369
+
370
+ // Register all tools
371
+ Object.values(aiCompleteTools).forEach(tool => {
372
+ // Tools are auto-registered when created with aui.tool()
373
+ });
374
+
375
+ export default aiCompleteTools;
@@ -0,0 +1,230 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ import { createAITool } from '../ai-control';
4
+
5
+ export const apiGet = createAITool('api.get')
6
+ .describe('Make a GET request to an API endpoint')
7
+ .tag('api', 'network', 'data')
8
+ .input(z.object({
9
+ url: z.string(),
10
+ headers: z.record(z.string()).optional(),
11
+ params: z.record(z.any()).optional()
12
+ }))
13
+ .execute(async ({ input, ctx }) => {
14
+ const url = new URL(input.url);
15
+ if (input.params) {
16
+ Object.entries(input.params).forEach(([key, value]) => {
17
+ url.searchParams.append(key, String(value));
18
+ });
19
+ }
20
+
21
+ const response = await ctx!.fetch(url.toString(), {
22
+ method: 'GET',
23
+ headers: input.headers
24
+ });
25
+
26
+ const contentType = response.headers.get('content-type');
27
+ const data = contentType?.includes('application/json')
28
+ ? await response.json()
29
+ : await response.text();
30
+
31
+ return {
32
+ status: response.status,
33
+ statusText: response.statusText,
34
+ data,
35
+ headers: Object.fromEntries(response.headers.entries())
36
+ };
37
+ })
38
+ .render(({ data }) => (
39
+ <div className="font-mono text-sm">
40
+ <div>Status: {data.status} {data.statusText}</div>
41
+ <pre className="bg-gray-100 p-2 rounded mt-2">
42
+ {typeof data.data === 'object'
43
+ ? JSON.stringify(data.data, null, 2)
44
+ : data.data}
45
+ </pre>
46
+ </div>
47
+ ));
48
+
49
+ export const apiPost = createAITool('api.post')
50
+ .describe('Make a POST request to an API endpoint')
51
+ .tag('api', 'network', 'data')
52
+ .input(z.object({
53
+ url: z.string(),
54
+ body: z.any(),
55
+ headers: z.record(z.string()).optional(),
56
+ json: z.boolean().optional().default(true)
57
+ }))
58
+ .execute(async ({ input, ctx }) => {
59
+ const headers: HeadersInit = {
60
+ ...input.headers
61
+ };
62
+
63
+ let body: any = input.body;
64
+ if (input.json) {
65
+ headers['Content-Type'] = 'application/json';
66
+ body = JSON.stringify(input.body);
67
+ }
68
+
69
+ const response = await ctx!.fetch(input.url, {
70
+ method: 'POST',
71
+ headers,
72
+ body
73
+ });
74
+
75
+ const contentType = response.headers.get('content-type');
76
+ const data = contentType?.includes('application/json')
77
+ ? await response.json()
78
+ : await response.text();
79
+
80
+ return {
81
+ status: response.status,
82
+ statusText: response.statusText,
83
+ data,
84
+ headers: Object.fromEntries(response.headers.entries())
85
+ };
86
+ });
87
+
88
+ export const apiPut = createAITool('api.put')
89
+ .describe('Make a PUT request to an API endpoint')
90
+ .tag('api', 'network', 'data')
91
+ .input(z.object({
92
+ url: z.string(),
93
+ body: z.any(),
94
+ headers: z.record(z.string()).optional()
95
+ }))
96
+ .execute(async ({ input, ctx }) => {
97
+ const response = await ctx!.fetch(input.url, {
98
+ method: 'PUT',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ ...input.headers
102
+ },
103
+ body: JSON.stringify(input.body)
104
+ });
105
+
106
+ const data = await response.json().catch(() => response.text());
107
+ return {
108
+ status: response.status,
109
+ data
110
+ };
111
+ });
112
+
113
+ export const apiDelete = createAITool('api.delete')
114
+ .describe('Make a DELETE request to an API endpoint')
115
+ .tag('api', 'network', 'data')
116
+ .input(z.object({
117
+ url: z.string(),
118
+ headers: z.record(z.string()).optional()
119
+ }))
120
+ .execute(async ({ input, ctx }) => {
121
+ const response = await ctx!.fetch(input.url, {
122
+ method: 'DELETE',
123
+ headers: input.headers
124
+ });
125
+
126
+ return {
127
+ status: response.status,
128
+ success: response.ok
129
+ };
130
+ });
131
+
132
+ export const apiGraphQL = createAITool('api.graphql')
133
+ .describe('Execute a GraphQL query or mutation')
134
+ .tag('api', 'graphql', 'data')
135
+ .input(z.object({
136
+ endpoint: z.string(),
137
+ query: z.string(),
138
+ variables: z.record(z.any()).optional(),
139
+ operationName: z.string().optional(),
140
+ headers: z.record(z.string()).optional()
141
+ }))
142
+ .execute(async ({ input, ctx }) => {
143
+ const response = await ctx!.fetch(input.endpoint, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ ...input.headers
148
+ },
149
+ body: JSON.stringify({
150
+ query: input.query,
151
+ variables: input.variables,
152
+ operationName: input.operationName
153
+ })
154
+ });
155
+
156
+ const result = await response.json();
157
+ return {
158
+ data: result.data,
159
+ errors: result.errors,
160
+ status: response.status
161
+ };
162
+ })
163
+ .render(({ data }) => (
164
+ <div className="space-y-2">
165
+ {data.errors && (
166
+ <div className="text-red-600">
167
+ Errors: {JSON.stringify(data.errors)}
168
+ </div>
169
+ )}
170
+ <pre className="bg-gray-100 p-2 rounded">
171
+ {JSON.stringify(data.data, null, 2)}
172
+ </pre>
173
+ </div>
174
+ ));
175
+
176
+ export const apiWebSocket = createAITool('api.websocket')
177
+ .describe('Establish WebSocket connection')
178
+ .tag('api', 'websocket', 'realtime')
179
+ .input(z.object({
180
+ url: z.string(),
181
+ protocols: z.array(z.string()).optional()
182
+ }))
183
+ .clientExecute(async ({ input }) => {
184
+ const ws = new WebSocket(input.url, input.protocols);
185
+
186
+ return new Promise((resolve, reject) => {
187
+ ws.onopen = () => {
188
+ resolve({
189
+ connected: true,
190
+ url: input.url,
191
+ readyState: ws.readyState,
192
+ send: (data: string) => ws.send(data),
193
+ close: () => ws.close(),
194
+ onMessage: (handler: (event: MessageEvent) => void) => {
195
+ ws.onmessage = handler;
196
+ }
197
+ });
198
+ };
199
+
200
+ ws.onerror = (error) => {
201
+ reject(new Error(`WebSocket connection failed: ${error}`));
202
+ };
203
+ });
204
+ });
205
+
206
+ export const apiSSE = createAITool('api.sse')
207
+ .describe('Connect to Server-Sent Events')
208
+ .tag('api', 'sse', 'realtime')
209
+ .input(z.object({
210
+ url: z.string(),
211
+ withCredentials: z.boolean().optional()
212
+ }))
213
+ .clientExecute(async ({ input }) => {
214
+ const eventSource = new EventSource(input.url, {
215
+ withCredentials: input.withCredentials
216
+ });
217
+
218
+ return {
219
+ connected: true,
220
+ url: input.url,
221
+ readyState: eventSource.readyState,
222
+ onMessage: (handler: (event: MessageEvent) => void) => {
223
+ eventSource.onmessage = handler;
224
+ },
225
+ onEvent: (event: string, handler: (event: MessageEvent) => void) => {
226
+ eventSource.addEventListener(event, handler);
227
+ },
228
+ close: () => eventSource.close()
229
+ };
230
+ });