@trainly/react
Version:
Dead simple RAG integration for React apps with OAuth authentication and custom scopes
1,073 lines (854 loc) • 28.1 kB
Markdown
# /react
**Dead simple RAG integration for React apps with V1 OAuth Authentication**
Go from `npm install` to working AI in under 5 minutes. Now supports direct OAuth integration with **permanent user subchats** and complete privacy protection.
## 🆕 **NEW: V1 Trusted Issuer Authentication**
Use your existing OAuth provider (Clerk, Auth0, Cognito) directly with Trainly! Users get permanent private workspaces, and developers never see raw files or queries.
### V1 Quick Start
#### 1. Install
```bash
npm install /react
```
#### 2. Register Your OAuth App (One-time)
```bash
curl -X POST "http://localhost:8000/v1/console/apps/register" \
-H "X-Admin-Token: admin_dev_token_123" \
-F "app_name=My App" \
-F "issuer=https://clerk.myapp.com" \
-F 'allowed_audiences=["my-clerk-frontend-api"]'
```
Save the `app_id` from the response!
#### 3. Setup with V1 (Clerk Example)
```tsx
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
import { TrainlyProvider } from "@trainly/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
<ClerkProvider>
<TrainlyProvider appId="your_app_id_from_step_2">
{children}
</TrainlyProvider>
</ClerkProvider>
</body>
</html>
);
}
```
#### 4. Use with OAuth Authentication
```tsx
// Any component
import { useAuth } from "@clerk/nextjs";
import { useTrainly } from "@trainly/react";
function MyComponent() {
const { getToken } = useAuth();
const { ask, connectWithOAuthToken } = useTrainly();
React.useEffect(() => {
async function setupTrainly() {
const idToken = await getToken();
await connectWithOAuthToken(idToken);
}
setupTrainly();
}, []);
const handleClick = async () => {
const answer = await ask("What files do I have?");
console.log(answer); // AI response from user's permanent private subchat!
};
return <button onClick={handleClick}>Ask My AI</button>;
}
```
## 🔒 **V1 Benefits**
- ✅ **Permanent User Data**: Same user = same private subchat forever
- ✅ **Complete Privacy**: Developer never sees user files or queries
- ✅ **Any OAuth Provider**: Clerk, Auth0, Cognito, Firebase, custom OIDC
- ✅ **Zero Migration**: Works with your existing OAuth setup
- ✅ **Simple Integration**: Just add `appId` and use `connectWithOAuthToken()`
## 🏷️ **NEW: Custom Scopes (Zero Config)**
Tag your documents with custom attributes for powerful filtering and organization:
```tsx
import { useTrainly } from "@trainly/react";
function MyApp() {
const { upload, ask } = useTrainly();
// 1. Upload with scopes - use any keys you want!
await upload(file, {
playlist_id: "xyz123",
workspace_id: "acme_corp",
project: "alpha",
});
// 2. Query with scope filters - only get results from matching documents
const answer = await ask("What are the key features?", {
scope_filters: { playlist_id: "xyz123" },
});
// ☝️ Only searches documents with playlist_id="xyz123"
// Query with multiple filters
const answer2 = await ask("Show me updates", {
scope_filters: {
workspace_id: "acme_corp",
project: "alpha",
},
});
// ☝️ Only searches documents matching ALL specified scopes
// Query everything (no filters)
const answer3 = await ask("What do I have?");
// ☝️ Searches ALL user's documents
}
```
**No setup required!** Just pass any key-value pairs - perfect for multi-tenant apps, playlist systems, workspace organization, and more.
**Use Cases:**
- 🎵 **Playlist Apps**: Filter by `playlist_id` to query specific playlists
- 🏢 **Multi-Tenant SaaS**: Filter by `tenant_id` or `workspace_id`
- 📁 **Project Management**: Filter by `project_id` or `team_id`
- 👥 **User Segmentation**: Filter by `user_tier`, `department`, etc.
[📖 Full Scopes Guide](./SCOPES_GUIDE.md)
## 📁 **File Management**
Users can manage their uploaded files directly:
```tsx
import { useTrainly, TrainlyFileManager } from "@trainly/react";
function MyApp() {
const { listFiles, deleteFile, upload } = useTrainly();
// List all user's files
const handleListFiles = async () => {
const result = await listFiles();
console.log(
`${result.total_files} files, ${result.total_size_bytes} bytes total`,
);
result.files.forEach((file) => {
console.log(
`${file.filename}: ${file.size_bytes} bytes, ${file.chunk_count} chunks`,
);
});
};
// Delete a specific file
const handleDeleteFile = async (fileId) => {
const result = await deleteFile(fileId);
console.log(
`Deleted ${result.filename}, freed ${result.size_bytes_freed} bytes`,
);
};
return (
<div>
<button onClick={handleListFiles}>List My Files</button>
{/* Pre-built file manager component */}
<TrainlyFileManager
onFileDeleted={(fileId, filename) => {
console.log(`File deleted: ${filename}`);
}}
onError={(error) => {
console.error("File operation failed:", error);
}}
showUploadButton={true}
maxFileSize={5} // MB
/>
</div>
);
}
```
### File Management Features
- 📋 **List Files**: View all uploaded documents with metadata
- 🗑️ **Delete Files**: Remove files and free up storage space
- 📊 **Storage Analytics**: Track file sizes and storage usage
## 🏷️ **NEW in v1.4.0: Custom Scopes**
Tag your documents with custom attributes for powerful data segmentation!
```tsx
import { useTrainly } from "@trainly/react";
function PlaylistUploader({ playlistId }) {
const { upload } = useTrainly();
const handleUpload = async (file: File) => {
// Upload with custom scope values
await upload(file, {
playlist_id: playlistId,
user_id: currentUser.id,
is_public: false,
});
};
return (
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
);
}
```
### Scope Features
- 🎯 **Data Segmentation**: Keep playlists, workspaces, or projects separate
- ⚡ **Faster Queries**: Filter at database level before vector search
- 🔒 **Complete Isolation**: Multi-tenant apps with full data privacy
- 🎨 **Flexible**: Define any custom attributes you need
### With TrainlyUpload Component
```tsx
<TrainlyUpload
variant="drag-drop"
scopeValues={{
playlist_id: "playlist_123",
workspace_id: "workspace_456",
}}
onUpload={(files) => console.log("Uploaded with scopes!")}
/>
```
### Complete Documentation
See **[SCOPES_GUIDE.md](./SCOPES_GUIDE.md)** for:
- Complete API reference
- Real-world examples
- Advanced patterns
- Testing & debugging
- Migration guide
**Quick Reference:**
- `upload(file, scopeValues)` - Upload with scopes
- `bulkUploadFiles(files, scopeValues)` - Bulk upload with scopes
- `<TrainlyUpload scopeValues={{...}} />` - Component with scopes
- 🔄 **Auto-Refresh**: File list updates after uploads/deletions
- 🎨 **Pre-built UI**: `TrainlyFileManager` component with styling
- 🔒 **Privacy-First**: Only works in V1 mode with OAuth authentication
## 📚 **Detailed File Management Documentation**
### **1. Listing Files**
Get all files uploaded to the user's permanent subchat:
```tsx
import { useTrainly } from "@trainly/react";
function FileList() {
const { listFiles } = useTrainly();
const handleListFiles = async () => {
try {
const result = await listFiles();
console.log(`Total files: ${result.total_files}`);
console.log(`Total storage: ${formatBytes(result.total_size_bytes)}`);
result.files.forEach((file) => {
console.log(`📄 ${file.filename}`);
console.log(` Size: ${formatBytes(file.size_bytes)}`);
console.log(` Chunks: ${file.chunk_count}`);
console.log(
` Uploaded: ${new Date(parseInt(file.upload_date)).toLocaleDateString()}`,
);
console.log(` ID: ${file.file_id}`);
});
} catch (error) {
console.error("Failed to list files:", error);
}
};
return <button onClick={handleListFiles}>List My Files</button>;
}
```
**Response Structure:**
```typescript
interface FileListResult {
success: boolean;
files: FileInfo[];
total_files: number;
total_size_bytes: number;
}
interface FileInfo {
file_id: string; // Unique identifier for deletion
filename: string; // Original filename
upload_date: string; // Unix timestamp (milliseconds)
size_bytes: number; // File size in bytes
chunk_count: number; // Number of text chunks created
}
```
### **2. Bulk Upload Files**
Upload multiple files at once (up to 10 files per request):
```tsx
import { useTrainly } from "@trainly/react";
function BulkFileUpload() {
const { bulkUploadFiles } = useTrainly();
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
if (files.length > 10) {
alert("Maximum 10 files allowed per bulk upload");
return;
}
setSelectedFiles(files);
};
const handleBulkUpload = async () => {
if (selectedFiles.length === 0) return;
try {
const result = await bulkUploadFiles(selectedFiles);
console.log(`Bulk upload completed: ${result.message}`);
console.log(
`Successful: ${result.successful_uploads}/${result.total_files}`,
);
console.log(`Total size: ${formatBytes(result.total_size_bytes)}`);
// Review individual file results
result.results.forEach((fileResult) => {
if (fileResult.success) {
console.log(`✅ ${fileResult.filename} - ${fileResult.message}`);
} else {
console.log(`❌ ${fileResult.filename} - ${fileResult.error}`);
}
});
// Clear selection after successful upload
setSelectedFiles([]);
} catch (error) {
console.error("Bulk upload failed:", error);
}
};
return (
<div>
<input
type="file"
multiple
accept=".pdf,.txt,.docx"
onChange={handleFileSelect}
/>
{selectedFiles.length > 0 && (
<div>
<p>Selected files: {selectedFiles.length}</p>
<ul>
{selectedFiles.map((file, index) => (
<li key={index}>
{file.name} ({formatBytes(file.size)})
</li>
))}
</ul>
<button onClick={handleBulkUpload}>
Upload {selectedFiles.length} Files
</button>
</div>
)}
</div>
);
}
// Helper function for formatting file sizes
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
```
**Bulk Upload Features:**
- ✅ **Efficient**: Upload up to 10 files in a single API call
- ✅ **Detailed Results**: Individual success/failure status for each file
- ✅ **Error Resilience**: Partial failures don't stop other files
- ✅ **Progress Tracking**: Total size and success metrics
- ✅ **Automatic Retry**: Token refresh handling built-in
### **3. Deleting Files**
Remove a specific file and free up storage space:
```tsx
import { useTrainly } from "@trainly/react";
function FileDeleter() {
const { deleteFile, listFiles } = useTrainly();
const handleDeleteFile = async (fileId: string, filename: string) => {
// Always confirm before deletion
const confirmed = confirm(
`Delete "${filename}"? This will permanently remove the file and cannot be undone.`,
);
if (!confirmed) return;
try {
const result = await deleteFile(fileId);
console.log(`✅ ${result.message}`);
console.log(`🗑️ Deleted: ${result.filename}`);
console.log(`💾 Storage freed: ${formatBytes(result.size_bytes_freed)}`);
console.log(`📊 Chunks removed: ${result.chunks_deleted}`);
// Optionally refresh file list
await listFiles();
} catch (error) {
console.error("Failed to delete file:", error);
alert(`Failed to delete file: ${error.message}`);
}
};
// Example: Delete first file
const deleteFirstFile = async () => {
const files = await listFiles();
if (files.files.length > 0) {
const firstFile = files.files[0];
await handleDeleteFile(firstFile.file_id, firstFile.filename);
}
};
return <button onClick={deleteFirstFile}>Delete First File</button>;
}
```
**Response Structure:**
```typescript
interface FileDeleteResult {
success: boolean;
message: string; // Human-readable success message
file_id: string; // ID of deleted file
filename: string; // Name of deleted file
chunks_deleted: number; // Number of chunks removed
size_bytes_freed: number; // Storage space freed up
}
```
### **4. Pre-built File Manager Component**
Use the ready-made component for complete file management:
```tsx
import { TrainlyFileManager } from "@trainly/react";
function MyApp() {
return (
<TrainlyFileManager
// Optional: Custom CSS class
className="my-custom-styles"
// Callback when file is deleted
onFileDeleted={(fileId, filename) => {
console.log(`File deleted: ${filename} (ID: ${fileId})`);
// Update your app state, show notification, etc.
}}
// Error handling callback
onError={(error) => {
console.error("File operation failed:", error);
// Show user-friendly error message
alert(`Error: ${error.message}`);
}}
// Show upload button in component
showUploadButton={true}
// Maximum file size in MB
maxFileSize={5}
/>
);
}
```
**Component Features:**
- 📋 **File List**: Shows all files with metadata
- 🔄 **Auto-Refresh**: Updates after uploads/deletions
- ⚠️ **Confirmation**: Asks before deleting files
- 📊 **Storage Stats**: Shows total files and storage used
- 🎨 **Styled**: Clean, professional appearance
- 📱 **Responsive**: Works on mobile and desktop
### **5. Complete Integration Example**
Here's a full example showing all file operations together:
```tsx
import React from "react";
import { useAuth } from "@clerk/nextjs"; // or your OAuth provider
import { useTrainly, TrainlyFileManager } from "@trainly/react";
export function CompleteFileExample() {
const { getToken } = useAuth();
const {
ask,
upload,
listFiles,
deleteFile,
connectWithOAuthToken,
isConnected,
} = useTrainly();
const [files, setFiles] = React.useState([]);
const [storageUsed, setStorageUsed] = React.useState(0);
// Connect to Trainly on mount
React.useEffect(() => {
async function connect() {
const token = await getToken();
if (token) {
await connectWithOAuthToken(token);
}
}
connect();
}, []);
// Load files when connected
React.useEffect(() => {
if (isConnected) {
refreshFiles();
}
}, [isConnected]);
const refreshFiles = async () => {
try {
const result = await listFiles();
setFiles(result.files);
setStorageUsed(result.total_size_bytes);
} catch (error) {
console.error("Failed to load files:", error);
}
};
const handleBulkDelete = async () => {
if (files.length === 0) {
alert("No files to delete");
return;
}
const confirmed = confirm(
`Delete ALL ${files.length} files? This cannot be undone.`,
);
if (!confirmed) return;
let deletedCount = 0;
let totalFreed = 0;
for (const file of files) {
try {
const result = await deleteFile(file.file_id);
deletedCount++;
totalFreed += result.size_bytes_freed;
console.log(`Deleted: ${result.filename}`);
} catch (error) {
console.error(`Failed to delete ${file.filename}:`, error);
}
}
alert(`Deleted ${deletedCount} files, freed ${formatBytes(totalFreed)}`);
await refreshFiles();
};
const formatBytes = (bytes) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
};
if (!isConnected) {
return <div>Connecting to Trainly...</div>;
}
return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "20px" }}>
<h1>📁 My File Workspace</h1>
{/* Storage Overview */}
<div
style={{
background: "#f8fafc",
padding: "20px",
borderRadius: "8px",
marginBottom: "20px",
}}
>
<h3>Storage Overview</h3>
<p>
<strong>{files.length} files</strong> using{" "}
<strong>{formatBytes(storageUsed)}</strong>
</p>
<div style={{ display: "flex", gap: "10px", marginTop: "10px" }}>
<button onClick={refreshFiles}>🔄 Refresh</button>
<button
onClick={handleBulkDelete}
disabled={files.length === 0}
style={{ background: "#dc2626", color: "white" }}
>
🗑️ Delete All Files
</button>
</div>
</div>
{/* File Manager Component */}
<TrainlyFileManager
onFileDeleted={(fileId, filename) => {
console.log(`File deleted: ${filename}`);
// Update local state
setFiles((prev) => prev.filter((f) => f.file_id !== fileId));
refreshFiles(); // Refresh to get accurate totals
}}
onError={(error) => {
alert(`Error: ${error.message}`);
}}
showUploadButton={true}
maxFileSize={5}
/>
{/* AI Integration */}
<div
style={{
marginTop: "30px",
padding: "20px",
background: "#f0f9ff",
borderRadius: "8px",
}}
>
<h3>🤖 Ask AI About Your Files</h3>
<button
onClick={async () => {
const answer = await ask(
"What files do I have? Give me a summary of each.",
);
alert(`AI Response:\n\n${answer}`);
}}
style={{ background: "#059669", color: "white" }}
>
Get File Summary from AI
</button>
</div>
</div>
);
}
```
### **6. Error Handling Best Practices**
```tsx
import { useTrainly } from "@trainly/react";
function RobustFileManager() {
const { deleteFile, listFiles } = useTrainly();
const safeDeleteFile = async (fileId: string, filename: string) => {
try {
// 1. Confirm with user
const confirmed = confirm(`Delete "${filename}"?`);
if (!confirmed) return;
// 2. Attempt deletion
const result = await deleteFile(fileId);
// 3. Success feedback
console.log(`✅ Success: ${result.message}`);
return result;
} catch (error) {
// 4. Handle specific error types
if (error.message.includes("404")) {
alert("File not found - it may have already been deleted");
} else if (error.message.includes("401")) {
alert("Authentication expired - please refresh the page");
} else {
alert(`Failed to delete file: ${error.message}`);
}
console.error("Delete error:", error);
throw error;
}
};
const safeListFiles = async () => {
try {
return await listFiles();
} catch (error) {
console.error("List files error:", error);
if (error.message.includes("V1 mode")) {
alert("File management requires V1 OAuth authentication");
} else {
alert(`Failed to load files: ${error.message}`);
}
return { success: false, files: [], total_files: 0, total_size_bytes: 0 };
}
};
return (
<div>
<button onClick={() => safeListFiles()}>Safe List Files</button>
<button onClick={() => safeDeleteFile("file_123", "example.pdf")}>
Safe Delete Example
</button>
</div>
);
}
```
### **7. TypeScript Support**
Full TypeScript definitions included:
```typescript
// Import types for better development experience
import type {
FileInfo,
FileListResult,
FileDeleteResult,
TrainlyFileManagerProps,
} from "@trainly/react";
// Type-safe file operations
const handleTypedFileOps = async () => {
const fileList: FileListResult = await listFiles();
const deleteResult: FileDeleteResult = await deleteFile("file_123");
// Full IntelliSense support
console.log(deleteResult.size_bytes_freed);
console.log(fileList.total_size_bytes);
};
```
### **8. Security & Privacy Notes**
- 🔒 **V1 Only**: File management only works with V1 Trusted Issuer authentication
- 👤 **User Isolation**: Users can only see and delete their own files
- 🛡️ **No Raw Access**: Developers never see file content, only AI responses
- 📊 **Privacy-Safe Analytics**: Storage tracking without exposing user data
- ⚠️ **Permanent Deletion**: Deleted files cannot be recovered
- 🔐 **OAuth Required**: Must be authenticated with valid OAuth token
### **9. Storage Management**
File operations automatically update storage analytics:
```tsx
// Storage is tracked automatically
const result = await deleteFile(fileId);
console.log(`Freed ${result.size_bytes_freed} bytes`);
// Check total storage
const files = await listFiles();
console.log(`Using ${files.total_size_bytes} bytes total`);
// Parent app analytics are updated automatically
// (visible in Trainly dashboard for developers)
```
---
## 🚀 Original Quick Start (Legacy)
### 1. Install
```bash
npm install /react
```
### 2. Setup (2 lines)
```tsx
// app/layout.tsx
import { TrainlyProvider } from "@trainly/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
<TrainlyProvider appSecret="as_your_app_secret">
{children}
</TrainlyProvider>
</body>
</html>
);
}
```
### 3. Use anywhere (3 lines)
```tsx
// Any component
import { useTrainly } from "@trainly/react";
function MyComponent() {
const { ask } = useTrainly();
const handleClick = async () => {
const answer = await ask("What is photosynthesis?");
console.log(answer); // Ready to use!
};
return <button onClick={handleClick}>Ask AI</button>;
}
```
**That's it!** No auth setup, no API routes, no session management.
## 📦 What's Included
### Core Hook
```tsx
const {
ask, // (question: string) => Promise<string>
upload, // (file: File) => Promise<void>
isLoading, // boolean
isConnected, // boolean
error, // string | null
} = useTrainly();
```
### Pre-built Components
```tsx
import { TrainlyChat, TrainlyUpload, TrainlyStatus } from '@trainly/react';
// Drop-in chat interface
<TrainlyChat height="400px" showCitations={true} />
// Drop-in file upload
<TrainlyUpload accept=".pdf,.doc,.txt" />
// Connection status indicator
<TrainlyStatus />
```
## 🎯 Complete Example
```tsx
import { TrainlyProvider, TrainlyChat, TrainlyUpload } from "@trainly/react";
function App() {
return (
<TrainlyProvider appSecret="as_demo_secret_123">
<div>
<h1>My Document Assistant</h1>
{/* File upload area */}
<TrainlyUpload onUpload={(files) => console.log("Uploaded:", files)} />
{/* Chat interface */}
<TrainlyChat
height="500px"
placeholder="Ask about your documents..."
showCitations={true}
/>
</div>
</TrainlyProvider>
);
}
```
## 🔧 Configuration Options
### Authentication Modes
```tsx
// Mode 1: V1 Trusted Issuer (NEW - recommended for OAuth apps)
<TrainlyProvider appId="app_v1_12345" /> // Register via console API first
// Mode 2: App Secret (legacy - for multi-user apps)
<TrainlyProvider appSecret="as_secret_123" />
// Mode 3: With user context (legacy)
<TrainlyProvider
appSecret="as_secret_123"
userId="user_123"
userEmail="user@example.com"
/>
// Mode 4: Direct API key (legacy - simple apps)
<TrainlyProvider apiKey="tk_chat_id_key" />
```
### V1 OAuth Provider Examples
```tsx
// With Clerk
<TrainlyProvider
appId="app_v1_clerk_123"
baseUrl="https://api.trainly.com"
/>
// With Auth0
<TrainlyProvider
appId="app_v1_auth0_456"
baseUrl="https://api.trainly.com"
/>
// With AWS Cognito
<TrainlyProvider
appId="app_v1_cognito_789"
baseUrl="https://api.trainly.com"
/>
```
### Component Customization
```tsx
<TrainlyChat
height="600px"
theme="dark"
placeholder="Ask me anything..."
showCitations={true}
enableFileUpload={true}
onMessage={(msg) => console.log(msg)}
onError={(err) => console.error(err)}
/>
<TrainlyUpload
variant="drag-drop" // or "button" or "minimal"
accept=".pdf,.doc,.txt"
maxSize="10MB"
multiple={false}
onUpload={(files) => console.log(files)}
/>
```
## 🎨 Styling
Components use Tailwind classes by default but can be fully customized:
```tsx
<TrainlyChat
className="my-custom-chat"
height="400px"
/>
// Override with CSS
.my-custom-chat {
border: 2px solid blue;
border-radius: 12px;
}
```
## 📖 API Reference
### useTrainly()
The main hook for interacting with Trainly.
```tsx
const {
// Core functions
ask: (question: string) => Promise<string>,
askWithCitations: (question: string) => Promise<{answer: string, citations: Citation[]}>,
upload: (file: File) => Promise<UploadResult>,
// NEW: V1 Authentication
connectWithOAuthToken: (idToken: string) => Promise<void>,
// State
isLoading: boolean,
isConnected: boolean,
error: TrainlyError | null,
// Advanced
clearError: () => void,
reconnect: () => Promise<void>,
// For chat components
messages: ChatMessage[],
sendMessage: (content: string) => Promise<void>,
clearMessages: () => void,
} = useTrainly();
```
### TrainlyProvider Props
```tsx
interface TrainlyProviderProps {
children: React.ReactNode;
appId?: string; // NEW: V1 app ID from console registration
appSecret?: string; // Legacy: App secret from Trainly dashboard
apiKey?: string; // Legacy: Direct API key (alternative to appSecret)
baseUrl?: string; // Custom API URL (defaults to trainly.com)
userId?: string; // Legacy: Your app's user ID
userEmail?: string; // Legacy: Your app's user email
}
```
## 🔍 Examples
See complete implementation examples in the [API Documentation](https://trainly.com/docs/v1-authentication).
## 🆚 **V1 vs Legacy Comparison**
| Feature | V1 Trusted Issuer | Legacy App Secret |
| -------------- | -------------------------------- | ------------------------- |
| **User Auth** | Your OAuth provider | Trainly OAuth flow |
| **User Data** | Permanent private subchat | Temporary or shared |
| **Privacy** | Complete (dev can't see files) | Limited |
| **Setup** | Register once, use OAuth tokens | Generate app secrets |
| **Migration** | Zero (uses existing OAuth) | Requires auth integration |
| **Permanence** | Same user = same subchat forever | Depends on implementation |
**Recommendation**: Use V1 for new apps and consider migrating existing apps for better privacy and user experience.
## 🛠️ Development
```bash
# Clone the repo
git clone https://github.com/trainly/react-sdk.git
cd react-sdk
# Install dependencies
npm install
# Build the package
npm run build
# Watch mode for development
npm run dev
```
## 📝 License
MIT - see LICENSE file for details.
## 🤝 Contributing
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
## 🆘 Support
- 📖 [Documentation](https://trainly.com/docs/react-sdk)
- 💬 [Discord Community](https://discord.gg/trainly)
- 📧 [Email Support](mailto:support.com)
- 🐛 [Report Issues](https://github.com/trainly/react-sdk/issues)
---
**Made with ❤️ by the Trainly team**
_The simplest way to add AI to your React app_