@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.
- package/README.md +133 -2
- package/dist/index.cjs +23 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +572 -20
- package/dist/index.d.ts +572 -20
- package/dist/index.js +23 -23
- package/dist/index.js.map +1 -1
- package/docs/CUSTOM_UI_EXAMPLES.md +309 -0
- package/docs/DYNAMIC_PAGE_SCHEMAS.md +261 -0
- package/docs/HEADLESS_QUICK_REFERENCE.md +426 -0
- package/docs/HEADLESS_USAGE.md +682 -0
- package/docs/MIGRATION_TO_HEADLESS.md +408 -0
- package/docs/README.md +43 -71
- package/docs/handbook/00-Overview.md +69 -0
- package/docs/handbook/01-Quickstart.md +133 -0
- package/docs/handbook/02-Architecture.md +75 -0
- package/docs/handbook/03-Components-and-Hooks.md +81 -0
- package/docs/handbook/04-Streaming-and-Transport.md +73 -0
- package/docs/handbook/05-Tools-and-UI.md +73 -0
- package/docs/handbook/06-Storage-and-History.md +63 -0
- package/docs/handbook/07-Dynamic-Pages.md +49 -0
- package/docs/handbook/08-Server-Integration.md +84 -0
- package/docs/handbook/09-Agent-Studio-Client.md +40 -0
- package/docs/handbook/10-Examples-and-Recipes.md +154 -0
- package/docs/handbook/11-API-Reference-Map.md +48 -0
- package/docs/handbook/README.md +24 -0
- package/examples/custom-tools-example.tsx +401 -0
- package/examples/custom-ui-customizations-example.tsx +543 -0
- package/examples/dynamic-page-example.tsx +380 -0
- package/examples/headless-chat-example.tsx +537 -0
- package/examples/minimal-headless-example.tsx +142 -0
- package/package.json +3 -2
|
@@ -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
|
+
"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
|
}
|