@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,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
|