@trainly/react 1.2.0 → 1.3.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 +527 -0
- package/dist/TrainlyProvider.js +75 -18
- package/dist/api/TrainlyClient.d.ts +2 -1
- package/dist/api/TrainlyClient.js +108 -0
- package/dist/types.d.ts +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,6 +146,533 @@ function MyApp() {
|
|
|
146
146
|
- 🎨 **Pre-built UI**: `TrainlyFileManager` component with styling
|
|
147
147
|
- 🔒 **Privacy-First**: Only works in V1 mode with OAuth authentication
|
|
148
148
|
|
|
149
|
+
## 📚 **Detailed File Management Documentation**
|
|
150
|
+
|
|
151
|
+
### **1. Listing Files**
|
|
152
|
+
|
|
153
|
+
Get all files uploaded to the user's permanent subchat:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { useTrainly } from "@trainly/react";
|
|
157
|
+
|
|
158
|
+
function FileList() {
|
|
159
|
+
const { listFiles } = useTrainly();
|
|
160
|
+
|
|
161
|
+
const handleListFiles = async () => {
|
|
162
|
+
try {
|
|
163
|
+
const result = await listFiles();
|
|
164
|
+
|
|
165
|
+
console.log(`Total files: ${result.total_files}`);
|
|
166
|
+
console.log(`Total storage: ${formatBytes(result.total_size_bytes)}`);
|
|
167
|
+
|
|
168
|
+
result.files.forEach((file) => {
|
|
169
|
+
console.log(`📄 ${file.filename}`);
|
|
170
|
+
console.log(` Size: ${formatBytes(file.size_bytes)}`);
|
|
171
|
+
console.log(` Chunks: ${file.chunk_count}`);
|
|
172
|
+
console.log(
|
|
173
|
+
` Uploaded: ${new Date(parseInt(file.upload_date)).toLocaleDateString()}`,
|
|
174
|
+
);
|
|
175
|
+
console.log(` ID: ${file.file_id}`);
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Failed to list files:", error);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return <button onClick={handleListFiles}>List My Files</button>;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Response Structure:**
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface FileListResult {
|
|
190
|
+
success: boolean;
|
|
191
|
+
files: FileInfo[];
|
|
192
|
+
total_files: number;
|
|
193
|
+
total_size_bytes: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface FileInfo {
|
|
197
|
+
file_id: string; // Unique identifier for deletion
|
|
198
|
+
filename: string; // Original filename
|
|
199
|
+
upload_date: string; // Unix timestamp (milliseconds)
|
|
200
|
+
size_bytes: number; // File size in bytes
|
|
201
|
+
chunk_count: number; // Number of text chunks created
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### **2. Bulk Upload Files**
|
|
206
|
+
|
|
207
|
+
Upload multiple files at once (up to 10 files per request):
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { useTrainly } from "@trainly/react";
|
|
211
|
+
|
|
212
|
+
function BulkFileUpload() {
|
|
213
|
+
const { bulkUploadFiles } = useTrainly();
|
|
214
|
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
215
|
+
|
|
216
|
+
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
217
|
+
const files = Array.from(event.target.files || []);
|
|
218
|
+
if (files.length > 10) {
|
|
219
|
+
alert("Maximum 10 files allowed per bulk upload");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
setSelectedFiles(files);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleBulkUpload = async () => {
|
|
226
|
+
if (selectedFiles.length === 0) return;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const result = await bulkUploadFiles(selectedFiles);
|
|
230
|
+
|
|
231
|
+
console.log(`Bulk upload completed: ${result.message}`);
|
|
232
|
+
console.log(
|
|
233
|
+
`Successful: ${result.successful_uploads}/${result.total_files}`,
|
|
234
|
+
);
|
|
235
|
+
console.log(`Total size: ${formatBytes(result.total_size_bytes)}`);
|
|
236
|
+
|
|
237
|
+
// Review individual file results
|
|
238
|
+
result.results.forEach((fileResult) => {
|
|
239
|
+
if (fileResult.success) {
|
|
240
|
+
console.log(`✅ ${fileResult.filename} - ${fileResult.message}`);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`❌ ${fileResult.filename} - ${fileResult.error}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Clear selection after successful upload
|
|
247
|
+
setSelectedFiles([]);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error("Bulk upload failed:", error);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div>
|
|
255
|
+
<input
|
|
256
|
+
type="file"
|
|
257
|
+
multiple
|
|
258
|
+
accept=".pdf,.txt,.docx"
|
|
259
|
+
onChange={handleFileSelect}
|
|
260
|
+
/>
|
|
261
|
+
|
|
262
|
+
{selectedFiles.length > 0 && (
|
|
263
|
+
<div>
|
|
264
|
+
<p>Selected files: {selectedFiles.length}</p>
|
|
265
|
+
<ul>
|
|
266
|
+
{selectedFiles.map((file, index) => (
|
|
267
|
+
<li key={index}>
|
|
268
|
+
{file.name} ({formatBytes(file.size)})
|
|
269
|
+
</li>
|
|
270
|
+
))}
|
|
271
|
+
</ul>
|
|
272
|
+
<button onClick={handleBulkUpload}>
|
|
273
|
+
Upload {selectedFiles.length} Files
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Helper function for formatting file sizes
|
|
282
|
+
function formatBytes(bytes: number): string {
|
|
283
|
+
if (bytes === 0) return "0 Bytes";
|
|
284
|
+
const k = 1024;
|
|
285
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
286
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
287
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Bulk Upload Features:**
|
|
292
|
+
|
|
293
|
+
- ✅ **Efficient**: Upload up to 10 files in a single API call
|
|
294
|
+
- ✅ **Detailed Results**: Individual success/failure status for each file
|
|
295
|
+
- ✅ **Error Resilience**: Partial failures don't stop other files
|
|
296
|
+
- ✅ **Progress Tracking**: Total size and success metrics
|
|
297
|
+
- ✅ **Automatic Retry**: Token refresh handling built-in
|
|
298
|
+
|
|
299
|
+
### **3. Deleting Files**
|
|
300
|
+
|
|
301
|
+
Remove a specific file and free up storage space:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { useTrainly } from "@trainly/react";
|
|
305
|
+
|
|
306
|
+
function FileDeleter() {
|
|
307
|
+
const { deleteFile, listFiles } = useTrainly();
|
|
308
|
+
|
|
309
|
+
const handleDeleteFile = async (fileId: string, filename: string) => {
|
|
310
|
+
// Always confirm before deletion
|
|
311
|
+
const confirmed = confirm(
|
|
312
|
+
`Delete "${filename}"? This will permanently remove the file and cannot be undone.`,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (!confirmed) return;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const result = await deleteFile(fileId);
|
|
319
|
+
|
|
320
|
+
console.log(`✅ ${result.message}`);
|
|
321
|
+
console.log(`🗑️ Deleted: ${result.filename}`);
|
|
322
|
+
console.log(`💾 Storage freed: ${formatBytes(result.size_bytes_freed)}`);
|
|
323
|
+
console.log(`📊 Chunks removed: ${result.chunks_deleted}`);
|
|
324
|
+
|
|
325
|
+
// Optionally refresh file list
|
|
326
|
+
await listFiles();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("Failed to delete file:", error);
|
|
329
|
+
alert(`Failed to delete file: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Example: Delete first file
|
|
334
|
+
const deleteFirstFile = async () => {
|
|
335
|
+
const files = await listFiles();
|
|
336
|
+
if (files.files.length > 0) {
|
|
337
|
+
const firstFile = files.files[0];
|
|
338
|
+
await handleDeleteFile(firstFile.file_id, firstFile.filename);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return <button onClick={deleteFirstFile}>Delete First File</button>;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Response Structure:**
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
interface FileDeleteResult {
|
|
350
|
+
success: boolean;
|
|
351
|
+
message: string; // Human-readable success message
|
|
352
|
+
file_id: string; // ID of deleted file
|
|
353
|
+
filename: string; // Name of deleted file
|
|
354
|
+
chunks_deleted: number; // Number of chunks removed
|
|
355
|
+
size_bytes_freed: number; // Storage space freed up
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### **4. Pre-built File Manager Component**
|
|
360
|
+
|
|
361
|
+
Use the ready-made component for complete file management:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
import { TrainlyFileManager } from "@trainly/react";
|
|
365
|
+
|
|
366
|
+
function MyApp() {
|
|
367
|
+
return (
|
|
368
|
+
<TrainlyFileManager
|
|
369
|
+
// Optional: Custom CSS class
|
|
370
|
+
className="my-custom-styles"
|
|
371
|
+
// Callback when file is deleted
|
|
372
|
+
onFileDeleted={(fileId, filename) => {
|
|
373
|
+
console.log(`File deleted: ${filename} (ID: ${fileId})`);
|
|
374
|
+
// Update your app state, show notification, etc.
|
|
375
|
+
}}
|
|
376
|
+
// Error handling callback
|
|
377
|
+
onError={(error) => {
|
|
378
|
+
console.error("File operation failed:", error);
|
|
379
|
+
// Show user-friendly error message
|
|
380
|
+
alert(`Error: ${error.message}`);
|
|
381
|
+
}}
|
|
382
|
+
// Show upload button in component
|
|
383
|
+
showUploadButton={true}
|
|
384
|
+
// Maximum file size in MB
|
|
385
|
+
maxFileSize={5}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Component Features:**
|
|
392
|
+
|
|
393
|
+
- 📋 **File List**: Shows all files with metadata
|
|
394
|
+
- 🔄 **Auto-Refresh**: Updates after uploads/deletions
|
|
395
|
+
- ⚠️ **Confirmation**: Asks before deleting files
|
|
396
|
+
- 📊 **Storage Stats**: Shows total files and storage used
|
|
397
|
+
- 🎨 **Styled**: Clean, professional appearance
|
|
398
|
+
- 📱 **Responsive**: Works on mobile and desktop
|
|
399
|
+
|
|
400
|
+
### **5. Complete Integration Example**
|
|
401
|
+
|
|
402
|
+
Here's a full example showing all file operations together:
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
import React from "react";
|
|
406
|
+
import { useAuth } from "@clerk/nextjs"; // or your OAuth provider
|
|
407
|
+
import { useTrainly, TrainlyFileManager } from "@trainly/react";
|
|
408
|
+
|
|
409
|
+
export function CompleteFileExample() {
|
|
410
|
+
const { getToken } = useAuth();
|
|
411
|
+
const {
|
|
412
|
+
ask,
|
|
413
|
+
upload,
|
|
414
|
+
listFiles,
|
|
415
|
+
deleteFile,
|
|
416
|
+
connectWithOAuthToken,
|
|
417
|
+
isConnected,
|
|
418
|
+
} = useTrainly();
|
|
419
|
+
|
|
420
|
+
const [files, setFiles] = React.useState([]);
|
|
421
|
+
const [storageUsed, setStorageUsed] = React.useState(0);
|
|
422
|
+
|
|
423
|
+
// Connect to Trainly on mount
|
|
424
|
+
React.useEffect(() => {
|
|
425
|
+
async function connect() {
|
|
426
|
+
const token = await getToken();
|
|
427
|
+
if (token) {
|
|
428
|
+
await connectWithOAuthToken(token);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
connect();
|
|
432
|
+
}, []);
|
|
433
|
+
|
|
434
|
+
// Load files when connected
|
|
435
|
+
React.useEffect(() => {
|
|
436
|
+
if (isConnected) {
|
|
437
|
+
refreshFiles();
|
|
438
|
+
}
|
|
439
|
+
}, [isConnected]);
|
|
440
|
+
|
|
441
|
+
const refreshFiles = async () => {
|
|
442
|
+
try {
|
|
443
|
+
const result = await listFiles();
|
|
444
|
+
setFiles(result.files);
|
|
445
|
+
setStorageUsed(result.total_size_bytes);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error("Failed to load files:", error);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const handleBulkDelete = async () => {
|
|
452
|
+
if (files.length === 0) {
|
|
453
|
+
alert("No files to delete");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const confirmed = confirm(
|
|
458
|
+
`Delete ALL ${files.length} files? This cannot be undone.`,
|
|
459
|
+
);
|
|
460
|
+
if (!confirmed) return;
|
|
461
|
+
|
|
462
|
+
let deletedCount = 0;
|
|
463
|
+
let totalFreed = 0;
|
|
464
|
+
|
|
465
|
+
for (const file of files) {
|
|
466
|
+
try {
|
|
467
|
+
const result = await deleteFile(file.file_id);
|
|
468
|
+
deletedCount++;
|
|
469
|
+
totalFreed += result.size_bytes_freed;
|
|
470
|
+
console.log(`Deleted: ${result.filename}`);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error(`Failed to delete ${file.filename}:`, error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
alert(`Deleted ${deletedCount} files, freed ${formatBytes(totalFreed)}`);
|
|
477
|
+
await refreshFiles();
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const formatBytes = (bytes) => {
|
|
481
|
+
if (bytes === 0) return "0 Bytes";
|
|
482
|
+
const k = 1024;
|
|
483
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
484
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
485
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
if (!isConnected) {
|
|
489
|
+
return <div>Connecting to Trainly...</div>;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "20px" }}>
|
|
494
|
+
<h1>📁 My File Workspace</h1>
|
|
495
|
+
|
|
496
|
+
{/* Storage Overview */}
|
|
497
|
+
<div
|
|
498
|
+
style={{
|
|
499
|
+
background: "#f8fafc",
|
|
500
|
+
padding: "20px",
|
|
501
|
+
borderRadius: "8px",
|
|
502
|
+
marginBottom: "20px",
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
<h3>Storage Overview</h3>
|
|
506
|
+
<p>
|
|
507
|
+
<strong>{files.length} files</strong> using{" "}
|
|
508
|
+
<strong>{formatBytes(storageUsed)}</strong>
|
|
509
|
+
</p>
|
|
510
|
+
<div style={{ display: "flex", gap: "10px", marginTop: "10px" }}>
|
|
511
|
+
<button onClick={refreshFiles}>🔄 Refresh</button>
|
|
512
|
+
<button
|
|
513
|
+
onClick={handleBulkDelete}
|
|
514
|
+
disabled={files.length === 0}
|
|
515
|
+
style={{ background: "#dc2626", color: "white" }}
|
|
516
|
+
>
|
|
517
|
+
🗑️ Delete All Files
|
|
518
|
+
</button>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
{/* File Manager Component */}
|
|
523
|
+
<TrainlyFileManager
|
|
524
|
+
onFileDeleted={(fileId, filename) => {
|
|
525
|
+
console.log(`File deleted: ${filename}`);
|
|
526
|
+
// Update local state
|
|
527
|
+
setFiles((prev) => prev.filter((f) => f.file_id !== fileId));
|
|
528
|
+
refreshFiles(); // Refresh to get accurate totals
|
|
529
|
+
}}
|
|
530
|
+
onError={(error) => {
|
|
531
|
+
alert(`Error: ${error.message}`);
|
|
532
|
+
}}
|
|
533
|
+
showUploadButton={true}
|
|
534
|
+
maxFileSize={5}
|
|
535
|
+
/>
|
|
536
|
+
|
|
537
|
+
{/* AI Integration */}
|
|
538
|
+
<div
|
|
539
|
+
style={{
|
|
540
|
+
marginTop: "30px",
|
|
541
|
+
padding: "20px",
|
|
542
|
+
background: "#f0f9ff",
|
|
543
|
+
borderRadius: "8px",
|
|
544
|
+
}}
|
|
545
|
+
>
|
|
546
|
+
<h3>🤖 Ask AI About Your Files</h3>
|
|
547
|
+
<button
|
|
548
|
+
onClick={async () => {
|
|
549
|
+
const answer = await ask(
|
|
550
|
+
"What files do I have? Give me a summary of each.",
|
|
551
|
+
);
|
|
552
|
+
alert(`AI Response:\n\n${answer}`);
|
|
553
|
+
}}
|
|
554
|
+
style={{ background: "#059669", color: "white" }}
|
|
555
|
+
>
|
|
556
|
+
Get File Summary from AI
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### **6. Error Handling Best Practices**
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
import { useTrainly } from "@trainly/react";
|
|
568
|
+
|
|
569
|
+
function RobustFileManager() {
|
|
570
|
+
const { deleteFile, listFiles } = useTrainly();
|
|
571
|
+
|
|
572
|
+
const safeDeleteFile = async (fileId: string, filename: string) => {
|
|
573
|
+
try {
|
|
574
|
+
// 1. Confirm with user
|
|
575
|
+
const confirmed = confirm(`Delete "${filename}"?`);
|
|
576
|
+
if (!confirmed) return;
|
|
577
|
+
|
|
578
|
+
// 2. Attempt deletion
|
|
579
|
+
const result = await deleteFile(fileId);
|
|
580
|
+
|
|
581
|
+
// 3. Success feedback
|
|
582
|
+
console.log(`✅ Success: ${result.message}`);
|
|
583
|
+
return result;
|
|
584
|
+
} catch (error) {
|
|
585
|
+
// 4. Handle specific error types
|
|
586
|
+
if (error.message.includes("404")) {
|
|
587
|
+
alert("File not found - it may have already been deleted");
|
|
588
|
+
} else if (error.message.includes("401")) {
|
|
589
|
+
alert("Authentication expired - please refresh the page");
|
|
590
|
+
} else {
|
|
591
|
+
alert(`Failed to delete file: ${error.message}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.error("Delete error:", error);
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const safeListFiles = async () => {
|
|
600
|
+
try {
|
|
601
|
+
return await listFiles();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error("List files error:", error);
|
|
604
|
+
|
|
605
|
+
if (error.message.includes("V1 mode")) {
|
|
606
|
+
alert("File management requires V1 OAuth authentication");
|
|
607
|
+
} else {
|
|
608
|
+
alert(`Failed to load files: ${error.message}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return { success: false, files: [], total_files: 0, total_size_bytes: 0 };
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<div>
|
|
617
|
+
<button onClick={() => safeListFiles()}>Safe List Files</button>
|
|
618
|
+
<button onClick={() => safeDeleteFile("file_123", "example.pdf")}>
|
|
619
|
+
Safe Delete Example
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### **7. TypeScript Support**
|
|
627
|
+
|
|
628
|
+
Full TypeScript definitions included:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// Import types for better development experience
|
|
632
|
+
import type {
|
|
633
|
+
FileInfo,
|
|
634
|
+
FileListResult,
|
|
635
|
+
FileDeleteResult,
|
|
636
|
+
TrainlyFileManagerProps,
|
|
637
|
+
} from "@trainly/react";
|
|
638
|
+
|
|
639
|
+
// Type-safe file operations
|
|
640
|
+
const handleTypedFileOps = async () => {
|
|
641
|
+
const fileList: FileListResult = await listFiles();
|
|
642
|
+
const deleteResult: FileDeleteResult = await deleteFile("file_123");
|
|
643
|
+
|
|
644
|
+
// Full IntelliSense support
|
|
645
|
+
console.log(deleteResult.size_bytes_freed);
|
|
646
|
+
console.log(fileList.total_size_bytes);
|
|
647
|
+
};
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### **8. Security & Privacy Notes**
|
|
651
|
+
|
|
652
|
+
- 🔒 **V1 Only**: File management only works with V1 Trusted Issuer authentication
|
|
653
|
+
- 👤 **User Isolation**: Users can only see and delete their own files
|
|
654
|
+
- 🛡️ **No Raw Access**: Developers never see file content, only AI responses
|
|
655
|
+
- 📊 **Privacy-Safe Analytics**: Storage tracking without exposing user data
|
|
656
|
+
- ⚠️ **Permanent Deletion**: Deleted files cannot be recovered
|
|
657
|
+
- 🔐 **OAuth Required**: Must be authenticated with valid OAuth token
|
|
658
|
+
|
|
659
|
+
### **9. Storage Management**
|
|
660
|
+
|
|
661
|
+
File operations automatically update storage analytics:
|
|
662
|
+
|
|
663
|
+
```tsx
|
|
664
|
+
// Storage is tracked automatically
|
|
665
|
+
const result = await deleteFile(fileId);
|
|
666
|
+
console.log(`Freed ${result.size_bytes_freed} bytes`);
|
|
667
|
+
|
|
668
|
+
// Check total storage
|
|
669
|
+
const files = await listFiles();
|
|
670
|
+
console.log(`Using ${files.total_size_bytes} bytes total`);
|
|
671
|
+
|
|
672
|
+
// Parent app analytics are updated automatically
|
|
673
|
+
// (visible in Trainly dashboard for developers)
|
|
674
|
+
```
|
|
675
|
+
|
|
149
676
|
---
|
|
150
677
|
|
|
151
678
|
## 🚀 Original Quick Start (Legacy)
|
package/dist/TrainlyProvider.js
CHANGED
|
@@ -389,7 +389,7 @@ export function TrainlyProvider(_a) {
|
|
|
389
389
|
}
|
|
390
390
|
});
|
|
391
391
|
}); };
|
|
392
|
-
var
|
|
392
|
+
var bulkUploadFiles = function (files) { return __awaiter(_this, void 0, void 0, function () {
|
|
393
393
|
var result, err_6, errorMessage, newToken, result, refreshError_3, error_6;
|
|
394
394
|
return __generator(this, function (_a) {
|
|
395
395
|
switch (_a.label) {
|
|
@@ -397,7 +397,7 @@ export function TrainlyProvider(_a) {
|
|
|
397
397
|
_a.trys.push([0, 2, 10, 11]);
|
|
398
398
|
setIsLoading(true);
|
|
399
399
|
setError(null);
|
|
400
|
-
return [4 /*yield*/, client.
|
|
400
|
+
return [4 /*yield*/, client.bulkUploadFiles(files)];
|
|
401
401
|
case 1:
|
|
402
402
|
result = _a.sent();
|
|
403
403
|
return [2 /*return*/, result];
|
|
@@ -412,7 +412,7 @@ export function TrainlyProvider(_a) {
|
|
|
412
412
|
_a.label = 3;
|
|
413
413
|
case 3:
|
|
414
414
|
_a.trys.push([3, 8, , 9]);
|
|
415
|
-
console.log("🔄 Token expired during
|
|
415
|
+
console.log("🔄 Token expired during bulk upload, refreshing...");
|
|
416
416
|
return [4 /*yield*/, getToken()];
|
|
417
417
|
case 4:
|
|
418
418
|
newToken = _a.sent();
|
|
@@ -420,20 +420,20 @@ export function TrainlyProvider(_a) {
|
|
|
420
420
|
return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
|
|
421
421
|
case 5:
|
|
422
422
|
_a.sent();
|
|
423
|
-
return [4 /*yield*/, client.
|
|
423
|
+
return [4 /*yield*/, client.bulkUploadFiles(files)];
|
|
424
424
|
case 6:
|
|
425
425
|
result = _a.sent();
|
|
426
|
-
console.log("✅
|
|
426
|
+
console.log("✅ Bulk upload succeeded after token refresh");
|
|
427
427
|
return [2 /*return*/, result];
|
|
428
428
|
case 7: return [3 /*break*/, 9];
|
|
429
429
|
case 8:
|
|
430
430
|
refreshError_3 = _a.sent();
|
|
431
|
-
console.error("❌ Token refresh failed:", refreshError_3);
|
|
431
|
+
console.error("❌ Token refresh failed during bulk upload:", refreshError_3);
|
|
432
432
|
return [3 /*break*/, 9];
|
|
433
433
|
case 9:
|
|
434
434
|
error_6 = {
|
|
435
|
-
code: "
|
|
436
|
-
message: "Failed to
|
|
435
|
+
code: "BULK_UPLOAD_FAILED",
|
|
436
|
+
message: "Failed to upload files",
|
|
437
437
|
details: err_6,
|
|
438
438
|
};
|
|
439
439
|
setError(error_6);
|
|
@@ -445,7 +445,7 @@ export function TrainlyProvider(_a) {
|
|
|
445
445
|
}
|
|
446
446
|
});
|
|
447
447
|
}); };
|
|
448
|
-
var
|
|
448
|
+
var listFiles = function () { return __awaiter(_this, void 0, void 0, function () {
|
|
449
449
|
var result, err_7, errorMessage, newToken, result, refreshError_4, error_7;
|
|
450
450
|
return __generator(this, function (_a) {
|
|
451
451
|
switch (_a.label) {
|
|
@@ -453,7 +453,7 @@ export function TrainlyProvider(_a) {
|
|
|
453
453
|
_a.trys.push([0, 2, 10, 11]);
|
|
454
454
|
setIsLoading(true);
|
|
455
455
|
setError(null);
|
|
456
|
-
return [4 /*yield*/, client.
|
|
456
|
+
return [4 /*yield*/, client.listFiles()];
|
|
457
457
|
case 1:
|
|
458
458
|
result = _a.sent();
|
|
459
459
|
return [2 /*return*/, result];
|
|
@@ -468,7 +468,7 @@ export function TrainlyProvider(_a) {
|
|
|
468
468
|
_a.label = 3;
|
|
469
469
|
case 3:
|
|
470
470
|
_a.trys.push([3, 8, , 9]);
|
|
471
|
-
console.log("🔄 Token expired during file
|
|
471
|
+
console.log("🔄 Token expired during file listing, refreshing...");
|
|
472
472
|
return [4 /*yield*/, getToken()];
|
|
473
473
|
case 4:
|
|
474
474
|
newToken = _a.sent();
|
|
@@ -476,10 +476,10 @@ export function TrainlyProvider(_a) {
|
|
|
476
476
|
return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
|
|
477
477
|
case 5:
|
|
478
478
|
_a.sent();
|
|
479
|
-
return [4 /*yield*/, client.
|
|
479
|
+
return [4 /*yield*/, client.listFiles()];
|
|
480
480
|
case 6:
|
|
481
481
|
result = _a.sent();
|
|
482
|
-
console.log("✅ File
|
|
482
|
+
console.log("✅ File listing succeeded after token refresh");
|
|
483
483
|
return [2 /*return*/, result];
|
|
484
484
|
case 7: return [3 /*break*/, 9];
|
|
485
485
|
case 8:
|
|
@@ -488,8 +488,8 @@ export function TrainlyProvider(_a) {
|
|
|
488
488
|
return [3 /*break*/, 9];
|
|
489
489
|
case 9:
|
|
490
490
|
error_7 = {
|
|
491
|
-
code: "
|
|
492
|
-
message: "Failed to
|
|
491
|
+
code: "LIST_FILES_FAILED",
|
|
492
|
+
message: "Failed to list files",
|
|
493
493
|
details: err_7,
|
|
494
494
|
};
|
|
495
495
|
setError(error_7);
|
|
@@ -501,8 +501,64 @@ export function TrainlyProvider(_a) {
|
|
|
501
501
|
}
|
|
502
502
|
});
|
|
503
503
|
}); };
|
|
504
|
+
var deleteFile = function (fileId) { return __awaiter(_this, void 0, void 0, function () {
|
|
505
|
+
var result, err_8, errorMessage, newToken, result, refreshError_5, error_8;
|
|
506
|
+
return __generator(this, function (_a) {
|
|
507
|
+
switch (_a.label) {
|
|
508
|
+
case 0:
|
|
509
|
+
_a.trys.push([0, 2, 10, 11]);
|
|
510
|
+
setIsLoading(true);
|
|
511
|
+
setError(null);
|
|
512
|
+
return [4 /*yield*/, client.deleteFile(fileId)];
|
|
513
|
+
case 1:
|
|
514
|
+
result = _a.sent();
|
|
515
|
+
return [2 /*return*/, result];
|
|
516
|
+
case 2:
|
|
517
|
+
err_8 = _a.sent();
|
|
518
|
+
errorMessage = err_8 instanceof Error ? err_8.message : String(err_8);
|
|
519
|
+
if (!(getToken &&
|
|
520
|
+
appId &&
|
|
521
|
+
(errorMessage.includes("401") ||
|
|
522
|
+
errorMessage.includes("authentication") ||
|
|
523
|
+
errorMessage.includes("Unauthorized")))) return [3 /*break*/, 9];
|
|
524
|
+
_a.label = 3;
|
|
525
|
+
case 3:
|
|
526
|
+
_a.trys.push([3, 8, , 9]);
|
|
527
|
+
console.log("🔄 Token expired during file deletion, refreshing...");
|
|
528
|
+
return [4 /*yield*/, getToken()];
|
|
529
|
+
case 4:
|
|
530
|
+
newToken = _a.sent();
|
|
531
|
+
if (!newToken) return [3 /*break*/, 7];
|
|
532
|
+
return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
|
|
533
|
+
case 5:
|
|
534
|
+
_a.sent();
|
|
535
|
+
return [4 /*yield*/, client.deleteFile(fileId)];
|
|
536
|
+
case 6:
|
|
537
|
+
result = _a.sent();
|
|
538
|
+
console.log("✅ File deletion succeeded after token refresh");
|
|
539
|
+
return [2 /*return*/, result];
|
|
540
|
+
case 7: return [3 /*break*/, 9];
|
|
541
|
+
case 8:
|
|
542
|
+
refreshError_5 = _a.sent();
|
|
543
|
+
console.error("❌ Token refresh failed:", refreshError_5);
|
|
544
|
+
return [3 /*break*/, 9];
|
|
545
|
+
case 9:
|
|
546
|
+
error_8 = {
|
|
547
|
+
code: "DELETE_FILE_FAILED",
|
|
548
|
+
message: "Failed to delete file",
|
|
549
|
+
details: err_8,
|
|
550
|
+
};
|
|
551
|
+
setError(error_8);
|
|
552
|
+
throw error_8;
|
|
553
|
+
case 10:
|
|
554
|
+
setIsLoading(false);
|
|
555
|
+
return [7 /*endfinally*/];
|
|
556
|
+
case 11: return [2 /*return*/];
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}); };
|
|
504
560
|
var sendMessage = function (content) { return __awaiter(_this, void 0, void 0, function () {
|
|
505
|
-
var userMessage, response, assistantMessage_1,
|
|
561
|
+
var userMessage, response, assistantMessage_1, err_9;
|
|
506
562
|
return __generator(this, function (_a) {
|
|
507
563
|
switch (_a.label) {
|
|
508
564
|
case 0:
|
|
@@ -529,9 +585,9 @@ export function TrainlyProvider(_a) {
|
|
|
529
585
|
setMessages(function (prev) { return __spreadArray(__spreadArray([], prev, true), [assistantMessage_1], false); });
|
|
530
586
|
return [3 /*break*/, 4];
|
|
531
587
|
case 3:
|
|
532
|
-
|
|
588
|
+
err_9 = _a.sent();
|
|
533
589
|
// Error is already set by askWithCitations
|
|
534
|
-
console.error("Failed to send message:",
|
|
590
|
+
console.error("Failed to send message:", err_9);
|
|
535
591
|
return [3 /*break*/, 4];
|
|
536
592
|
case 4: return [2 /*return*/];
|
|
537
593
|
}
|
|
@@ -544,6 +600,7 @@ export function TrainlyProvider(_a) {
|
|
|
544
600
|
ask: ask,
|
|
545
601
|
askWithCitations: askWithCitations,
|
|
546
602
|
upload: upload,
|
|
603
|
+
bulkUploadFiles: bulkUploadFiles, // NEW: Bulk file upload method
|
|
547
604
|
listFiles: listFiles, // NEW: File management methods
|
|
548
605
|
deleteFile: deleteFile,
|
|
549
606
|
connectWithOAuthToken: connectWithOAuthToken, // NEW: V1 OAuth connection method
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TrainlyConfig, Citation, UploadResult, FileListResult, FileDeleteResult } from "../types";
|
|
1
|
+
import { TrainlyConfig, Citation, UploadResult, FileListResult, FileDeleteResult, BulkUploadResult } from "../types";
|
|
2
2
|
interface QueryResponse {
|
|
3
3
|
answer: string;
|
|
4
4
|
citations?: Citation[];
|
|
@@ -19,6 +19,7 @@ export declare class TrainlyClient {
|
|
|
19
19
|
includeCitations?: boolean;
|
|
20
20
|
}): Promise<QueryResponse>;
|
|
21
21
|
upload(file: File): Promise<UploadResult>;
|
|
22
|
+
bulkUploadFiles(files: File[]): Promise<BulkUploadResult>;
|
|
22
23
|
listFiles(): Promise<FileListResult>;
|
|
23
24
|
deleteFile(fileId: string): Promise<FileDeleteResult>;
|
|
24
25
|
private extractChatId;
|
|
@@ -324,6 +324,114 @@ var TrainlyClient = /** @class */ (function () {
|
|
|
324
324
|
});
|
|
325
325
|
});
|
|
326
326
|
};
|
|
327
|
+
TrainlyClient.prototype.bulkUploadFiles = function (files) {
|
|
328
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
329
|
+
var formData_1, response, error, data, results, successful_uploads, total_size_bytes, _i, files_1, file, uploadResult, error_1;
|
|
330
|
+
return __generator(this, function (_a) {
|
|
331
|
+
switch (_a.label) {
|
|
332
|
+
case 0:
|
|
333
|
+
if (!this.scopedToken) {
|
|
334
|
+
throw new Error("Not connected. Call connect() or connectWithOAuthToken() first.");
|
|
335
|
+
}
|
|
336
|
+
if (!files || files.length === 0) {
|
|
337
|
+
throw new Error("No files provided for bulk upload.");
|
|
338
|
+
}
|
|
339
|
+
if (files.length > 10) {
|
|
340
|
+
throw new Error("Too many files. Maximum 10 files per bulk upload.");
|
|
341
|
+
}
|
|
342
|
+
if (!(this.isV1Mode && this.config.appId)) return [3 /*break*/, 5];
|
|
343
|
+
formData_1 = new FormData();
|
|
344
|
+
// Append all files to the form data
|
|
345
|
+
files.forEach(function (file) {
|
|
346
|
+
formData_1.append("files", file);
|
|
347
|
+
});
|
|
348
|
+
return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/me/chats/files/upload-bulk"), {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
Authorization: "Bearer ".concat(this.scopedToken),
|
|
352
|
+
"X-App-ID": this.config.appId,
|
|
353
|
+
},
|
|
354
|
+
body: formData_1,
|
|
355
|
+
})];
|
|
356
|
+
case 1:
|
|
357
|
+
response = _a.sent();
|
|
358
|
+
if (!!response.ok) return [3 /*break*/, 3];
|
|
359
|
+
return [4 /*yield*/, response.json()];
|
|
360
|
+
case 2:
|
|
361
|
+
error = _a.sent();
|
|
362
|
+
throw new Error("V1 bulk upload failed: ".concat(error.detail || response.statusText));
|
|
363
|
+
case 3: return [4 /*yield*/, response.json()];
|
|
364
|
+
case 4:
|
|
365
|
+
data = _a.sent();
|
|
366
|
+
return [2 /*return*/, {
|
|
367
|
+
success: data.success,
|
|
368
|
+
total_files: data.total_files,
|
|
369
|
+
successful_uploads: data.successful_uploads,
|
|
370
|
+
failed_uploads: data.failed_uploads,
|
|
371
|
+
total_size_bytes: data.total_size_bytes,
|
|
372
|
+
chat_id: data.chat_id,
|
|
373
|
+
user_id: data.user_id,
|
|
374
|
+
results: data.results,
|
|
375
|
+
message: data.message,
|
|
376
|
+
}];
|
|
377
|
+
case 5:
|
|
378
|
+
results = [];
|
|
379
|
+
successful_uploads = 0;
|
|
380
|
+
total_size_bytes = 0;
|
|
381
|
+
_i = 0, files_1 = files;
|
|
382
|
+
_a.label = 6;
|
|
383
|
+
case 6:
|
|
384
|
+
if (!(_i < files_1.length)) return [3 /*break*/, 11];
|
|
385
|
+
file = files_1[_i];
|
|
386
|
+
_a.label = 7;
|
|
387
|
+
case 7:
|
|
388
|
+
_a.trys.push([7, 9, , 10]);
|
|
389
|
+
return [4 /*yield*/, this.upload(file)];
|
|
390
|
+
case 8:
|
|
391
|
+
uploadResult = _a.sent();
|
|
392
|
+
results.push({
|
|
393
|
+
filename: uploadResult.filename,
|
|
394
|
+
success: uploadResult.success,
|
|
395
|
+
error: null,
|
|
396
|
+
file_id: null, // Single upload doesn't return file_id
|
|
397
|
+
size_bytes: uploadResult.size,
|
|
398
|
+
processing_status: uploadResult.success ? "completed" : "failed",
|
|
399
|
+
message: uploadResult.message,
|
|
400
|
+
});
|
|
401
|
+
if (uploadResult.success) {
|
|
402
|
+
successful_uploads++;
|
|
403
|
+
total_size_bytes += uploadResult.size;
|
|
404
|
+
}
|
|
405
|
+
return [3 /*break*/, 10];
|
|
406
|
+
case 9:
|
|
407
|
+
error_1 = _a.sent();
|
|
408
|
+
results.push({
|
|
409
|
+
filename: file.name,
|
|
410
|
+
success: false,
|
|
411
|
+
error: error_1 instanceof Error ? error_1.message : String(error_1),
|
|
412
|
+
file_id: null,
|
|
413
|
+
size_bytes: file.size,
|
|
414
|
+
processing_status: "failed",
|
|
415
|
+
});
|
|
416
|
+
return [3 /*break*/, 10];
|
|
417
|
+
case 10:
|
|
418
|
+
_i++;
|
|
419
|
+
return [3 /*break*/, 6];
|
|
420
|
+
case 11: return [2 /*return*/, {
|
|
421
|
+
success: successful_uploads > 0,
|
|
422
|
+
total_files: files.length,
|
|
423
|
+
successful_uploads: successful_uploads,
|
|
424
|
+
failed_uploads: files.length - successful_uploads,
|
|
425
|
+
total_size_bytes: total_size_bytes,
|
|
426
|
+
chat_id: this.currentUserId || "",
|
|
427
|
+
user_id: this.currentUserId || "",
|
|
428
|
+
results: results,
|
|
429
|
+
message: "Bulk upload completed: ".concat(successful_uploads, "/").concat(files.length, " files processed successfully"),
|
|
430
|
+
}];
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
};
|
|
327
435
|
TrainlyClient.prototype.listFiles = function () {
|
|
328
436
|
return __awaiter(this, void 0, void 0, function () {
|
|
329
437
|
var response, error, data;
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,26 @@ export interface UploadResult {
|
|
|
34
34
|
size: number;
|
|
35
35
|
message?: string;
|
|
36
36
|
}
|
|
37
|
+
export interface BulkUploadFileResult {
|
|
38
|
+
filename: string;
|
|
39
|
+
success: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
file_id: string | null;
|
|
42
|
+
size_bytes: number;
|
|
43
|
+
processing_status: string;
|
|
44
|
+
message?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface BulkUploadResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
total_files: number;
|
|
49
|
+
successful_uploads: number;
|
|
50
|
+
failed_uploads: number;
|
|
51
|
+
total_size_bytes: number;
|
|
52
|
+
chat_id: string;
|
|
53
|
+
user_id: string;
|
|
54
|
+
results: BulkUploadFileResult[];
|
|
55
|
+
message: string;
|
|
56
|
+
}
|
|
37
57
|
export interface FileInfo {
|
|
38
58
|
file_id: string;
|
|
39
59
|
filename: string;
|
|
@@ -76,6 +96,7 @@ export interface TrainlyContextValue {
|
|
|
76
96
|
upload: (file: File) => Promise<UploadResult>;
|
|
77
97
|
listFiles: () => Promise<FileListResult>;
|
|
78
98
|
deleteFile: (fileId: string) => Promise<FileDeleteResult>;
|
|
99
|
+
bulkUploadFiles: (files: File[]) => Promise<BulkUploadResult>;
|
|
79
100
|
connectWithOAuthToken: (idToken: string) => Promise<void>;
|
|
80
101
|
isLoading: boolean;
|
|
81
102
|
isConnected: boolean;
|