@hsafa/ui-sdk 0.3.3 → 0.4.1

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.
@@ -0,0 +1,682 @@
1
+ # Headless Hsafa Agent - Build Your Own UI
2
+
3
+ The Hsafa SDK provides headless hooks that let you build completely custom chat interfaces while leveraging all the powerful agent capabilities.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Start](#quick-start)
8
+ - [Core Hooks](#core-hooks)
9
+ - [useHsafaAgent](#usehsafaagent)
10
+ - [useFileUpload](#usefileupload)
11
+ - [useChatStorage](#usechatstorage)
12
+ - [useMessageEditor](#usemessageeditor)
13
+ - [useAutoScroll](#useautoscroll)
14
+ - [Complete Examples](#complete-examples)
15
+ - [API Reference](#api-reference)
16
+
17
+ ## Quick Start
18
+
19
+ Here's the simplest example of using the headless API:
20
+
21
+ ```tsx
22
+ import { useHsafaAgent } from '@hsafa/sdk';
23
+
24
+ function MyCustomChat() {
25
+ const agent = useHsafaAgent({
26
+ agentId: 'my-agent-id',
27
+ baseUrl: 'http://localhost:3000',
28
+ });
29
+
30
+ return (
31
+ <div>
32
+ {/* Messages */}
33
+ <div>
34
+ {agent.messages.map(msg => (
35
+ <div key={msg.id}>
36
+ <strong>{msg.role}:</strong> {msg.content}
37
+ </div>
38
+ ))}
39
+ </div>
40
+
41
+ {/* Input */}
42
+ <div>
43
+ <input
44
+ value={agent.input}
45
+ onChange={(e) => agent.setInput(e.target.value)}
46
+ onKeyPress={(e) => {
47
+ if (e.key === 'Enter' && !e.shiftKey) {
48
+ e.preventDefault();
49
+ agent.sendMessage();
50
+ }
51
+ }}
52
+ disabled={agent.isLoading}
53
+ />
54
+ <button onClick={() => agent.sendMessage()} disabled={agent.isLoading}>
55
+ {agent.isLoading ? 'Sending...' : 'Send'}
56
+ </button>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Core Hooks
64
+
65
+ ### useHsafaAgent
66
+
67
+ The main hook that provides all agent functionality.
68
+
69
+ ```tsx
70
+ import { useHsafaAgent } from '@hsafa/sdk';
71
+
72
+ const agent = useHsafaAgent({
73
+ agentId: 'my-agent-id',
74
+ baseUrl: 'http://localhost:3000',
75
+
76
+ // Optional: Add custom tools
77
+ tools: {
78
+ customTool: async (input) => {
79
+ console.log('Custom tool called with:', input);
80
+ return { result: 'Success!' };
81
+ },
82
+ },
83
+
84
+ // Optional: Add custom UI components
85
+ uiComponents: {
86
+ MyCustomComponent: ({ data }) => <div>{data.message}</div>,
87
+ },
88
+
89
+ // Optional: Enable dynamic pages
90
+ dynamicPageTypes: [
91
+ {
92
+ type: 'product-catalog',
93
+ schema: { /* ... */ },
94
+ },
95
+ ],
96
+
97
+ // Optional: Callbacks
98
+ onFinish: (message) => console.log('Message finished:', message),
99
+ onError: (error) => console.error('Error:', error),
100
+ onMessagesChange: (messages) => console.log('Messages updated:', messages),
101
+
102
+ // Optional: Theme colors for built-in forms
103
+ colors: {
104
+ primaryColor: '#3b82f6',
105
+ backgroundColor: '#ffffff',
106
+ textColor: '#000000',
107
+ },
108
+ });
109
+
110
+ // Access the API
111
+ agent.input // Current input text
112
+ agent.setInput() // Set input text
113
+ agent.messages // All messages
114
+ agent.isLoading // Loading state
115
+ agent.status // 'idle' | 'submitted' | 'streaming'
116
+ agent.error // Error if any
117
+ agent.sendMessage() // Send a message
118
+ agent.stop() // Stop generation
119
+ agent.newChat() // Start new chat
120
+ agent.setMessages() // Load messages (for history)
121
+ agent.chatId // Current chat ID
122
+ agent.tools // All available tools
123
+ agent.uiComponents // All UI components
124
+ ```
125
+
126
+ ### useFileUpload
127
+
128
+ Handle file uploads for messages.
129
+
130
+ ```tsx
131
+ import { useFileUpload } from '@hsafa/sdk';
132
+
133
+ function FileUploadExample() {
134
+ const agent = useHsafaAgent({ agentId: 'my-agent', baseUrl: 'http://localhost:3000' });
135
+ const fileUpload = useFileUpload('http://localhost:3000');
136
+
137
+ const handleSend = async () => {
138
+ await agent.sendMessage({
139
+ text: agent.input,
140
+ files: fileUpload.attachments.map(att => ({
141
+ type: 'file',
142
+ url: att.url,
143
+ mediaType: att.mimeType,
144
+ name: att.name,
145
+ size: att.size,
146
+ })),
147
+ });
148
+ fileUpload.clearAttachments();
149
+ };
150
+
151
+ return (
152
+ <div>
153
+ {/* File input */}
154
+ <input
155
+ type="file"
156
+ ref={fileUpload.fileInputRef}
157
+ onChange={(e) => fileUpload.handleFileSelection(e.target.files, console.error)}
158
+ multiple
159
+ hidden
160
+ />
161
+ <button onClick={() => fileUpload.fileInputRef.current?.click()}>
162
+ Attach Files
163
+ </button>
164
+
165
+ {/* Show attachments */}
166
+ {fileUpload.attachments.map(att => (
167
+ <div key={att.id}>
168
+ {att.name} ({fileUpload.formatBytes(att.size || 0)})
169
+ <button onClick={() => fileUpload.handleRemoveAttachment(att.id)}>×</button>
170
+ </div>
171
+ ))}
172
+
173
+ {/* Send */}
174
+ <button onClick={handleSend} disabled={fileUpload.uploading}>
175
+ {fileUpload.uploading ? 'Uploading...' : 'Send'}
176
+ </button>
177
+ </div>
178
+ );
179
+ }
180
+ ```
181
+
182
+ ### useChatStorage
183
+
184
+ Persist and manage chat history.
185
+
186
+ ```tsx
187
+ import { useHsafaAgent, useChatStorage } from '@hsafa/sdk';
188
+
189
+ function ChatWithHistory() {
190
+ const agent = useHsafaAgent({ agentId: 'my-agent', baseUrl: 'http://localhost:3000' });
191
+ const storage = useChatStorage({
192
+ agentId: 'my-agent',
193
+ chatId: agent.chatId,
194
+ messages: agent.messages,
195
+ isLoading: agent.isLoading,
196
+ autoSave: true, // Auto-save messages
197
+ autoRestore: true, // Auto-restore last chat on mount
198
+ });
199
+
200
+ return (
201
+ <div>
202
+ {/* Chat history sidebar */}
203
+ <div>
204
+ <h3>Chat History</h3>
205
+ <button onClick={() => storage.createNewChat(agent.newChat)}>
206
+ New Chat
207
+ </button>
208
+ {storage.chatList.map(chat => (
209
+ <div
210
+ key={chat.id}
211
+ onClick={() => storage.switchToChat(chat.id, agent.setMessages)}
212
+ style={{
213
+ fontWeight: chat.id === agent.chatId ? 'bold' : 'normal'
214
+ }}
215
+ >
216
+ {chat.title}
217
+ <button onClick={() => storage.deleteChat(chat.id)}>Delete</button>
218
+ </div>
219
+ ))}
220
+ </div>
221
+
222
+ {/* Chat interface */}
223
+ <div>
224
+ {agent.messages.map(msg => (
225
+ <div key={msg.id}>{msg.content}</div>
226
+ ))}
227
+ </div>
228
+ </div>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### useMessageEditor
234
+
235
+ Edit messages and regenerate responses.
236
+
237
+ ```tsx
238
+ import { useHsafaAgent, useMessageEditor } from '@hsafa/sdk';
239
+
240
+ function EditableChat() {
241
+ const agent = useHsafaAgent({ agentId: 'my-agent', baseUrl: 'http://localhost:3000' });
242
+ const editor = useMessageEditor({
243
+ messages: agent.messages,
244
+ isLoading: agent.isLoading,
245
+ sendMessage: agent.sendMessage,
246
+ setMessages: agent.setMessages,
247
+ baseUrl: 'http://localhost:3000',
248
+ });
249
+
250
+ return (
251
+ <div>
252
+ {agent.messages.map(msg => {
253
+ if (msg.role !== 'user') return <div key={msg.id}>{msg.content}</div>;
254
+
255
+ return editor.isEditing(msg.id) ? (
256
+ <div key={msg.id}>
257
+ <textarea
258
+ value={editor.editingText}
259
+ onChange={(e) => editor.setEditingText(e.target.value)}
260
+ />
261
+ <button onClick={() => editor.saveEdit(msg.id)}>Save & Regenerate</button>
262
+ <button onClick={editor.cancelEdit}>Cancel</button>
263
+ </div>
264
+ ) : (
265
+ <div key={msg.id}>
266
+ {msg.content}
267
+ <button onClick={() => editor.startEdit(msg.id, msg.content || '')}>
268
+ Edit
269
+ </button>
270
+ </div>
271
+ );
272
+ })}
273
+ </div>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ### useAutoScroll
279
+
280
+ Auto-scroll to bottom during streaming.
281
+
282
+ ```tsx
283
+ import { useHsafaAgent, useAutoScroll } from '@hsafa/sdk';
284
+
285
+ function AutoScrollChat() {
286
+ const agent = useHsafaAgent({ agentId: 'my-agent', baseUrl: 'http://localhost:3000' });
287
+ const scrollRef = useAutoScroll<HTMLDivElement>(agent.isLoading);
288
+
289
+ return (
290
+ <div ref={scrollRef} style={{ height: '500px', overflow: 'auto' }}>
291
+ {agent.messages.map(msg => (
292
+ <div key={msg.id}>{msg.content}</div>
293
+ ))}
294
+ </div>
295
+ );
296
+ }
297
+ ```
298
+
299
+ ## Complete Examples
300
+
301
+ ### Minimal Chat Interface
302
+
303
+ ```tsx
304
+ import { useHsafaAgent } from '@hsafa/sdk';
305
+
306
+ function MinimalChat() {
307
+ const agent = useHsafaAgent({
308
+ agentId: 'my-agent',
309
+ baseUrl: 'http://localhost:3000',
310
+ });
311
+
312
+ return (
313
+ <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
314
+ <h1>My Custom Chat</h1>
315
+
316
+ {/* Messages */}
317
+ <div style={{
318
+ height: '400px',
319
+ overflow: 'auto',
320
+ border: '1px solid #ccc',
321
+ padding: '10px',
322
+ marginBottom: '10px'
323
+ }}>
324
+ {agent.messages.length === 0 ? (
325
+ <p style={{ color: '#888' }}>Start a conversation...</p>
326
+ ) : (
327
+ agent.messages.map(msg => (
328
+ <div
329
+ key={msg.id}
330
+ style={{
331
+ marginBottom: '10px',
332
+ padding: '10px',
333
+ backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
334
+ borderRadius: '8px'
335
+ }}
336
+ >
337
+ <strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong>
338
+ <div>{msg.content}</div>
339
+ </div>
340
+ ))
341
+ )}
342
+ {agent.isLoading && <div>Agent is typing...</div>}
343
+ </div>
344
+
345
+ {/* Input */}
346
+ <div style={{ display: 'flex', gap: '10px' }}>
347
+ <input
348
+ type="text"
349
+ value={agent.input}
350
+ onChange={(e) => agent.setInput(e.target.value)}
351
+ onKeyPress={(e) => {
352
+ if (e.key === 'Enter' && !agent.isLoading) {
353
+ agent.sendMessage();
354
+ }
355
+ }}
356
+ placeholder="Type a message..."
357
+ style={{
358
+ flex: 1,
359
+ padding: '10px',
360
+ fontSize: '14px',
361
+ border: '1px solid #ccc',
362
+ borderRadius: '4px'
363
+ }}
364
+ disabled={agent.isLoading}
365
+ />
366
+ <button
367
+ onClick={() => agent.sendMessage()}
368
+ disabled={agent.isLoading || !agent.input.trim()}
369
+ style={{
370
+ padding: '10px 20px',
371
+ fontSize: '14px',
372
+ backgroundColor: '#1976d2',
373
+ color: 'white',
374
+ border: 'none',
375
+ borderRadius: '4px',
376
+ cursor: agent.isLoading ? 'not-allowed' : 'pointer'
377
+ }}
378
+ >
379
+ {agent.isLoading ? 'Sending...' : 'Send'}
380
+ </button>
381
+ </div>
382
+
383
+ {/* Error display */}
384
+ {agent.error && (
385
+ <div style={{
386
+ marginTop: '10px',
387
+ padding: '10px',
388
+ backgroundColor: '#ffebee',
389
+ color: '#c62828',
390
+ borderRadius: '4px'
391
+ }}>
392
+ Error: {agent.error.message}
393
+ </div>
394
+ )}
395
+ </div>
396
+ );
397
+ }
398
+ ```
399
+
400
+ ### Full-Featured Chat with All Hooks
401
+
402
+ ```tsx
403
+ import {
404
+ useHsafaAgent,
405
+ useFileUpload,
406
+ useChatStorage,
407
+ useMessageEditor,
408
+ useAutoScroll
409
+ } from '@hsafa/sdk';
410
+
411
+ function FullFeaturedChat() {
412
+ const [showHistory, setShowHistory] = useState(false);
413
+
414
+ const agent = useHsafaAgent({
415
+ agentId: 'my-agent',
416
+ baseUrl: 'http://localhost:3000',
417
+ tools: {
418
+ // Your custom tools
419
+ },
420
+ });
421
+
422
+ const fileUpload = useFileUpload('http://localhost:3000');
423
+
424
+ const storage = useChatStorage({
425
+ agentId: 'my-agent',
426
+ chatId: agent.chatId,
427
+ messages: agent.messages,
428
+ isLoading: agent.isLoading,
429
+ });
430
+
431
+ const editor = useMessageEditor({
432
+ messages: agent.messages,
433
+ isLoading: agent.isLoading,
434
+ sendMessage: agent.sendMessage,
435
+ setMessages: agent.setMessages,
436
+ baseUrl: 'http://localhost:3000',
437
+ });
438
+
439
+ const scrollRef = useAutoScroll<HTMLDivElement>(agent.isLoading);
440
+
441
+ const handleSend = async () => {
442
+ await agent.sendMessage({
443
+ text: agent.input,
444
+ files: fileUpload.attachments.map(att => ({
445
+ type: 'file',
446
+ url: att.url,
447
+ mediaType: att.mimeType,
448
+ name: att.name,
449
+ })),
450
+ });
451
+ fileUpload.clearAttachments();
452
+ };
453
+
454
+ return (
455
+ <div style={{ display: 'flex', height: '100vh' }}>
456
+ {/* Sidebar - Chat History */}
457
+ {showHistory && (
458
+ <div style={{ width: '250px', borderRight: '1px solid #ccc', padding: '10px' }}>
459
+ <h3>Conversations</h3>
460
+ <button onClick={() => {
461
+ storage.createNewChat(agent.newChat);
462
+ setShowHistory(false);
463
+ }}>
464
+ New Chat
465
+ </button>
466
+ <div style={{ marginTop: '10px' }}>
467
+ {storage.chatList.map(chat => (
468
+ <div
469
+ key={chat.id}
470
+ onClick={() => {
471
+ storage.switchToChat(chat.id, agent.setMessages);
472
+ setShowHistory(false);
473
+ }}
474
+ style={{
475
+ padding: '8px',
476
+ cursor: 'pointer',
477
+ backgroundColor: chat.id === agent.chatId ? '#e3f2fd' : 'transparent',
478
+ borderRadius: '4px',
479
+ marginBottom: '5px'
480
+ }}
481
+ >
482
+ {chat.title}
483
+ </div>
484
+ ))}
485
+ </div>
486
+ </div>
487
+ )}
488
+
489
+ {/* Main Chat */}
490
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
491
+ {/* Header */}
492
+ <div style={{
493
+ padding: '15px',
494
+ borderBottom: '1px solid #ccc',
495
+ display: 'flex',
496
+ justifyContent: 'space-between',
497
+ alignItems: 'center'
498
+ }}>
499
+ <h2>Chat</h2>
500
+ <div>
501
+ <button onClick={() => setShowHistory(!showHistory)}>
502
+ History
503
+ </button>
504
+ <button onClick={agent.newChat}>New Chat</button>
505
+ </div>
506
+ </div>
507
+
508
+ {/* Messages */}
509
+ <div ref={scrollRef} style={{ flex: 1, overflow: 'auto', padding: '20px' }}>
510
+ {agent.messages.map(msg => {
511
+ if (msg.role === 'user') {
512
+ return editor.isEditing(msg.id) ? (
513
+ <div key={msg.id} style={{ marginBottom: '15px' }}>
514
+ <textarea
515
+ value={editor.editingText}
516
+ onChange={(e) => editor.setEditingText(e.target.value)}
517
+ style={{ width: '100%', minHeight: '60px', padding: '8px' }}
518
+ />
519
+ <button onClick={() => editor.saveEdit(msg.id)}>
520
+ Save & Regenerate
521
+ </button>
522
+ <button onClick={editor.cancelEdit}>Cancel</button>
523
+ </div>
524
+ ) : (
525
+ <div key={msg.id} style={{ marginBottom: '15px', textAlign: 'right' }}>
526
+ <div style={{
527
+ display: 'inline-block',
528
+ padding: '10px',
529
+ backgroundColor: '#1976d2',
530
+ color: 'white',
531
+ borderRadius: '8px',
532
+ maxWidth: '70%'
533
+ }}>
534
+ {msg.content}
535
+ </div>
536
+ <button
537
+ onClick={() => editor.startEdit(msg.id, msg.content || '')}
538
+ style={{ marginLeft: '5px' }}
539
+ >
540
+ Edit
541
+ </button>
542
+ </div>
543
+ );
544
+ }
545
+
546
+ return (
547
+ <div key={msg.id} style={{ marginBottom: '15px' }}>
548
+ <div style={{
549
+ display: 'inline-block',
550
+ padding: '10px',
551
+ backgroundColor: '#f5f5f5',
552
+ borderRadius: '8px',
553
+ maxWidth: '70%'
554
+ }}>
555
+ {msg.content}
556
+ </div>
557
+ </div>
558
+ );
559
+ })}
560
+ {agent.isLoading && <div>Agent is typing...</div>}
561
+ </div>
562
+
563
+ {/* Input Area */}
564
+ <div style={{ borderTop: '1px solid #ccc', padding: '15px' }}>
565
+ {/* File attachments */}
566
+ {fileUpload.attachments.length > 0 && (
567
+ <div style={{ marginBottom: '10px' }}>
568
+ {fileUpload.attachments.map(att => (
569
+ <div key={att.id} style={{
570
+ display: 'inline-block',
571
+ padding: '5px 10px',
572
+ backgroundColor: '#e3f2fd',
573
+ borderRadius: '4px',
574
+ marginRight: '5px'
575
+ }}>
576
+ {att.name}
577
+ <button onClick={() => fileUpload.handleRemoveAttachment(att.id)}>
578
+ ×
579
+ </button>
580
+ </div>
581
+ ))}
582
+ </div>
583
+ )}
584
+
585
+ {/* Input */}
586
+ <div style={{ display: 'flex', gap: '10px' }}>
587
+ <input
588
+ type="file"
589
+ ref={fileUpload.fileInputRef}
590
+ onChange={(e) => fileUpload.handleFileSelection(e.target.files, console.error)}
591
+ multiple
592
+ hidden
593
+ />
594
+ <button
595
+ onClick={() => fileUpload.fileInputRef.current?.click()}
596
+ disabled={fileUpload.uploading}
597
+ >
598
+ 📎
599
+ </button>
600
+ <input
601
+ type="text"
602
+ value={agent.input}
603
+ onChange={(e) => agent.setInput(e.target.value)}
604
+ onKeyPress={(e) => {
605
+ if (e.key === 'Enter' && !e.shiftKey && !agent.isLoading) {
606
+ e.preventDefault();
607
+ handleSend();
608
+ }
609
+ }}
610
+ placeholder="Type a message..."
611
+ style={{ flex: 1, padding: '10px', fontSize: '14px' }}
612
+ disabled={agent.isLoading}
613
+ />
614
+ {agent.isLoading ? (
615
+ <button onClick={agent.stop}>Stop</button>
616
+ ) : (
617
+ <button onClick={handleSend} disabled={fileUpload.uploading}>
618
+ Send
619
+ </button>
620
+ )}
621
+ </div>
622
+ </div>
623
+ </div>
624
+ </div>
625
+ );
626
+ }
627
+ ```
628
+
629
+ ### Custom Tool Example
630
+
631
+ ```tsx
632
+ import { useHsafaAgent } from '@hsafa/sdk';
633
+
634
+ function ChatWithCustomTools() {
635
+ const agent = useHsafaAgent({
636
+ agentId: 'my-agent',
637
+ baseUrl: 'http://localhost:3000',
638
+ tools: {
639
+ // Simple function tool
640
+ getCurrentWeather: async ({ location }) => {
641
+ const response = await fetch(`https://api.weather.com/${location}`);
642
+ const data = await response.json();
643
+ return { temperature: data.temp, conditions: data.conditions };
644
+ },
645
+
646
+ // Tool with streaming support
647
+ searchDatabase: {
648
+ tool: async ({ query }) => {
649
+ const results = await searchDB(query);
650
+ return { results };
651
+ },
652
+ executeEachToken: true, // Execute on each token update
653
+ },
654
+ },
655
+ });
656
+
657
+ // Rest of your UI...
658
+ }
659
+ ```
660
+
661
+ ## API Reference
662
+
663
+ See individual hook files for complete TypeScript interfaces:
664
+ - `/hooks/useHsafaAgent.ts` - Main agent hook
665
+ - `/hooks/useFileUpload.ts` - File upload handling
666
+ - `/hooks/useChatStorage.ts` - Chat persistence
667
+ - `/hooks/useMessageEditor.ts` - Message editing
668
+ - `/hooks/useAutoScroll.ts` - Auto-scroll behavior
669
+
670
+ ## Tips
671
+
672
+ 1. **Always provide a baseUrl**: Either through the hook config or the HsafaProvider
673
+ 2. **Handle loading states**: Disable inputs when `agent.isLoading` is true
674
+ 3. **Handle errors**: Display `agent.error` to users
675
+ 4. **Clear attachments**: Call `fileUpload.clearAttachments()` after sending
676
+ 5. **Use TypeScript**: All hooks are fully typed for better DX
677
+
678
+ ## Next Steps
679
+
680
+ - Check out the [Dynamic Pages documentation](./DYNAMIC_PAGE_SCHEMAS.md) for building dynamic UIs
681
+ - See the [Tool Development Guide](./TOOL_DEVELOPMENT.md) for creating custom tools
682
+ - Browse the `/examples` folder for more use cases