@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,543 @@
1
+ /**
2
+ * Custom UI Customizations Example
3
+ *
4
+ * This example demonstrates the customization features:
5
+ * 1. Custom Tool UI with HsafaUI (receives addToolResult automatically)
6
+ * 2. Custom Edit Modal
7
+ * 3. Component Above Chat Input
8
+ */
9
+
10
+ import React, { useState } from 'react';
11
+ import { HsafaChat, CustomToolUIRenderProps, CustomEditModalRenderProps, Attachment } from '../src';
12
+
13
+ // ============================================================================
14
+ // 1. Custom Tool UI - Interactive Choice Component
15
+ // ============================================================================
16
+
17
+ function InteractiveChoiceUI({
18
+ toolName,
19
+ toolCallId,
20
+ input,
21
+ output,
22
+ status,
23
+ addToolResult
24
+ }: CustomToolUIRenderProps) {
25
+ const [selectedOption, setSelectedOption] = useState<string | null>(null);
26
+ const options = input?.options || ['Option 1', 'Option 2', 'Option 3'];
27
+ const question = input?.question || 'Please make a selection:';
28
+
29
+ const handleSelect = (option: string) => {
30
+ setSelectedOption(option);
31
+
32
+ // Send result back to the agent
33
+ addToolResult({
34
+ tool: toolName,
35
+ toolCallId: toolCallId,
36
+ output: {
37
+ selected: option,
38
+ timestamp: new Date().toISOString(),
39
+ }
40
+ });
41
+ };
42
+
43
+ return (
44
+ <div style={{
45
+ padding: '16px',
46
+ backgroundColor: '#F3F4F6',
47
+ borderRadius: '12px',
48
+ border: '2px solid #E5E7EB'
49
+ }}>
50
+ <h4 style={{ margin: '0 0 12px 0', fontSize: '15px', fontWeight: 600 }}>
51
+ {question}
52
+ </h4>
53
+
54
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
55
+ {options.map((option: string, idx: number) => (
56
+ <button
57
+ key={idx}
58
+ onClick={() => handleSelect(option)}
59
+ disabled={selectedOption !== null}
60
+ style={{
61
+ padding: '12px 16px',
62
+ backgroundColor: selectedOption === option ? '#10B981' : '#FFFFFF',
63
+ color: selectedOption === option ? '#FFFFFF' : '#111827',
64
+ border: selectedOption === option ? '2px solid #059669' : '2px solid #E5E7EB',
65
+ borderRadius: '8px',
66
+ fontSize: '14px',
67
+ fontWeight: 500,
68
+ cursor: selectedOption ? 'default' : 'pointer',
69
+ transition: 'all 0.2s',
70
+ textAlign: 'left'
71
+ }}
72
+ >
73
+ {selectedOption === option && '✓ '}
74
+ {option}
75
+ </button>
76
+ ))}
77
+ </div>
78
+
79
+ {selectedOption && (
80
+ <div style={{
81
+ marginTop: '12px',
82
+ padding: '8px 12px',
83
+ backgroundColor: '#D1FAE5',
84
+ color: '#065F46',
85
+ borderRadius: '6px',
86
+ fontSize: '13px',
87
+ fontWeight: 500
88
+ }}>
89
+ ✓ Selection sent to agent: {selectedOption}
90
+ </div>
91
+ )}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ // ============================================================================
97
+ // 2. Custom Tool UI - Approval Component
98
+ // ============================================================================
99
+
100
+ function ApprovalUI({
101
+ toolName,
102
+ toolCallId,
103
+ input,
104
+ addToolResult
105
+ }: CustomToolUIRenderProps) {
106
+ const [decision, setDecision] = useState<'approved' | 'rejected' | null>(null);
107
+ const [comment, setComment] = useState('');
108
+
109
+ const handleDecision = (approved: boolean) => {
110
+ const decisionType = approved ? 'approved' : 'rejected';
111
+ setDecision(decisionType);
112
+
113
+ addToolResult({
114
+ tool: toolName,
115
+ toolCallId: toolCallId,
116
+ output: {
117
+ approved,
118
+ comment: comment || undefined,
119
+ decidedAt: new Date().toISOString(),
120
+ }
121
+ });
122
+ };
123
+
124
+ return (
125
+ <div style={{
126
+ padding: '16px',
127
+ backgroundColor: '#FEF3C7',
128
+ borderRadius: '12px',
129
+ border: '2px solid #FCD34D'
130
+ }}>
131
+ <h4 style={{ margin: '0 0 8px 0', fontSize: '15px', fontWeight: 600 }}>
132
+ ⚠️ Approval Required
133
+ </h4>
134
+ <p style={{ margin: '0 0 12px 0', fontSize: '14px', color: '#78350F' }}>
135
+ {input?.message || 'This action requires your approval.'}
136
+ </p>
137
+
138
+ <textarea
139
+ value={comment}
140
+ onChange={(e) => setComment(e.target.value)}
141
+ placeholder="Optional comment..."
142
+ disabled={decision !== null}
143
+ style={{
144
+ width: '100%',
145
+ padding: '8px',
146
+ fontSize: '13px',
147
+ borderRadius: '6px',
148
+ border: '1px solid #D97706',
149
+ marginBottom: '12px',
150
+ fontFamily: 'inherit',
151
+ resize: 'vertical',
152
+ minHeight: '60px'
153
+ }}
154
+ />
155
+
156
+ <div style={{ display: 'flex', gap: '8px' }}>
157
+ <button
158
+ onClick={() => handleDecision(true)}
159
+ disabled={decision !== null}
160
+ style={{
161
+ flex: 1,
162
+ padding: '10px',
163
+ backgroundColor: decision === 'approved' ? '#10B981' : '#34D399',
164
+ color: 'white',
165
+ border: 'none',
166
+ borderRadius: '8px',
167
+ fontSize: '14px',
168
+ fontWeight: 600,
169
+ cursor: decision ? 'default' : 'pointer'
170
+ }}
171
+ >
172
+ {decision === 'approved' ? '✓ Approved' : 'Approve'}
173
+ </button>
174
+ <button
175
+ onClick={() => handleDecision(false)}
176
+ disabled={decision !== null}
177
+ style={{
178
+ flex: 1,
179
+ padding: '10px',
180
+ backgroundColor: decision === 'rejected' ? '#EF4444' : '#F87171',
181
+ color: 'white',
182
+ border: 'none',
183
+ borderRadius: '8px',
184
+ fontSize: '14px',
185
+ fontWeight: 600,
186
+ cursor: decision ? 'default' : 'pointer'
187
+ }}
188
+ >
189
+ {decision === 'rejected' ? '✗ Rejected' : 'Reject'}
190
+ </button>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ // ============================================================================
197
+ // 3. Custom Edit Modal
198
+ // ============================================================================
199
+
200
+ function CustomEditModal({
201
+ message,
202
+ text,
203
+ attachments,
204
+ onTextChange,
205
+ onSave,
206
+ onCancel,
207
+ onAddAttachments,
208
+ onRemoveAttachment,
209
+ uploading
210
+ }: CustomEditModalRenderProps) {
211
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
212
+
213
+ const handleFileClick = () => {
214
+ fileInputRef.current?.click();
215
+ };
216
+
217
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
218
+ if (e.target.files) {
219
+ await onAddAttachments(e.target.files);
220
+ }
221
+ };
222
+
223
+ return (
224
+ <div style={{
225
+ position: 'fixed',
226
+ top: 0,
227
+ left: 0,
228
+ right: 0,
229
+ bottom: 0,
230
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
231
+ display: 'flex',
232
+ alignItems: 'center',
233
+ justifyContent: 'center',
234
+ zIndex: 10000,
235
+ padding: '20px'
236
+ }}>
237
+ <div style={{
238
+ backgroundColor: '#1F2937',
239
+ padding: '24px',
240
+ borderRadius: '16px',
241
+ maxWidth: '700px',
242
+ width: '100%',
243
+ maxHeight: '85vh',
244
+ overflow: 'auto',
245
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)'
246
+ }}>
247
+ {/* Header */}
248
+ <div style={{
249
+ display: 'flex',
250
+ alignItems: 'center',
251
+ justifyContent: 'space-between',
252
+ marginBottom: '20px'
253
+ }}>
254
+ <h2 style={{
255
+ margin: 0,
256
+ fontSize: '20px',
257
+ fontWeight: 700,
258
+ color: '#F9FAFB'
259
+ }}>
260
+ ✏️ Edit Message
261
+ </h2>
262
+ <button
263
+ onClick={onCancel}
264
+ style={{
265
+ background: 'none',
266
+ border: 'none',
267
+ fontSize: '24px',
268
+ color: '#9CA3AF',
269
+ cursor: 'pointer',
270
+ padding: '4px'
271
+ }}
272
+ >
273
+ ×
274
+ </button>
275
+ </div>
276
+
277
+ {/* Textarea */}
278
+ <textarea
279
+ value={text}
280
+ onChange={(e) => onTextChange(e.target.value)}
281
+ autoFocus
282
+ style={{
283
+ width: '100%',
284
+ minHeight: '150px',
285
+ padding: '12px',
286
+ backgroundColor: '#374151',
287
+ color: '#F9FAFB',
288
+ border: '2px solid #4B5563',
289
+ borderRadius: '8px',
290
+ fontSize: '15px',
291
+ fontFamily: 'inherit',
292
+ resize: 'vertical',
293
+ lineHeight: '1.6'
294
+ }}
295
+ placeholder="Edit your message..."
296
+ />
297
+
298
+ {/* Attachments Section */}
299
+ {(attachments.length > 0 || true) && (
300
+ <div style={{ marginTop: '16px' }}>
301
+ <h3 style={{
302
+ fontSize: '14px',
303
+ fontWeight: 600,
304
+ marginBottom: '8px',
305
+ color: '#D1D5DB'
306
+ }}>
307
+ 📎 Attachments ({attachments.length})
308
+ </h3>
309
+
310
+ {attachments.map((att) => (
311
+ <div key={att.id} style={{
312
+ display: 'flex',
313
+ alignItems: 'center',
314
+ gap: '12px',
315
+ padding: '10px 12px',
316
+ backgroundColor: '#374151',
317
+ borderRadius: '8px',
318
+ marginBottom: '6px'
319
+ }}>
320
+ <span style={{
321
+ flex: 1,
322
+ fontSize: '13px',
323
+ color: '#F9FAFB',
324
+ overflow: 'hidden',
325
+ textOverflow: 'ellipsis',
326
+ whiteSpace: 'nowrap'
327
+ }}>
328
+ {att.name}
329
+ </span>
330
+ <button
331
+ onClick={() => onRemoveAttachment(att.id)}
332
+ style={{
333
+ padding: '4px 12px',
334
+ backgroundColor: '#DC2626',
335
+ color: 'white',
336
+ border: 'none',
337
+ borderRadius: '6px',
338
+ cursor: 'pointer',
339
+ fontSize: '12px',
340
+ fontWeight: 500
341
+ }}
342
+ >
343
+ Remove
344
+ </button>
345
+ </div>
346
+ ))}
347
+
348
+ <input
349
+ ref={fileInputRef}
350
+ type="file"
351
+ multiple
352
+ onChange={handleFileChange}
353
+ style={{ display: 'none' }}
354
+ />
355
+
356
+ <button
357
+ onClick={handleFileClick}
358
+ disabled={uploading}
359
+ style={{
360
+ marginTop: '8px',
361
+ padding: '8px 16px',
362
+ backgroundColor: '#4B5563',
363
+ color: '#F9FAFB',
364
+ border: '2px dashed #6B7280',
365
+ borderRadius: '8px',
366
+ cursor: uploading ? 'default' : 'pointer',
367
+ fontSize: '13px',
368
+ fontWeight: 500,
369
+ width: '100%'
370
+ }}
371
+ >
372
+ {uploading ? 'Uploading...' : '+ Add Files'}
373
+ </button>
374
+ </div>
375
+ )}
376
+
377
+ {/* Message Info */}
378
+ <div style={{
379
+ marginTop: '16px',
380
+ padding: '12px',
381
+ backgroundColor: '#374151',
382
+ borderRadius: '8px',
383
+ fontSize: '12px',
384
+ color: '#9CA3AF'
385
+ }}>
386
+ <p style={{ margin: '0 0 4px 0' }}>
387
+ <strong style={{ color: '#D1D5DB' }}>Message ID:</strong> {message.id}
388
+ </p>
389
+ {message.createdAt && (
390
+ <p style={{ margin: 0 }}>
391
+ <strong style={{ color: '#D1D5DB' }}>Created:</strong>{' '}
392
+ {new Date(message.createdAt).toLocaleString()}
393
+ </p>
394
+ )}
395
+ </div>
396
+
397
+ {/* Action Buttons */}
398
+ <div style={{
399
+ display: 'flex',
400
+ gap: '12px',
401
+ marginTop: '24px'
402
+ }}>
403
+ <button
404
+ onClick={onCancel}
405
+ style={{
406
+ flex: 1,
407
+ padding: '12px',
408
+ backgroundColor: '#4B5563',
409
+ color: '#F9FAFB',
410
+ border: 'none',
411
+ borderRadius: '8px',
412
+ fontSize: '15px',
413
+ fontWeight: 600,
414
+ cursor: 'pointer'
415
+ }}
416
+ >
417
+ Cancel
418
+ </button>
419
+ <button
420
+ onClick={onSave}
421
+ style={{
422
+ flex: 2,
423
+ padding: '12px',
424
+ backgroundColor: '#3B82F6',
425
+ color: 'white',
426
+ border: 'none',
427
+ borderRadius: '8px',
428
+ fontSize: '15px',
429
+ fontWeight: 600,
430
+ cursor: 'pointer'
431
+ }}
432
+ >
433
+ 💾 Save & Regenerate
434
+ </button>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ );
439
+ }
440
+
441
+ // ============================================================================
442
+ // 4. Component Above Chat Input
443
+ // ============================================================================
444
+
445
+ function QuickActionsBar() {
446
+ const quickActions = [
447
+ { emoji: '📝', label: 'Summarize', action: 'summarize' },
448
+ { emoji: '🔍', label: 'Analyze', action: 'analyze' },
449
+ { emoji: '💡', label: 'Suggest', action: 'suggest' },
450
+ { emoji: '🌐', label: 'Translate', action: 'translate' },
451
+ ];
452
+
453
+ return (
454
+ <div style={{
455
+ padding: '12px',
456
+ backgroundColor: '#1F2937',
457
+ borderRadius: '8px',
458
+ marginBottom: '12px',
459
+ border: '1px solid #374151'
460
+ }}>
461
+ <p style={{
462
+ fontSize: '11px',
463
+ color: '#9CA3AF',
464
+ marginBottom: '8px',
465
+ fontWeight: 600,
466
+ textTransform: 'uppercase',
467
+ letterSpacing: '0.5px'
468
+ }}>
469
+ Quick Actions
470
+ </p>
471
+ <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
472
+ {quickActions.map((action) => (
473
+ <button
474
+ key={action.action}
475
+ onClick={() => console.log('Quick action:', action.action)}
476
+ style={{
477
+ padding: '6px 12px',
478
+ backgroundColor: '#374151',
479
+ color: '#F9FAFB',
480
+ border: '1px solid #4B5563',
481
+ borderRadius: '6px',
482
+ fontSize: '13px',
483
+ cursor: 'pointer',
484
+ fontWeight: 500,
485
+ display: 'flex',
486
+ alignItems: 'center',
487
+ gap: '4px',
488
+ transition: 'all 0.2s'
489
+ }}
490
+ onMouseEnter={(e) => {
491
+ e.currentTarget.style.backgroundColor = '#4B5563';
492
+ e.currentTarget.style.borderColor = '#6B7280';
493
+ }}
494
+ onMouseLeave={(e) => {
495
+ e.currentTarget.style.backgroundColor = '#374151';
496
+ e.currentTarget.style.borderColor = '#4B5563';
497
+ }}
498
+ >
499
+ <span>{action.emoji}</span>
500
+ <span>{action.label}</span>
501
+ </button>
502
+ ))}
503
+ </div>
504
+ </div>
505
+ );
506
+ }
507
+
508
+ // ============================================================================
509
+ // Main Example Component
510
+ // ============================================================================
511
+
512
+ export default function CustomUICustomizationsExample() {
513
+ return (
514
+ <HsafaChat
515
+ agentId="custom-ui-demo"
516
+ theme="dark"
517
+
518
+ // Custom Tool UI Components (automatically receive addToolResult)
519
+ HsafaUI={{
520
+ 'getUserChoice': InteractiveChoiceUI,
521
+ 'requestApproval': ApprovalUI,
522
+ }}
523
+
524
+ // Custom Edit Modal
525
+ customEditModal={CustomEditModal}
526
+
527
+ // Component Above Input
528
+ componentAboveInput={QuickActionsBar}
529
+
530
+ // Example custom tools
531
+ HsafaTools={{
532
+ getUserChoice: async (input: any) => {
533
+ // This tool will use the custom InteractiveChoiceUI
534
+ return { received: true };
535
+ },
536
+ requestApproval: async (input: any) => {
537
+ // This tool will use the custom ApprovalUI
538
+ return { received: true };
539
+ },
540
+ }}
541
+ />
542
+ );
543
+ }