@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 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)
@@ -389,7 +389,7 @@ export function TrainlyProvider(_a) {
389
389
  }
390
390
  });
391
391
  }); };
392
- var listFiles = function () { return __awaiter(_this, void 0, void 0, function () {
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.listFiles()];
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 file listing, refreshing...");
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.listFiles()];
423
+ return [4 /*yield*/, client.bulkUploadFiles(files)];
424
424
  case 6:
425
425
  result = _a.sent();
426
- console.log("✅ File listing succeeded after token refresh");
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: "LIST_FILES_FAILED",
436
- message: "Failed to list files",
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 deleteFile = function (fileId) { return __awaiter(_this, void 0, void 0, function () {
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.deleteFile(fileId)];
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 deletion, refreshing...");
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.deleteFile(fileId)];
479
+ return [4 /*yield*/, client.listFiles()];
480
480
  case 6:
481
481
  result = _a.sent();
482
- console.log("✅ File deletion succeeded after token refresh");
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: "DELETE_FILE_FAILED",
492
- message: "Failed to delete file",
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, err_8;
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
- err_8 = _a.sent();
588
+ err_9 = _a.sent();
533
589
  // Error is already set by askWithCitations
534
- console.error("Failed to send message:", err_8);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainly/react",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Dead simple RAG integration for React apps with OAuth authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",