@trainly/react 1.2.0 → 1.4.0

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)
@@ -333,7 +333,7 @@ export function TrainlyProvider(_a) {
333
333
  }
334
334
  });
335
335
  }); };
336
- var upload = function (file) { return __awaiter(_this, void 0, void 0, function () {
336
+ var upload = function (file, scopeValues) { return __awaiter(_this, void 0, void 0, function () {
337
337
  var result, err_5, errorMessage, newToken, result, refreshError_2, error_5;
338
338
  return __generator(this, function (_a) {
339
339
  switch (_a.label) {
@@ -341,7 +341,7 @@ export function TrainlyProvider(_a) {
341
341
  _a.trys.push([0, 2, 10, 11]);
342
342
  setIsLoading(true);
343
343
  setError(null);
344
- return [4 /*yield*/, client.upload(file)];
344
+ return [4 /*yield*/, client.upload(file, scopeValues)];
345
345
  case 1:
346
346
  result = _a.sent();
347
347
  return [2 /*return*/, result];
@@ -364,7 +364,7 @@ export function TrainlyProvider(_a) {
364
364
  return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
365
365
  case 5:
366
366
  _a.sent();
367
- return [4 /*yield*/, client.upload(file)];
367
+ return [4 /*yield*/, client.upload(file, scopeValues)];
368
368
  case 6:
369
369
  result = _a.sent();
370
370
  console.log("✅ Upload succeeded after token refresh");
@@ -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, scopeValues) { 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, scopeValues)];
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, scopeValues)];
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[];
@@ -18,7 +18,8 @@ export declare class TrainlyClient {
18
18
  ask(question: string, options?: {
19
19
  includeCitations?: boolean;
20
20
  }): Promise<QueryResponse>;
21
- upload(file: File): Promise<UploadResult>;
21
+ upload(file: File, scopeValues?: Record<string, string | number | boolean>): Promise<UploadResult>;
22
+ bulkUploadFiles(files: File[], scopeValues?: Record<string, string | number | boolean>): Promise<BulkUploadResult>;
22
23
  listFiles(): Promise<FileListResult>;
23
24
  deleteFile(fileId: string): Promise<FileDeleteResult>;
24
25
  private extractChatId;
@@ -218,7 +218,7 @@ var TrainlyClient = /** @class */ (function () {
218
218
  });
219
219
  });
220
220
  };
221
- TrainlyClient.prototype.upload = function (file) {
221
+ TrainlyClient.prototype.upload = function (file, scopeValues) {
222
222
  return __awaiter(this, void 0, void 0, function () {
223
223
  var formData, response, error, data, formData, response, error, presignedResponse, error, _a, upload_url, upload_headers, formData, uploadResponse;
224
224
  return __generator(this, function (_b) {
@@ -230,6 +230,10 @@ var TrainlyClient = /** @class */ (function () {
230
230
  if (!(this.isV1Mode && this.config.appId)) return [3 /*break*/, 5];
231
231
  formData = new FormData();
232
232
  formData.append("file", file);
233
+ // Add scope values if provided
234
+ if (scopeValues && Object.keys(scopeValues).length > 0) {
235
+ formData.append("scope_values", JSON.stringify(scopeValues));
236
+ }
233
237
  return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/me/chats/files/upload"), {
234
238
  method: "POST",
235
239
  headers: {
@@ -258,6 +262,10 @@ var TrainlyClient = /** @class */ (function () {
258
262
  if (!this.config.apiKey) return [3 /*break*/, 9];
259
263
  formData = new FormData();
260
264
  formData.append("file", file);
265
+ // Add scope values if provided
266
+ if (scopeValues && Object.keys(scopeValues).length > 0) {
267
+ formData.append("scope_values", JSON.stringify(scopeValues));
268
+ }
261
269
  return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/").concat(this.extractChatId(), "/upload_file"), {
262
270
  method: "POST",
263
271
  headers: {
@@ -324,6 +332,118 @@ var TrainlyClient = /** @class */ (function () {
324
332
  });
325
333
  });
326
334
  };
335
+ TrainlyClient.prototype.bulkUploadFiles = function (files, scopeValues) {
336
+ return __awaiter(this, void 0, void 0, function () {
337
+ var formData_1, response, error, data, results, successful_uploads, total_size_bytes, _i, files_1, file, uploadResult, error_1;
338
+ return __generator(this, function (_a) {
339
+ switch (_a.label) {
340
+ case 0:
341
+ if (!this.scopedToken) {
342
+ throw new Error("Not connected. Call connect() or connectWithOAuthToken() first.");
343
+ }
344
+ if (!files || files.length === 0) {
345
+ throw new Error("No files provided for bulk upload.");
346
+ }
347
+ if (files.length > 10) {
348
+ throw new Error("Too many files. Maximum 10 files per bulk upload.");
349
+ }
350
+ if (!(this.isV1Mode && this.config.appId)) return [3 /*break*/, 5];
351
+ formData_1 = new FormData();
352
+ // Append all files to the form data
353
+ files.forEach(function (file) {
354
+ formData_1.append("files", file);
355
+ });
356
+ // Add scope values if provided
357
+ if (scopeValues && Object.keys(scopeValues).length > 0) {
358
+ formData_1.append("scope_values", JSON.stringify(scopeValues));
359
+ }
360
+ return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/me/chats/files/upload-bulk"), {
361
+ method: "POST",
362
+ headers: {
363
+ Authorization: "Bearer ".concat(this.scopedToken),
364
+ "X-App-ID": this.config.appId,
365
+ },
366
+ body: formData_1,
367
+ })];
368
+ case 1:
369
+ response = _a.sent();
370
+ if (!!response.ok) return [3 /*break*/, 3];
371
+ return [4 /*yield*/, response.json()];
372
+ case 2:
373
+ error = _a.sent();
374
+ throw new Error("V1 bulk upload failed: ".concat(error.detail || response.statusText));
375
+ case 3: return [4 /*yield*/, response.json()];
376
+ case 4:
377
+ data = _a.sent();
378
+ return [2 /*return*/, {
379
+ success: data.success,
380
+ total_files: data.total_files,
381
+ successful_uploads: data.successful_uploads,
382
+ failed_uploads: data.failed_uploads,
383
+ total_size_bytes: data.total_size_bytes,
384
+ chat_id: data.chat_id,
385
+ user_id: data.user_id,
386
+ results: data.results,
387
+ message: data.message,
388
+ }];
389
+ case 5:
390
+ results = [];
391
+ successful_uploads = 0;
392
+ total_size_bytes = 0;
393
+ _i = 0, files_1 = files;
394
+ _a.label = 6;
395
+ case 6:
396
+ if (!(_i < files_1.length)) return [3 /*break*/, 11];
397
+ file = files_1[_i];
398
+ _a.label = 7;
399
+ case 7:
400
+ _a.trys.push([7, 9, , 10]);
401
+ return [4 /*yield*/, this.upload(file, scopeValues)];
402
+ case 8:
403
+ uploadResult = _a.sent();
404
+ results.push({
405
+ filename: uploadResult.filename,
406
+ success: uploadResult.success,
407
+ error: null,
408
+ file_id: null, // Single upload doesn't return file_id
409
+ size_bytes: uploadResult.size,
410
+ processing_status: uploadResult.success ? "completed" : "failed",
411
+ message: uploadResult.message,
412
+ });
413
+ if (uploadResult.success) {
414
+ successful_uploads++;
415
+ total_size_bytes += uploadResult.size;
416
+ }
417
+ return [3 /*break*/, 10];
418
+ case 9:
419
+ error_1 = _a.sent();
420
+ results.push({
421
+ filename: file.name,
422
+ success: false,
423
+ error: error_1 instanceof Error ? error_1.message : String(error_1),
424
+ file_id: null,
425
+ size_bytes: file.size,
426
+ processing_status: "failed",
427
+ });
428
+ return [3 /*break*/, 10];
429
+ case 10:
430
+ _i++;
431
+ return [3 /*break*/, 6];
432
+ case 11: return [2 /*return*/, {
433
+ success: successful_uploads > 0,
434
+ total_files: files.length,
435
+ successful_uploads: successful_uploads,
436
+ failed_uploads: files.length - successful_uploads,
437
+ total_size_bytes: total_size_bytes,
438
+ chat_id: this.currentUserId || "",
439
+ user_id: this.currentUserId || "",
440
+ results: results,
441
+ message: "Bulk upload completed: ".concat(successful_uploads, "/").concat(files.length, " files processed successfully"),
442
+ }];
443
+ }
444
+ });
445
+ });
446
+ };
327
447
  TrainlyClient.prototype.listFiles = function () {
328
448
  return __awaiter(this, void 0, void 0, function () {
329
449
  var response, error, data;
@@ -6,5 +6,6 @@ export interface TrainlyUploadProps {
6
6
  className?: string;
7
7
  onUpload?: (files: File[]) => void;
8
8
  onError?: (error: string) => void;
9
+ scopeValues?: Record<string, string | number | boolean>;
9
10
  }
10
- export declare function TrainlyUpload({ variant, accept, maxSize, multiple, className, onUpload, onError, }: TrainlyUploadProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function TrainlyUpload({ variant, accept, maxSize, multiple, className, onUpload, onError, scopeValues, }: TrainlyUploadProps): import("react/jsx-runtime").JSX.Element;
@@ -40,7 +40,7 @@ import * as React from "react";
40
40
  import { useTrainly } from "../useTrainly";
41
41
  export function TrainlyUpload(_a) {
42
42
  var _this = this;
43
- var _b = _a.variant, variant = _b === void 0 ? "drag-drop" : _b, _c = _a.accept, accept = _c === void 0 ? ".pdf,.doc,.docx,.txt,.md" : _c, _d = _a.maxSize, maxSize = _d === void 0 ? "10MB" : _d, _e = _a.multiple, multiple = _e === void 0 ? false : _e, _f = _a.className, className = _f === void 0 ? "" : _f, onUpload = _a.onUpload, onError = _a.onError;
43
+ var _b = _a.variant, variant = _b === void 0 ? "drag-drop" : _b, _c = _a.accept, accept = _c === void 0 ? ".pdf,.doc,.docx,.txt,.md" : _c, _d = _a.maxSize, maxSize = _d === void 0 ? "10MB" : _d, _e = _a.multiple, multiple = _e === void 0 ? false : _e, _f = _a.className, className = _f === void 0 ? "" : _f, onUpload = _a.onUpload, onError = _a.onError, scopeValues = _a.scopeValues;
44
44
  var _g = useTrainly(), upload = _g.upload, isLoading = _g.isLoading;
45
45
  var _h = React.useState(false), isDragOver = _h[0], setIsDragOver = _h[1];
46
46
  var fileInputRef = React.useRef(null);
@@ -70,7 +70,7 @@ export function TrainlyUpload(_a) {
70
70
  case 2:
71
71
  if (!(_a < fileArray_2.length)) return [3 /*break*/, 5];
72
72
  file = fileArray_2[_a];
73
- return [4 /*yield*/, upload(file)];
73
+ return [4 /*yield*/, upload(file, scopeValues)];
74
74
  case 3:
75
75
  _b.sent();
76
76
  _b.label = 4;
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;
@@ -73,9 +93,10 @@ export interface TrainlyContextValue {
73
93
  answer: string;
74
94
  citations: Citation[];
75
95
  }>;
76
- upload: (file: File) => Promise<UploadResult>;
96
+ upload: (file: File, scopeValues?: Record<string, string | number | boolean>) => Promise<UploadResult>;
77
97
  listFiles: () => Promise<FileListResult>;
78
98
  deleteFile: (fileId: string) => Promise<FileDeleteResult>;
99
+ bulkUploadFiles: (files: File[], scopeValues?: Record<string, string | number | boolean>) => 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.4.0",
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",