@trainly/react 1.1.2 → 1.2.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
@@ -86,6 +86,66 @@ 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
+
89
149
  ---
90
150
 
91
151
  ## 🚀 Original Quick Start (Legacy)
@@ -67,9 +67,67 @@ export function TrainlyProvider(_a) {
67
67
  var _d = React.useState(false), isConnected = _d[0], setIsConnected = _d[1];
68
68
  var _e = React.useState(null), error = _e[0], setError = _e[1];
69
69
  var _f = React.useState([]), messages = _f[0], setMessages = _f[1];
70
- // Auto-connect on mount
70
+ var _g = React.useState(null), currentToken = _g[0], setCurrentToken = _g[1];
71
+ var refreshIntervalRef = React.useRef(null);
72
+ // Auto-connect on mount (only for non-OAuth modes)
71
73
  React.useEffect(function () {
72
- connect();
74
+ if (!appId && !getToken) {
75
+ connect();
76
+ }
77
+ }, []);
78
+ // NEW: Automatic OAuth state management
79
+ React.useEffect(function () {
80
+ if (!getToken || !appId)
81
+ return;
82
+ var manageOAuthConnection = function () { return __awaiter(_this, void 0, void 0, function () {
83
+ var token, error_1;
84
+ return __generator(this, function (_a) {
85
+ switch (_a.label) {
86
+ case 0:
87
+ _a.trys.push([0, 6, , 7]);
88
+ return [4 /*yield*/, getToken()];
89
+ case 1:
90
+ token = _a.sent();
91
+ if (!(token && token !== currentToken)) return [3 /*break*/, 4];
92
+ if (!(!isConnected || token !== currentToken)) return [3 /*break*/, 3];
93
+ console.log("🔐 New OAuth session detected, connecting to Trainly...");
94
+ return [4 /*yield*/, connectWithOAuthToken(token)];
95
+ case 2:
96
+ _a.sent();
97
+ _a.label = 3;
98
+ case 3: return [3 /*break*/, 5];
99
+ case 4:
100
+ if (!token && isConnected) {
101
+ // User signed out, disconnect from Trainly
102
+ console.log("🚪 User signed out, disconnecting from Trainly...");
103
+ clearRefreshInterval();
104
+ setIsConnected(false);
105
+ setCurrentToken(null);
106
+ setError(null);
107
+ }
108
+ _a.label = 5;
109
+ case 5: return [3 /*break*/, 7];
110
+ case 6:
111
+ error_1 = _a.sent();
112
+ console.error("OAuth state management error:", error_1);
113
+ return [3 /*break*/, 7];
114
+ case 7: return [2 /*return*/];
115
+ }
116
+ });
117
+ }); };
118
+ // Check OAuth state immediately
119
+ manageOAuthConnection();
120
+ // Set up periodic OAuth state checking (every 10 seconds)
121
+ var stateCheckInterval = setInterval(manageOAuthConnection, 10000);
122
+ return function () {
123
+ clearInterval(stateCheckInterval);
124
+ };
125
+ }, [getToken, appId, currentToken, isConnected]);
126
+ // Cleanup on unmount
127
+ React.useEffect(function () {
128
+ return function () {
129
+ clearRefreshInterval();
130
+ };
73
131
  }, []);
74
132
  var connect = function () { return __awaiter(_this, void 0, void 0, function () {
75
133
  var err_1;
@@ -100,7 +158,62 @@ export function TrainlyProvider(_a) {
100
158
  }
101
159
  });
102
160
  }); };
