@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,537 @@
1
+ /**
2
+ * Example: Building a Custom Chat UI with Headless Hooks
3
+ *
4
+ * This example demonstrates how to build a completely custom chat interface
5
+ * using the headless hooks provided by the Hsafa SDK.
6
+ */
7
+
8
+ import React, { useState } from 'react';
9
+ import {
10
+ useHsafaAgent,
11
+ useFileUpload,
12
+ useChatStorage,
13
+ useMessageEditor,
14
+ useAutoScroll
15
+ } from '@hsafa/ui-sdk';
16
+
17
+ export function HeadlessChatExample() {
18
+ const [showSidebar, setShowSidebar] = useState(false);
19
+
20
+ // 1. Initialize the agent
21
+ const agent = useHsafaAgent({
22
+ agentId: 'customer-support',
23
+ baseUrl: 'http://localhost:3000',
24
+
25
+ // Add custom tools
26
+ tools: {
27
+ checkOrderStatus: async ({ orderId }) => {
28
+ // Your custom logic here
29
+ return { status: 'shipped', tracking: '123456' };
30
+ },
31
+ },
32
+
33
+ // Add custom UI components
34
+ uiComponents: {
35
+ OrderStatusCard: ({ data }) => (
36
+ <div style={{
37
+ padding: '15px',
38
+ backgroundColor: '#f0f9ff',
39
+ borderRadius: '8px',
40
+ border: '1px solid #0ea5e9'
41
+ }}>
42
+ <h4>Order Status</h4>
43
+ <p>Status: {data.status}</p>
44
+ <p>Tracking: {data.tracking}</p>
45
+ </div>
46
+ ),
47
+ },
48
+
49
+ // Callbacks
50
+ onFinish: (message) => {
51
+ console.log('Message completed:', message);
52
+ },
53
+ onError: (error) => {
54
+ console.error('Chat error:', error);
55
+ alert(`Error: ${error.message}`);
56
+ },
57
+
58
+ // Theme colors
59
+ colors: {
60
+ primaryColor: '#0ea5e9',
61
+ backgroundColor: '#ffffff',
62
+ textColor: '#0f172a',
63
+ },
64
+ });
65
+
66
+ // 2. File upload handling
67
+ const fileUpload = useFileUpload('http://localhost:3000');
68
+
69
+ // 3. Chat storage (history)
70
+ const storage = useChatStorage({
71
+ agentId: 'customer-support',
72
+ chatId: agent.chatId,
73
+ messages: agent.messages,
74
+ isLoading: agent.isLoading,
75
+ autoSave: true,
76
+ autoRestore: true,
77
+ });
78
+
79
+ // 4. Message editing
80
+ const editor = useMessageEditor({
81
+ messages: agent.messages,
82
+ isLoading: agent.isLoading,
83
+ sendMessage: agent.sendMessage,
84
+ setMessages: agent.setMessages,
85
+ baseUrl: 'http://localhost:3000',
86
+ });
87
+
88
+ // 5. Auto-scroll
89
+ const scrollRef = useAutoScroll<HTMLDivElement>(agent.isLoading);
90
+
91
+ // Send message handler
92
+ const handleSendMessage = async () => {
93
+ try {
94
+ await agent.sendMessage({
95
+ text: agent.input,
96
+ files: fileUpload.attachments.map(att => ({
97
+ type: 'file' as const,
98
+ url: att.url,
99
+ mediaType: att.mimeType || 'application/octet-stream',
100
+ name: att.name,
101
+ size: att.size,
102
+ })),
103
+ });
104
+ fileUpload.clearAttachments();
105
+ } catch (error) {
106
+ console.error('Failed to send:', error);
107
+ }
108
+ };
109
+
110
+ return (
111
+ <div style={{
112
+ display: 'flex',
113
+ height: '100vh',
114
+ fontFamily: 'system-ui, -apple-system, sans-serif'
115
+ }}>
116
+ {/* Sidebar - Chat History */}
117
+ {showSidebar && (
118
+ <aside style={{
119
+ width: '280px',
120
+ borderRight: '1px solid #e2e8f0',
121
+ display: 'flex',
122
+ flexDirection: 'column',
123
+ backgroundColor: '#f8fafc'
124
+ }}>
125
+ <div style={{ padding: '20px', borderBottom: '1px solid #e2e8f0' }}>
126
+ <h2 style={{ margin: 0, fontSize: '18px', fontWeight: '600' }}>
127
+ Conversations
128
+ </h2>
129
+ </div>
130
+
131
+ <div style={{ padding: '10px' }}>
132
+ <button
133
+ onClick={() => {
134
+ storage.createNewChat(agent.newChat);
135
+ setShowSidebar(false);
136
+ }}
137
+ style={{
138
+ width: '100%',
139
+ padding: '10px',
140
+ backgroundColor: '#0ea5e9',
141
+ color: 'white',
142
+ border: 'none',
143
+ borderRadius: '6px',
144
+ cursor: 'pointer',
145
+ fontSize: '14px',
146
+ fontWeight: '500'
147
+ }}
148
+ >
149
+ + New Chat
150
+ </button>
151
+ </div>
152
+
153
+ <div style={{ flex: 1, overflow: 'auto', padding: '10px' }}>
154
+ {storage.chatList.map(chat => (
155
+ <div
156
+ key={chat.id}
157
+ onClick={() => {
158
+ storage.switchToChat(chat.id, agent.setMessages);
159
+ setShowSidebar(false);
160
+ }}
161
+ style={{
162
+ padding: '12px',
163
+ cursor: 'pointer',
164
+ backgroundColor: chat.id === agent.chatId ? '#e0f2fe' : 'transparent',
165
+ borderRadius: '6px',
166
+ marginBottom: '4px',
167
+ transition: 'background-color 0.2s',
168
+ border: chat.id === agent.chatId ? '1px solid #0ea5e9' : '1px solid transparent'
169
+ }}
170
+ >
171
+ <div style={{
172
+ fontSize: '14px',
173
+ fontWeight: '500',
174
+ color: '#0f172a',
175
+ marginBottom: '4px',
176
+ overflow: 'hidden',
177
+ textOverflow: 'ellipsis',
178
+ whiteSpace: 'nowrap'
179
+ }}>
180
+ {chat.title}
181
+ </div>
182
+ <div style={{ fontSize: '12px', color: '#64748b' }}>
183
+ {new Date(chat.updatedAt).toLocaleDateString()}
184
+ </div>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ </aside>
189
+ )}
190
+
191
+ {/* Main Chat Area */}
192
+ <main style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
193
+ {/* Header */}
194
+ <header style={{
195
+ padding: '20px',
196
+ borderBottom: '1px solid #e2e8f0',
197
+ display: 'flex',
198
+ justifyContent: 'space-between',
199
+ alignItems: 'center',
200
+ backgroundColor: 'white'
201
+ }}>
202
+ <h1 style={{ margin: 0, fontSize: '20px', fontWeight: '600' }}>
203
+ Customer Support Chat
204
+ </h1>
205
+ <div style={{ display: 'flex', gap: '10px' }}>
206
+ <button
207
+ onClick={() => setShowSidebar(!showSidebar)}
208
+ style={{
209
+ padding: '8px 16px',
210
+ backgroundColor: '#f1f5f9',
211
+ border: '1px solid #e2e8f0',
212
+ borderRadius: '6px',
213
+ cursor: 'pointer',
214
+ fontSize: '14px'
215
+ }}
216
+ >
217
+ {showSidebar ? 'Hide' : 'Show'} History
218
+ </button>
219
+ <button
220
+ onClick={agent.newChat}
221
+ style={{
222
+ padding: '8px 16px',
223
+ backgroundColor: '#f1f5f9',
224
+ border: '1px solid #e2e8f0',
225
+ borderRadius: '6px',
226
+ cursor: 'pointer',
227
+ fontSize: '14px'
228
+ }}
229
+ >
230
+ New Chat
231
+ </button>
232
+ </div>
233
+ </header>
234
+
235
+ {/* Messages */}
236
+ <div
237
+ ref={scrollRef}
238
+ style={{
239
+ flex: 1,
240
+ overflow: 'auto',
241
+ padding: '20px',
242
+ backgroundColor: '#f8fafc'
243
+ }}
244
+ >
245
+ {agent.messages.length === 0 ? (
246
+ <div style={{
247
+ textAlign: 'center',
248
+ padding: '60px 20px',
249
+ color: '#64748b'
250
+ }}>
251
+ <h3 style={{ fontSize: '18px', marginBottom: '10px' }}>
252
+ Welcome to Support Chat
253
+ </h3>
254
+ <p style={{ fontSize: '14px' }}>
255
+ How can we help you today?
256
+ </p>
257
+ </div>
258
+ ) : (
259
+ agent.messages.map(msg => {
260
+ if (msg.role === 'user') {
261
+ // User message with edit capability
262
+ return editor.isEditing(msg.id) ? (
263
+ <div key={msg.id} style={{ marginBottom: '20px', textAlign: 'right' }}>
264
+ <div style={{
265
+ display: 'inline-block',
266
+ maxWidth: '70%',
267
+ textAlign: 'left'
268
+ }}>
269
+ <textarea
270
+ value={editor.editingText}
271
+ onChange={(e) => editor.setEditingText(e.target.value)}
272
+ style={{
273
+ width: '100%',
274
+ minHeight: '80px',
275
+ padding: '12px',
276
+ fontSize: '14px',
277
+ border: '1px solid #e2e8f0',
278
+ borderRadius: '8px',
279
+ resize: 'vertical'
280
+ }}
281
+ />
282
+ <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
283
+ <button
284
+ onClick={() => editor.saveEdit(msg.id)}
285
+ disabled={editor.editingText.trim() === ''}
286
+ style={{
287
+ padding: '6px 12px',
288
+ backgroundColor: '#0ea5e9',
289
+ color: 'white',
290
+ border: 'none',
291
+ borderRadius: '4px',
292
+ cursor: 'pointer',
293
+ fontSize: '13px'
294
+ }}
295
+ >
296
+ Save & Regenerate
297
+ </button>
298
+ <button
299
+ onClick={editor.cancelEdit}
300
+ style={{
301
+ padding: '6px 12px',
302
+ backgroundColor: '#f1f5f9',
303
+ border: '1px solid #e2e8f0',
304
+ borderRadius: '4px',
305
+ cursor: 'pointer',
306
+ fontSize: '13px'
307
+ }}
308
+ >
309
+ Cancel
310
+ </button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ ) : (
315
+ <div key={msg.id} style={{ marginBottom: '20px', textAlign: 'right' }}>
316
+ <div style={{
317
+ display: 'inline-block',
318
+ maxWidth: '70%'
319
+ }}>
320
+ <div style={{
321
+ padding: '12px 16px',
322
+ backgroundColor: '#0ea5e9',
323
+ color: 'white',
324
+ borderRadius: '12px',
325
+ fontSize: '14px',
326
+ lineHeight: '1.5',
327
+ wordWrap: 'break-word'
328
+ }}>
329
+ {msg.content}
330
+ </div>
331
+ <button
332
+ onClick={() => editor.startEdit(msg.id, msg.content || '')}
333
+ style={{
334
+ marginTop: '4px',
335
+ padding: '4px 8px',
336
+ backgroundColor: 'transparent',
337
+ border: 'none',
338
+ color: '#64748b',
339
+ cursor: 'pointer',
340
+ fontSize: '12px'
341
+ }}
342
+ >
343
+ ✏️ Edit
344
+ </button>
345
+ </div>
346
+ </div>
347
+ );
348
+ }
349
+
350
+ // Assistant message
351
+ return (
352
+ <div key={msg.id} style={{ marginBottom: '20px' }}>
353
+ <div style={{
354
+ display: 'inline-block',
355
+ maxWidth: '70%',
356
+ padding: '12px 16px',
357
+ backgroundColor: 'white',
358
+ border: '1px solid #e2e8f0',
359
+ borderRadius: '12px',
360
+ fontSize: '14px',
361
+ lineHeight: '1.5',
362
+ wordWrap: 'break-word'
363
+ }}>
364
+ {msg.content}
365
+ </div>
366
+ </div>
367
+ );
368
+ })
369
+ )}
370
+
371
+ {agent.isLoading && (
372
+ <div style={{ marginBottom: '20px' }}>
373
+ <div style={{
374
+ display: 'inline-block',
375
+ padding: '12px 16px',
376
+ backgroundColor: 'white',
377
+ border: '1px solid #e2e8f0',
378
+ borderRadius: '12px',
379
+ fontSize: '14px',
380
+ color: '#64748b'
381
+ }}>
382
+ <span className="typing-indicator">Agent is typing...</span>
383
+ </div>
384
+ </div>
385
+ )}
386
+ </div>
387
+
388
+ {/* Input Area */}
389
+ <div style={{
390
+ borderTop: '1px solid #e2e8f0',
391
+ padding: '20px',
392
+ backgroundColor: 'white'
393
+ }}>
394
+ {/* File attachments preview */}
395
+ {fileUpload.attachments.length > 0 && (
396
+ <div style={{ marginBottom: '12px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
397
+ {fileUpload.attachments.map(att => (
398
+ <div key={att.id} style={{
399
+ display: 'flex',
400
+ alignItems: 'center',
401
+ gap: '8px',
402
+ padding: '8px 12px',
403
+ backgroundColor: '#f0f9ff',
404
+ border: '1px solid #bae6fd',
405
+ borderRadius: '6px',
406
+ fontSize: '13px'
407
+ }}>
408
+ <span>📎 {att.name}</span>
409
+ <span style={{ color: '#64748b' }}>
410
+ ({fileUpload.formatBytes(att.size || 0)})
411
+ </span>
412
+ <button
413
+ onClick={() => fileUpload.handleRemoveAttachment(att.id)}
414
+ style={{
415
+ background: 'none',
416
+ border: 'none',
417
+ color: '#64748b',
418
+ cursor: 'pointer',
419
+ fontSize: '16px',
420
+ padding: '0 4px'
421
+ }}
422
+ >
423
+ ×
424
+ </button>
425
+ </div>
426
+ ))}
427
+ </div>
428
+ )}
429
+
430
+ {/* Error display */}
431
+ {agent.error && (
432
+ <div style={{
433
+ marginBottom: '12px',
434
+ padding: '12px',
435
+ backgroundColor: '#fef2f2',
436
+ border: '1px solid #fecaca',
437
+ borderRadius: '6px',
438
+ color: '#dc2626',
439
+ fontSize: '14px'
440
+ }}>
441
+ Error: {agent.error.message}
442
+ </div>
443
+ )}
444
+
445
+ {/* Input controls */}
446
+ <div style={{ display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
447
+ <input
448
+ type="file"
449
+ ref={fileUpload.fileInputRef}
450
+ onChange={(e) => fileUpload.handleFileSelection(e.target.files, console.error)}
451
+ multiple
452
+ hidden
453
+ />
454
+ <button
455
+ onClick={() => fileUpload.fileInputRef.current?.click()}
456
+ disabled={fileUpload.uploading || agent.isLoading}
457
+ style={{
458
+ padding: '10px',
459
+ backgroundColor: '#f1f5f9',
460
+ border: '1px solid #e2e8f0',
461
+ borderRadius: '8px',
462
+ cursor: fileUpload.uploading ? 'not-allowed' : 'pointer',
463
+ fontSize: '18px'
464
+ }}
465
+ title="Attach files"
466
+ >
467
+ 📎
468
+ </button>
469
+
470
+ <textarea
471
+ value={agent.input}
472
+ onChange={(e) => agent.setInput(e.target.value)}
473
+ onKeyPress={(e) => {
474
+ if (e.key === 'Enter' && !e.shiftKey && !agent.isLoading) {
475
+ e.preventDefault();
476
+ handleSendMessage();
477
+ }
478
+ }}
479
+ placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
480
+ disabled={agent.isLoading}
481
+ style={{
482
+ flex: 1,
483
+ padding: '12px',
484
+ fontSize: '14px',
485
+ border: '1px solid #e2e8f0',
486
+ borderRadius: '8px',
487
+ resize: 'none',
488
+ minHeight: '48px',
489
+ maxHeight: '120px',
490
+ fontFamily: 'inherit'
491
+ }}
492
+ rows={1}
493
+ />
494
+
495
+ {agent.isLoading ? (
496
+ <button
497
+ onClick={agent.stop}
498
+ style={{
499
+ padding: '12px 24px',
500
+ backgroundColor: '#ef4444',
501
+ color: 'white',
502
+ border: 'none',
503
+ borderRadius: '8px',
504
+ cursor: 'pointer',
505
+ fontSize: '14px',
506
+ fontWeight: '500'
507
+ }}
508
+ >
509
+ Stop
510
+ </button>
511
+ ) : (
512
+ <button
513
+ onClick={handleSendMessage}
514
+ disabled={(!agent.input.trim() && fileUpload.attachments.length === 0) || fileUpload.uploading}
515
+ style={{
516
+ padding: '12px 24px',
517
+ backgroundColor: '#0ea5e9',
518
+ color: 'white',
519
+ border: 'none',
520
+ borderRadius: '8px',
521
+ cursor: (!agent.input.trim() && fileUpload.attachments.length === 0) || fileUpload.uploading ? 'not-allowed' : 'pointer',
522
+ fontSize: '14px',
523
+ fontWeight: '500',
524
+ opacity: (!agent.input.trim() && fileUpload.attachments.length === 0) || fileUpload.uploading ? 0.5 : 1
525
+ }}
526
+ >
527
+ {fileUpload.uploading ? 'Uploading...' : 'Send'}
528
+ </button>
529
+ )}
530
+ </div>
531
+ </div>
532
+ </main>
533
+ </div>
534
+ );
535
+ }
536
+
537
+ export default HeadlessChatExample;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Minimal Headless Chat Example
3
+ *
4
+ * This is the simplest possible implementation of a custom chat UI
5
+ * using the useHsafaAgent hook.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { useHsafaAgent } from '@hsafa/ui-sdk';
10
+
11
+ export function MinimalHeadlessChat() {
12
+ const agent = useHsafaAgent({
13
+ agentId: 'my-agent',
14
+ baseUrl: 'http://localhost:3000',
15
+ });
16
+
17
+ return (
18
+ <div style={{
19
+ maxWidth: '600px',
20
+ margin: '50px auto',
21
+ fontFamily: 'Arial, sans-serif'
22
+ }}>
23
+ <h1>Minimal Chat Example</h1>
24
+
25
+ {/* Messages Display */}
26
+ <div style={{
27
+ height: '400px',
28
+ border: '1px solid #ddd',
29
+ borderRadius: '8px',
30
+ padding: '15px',
31
+ overflow: 'auto',
32
+ marginBottom: '15px',
33
+ backgroundColor: '#f9f9f9'
34
+ }}>
35
+ {agent.messages.length === 0 ? (
36
+ <p style={{ color: '#999', textAlign: 'center', marginTop: '50px' }}>
37
+ No messages yet. Start chatting!
38
+ </p>
39
+ ) : (
40
+ agent.messages.map(msg => (
41
+ <div
42
+ key={msg.id}
43
+ style={{
44
+ marginBottom: '12px',
45
+ padding: '10px',
46
+ borderRadius: '6px',
47
+ backgroundColor: msg.role === 'user' ? '#007bff' : '#fff',
48
+ color: msg.role === 'user' ? '#fff' : '#000',
49
+ border: msg.role === 'assistant' ? '1px solid #ddd' : 'none',
50
+ maxWidth: '80%',
51
+ marginLeft: msg.role === 'user' ? 'auto' : '0',
52
+ marginRight: msg.role === 'user' ? '0' : 'auto',
53
+ }}
54
+ >
55
+ <strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong>
56
+ <div style={{ marginTop: '5px' }}>{msg.content}</div>
57
+ </div>
58
+ ))
59
+ )}
60
+
61
+ {agent.isLoading && (
62
+ <div style={{ color: '#666', fontStyle: 'italic' }}>
63
+ Agent is typing...
64
+ </div>
65
+ )}
66
+ </div>
67
+
68
+ {/* Input Area */}
69
+ <div style={{ display: 'flex', gap: '10px' }}>
70
+ <input
71
+ type="text"
72
+ value={agent.input}
73
+ onChange={(e) => agent.setInput(e.target.value)}
74
+ onKeyPress={(e) => {
75
+ if (e.key === 'Enter' && !agent.isLoading) {
76
+ agent.sendMessage();
77
+ }
78
+ }}
79
+ placeholder="Type your message..."
80
+ disabled={agent.isLoading}
81
+ style={{
82
+ flex: 1,
83
+ padding: '12px',
84
+ fontSize: '14px',
85
+ border: '1px solid #ddd',
86
+ borderRadius: '6px',
87
+ outline: 'none',
88
+ }}
89
+ />
90
+
91
+ <button
92
+ onClick={() => agent.sendMessage()}
93
+ disabled={agent.isLoading || !agent.input.trim()}
94
+ style={{
95
+ padding: '12px 24px',
96
+ fontSize: '14px',
97
+ backgroundColor: agent.isLoading || !agent.input.trim() ? '#ccc' : '#007bff',
98
+ color: '#fff',
99
+ border: 'none',
100
+ borderRadius: '6px',
101
+ cursor: agent.isLoading || !agent.input.trim() ? 'not-allowed' : 'pointer',
102
+ fontWeight: 'bold',
103
+ }}
104
+ >
105
+ {agent.isLoading ? 'Sending...' : 'Send'}
106
+ </button>
107
+ </div>
108
+
109
+ {/* Error Display */}
110
+ {agent.error && (
111
+ <div style={{
112
+ marginTop: '10px',
113
+ padding: '10px',
114
+ backgroundColor: '#ffebee',
115
+ color: '#c62828',
116
+ borderRadius: '6px',
117
+ fontSize: '14px'
118
+ }}>
119
+ Error: {agent.error.message}
120
+ </div>
121
+ )}
122
+
123
+ {/* New Chat Button */}
124
+ <button
125
+ onClick={agent.newChat}
126
+ style={{
127
+ marginTop: '15px',
128
+ padding: '8px 16px',
129
+ fontSize: '13px',
130
+ backgroundColor: '#f5f5f5',
131
+ border: '1px solid #ddd',
132
+ borderRadius: '6px',
133
+ cursor: 'pointer',
134
+ }}
135
+ >
136
+ Start New Chat
137
+ </button>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ export default MinimalHeadlessChat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsafa/ui-sdk",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "React SDK for integrating AI agents built with HSAFA AI Agent Studio",
5
5
  "type": "module",
6
6
  "files": [
@@ -112,6 +112,7 @@
112
112
  },
113
113
  "dependencies": {
114
114
  "lucide-react": "^0.509.0",
115
- "mermaid": "^11.4.0"
115
+ "mermaid": "^11.4.0",
116
+ "fast-json-patch": "^3.1.1"
116
117
  }
117
118
  }