@trainly/react 1.1.3 → 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
@@ -86,6 +86,593 @@ function MyComponent() {
86
86
  - ✅ **Zero Migration**: Works with your existing OAuth setup
87
87
  - ✅ **Simple Integration**: Just add `appId` and use `connectWithOAuthToken()`
88
88
 
89
+ ## 📁 **NEW: File Management**
90
+
91
+ Now users can manage their uploaded files directly:
92
+
93
+ ```tsx
94
+ import { useTrainly, TrainlyFileManager } from "@trainly/react";
95
+
96
+ function MyApp() {
97
+ const { listFiles, deleteFile, upload } = useTrainly();
98
+
99
+ // List all user's files
100
+ const handleListFiles = async () => {
101
+ const result = await listFiles();
102
+ console.log(
103
+ `${result.total_files} files, ${result.total_size_bytes} bytes total`,
104
+ );
105
+ result.files.forEach((file) => {
106
+ console.log(
107
+ `${file.filename}: ${file.size_bytes} bytes, ${file.chunk_count} chunks`,
108
+ );
109
+ });
110
+ };
111
+
112
+ // Delete a specific file
113
+ const handleDeleteFile = async (fileId) => {
114
+ const result = await deleteFile(fileId);
115
+ console.log(
116
+ `Deleted ${result.filename}, freed ${result.size_bytes_freed} bytes`,
117
+ );
118
+ };
119
+
120
+ return (
121
+ <div>
122
+ <button onClick={handleListFiles}>List My Files</button>
123
+
124
+ {/* Pre-built file manager component */}
125
+ <TrainlyFileManager
126
+ onFileDeleted={(fileId, filename) => {
127
+ console.log(`File deleted: ${filename}`);
128
+ }}
129
+ onError={(error) => {
130
+ console.error("File operation failed:", error);
131
+ }}
132
+ showUploadButton={true}
133
+ maxFileSize={5} // MB
134
+ />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ### File Management Features
141
+
142
+ - 📋 **List Files**: View all uploaded documents with metadata
143
+ - 🗑️ **Delete Files**: Remove files and free up storage space
144
+ - 📊 **Storage Analytics**: Track file sizes and storage usage
145
+ - 🔄 **Auto-Refresh**: File list updates after uploads/deletions
146
+ - 🎨 **Pre-built UI**: `TrainlyFileManager` component with styling
147
+ - 🔒 **Privacy-First**: Only works in V1 mode with OAuth authentication
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
+
89
676
  ---
90
677
 
91
678
  ## 🚀 Original Quick Start (Legacy)