103
- // NEW: V1 OAuth Token connection method
161
+ // Clear any existing refresh interval
162
+ var clearRefreshInterval = function () {
163
+ if (refreshIntervalRef.current) {
164
+ clearInterval(refreshIntervalRef.current);
165
+ refreshIntervalRef.current = null;
166
+ }
167
+ };
168
+ // Set up automatic token refresh
169
+ var setupTokenRefresh = function (token) {
170
+ if (!getToken || !appId)
171
+ return;
172
+ // Clear any existing interval
173
+ clearRefreshInterval();
174
+ // Decode token to get expiration (without verification)
175
+ try {
176
+ var payload = JSON.parse(atob(token.split(".")[1]));
177
+ var exp = payload.exp * 1000; // Convert to milliseconds
178
+ var now = Date.now();
179
+ var timeUntilExpiry = exp - now;
180
+ // Refresh 30 seconds before expiry (or immediately if already expired)
181
+ var refreshIn = Math.max(timeUntilExpiry - 30000, 1000);
182
+ console.log("\uD83D\uDD04 Token refresh scheduled in ".concat(Math.round(refreshIn / 1000), " seconds"));
183
+ refreshIntervalRef.current = setTimeout(function () { return __awaiter(_this, void 0, void 0, function () {
184
+ var newToken, error_2;
185
+ return __generator(this, function (_a) {
186
+ switch (_a.label) {
187
+ case 0:
188
+ _a.trys.push([0, 4, , 5]);
189
+ console.log("🔄 Auto-refreshing token...");
190
+ return [4 /*yield*/, getToken()];
191
+ case 1:
192
+ newToken = _a.sent();
193
+ if (!(newToken && newToken !== currentToken)) return [3 /*break*/, 3];
194
+ return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
195
+ case 2:
196
+ _a.sent();
197
+ setCurrentToken(newToken);
198
+ setupTokenRefresh(newToken); // Schedule next refresh
199
+ console.log("✅ Token auto-refreshed successfully");
200
+ _a.label = 3;
201
+ case 3: return [3 /*break*/, 5];
202
+ case 4:
203
+ error_2 = _a.sent();
204
+ console.error("❌ Auto token refresh failed:", error_2);
205
+ setIsConnected(false);
206
+ return [3 /*break*/, 5];
207
+ case 5: return [2 /*return*/];
208
+ }
209
+ });
210
+ }); }, refreshIn);
211
+ }
212
+ catch (error) {
213
+ console.warn("Could not decode token for refresh scheduling:", error);
214
+ }
215
+ };
216
+ // NEW: V1 OAuth Token connection method with auto-refresh
104
217
  var connectWithOAuthToken = function (idToken) { return __awaiter(_this, void 0, void 0, function () {
105
218
  var err_2;
106
219
  return __generator(this, function (_a) {
@@ -112,7 +225,10 @@ export function TrainlyProvider(_a) {
112
225
  return [4 /*yield*/, client.connectWithOAuthToken(idToken)];
113
226
  case 1:
114
227
  _a.sent();
228
+ setCurrentToken(idToken);
115
229
  setIsConnected(true);
230
+ // Set up automatic token refresh
231
+ setupTokenRefresh(idToken);
116
232
  return [3 /*break*/, 4];
117
233
  case 2:
118
234
  err_2 = _a.sent();
@@ -131,7 +247,7 @@ export function TrainlyProvider(_a) {
131
247
  });
132
248
  }); };
133
249
  var ask = function (question) { return __awaiter(_this, void 0, void 0, function () {
134
- var response, err_3, errorMessage, newToken, response, refreshError_1, error_1;
250
+ var response, err_3, errorMessage, newToken, response, refreshError_1, error_3;
135
251
  return __generator(this, function (_a) {
136
252
  switch (_a.label) {
137
253
  case 0:
@@ -172,13 +288,13 @@ export function TrainlyProvider(_a) {
172
288
  console.error("❌ Token refresh failed:", refreshError_1);
173
289
  return [3 /*break*/, 9];
174
290
  case 9:
175
- error_1 = {
291
+ error_3 = {
176
292
  code: "QUERY_FAILED",
177
293
  message: "Failed to get answer",
178
294
  details: err_3,
179
295
  };
180
- setError(error_1);
181
- throw error_1;
296
+ setError(error_3);
297
+ throw error_3;
182
298
  case 10:
183
299
  setIsLoading(false);
184
300
  return [7 /*endfinally*/];
@@ -187,7 +303,7 @@ export function TrainlyProvider(_a) {
187
303
  });
188
304
  }); };
189
305
  var askWithCitations = function (question) { return __awaiter(_this, void 0, void 0, function () {
190
- var response, err_4, error_2;
306
+ var response, err_4, error_4;
191
307
  return __generator(this, function (_a) {
192
308
  switch (_a.label) {
193
309
  case 0:
@@ -203,13 +319,13 @@ export function TrainlyProvider(_a) {
203
319
  }];
204
320
  case 2:
205
321
  err_4 = _a.sent();
206
- error_2 = {
322
+ error_4 = {
207
323
  code: "QUERY_FAILED",
208
324
  message: "Failed to get answer with citations",
209
325
  details: err_4,
210
326
  };
211
- setError(error_2);
212
- throw error_2;
327
+ setError(error_4);
328
+ throw error_4;
213
329
  case 3:
214
330
  setIsLoading(false);
215
331
  return [7 /*endfinally*/];
@@ -218,7 +334,7 @@ export function TrainlyProvider(_a) {
218
334
  });
219
335
  }); };
220
336
  var upload = function (file) { return __awaiter(_this, void 0, void 0, function () {
221
- var result, err_5, errorMessage, newToken, result, refreshError_2, error_3;
337
+ var result, err_5, errorMessage, newToken, result, refreshError_2, error_5;
222
338
  return __generator(this, function (_a) {
223
339
  switch (_a.label) {
224
340
  case 0:
@@ -259,13 +375,125 @@ export function TrainlyProvider(_a) {
259
375
  console.error("❌ Token refresh failed:", refreshError_2);
260
376
  return [3 /*break*/, 9];
261
377
  case 9:
262
- error_3 = {
378
+ error_5 = {
263
379
  code: "UPLOAD_FAILED",
264
380
  message: "Failed to upload file",
265
381
  details: err_5,
266
382
  };
267
- setError(error_3);
268
- throw error_3;
383
+ setError(error_5);
384
+ throw error_5;
385
+ case 10:
386
+ setIsLoading(false);
387
+ return [7 /*endfinally*/];
388
+ case 11: return [2 /*return*/];
389
+ }
390
+ });
391
+ }); };
392
+ var listFiles = function () { return __awaiter(_this, void 0, void 0, function () {
393
+ var result, err_6, errorMessage, newToken, result, refreshError_3, error_6;
394
+ return __generator(this, function (_a) {
395
+ switch (_a.label) {
396
+ case 0:
397
+ _a.trys.push([0, 2, 10, 11]);
398
+ setIsLoading(true);
399
+ setError(null);
400
+ return [4 /*yield*/, client.listFiles()];
401
+ case 1:
402
+ result = _a.sent();
403
+ return [2 /*return*/, result];
404
+ case 2:
405
+ err_6 = _a.sent();
406
+ errorMessage = err_6 instanceof Error ? err_6.message : String(err_6);
407
+ if (!(getToken &&
408
+ appId &&
409
+ (errorMessage.includes("401") ||
410
+ errorMessage.includes("authentication") ||
411
+ errorMessage.includes("Unauthorized")))) return [3 /*break*/, 9];
412
+ _a.label = 3;
413
+ case 3:
414
+ _a.trys.push([3, 8, , 9]);
415
+ console.log("🔄 Token expired during file listing, refreshing...");
416
+ return [4 /*yield*/, getToken()];
417
+ case 4:
418
+ newToken = _a.sent();
419
+ if (!newToken) return [3 /*break*/, 7];
420
+ return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
421
+ case 5:
422
+ _a.sent();
423
+ return [4 /*yield*/, client.listFiles()];
424
+ case 6:
425
+ result = _a.sent();
426
+ console.log("✅ File listing succeeded after token refresh");
427
+ return [2 /*return*/, result];
428
+ case 7: return [3 /*break*/, 9];
429
+ case 8:
430
+ refreshError_3 = _a.sent();
431
+ console.error("❌ Token refresh failed:", refreshError_3);
432
+ return [3 /*break*/, 9];
433
+ case 9:
434
+ error_6 = {
435
+ code: "LIST_FILES_FAILED",
436
+ message: "Failed to list files",
437
+ details: err_6,
438
+ };
439
+ setError(error_6);
440
+ throw error_6;
441
+ case 10:
442
+ setIsLoading(false);
443
+ return [7 /*endfinally*/];
444
+ case 11: return [2 /*return*/];
445
+ }
446
+ });
447
+ }); };
448
+ var deleteFile = function (fileId) { return __awaiter(_this, void 0, void 0, function () {
449
+ var result, err_7, errorMessage, newToken, result, refreshError_4, error_7;
450
+ return __generator(this, function (_a) {
451
+ switch (_a.label) {
452
+ case 0:
453
+ _a.trys.push([0, 2, 10, 11]);
454
+ setIsLoading(true);
455
+ setError(null);
456
+ return [4 /*yield*/, client.deleteFile(fileId)];
457
+ case 1:
458
+ result = _a.sent();
459
+ return [2 /*return*/, result];
460
+ case 2:
461
+ err_7 = _a.sent();
462
+ errorMessage = err_7 instanceof Error ? err_7.message : String(err_7);
463
+ if (!(getToken &&
464
+ appId &&
465
+ (errorMessage.includes("401") ||
466
+ errorMessage.includes("authentication") ||
467
+ errorMessage.includes("Unauthorized")))) return [3 /*break*/, 9];
468
+ _a.label = 3;
469
+ case 3:
470
+ _a.trys.push([3, 8, , 9]);
471
+ console.log("🔄 Token expired during file deletion, refreshing...");
472
+ return [4 /*yield*/, getToken()];
473
+ case 4:
474
+ newToken = _a.sent();
475
+ if (!newToken) return [3 /*break*/, 7];
476
+ return [4 /*yield*/, client.connectWithOAuthToken(newToken)];
477
+ case 5:
478
+ _a.sent();
479
+ return [4 /*yield*/, client.deleteFile(fileId)];
480
+ case 6:
481
+ result = _a.sent();
482
+ console.log("✅ File deletion succeeded after token refresh");
483
+ return [2 /*return*/, result];
484
+ case 7: return [3 /*break*/, 9];
485
+ case 8:
486
+ refreshError_4 = _a.sent();
487
+ console.error("❌ Token refresh failed:", refreshError_4);
488
+ return [3 /*break*/, 9];
489
+ case 9:
490
+ error_7 = {
491
+ code: "DELETE_FILE_FAILED",
492
+ message: "Failed to delete file",
493
+ details: err_7,
494
+ };
495
+ setError(error_7);
496
+ throw error_7;
269
497
  case 10:
270
498
  setIsLoading(false);
271
499
  return [7 /*endfinally*/];
@@ -274,7 +502,7 @@ export function TrainlyProvider(_a) {
274
502
  });
275
503
  }); };
276
504
  var sendMessage = function (content) { return __awaiter(_this, void 0, void 0, function () {
277
- var userMessage, response, assistantMessage_1, err_6;
505
+ var userMessage, response, assistantMessage_1, err_8;
278
506
  return __generator(this, function (_a) {
279
507
  switch (_a.label) {
280
508
  case 0:
@@ -301,9 +529,9 @@ export function TrainlyProvider(_a) {
301
529
  setMessages(function (prev) { return __spreadArray(__spreadArray([], prev, true), [assistantMessage_1], false); });
302
530
  return [3 /*break*/, 4];
303
531
  case 3:
304
- err_6 = _a.sent();
532
+ err_8 = _a.sent();
305
533
  // Error is already set by askWithCitations
306
- console.error("Failed to send message:", err_6);
534
+ console.error("Failed to send message:", err_8);
307
535
  return [3 /*break*/, 4];
308
536
  case 4: return [2 /*return*/];
309
537
  }
@@ -316,6 +544,8 @@ export function TrainlyProvider(_a) {
316
544
  ask: ask,
317
545
  askWithCitations: askWithCitations,
318
546
  upload: upload,
547
+ listFiles: listFiles, // NEW: File management methods
548
+ deleteFile: deleteFile,
319
549
  connectWithOAuthToken: connectWithOAuthToken, // NEW: V1 OAuth connection method
320
550
  isLoading: isLoading,
321
551
  isConnected: isConnected,
@@ -1,4 +1,4 @@
1
- import { TrainlyConfig, Citation, UploadResult } from "../types";
1
+ import { TrainlyConfig, Citation, UploadResult, FileListResult, FileDeleteResult } from "../types";
2
2
  interface QueryResponse {
3
3
  answer: string;
4
4
  citations?: Citation[];
@@ -19,6 +19,8 @@ export declare class TrainlyClient {
19
19
  includeCitations?: boolean;
20
20
  }): Promise<QueryResponse>;
21
21
  upload(file: File): Promise<UploadResult>;
22
+ listFiles(): Promise<FileListResult>;
23
+ deleteFile(fileId: string): Promise<FileDeleteResult>;
22
24
  private extractChatId;
23
25
  private generateAnonymousId;
24
26
  }
@@ -324,6 +324,81 @@ var TrainlyClient = /** @class */ (function () {
324
324
  });
325
325
  });
326
326
  };
327
+ TrainlyClient.prototype.listFiles = function () {
328
+ return __awaiter(this, void 0, void 0, function () {
329
+ var response, error, data;
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 (!(this.isV1Mode && this.config.appId)) return [3 /*break*/, 5];
337
+ return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/me/chats/files"), {
338
+ method: "GET",
339
+ headers: {
340
+ Authorization: "Bearer ".concat(this.scopedToken),
341
+ "X-App-ID": this.config.appId,
342
+ },
343
+ })];
344
+ case 1:
345
+ response = _a.sent();
346
+ if (!!response.ok) return [3 /*break*/, 3];
347
+ return [4 /*yield*/, response.json()];
348
+ case 2:
349
+ error = _a.sent();
350
+ throw new Error("V1 list files failed: ".concat(error.detail || response.statusText));
351
+ case 3: return [4 /*yield*/, response.json()];
352
+ case 4:
353
+ data = _a.sent();
354
+ return [2 /*return*/, data];
355
+ case 5:
356
+ // For other modes, this functionality is not yet implemented
357
+ // as it requires chat-specific API endpoints
358
+ throw new Error("File listing is currently only available in V1 Trusted Issuer mode");
359
+ }
360
+ });
361
+ });
362
+ };
363
+ TrainlyClient.prototype.deleteFile = function (fileId) {
364
+ return __awaiter(this, void 0, void 0, function () {
365
+ var response, error, data;
366
+ return __generator(this, function (_a) {
367
+ switch (_a.label) {
368
+ case 0:
369
+ if (!this.scopedToken) {
370
+ throw new Error("Not connected. Call connect() or connectWithOAuthToken() first.");
371
+ }
372
+ if (!fileId) {
373
+ throw new Error("File ID is required");
374
+ }
375
+ if (!(this.isV1Mode && this.config.appId)) return [3 /*break*/, 5];
376
+ return [4 /*yield*/, fetch("".concat(this.config.baseUrl, "/v1/me/chats/files/").concat(encodeURIComponent(fileId)), {
377
+ method: "DELETE",
378
+ headers: {
379
+ Authorization: "Bearer ".concat(this.scopedToken),
380
+ "X-App-ID": this.config.appId,
381
+ },
382
+ })];
383
+ case 1:
384
+ response = _a.sent();
385
+ if (!!response.ok) return [3 /*break*/, 3];
386
+ return [4 /*yield*/, response.json()];
387
+ case 2:
388
+ error = _a.sent();
389
+ throw new Error("V1 delete file failed: ".concat(error.detail || response.statusText));
390
+ case 3: return [4 /*yield*/, response.json()];
391
+ case 4:
392
+ data = _a.sent();
393
+ return [2 /*return*/, data];
394
+ case 5:
395
+ // For other modes, this functionality is not yet implemented
396
+ // as it requires chat-specific API endpoints
397
+ throw new Error("File deletion is currently only available in V1 Trusted Issuer mode");
398
+ }
399
+ });
400
+ });
401
+ };
327
402
  TrainlyClient.prototype.extractChatId = function () {
328
403
  if (!this.config.apiKey) {
329
404
  throw new Error("API key not provided");
@@ -0,0 +1,8 @@
1
+ export interface TrainlyFileManagerProps {
2
+ className?: string;
3
+ onFileDeleted?: (fileId: string, filename: string) => void;
4
+ onError?: (error: Error) => void;
5
+ showUploadButton?: boolean;
6
+ maxFileSize?: number;
7
+ }
8
+ export declare function TrainlyFileManager({ className, onFileDeleted, onError, showUploadButton, maxFileSize, }: TrainlyFileManagerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,304 @@
1
+ "use client";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
50
+ import * as React from "react";
51
+ import { useTrainly } from "../useTrainly";
52
+ export function TrainlyFileManager(_a) {
53
+ var _this = this;
54
+ var _b = _a.className, className = _b === void 0 ? "" : _b, onFileDeleted = _a.onFileDeleted, onError = _a.onError, _c = _a.showUploadButton, showUploadButton = _c === void 0 ? true : _c, _d = _a.maxFileSize, maxFileSize = _d === void 0 ? 5 : _d;
55
+ var _e = useTrainly(), listFiles = _e.listFiles, deleteFile = _e.deleteFile, upload = _e.upload, isLoading = _e.isLoading, error = _e.error;
56
+ var _f = React.useState([]), files = _f[0], setFiles = _f[1];
57
+ var _g = React.useState(false), isLoadingFiles = _g[0], setIsLoadingFiles = _g[1];
58
+ var _h = React.useState(null), isDeletingFile = _h[0], setIsDeletingFile = _h[1];
59
+ var _j = React.useState(false), isUploading = _j[0], setIsUploading = _j[1];
60
+ var fileInputRef = React.useRef(null);
61
+ // Load files on component mount
62
+ React.useEffect(function () {
63
+ loadFiles();
64
+ }, []);
65
+ var loadFiles = function () { return __awaiter(_this, void 0, void 0, function () {
66
+ var result, err_1, error_1;
67
+ return __generator(this, function (_a) {
68
+ switch (_a.label) {
69
+ case 0:
70
+ _a.trys.push([0, 2, 3, 4]);
71
+ setIsLoadingFiles(true);
72
+ return [4 /*yield*/, listFiles()];
73
+ case 1:
74
+ result = _a.sent();
75
+ setFiles(result.files);
76
+ return [3 /*break*/, 4];
77
+ case 2:
78
+ err_1 = _a.sent();
79
+ error_1 = err_1 instanceof Error ? err_1 : new Error(String(err_1));
80
+ console.error("Failed to load files:", error_1);
81
+ onError === null || onError === void 0 ? void 0 : onError(error_1);
82
+ return [3 /*break*/, 4];
83
+ case 3:
84
+ setIsLoadingFiles(false);
85
+ return [7 /*endfinally*/];
86
+ case 4: return [2 /*return*/];
87
+ }
88
+ });
89
+ }); };
90
+ var handleDeleteFile = function (fileId, filename) { return __awaiter(_this, void 0, void 0, function () {
91
+ var result, err_2, error_2;
92
+ return __generator(this, function (_a) {
93
+ switch (_a.label) {
94
+ case 0:
95
+ if (!confirm("Are you sure you want to delete \"".concat(filename, "\"? This action cannot be undone."))) {
96
+ return [2 /*return*/];
97
+ }
98
+ _a.label = 1;
99
+ case 1:
100
+ _a.trys.push([1, 3, 4, 5]);
101
+ setIsDeletingFile(fileId);
102
+ return [4 /*yield*/, deleteFile(fileId)];
103
+ case 2:
104
+ result = _a.sent();
105
+ // Remove file from local state
106
+ setFiles(function (prev) { return prev.filter(function (f) { return f.file_id !== fileId; }); });
107
+ // Notify parent component
108
+ onFileDeleted === null || onFileDeleted === void 0 ? void 0 : onFileDeleted(fileId, filename);
109
+ console.log("File deleted: ".concat(result.message));
110
+ return [3 /*break*/, 5];
111
+ case 3:
112
+ err_2 = _a.sent();
113
+ error_2 = err_2 instanceof Error ? err_2 : new Error(String(err_2));
114
+ console.error("Failed to delete file:", error_2);
115
+ onError === null || onError === void 0 ? void 0 : onError(error_2);
116
+ return [3 /*break*/, 5];
117
+ case 4:
118
+ setIsDeletingFile(null);
119
+ return [7 /*endfinally*/];
120
+ case 5: return [2 /*return*/];
121
+ }
122
+ });
123
+ }); };
124
+ var handleFileUpload = function (event) { return __awaiter(_this, void 0, void 0, function () {
125
+ var file, error_3, result, err_3, error_4;
126
+ var _a;
127
+ return __generator(this, function (_b) {
128
+ switch (_b.label) {
129
+ case 0:
130
+ file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
131
+ if (!file)
132
+ return [2 /*return*/];
133
+ // Check file size
134
+ if (file.size > maxFileSize * 1024 * 1024) {
135
+ error_3 = new Error("File size must be less than ".concat(maxFileSize, "MB"));
136
+ onError === null || onError === void 0 ? void 0 : onError(error_3);
137
+ return [2 /*return*/];
138
+ }
139
+ _b.label = 1;
140
+ case 1:
141
+ _b.trys.push([1, 5, 6, 7]);
142
+ setIsUploading(true);
143
+ return [4 /*yield*/, upload(file)];
144
+ case 2:
145
+ result = _b.sent();
146
+ if (!result.success) return [3 /*break*/, 4];
147
+ // Reload files to show the new upload
148
+ return [4 /*yield*/, loadFiles()];
149
+ case 3:
150
+ // Reload files to show the new upload
151
+ _b.sent();
152
+ console.log("File uploaded: ".concat(result.filename));
153
+ _b.label = 4;
154
+ case 4: return [3 /*break*/, 7];
155
+ case 5:
156
+ err_3 = _b.sent();
157
+ error_4 = err_3 instanceof Error ? err_3 : new Error(String(err_3));
158
+ console.error("Failed to upload file:", error_4);
159
+ onError === null || onError === void 0 ? void 0 : onError(error_4);
160
+ return [3 /*break*/, 7];
161
+ case 6:
162
+ setIsUploading(false);
163
+ // Clear the input
164
+ if (fileInputRef.current) {
165
+ fileInputRef.current.value = "";
166
+ }
167
+ return [7 /*endfinally*/];
168
+ case 7: return [2 /*return*/];
169
+ }
170
+ });
171
+ }); };
172
+ var formatFileSize = function (bytes) {
173
+ if (bytes === 0)
174
+ return "0 Bytes";
175
+ var k = 1024;
176
+ var sizes = ["Bytes", "KB", "MB", "GB"];
177
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
178
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
179
+ };
180
+ var formatDate = function (dateString) {
181
+ try {
182
+ var date = new Date(parseInt(dateString));
183
+ return date.toLocaleDateString() + " " + date.toLocaleTimeString();
184
+ }
185
+ catch (_a) {
186
+ return dateString;
187
+ }
188
+ };
189
+ var totalSize = files.reduce(function (sum, file) { return sum + file.size_bytes; }, 0);
190
+ var styles = {
191
+ container: {
192
+ border: "1px solid #e5e7eb",
193
+ borderRadius: "8px",
194
+ padding: "16px",
195
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif",
196
+ },
197
+ header: {
198
+ display: "flex",
199
+ justifyContent: "space-between",
200
+ alignItems: "flex-start",
201
+ marginBottom: "16px",
202
+ },
203
+ title: {
204
+ margin: "0 0 4px 0",
205
+ fontSize: "18px",
206
+ fontWeight: "600",
207
+ color: "#1f2937",
208
+ },
209
+ totalSize: {
210
+ margin: "0",
211
+ fontSize: "14px",
212
+ color: "#6b7280",
213
+ },
214
+ uploadButton: {
215
+ backgroundColor: "#3b82f6",
216
+ color: "white",
217
+ border: "none",
218
+ borderRadius: "6px",
219
+ padding: "8px 16px",
220
+ fontSize: "14px",
221
+ cursor: "pointer",
222
+ transition: "background-color 0.2s",
223
+ },
224
+ uploadButtonDisabled: {
225
+ backgroundColor: "#9ca3af",
226
+ cursor: "not-allowed",
227
+ },
228
+ error: {
229
+ backgroundColor: "#fef2f2",
230
+ color: "#dc2626",
231
+ padding: "12px",
232
+ borderRadius: "6px",
233
+ marginBottom: "16px",
234
+ fontSize: "14px",
235
+ },
236
+ loading: {
237
+ textAlign: "center",
238
+ padding: "32px",
239
+ color: "#6b7280",
240
+ fontSize: "14px",
241
+ },
242
+ emptyState: {
243
+ textAlign: "center",
244
+ padding: "32px",
245
+ color: "#6b7280",
246
+ },
247
+ emptyStateText: {
248
+ margin: "0 0 8px 0",
249
+ fontSize: "14px",
250
+ },
251
+ fileItem: {
252
+ display: "flex",
253
+ justifyContent: "space-between",
254
+ alignItems: "center",
255
+ padding: "12px",
256
+ border: "1px solid #f3f4f6",
257
+ borderRadius: "6px",
258
+ backgroundColor: "#f9fafb",
259
+ marginBottom: "8px",
260
+ transition: "background-color 0.2s",
261
+ },
262
+ fileName: {
263
+ fontWeight: "500",
264
+ color: "#1f2937",
265
+ fontSize: "14px",
266
+ marginBottom: "4px",
267
+ },
268
+ fileMeta: {
269
+ fontSize: "12px",
270
+ color: "#6b7280",
271
+ },
272
+ deleteButton: {
273
+ backgroundColor: "#dc2626",
274
+ color: "white",
275
+ border: "none",
276
+ borderRadius: "4px",
277
+ padding: "6px 12px",
278
+ fontSize: "12px",
279
+ cursor: "pointer",
280
+ transition: "background-color 0.2s",
281
+ },
282
+ deleteButtonDisabled: {
283
+ backgroundColor: "#9ca3af",
284
+ cursor: "not-allowed",
285
+ },
286
+ };
287
+ return (_jsxs("div", { className: className, style: styles.container, children: [_jsxs("div", { style: styles.header, children: [_jsxs("div", { children: [_jsxs("h3", { style: styles.title, children: ["Your Files (", files.length, ")"] }), _jsxs("p", { style: styles.totalSize, children: ["Total: ", formatFileSize(totalSize)] })] }), showUploadButton && (_jsxs("div", { children: [_jsx("input", { ref: fileInputRef, type: "file", onChange: handleFileUpload, disabled: isUploading || isLoading, accept: ".pdf,.docx,.txt,.md,.csv,.json,.html,.xml,.yaml,.yml,.js,.py,.java,.cpp,.c,.h,.cs,.php,.rb,.sh,.bat,.ps1", style: { display: "none" } }), _jsx("button", { onClick: function () { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, disabled: isUploading || isLoading, style: __assign(__assign({}, styles.uploadButton), (isUploading || isLoading
288
+ ? styles.uploadButtonDisabled
289
+ : {})), children: isUploading ? "Uploading..." : "Upload File" })] }))] }), error && _jsx("div", { style: styles.error, children: error.message }), isLoadingFiles ? (_jsx("div", { style: styles.loading, children: "Loading files..." })) : files.length === 0 ? (_jsxs("div", { style: styles.emptyState, children: [_jsx("p", { style: styles.emptyStateText, children: "No files uploaded yet." }), showUploadButton && (_jsx("p", { style: styles.emptyStateText, children: "Click \"Upload File\" to add your first document." }))] })) : (_jsx("div", { children: files.map(function (file) { return (_jsxs("div", { style: styles.fileItem, onMouseEnter: function (e) {
290
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
291
+ }, onMouseLeave: function (e) {
292
+ e.currentTarget.style.backgroundColor = "#f9fafb";
293
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: styles.fileName, children: file.filename }), _jsxs("div", { style: styles.fileMeta, children: [formatFileSize(file.size_bytes), " \u2022 ", file.chunk_count, " chunks \u2022 ", formatDate(file.upload_date)] })] }), _jsx("div", { children: _jsx("button", { onClick: function () { return handleDeleteFile(file.file_id, file.filename); }, disabled: isDeletingFile === file.file_id || isLoading, style: __assign(__assign({}, styles.deleteButton), (isDeletingFile === file.file_id || isLoading
294
+ ? styles.deleteButtonDisabled
295
+ : {})), onMouseEnter: function (e) {
296
+ if (!e.currentTarget.disabled) {
297
+ e.currentTarget.style.backgroundColor = "#b91c1c";
298
+ }
299
+ }, onMouseLeave: function (e) {
300
+ if (!e.currentTarget.disabled) {
301
+ e.currentTarget.style.backgroundColor = "#dc2626";
302
+ }
303
+ }, children: isDeletingFile === file.file_id ? "Deleting..." : "Delete" }) })] }, file.file_id)); }) }))] }));
304
+ }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export { useTrainly } from "./useTrainly";
3
3
  export { TrainlyChat } from "./components/TrainlyChat";
4
4
  export { TrainlyUpload } from "./components/TrainlyUpload";
5
5
  export { TrainlyStatus } from "./components/TrainlyStatus";
6
- export type { TrainlyProviderProps, TrainlyConfig, ChatMessage, Citation, UploadResult, TrainlyError, } from "./types";
6
+ export { TrainlyFileManager } from "./components/TrainlyFileManager";
7
+ export type { TrainlyProviderProps, TrainlyConfig, ChatMessage, Citation, UploadResult, FileInfo, FileListResult, FileDeleteResult, TrainlyError, TrainlyFileManagerProps, } from "./types";
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export { useTrainly } from "./useTrainly";
5
5
  export { TrainlyChat } from "./components/TrainlyChat";
6
6
  export { TrainlyUpload } from "./components/TrainlyUpload";
7
7
  export { TrainlyStatus } from "./components/TrainlyStatus";
8
+ export { TrainlyFileManager } from "./components/TrainlyFileManager";
package/dist/types.d.ts CHANGED
@@ -34,11 +34,39 @@ export interface UploadResult {
34
34
  size: number;
35
35
  message?: string;
36
36
  }
37
+ export interface FileInfo {
38
+ file_id: string;
39
+ filename: string;
40
+ upload_date: string;
41
+ size_bytes: number;
42
+ chunk_count: number;
43
+ }
44
+ export interface FileListResult {
45
+ success: boolean;
46
+ files: FileInfo[];
47
+ total_files: number;
48
+ total_size_bytes: number;
49
+ }
50
+ export interface FileDeleteResult {
51
+ success: boolean;
52
+ message: string;
53
+ file_id: string;
54
+ filename: string;
55
+ chunks_deleted: number;
56
+ size_bytes_freed: number;
57
+ }
37
58
  export interface TrainlyError {
38
59
  code: string;
39
60
  message: string;
40
61
  details?: any;
41
62
  }
63
+ export interface TrainlyFileManagerProps {
64
+ className?: string;
65
+ onFileDeleted?: (fileId: string, filename: string) => void;
66
+ onError?: (error: Error) => void;
67
+ showUploadButton?: boolean;
68
+ maxFileSize?: number;
69
+ }
42
70
  export interface TrainlyContextValue {
43
71
  ask: (question: string) => Promise<string>;
44
72
  askWithCitations: (question: string) => Promise<{
@@ -46,6 +74,8 @@ export interface TrainlyContextValue {
46
74
  citations: Citation[];
47
75
  }>;
48
76
  upload: (file: File) => Promise<UploadResult>;
77
+ listFiles: () => Promise<FileListResult>;
78
+ deleteFile: (fileId: string) => Promise<FileDeleteResult>;
49
79
  connectWithOAuthToken: (idToken: string) => Promise<void>;
50
80
  isLoading: boolean;
51
81
  isConnected: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainly/react",
3
- "version": "1.1.2",
3
+ "version": "1.2.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",
@@ -25,7 +25,10 @@
25
25
  "auth0",
26
26
  "retrieval-augmented-generation",
27
27
  "semantic-search",
28
- "document-chat"
28
+ "document-chat",
29
+ "file-management",
30
+ "file-upload",
31
+ "file-deletion"
29
32
  ],
30
33
  "author": "Trainly",
31
34
  "license": "MIT",