Pulastya B commited on
Commit
2a82ed5
Β·
1 Parent(s): 7af9e82

Added the functionality for users to save their generated assets in huggingface spaces using write tokens

Browse files
FILE_STORAGE_GUIDE.md CHANGED
@@ -1,252 +0,0 @@
1
- # File Storage Architecture - Implementation Guide
2
-
3
- ## Overview
4
-
5
- This document outlines the complete file storage architecture for persisting user files (plots, CSVs, reports, models) across sessions.
6
-
7
- ## Architecture
8
-
9
- ```
10
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
11
- β”‚ STORAGE ARCHITECTURE β”‚
12
- β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
13
- β”‚ β”‚
14
- β”‚ Frontend (React) β”‚
15
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
16
- β”‚ β”‚ β€’ PlotRenderer.tsx - Renders Plotly charts from JSON β”‚ β”‚
17
- β”‚ β”‚ β€’ Assets panel - Shows user files from Supabase β”‚ β”‚
18
- β”‚ β”‚ β€’ Download buttons - Uses presigned R2 URLs β”‚ β”‚
19
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
20
- β”‚ β”‚ β”‚
21
- β”‚ β–Ό β”‚
22
- β”‚ Backend (FastAPI) β”‚
23
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
24
- β”‚ β”‚ /api/files - List user files β”‚ β”‚
25
- β”‚ β”‚ /api/files/{id} - Get file with download URL β”‚ β”‚
26
- β”‚ β”‚ /api/files/stats/{user_id} - Storage statistics β”‚ β”‚
27
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
28
- β”‚ β”‚ β”‚
29
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
30
- β”‚ β–Ό β–Ό β”‚
31
- β”‚ Supabase (Metadata) Cloudflare R2 (Files) β”‚
32
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
33
- β”‚ β”‚ user_files β”‚ β”‚ /users/{user_id}/ β”‚ β”‚
34
- β”‚ β”‚ - id β”‚ ──────────► β”‚ /plots/*.json.gz β”‚ β”‚
35
- β”‚ β”‚ - user_id β”‚ β”‚ /data/*.csv.gz β”‚ β”‚
36
- β”‚ β”‚ - r2_key β”‚ β”‚ /reports/*.html β”‚ β”‚
37
- β”‚ β”‚ - expires_at β”‚ β”‚ /models/*.pkl.gz β”‚ β”‚
38
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
39
- β”‚ β”‚
40
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
41
- ```
42
-
43
- ## Setup Steps
44
-
45
- ### 1. Cloudflare R2 Setup
46
-
47
- 1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com)
48
- 2. Navigate to R2 β†’ Create Bucket β†’ Name it `ds-agent-files`
49
- 3. Go to R2 β†’ Manage R2 API Tokens β†’ Create API Token
50
- 4. Note down:
51
- - Account ID (from URL or overview page)
52
- - Access Key ID
53
- - Secret Access Key
54
-
55
- ### 2. Environment Variables
56
-
57
- Add to your `.env` file:
58
-
59
- ```bash
60
- # Cloudflare R2
61
- R2_ACCOUNT_ID=your_account_id
62
- R2_ACCESS_KEY_ID=your_access_key
63
- R2_SECRET_ACCESS_KEY=your_secret_key
64
- R2_BUCKET_NAME=ds-agent-files
65
- R2_PUBLIC_URL= # Optional: custom domain
66
-
67
- # Supabase (existing)
68
- SUPABASE_URL=your_supabase_url
69
- SUPABASE_SERVICE_KEY=your_service_key
70
- ```
71
-
72
- ### 3. Supabase Table
73
-
74
- Run this SQL in Supabase SQL Editor:
75
-
76
- ```sql
77
- CREATE TABLE user_files (
78
- id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
79
- user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
80
- session_id TEXT,
81
- file_type TEXT NOT NULL CHECK (file_type IN ('plot', 'csv', 'report', 'model')),
82
- file_name TEXT NOT NULL,
83
- r2_key TEXT NOT NULL UNIQUE,
84
- size_bytes BIGINT,
85
- mime_type TEXT,
86
- metadata JSONB DEFAULT '{}',
87
- created_at TIMESTAMPTZ DEFAULT NOW(),
88
- expires_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '7 days'),
89
- is_deleted BOOLEAN DEFAULT FALSE
90
- );
91
-
92
- -- Indexes
93
- CREATE INDEX idx_user_files_user_id ON user_files(user_id);
94
- CREATE INDEX idx_user_files_session ON user_files(session_id);
95
- CREATE INDEX idx_user_files_expires ON user_files(expires_at) WHERE NOT is_deleted;
96
-
97
- -- RLS Policies
98
- ALTER TABLE user_files ENABLE ROW LEVEL SECURITY;
99
-
100
- CREATE POLICY "Users can view own files" ON user_files
101
- FOR SELECT USING (auth.uid() = user_id);
102
-
103
- CREATE POLICY "Users can insert own files" ON user_files
104
- FOR INSERT WITH CHECK (auth.uid() = user_id);
105
-
106
- CREATE POLICY "Users can delete own files" ON user_files
107
- FOR DELETE USING (auth.uid() = user_id);
108
- ```
109
-
110
- ### 4. Python Dependencies
111
-
112
- Add to `requirements.txt`:
113
-
114
- ```
115
- boto3>=1.28.0
116
- ```
117
-
118
- ## Usage in Orchestrator
119
-
120
- When generating files in the orchestrator, save them to R2:
121
-
122
- ```python
123
- from src.storage.r2_storage import store_plotly_figure, store_dataframe_csv
124
- from src.storage.user_files_service import get_files_service, FileType
125
-
126
- # Store a Plotly figure
127
- def save_plot(user_id: str, session_id: str, fig, plot_name: str):
128
- r2_key, size = store_plotly_figure(user_id, fig, plot_name)
129
-
130
- # Record in Supabase
131
- files_service = get_files_service()
132
- files_service.create_file_record(
133
- user_id=user_id,
134
- file_type=FileType.PLOT,
135
- file_name=plot_name,
136
- r2_key=r2_key,
137
- size_bytes=size,
138
- session_id=session_id,
139
- mime_type='application/json',
140
- metadata={'plot_type': 'plotly'}
141
- )
142
-
143
- return r2_key
144
-
145
- # Store a CSV
146
- def save_csv(user_id: str, session_id: str, df, filename: str):
147
- r2_key, compressed_size, original_size = store_dataframe_csv(
148
- user_id, df, filename, "Processed dataset"
149
- )
150
-
151
- files_service = get_files_service()
152
- files_service.create_file_record(
153
- user_id=user_id,
154
- file_type=FileType.CSV,
155
- file_name=filename,
156
- r2_key=r2_key,
157
- size_bytes=compressed_size,
158
- session_id=session_id,
159
- mime_type='text/csv',
160
- metadata={
161
- 'original_size': original_size,
162
- 'compression_ratio': f"{(1 - compressed_size/original_size)*100:.1f}%"
163
- }
164
- )
165
-
166
- return r2_key
167
- ```
168
-
169
- ## Storage Efficiency
170
-
171
- ### Plot Storage (Before vs After)
172
-
173
- | Format | Size | Load Time |
174
- |--------|------|-----------|
175
- | Plotly HTML | 200KB - 2MB | 2-5 seconds |
176
- | Plotly JSON (gzip) | 5KB - 20KB | <0.5 seconds |
177
-
178
- **95% reduction in storage!**
179
-
180
- ### CSV Compression
181
-
182
- | Original Size | Compressed (gzip) | Ratio |
183
- |---------------|-------------------|-------|
184
- | 10MB | 1-2MB | 80-90% |
185
- | 100MB | 10-20MB | 80-90% |
186
- | 1GB | 100-200MB | 80-90% |
187
-
188
- ## Cleanup Strategy
189
-
190
- ### Automatic Expiration
191
-
192
- Files expire after 7 days by default. Run this cleanup job daily:
193
-
194
- ```python
195
- from src.storage.r2_storage import get_r2_service
196
- from src.storage.user_files_service import get_files_service
197
-
198
- def cleanup_expired_files():
199
- files_service = get_files_service()
200
- r2_service = get_r2_service()
201
-
202
- # Get expired files from Supabase
203
- expired = files_service.get_expired_files()
204
-
205
- for file in expired:
206
- # Delete from R2
207
- r2_service.delete_file(file.r2_key)
208
- # Delete from Supabase
209
- files_service.hard_delete_file(file.id)
210
-
211
- return len(expired)
212
- ```
213
-
214
- ### User Download Prompt
215
-
216
- When files are about to expire (1 day left), show a notification:
217
-
218
- ```typescript
219
- // Frontend
220
- const expiringFiles = files.filter(f =>
221
- new Date(f.expires_at) < new Date(Date.now() + 24 * 60 * 60 * 1000)
222
- );
223
-
224
- if (expiringFiles.length > 0) {
225
- showNotification(
226
- `${expiringFiles.length} files expiring soon! Download them now.`
227
- );
228
- }
229
- ```
230
-
231
- ## Cost Estimates
232
-
233
- ### Cloudflare R2 (10GB free, then $0.015/GB)
234
-
235
- | Users | Files/User | Avg Size | Total Storage | Monthly Cost |
236
- |-------|------------|----------|---------------|--------------|
237
- | 100 | 50 | 500KB | 2.5GB | FREE |
238
- | 1,000 | 50 | 500KB | 25GB | $0.23 |
239
- | 10,000 | 50 | 500KB | 250GB | $3.60 |
240
-
241
- **Zero egress fees = users can download unlimited files for free!**
242
-
243
- ## Next Steps
244
-
245
- 1. βœ… Created R2StorageService (`src/storage/r2_storage.py`)
246
- 2. βœ… Created UserFilesService (`src/storage/user_files_service.py`)
247
- 3. βœ… Added API endpoints to `app.py`
248
- 4. βœ… Created PlotRenderer component
249
- 5. ⏳ TODO: Integrate with orchestrator to save files during workflow
250
- 6. ⏳ TODO: Update frontend Assets panel to fetch from API
251
- 7. ⏳ TODO: Add expiration notifications
252
- 8. ⏳ TODO: Set up daily cleanup cron job
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FRRONTEEEND/components/ChatInterface.tsx CHANGED
@@ -1,13 +1,24 @@
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
- import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload, Package, FileText, BarChart3, ChevronRight, LogOut } from 'lucide-react';
5
  import { cn } from '../lib/utils';
6
  import { Logo } from './Logo';
7
  import ReactMarkdown from 'react-markdown';
8
  import remarkGfm from 'remark-gfm';
9
  import { useAuth } from '../lib/AuthContext';
10
- import { trackQuery, incrementSessionQueries } from '../lib/supabase';
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  interface Message {
13
  id: string;
@@ -192,6 +203,11 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
192
  const [reportModalUrl, setReportModalUrl] = useState<string | null>(null);
193
  const [reportModalTitle, setReportModalTitle] = useState<string>('Visualization');
194
  const [showAssets, setShowAssets] = useState(false);
 
 
 
 
 
195
  const fileInputRef = useRef<HTMLInputElement>(null);
196
  const scrollRef = useRef<HTMLDivElement>(null);
197
  const eventSourceRef = useRef<EventSource | null>(null);
@@ -202,6 +218,17 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
202
 
203
  const activeSession = sessions.find(s => s.id === activeSessionId) || sessions[0];
204
 
 
 
 
 
 
 
 
 
 
 
 
205
  // Persist sessions to localStorage whenever they change
206
  useEffect(() => {
207
  saveSessionsToStorage(sessions);
@@ -317,16 +344,22 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
317
  console.log('❌ Analysis failed', data);
318
  setIsTyping(false);
319
 
320
- // Show error message to user
321
- setMessages((prev) => [
322
- ...prev,
323
- {
324
- id: Date.now().toString(),
325
- role: 'assistant',
326
- content: data.message || data.error || '❌ Analysis failed',
327
- session_id: activeSessionId,
328
- },
329
- ]);
 
 
 
 
 
 
330
 
331
  setCurrentStep('');
332
  } else if (data.type === 'analysis_complete') {
@@ -876,7 +909,11 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
876
  <ArrowLeft className="w-5 h-5" />
877
  </button>
878
  <div className="flex gap-2">
879
- <button className="p-2 hover:bg-white/5 rounded-lg transition-colors text-white/40 hover:text-white">
 
 
 
 
880
  <Settings className="w-5 h-5" />
881
  </button>
882
  </div>
@@ -1222,19 +1259,107 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
1222
  transition={{ type: "spring", damping: 25, stiffness: 200 }}
1223
  className="w-[320px] border-l border-white/5 bg-[#0a0a0a]/95 backdrop-blur-xl flex flex-col"
1224
  >
1225
- <div className="p-4 border-b border-white/5 flex items-center justify-between">
1226
- <div className="flex items-center gap-2">
1227
- <Package className="w-5 h-5 text-emerald-400" />
1228
- <h3 className="font-bold text-sm">Assets</h3>
 
 
 
 
 
 
 
 
1229
  </div>
1230
- <button
1231
- onClick={() => setShowAssets(false)}
1232
- className="p-1.5 rounded-lg hover:bg-white/5 transition-colors"
1233
- >
1234
- <X className="w-4 h-4" />
1235
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1236
  </div>
1237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1238
  <div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
1239
  {(() => {
1240
  const allPlots: Array<{title: string, url: string, type?: string}> = [];
@@ -1476,6 +1601,18 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
1476
  background: rgba(255, 255, 255, 0.1);
1477
  }
1478
  `}</style>
 
 
 
 
 
 
 
 
 
 
 
 
1479
  </div>
1480
  );
1481
  };
 
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
+ import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload, Package, FileText, BarChart3, ChevronRight, LogOut, AlertTriangle, Loader2, Check, CloudUpload } from 'lucide-react';
5
  import { cn } from '../lib/utils';
6
  import { Logo } from './Logo';
7
  import ReactMarkdown from 'react-markdown';
8
  import remarkGfm from 'remark-gfm';
9
  import { useAuth } from '../lib/AuthContext';
10
+ import { trackQuery, incrementSessionQueries, getHuggingFaceStatus } from '../lib/supabase';
11
+ import { SettingsModal } from './SettingsModal';
12
+
13
+ // HuggingFace logo SVG component for the export button
14
+ const HuggingFaceLogo = ({ className = "w-4 h-4" }: { className?: string }) => (
15
+ <svg className={className} viewBox="0 0 120 120" fill="currentColor">
16
+ <path d="M60 0C26.863 0 0 26.863 0 60s26.863 60 60 60 60-26.863 60-60S93.137 0 60 0zm0 10c27.614 0 50 22.386 50 50s-22.386 50-50 50S10 87.614 10 60 32.386 10 60 10z"/>
17
+ <circle cx="40" cy="50" r="8"/>
18
+ <circle cx="80" cy="50" r="8"/>
19
+ <path d="M40 75c0 11.046 8.954 20 20 20s20-8.954 20-20H40z"/>
20
+ </svg>
21
+ );
22
 
23
  interface Message {
24
  id: string;
 
203
  const [reportModalUrl, setReportModalUrl] = useState<string | null>(null);
204
  const [reportModalTitle, setReportModalTitle] = useState<string>('Visualization');
205
  const [showAssets, setShowAssets] = useState(false);
206
+ const [showSettings, setShowSettings] = useState(false);
207
+ const [hfConnected, setHfConnected] = useState(false);
208
+ const [isExporting, setIsExporting] = useState(false);
209
+ const [exportSuccess, setExportSuccess] = useState(false);
210
+ const [exportError, setExportError] = useState<string | null>(null);
211
  const fileInputRef = useRef<HTMLInputElement>(null);
212
  const scrollRef = useRef<HTMLDivElement>(null);
213
  const eventSourceRef = useRef<EventSource | null>(null);
 
218
 
219
  const activeSession = sessions.find(s => s.id === activeSessionId) || sessions[0];
220
 
221
+ // Check HuggingFace connection status
222
+ useEffect(() => {
223
+ const checkHfStatus = async () => {
224
+ if (user?.id) {
225
+ const status = await getHuggingFaceStatus(user.id);
226
+ setHfConnected(status.connected);
227
+ }
228
+ };
229
+ checkHfStatus();
230
+ }, [user]);
231
+
232
  // Persist sessions to localStorage whenever they change
233
  useEffect(() => {
234
  saveSessionsToStorage(sessions);
 
344
  console.log('❌ Analysis failed', data);
345
  setIsTyping(false);
346
 
347
+ // Show error message to user - add to sessions
348
+ setSessions(prev => prev.map(s => {
349
+ if (s.id === activeSessionId) {
350
+ return {
351
+ ...s,
352
+ messages: [...s.messages, {
353
+ id: Date.now().toString(),
354
+ role: 'assistant' as const,
355
+ content: data.message || data.error || '❌ Analysis failed',
356
+ timestamp: new Date(),
357
+ }],
358
+ updatedAt: new Date()
359
+ };
360
+ }
361
+ return s;
362
+ }));
363
 
364
  setCurrentStep('');
365
  } else if (data.type === 'analysis_complete') {
 
909
  <ArrowLeft className="w-5 h-5" />
910
  </button>
911
  <div className="flex gap-2">
912
+ <button
913
+ onClick={() => setShowSettings(true)}
914
+ className="p-2 hover:bg-white/5 rounded-lg transition-colors text-white/40 hover:text-white"
915
+ title="Settings"
916
+ >
917
  <Settings className="w-5 h-5" />
918
  </button>
919
  </div>
 
1259
  transition={{ type: "spring", damping: 25, stiffness: 200 }}
1260
  className="w-[320px] border-l border-white/5 bg-[#0a0a0a]/95 backdrop-blur-xl flex flex-col"
1261
  >
1262
+ <div className="p-4 border-b border-white/5">
1263
+ <div className="flex items-center justify-between mb-3">
1264
+ <div className="flex items-center gap-2">
1265
+ <Package className="w-5 h-5 text-emerald-400" />
1266
+ <h3 className="font-bold text-sm">Assets</h3>
1267
+ </div>
1268
+ <button
1269
+ onClick={() => setShowAssets(false)}
1270
+ className="p-1.5 rounded-lg hover:bg-white/5 transition-colors"
1271
+ >
1272
+ <X className="w-4 h-4" />
1273
+ </button>
1274
  </div>
1275
+
1276
+ {/* Export to HuggingFace Button */}
1277
+ {hfConnected ? (
1278
+ <button
1279
+ onClick={async () => {
1280
+ setIsExporting(true);
1281
+ setExportError(null);
1282
+ setExportSuccess(false);
1283
+ try {
1284
+ // Call export API endpoint
1285
+ const response = await fetch('/api/export/huggingface', {
1286
+ method: 'POST',
1287
+ headers: { 'Content-Type': 'application/json' },
1288
+ body: JSON.stringify({
1289
+ user_id: user?.id,
1290
+ session_id: activeSessionId
1291
+ })
1292
+ });
1293
+ if (response.ok) {
1294
+ setExportSuccess(true);
1295
+ setTimeout(() => setExportSuccess(false), 5000);
1296
+ } else {
1297
+ const data = await response.json();
1298
+ setExportError(data.detail || 'Export failed');
1299
+ }
1300
+ } catch (err) {
1301
+ setExportError('Failed to export. Please try again.');
1302
+ } finally {
1303
+ setIsExporting(false);
1304
+ }
1305
+ }}
1306
+ disabled={isExporting}
1307
+ className={cn(
1308
+ "w-full py-2.5 px-3 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-all",
1309
+ isExporting
1310
+ ? "bg-yellow-500/20 text-yellow-300/60 cursor-wait"
1311
+ : exportSuccess
1312
+ ? "bg-green-500/20 text-green-400 border border-green-500/30"
1313
+ : "bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 hover:bg-yellow-500/30"
1314
+ )}
1315
+ >
1316
+ {isExporting ? (
1317
+ <>
1318
+ <Loader2 className="w-4 h-4 animate-spin" />
1319
+ Exporting...
1320
+ </>
1321
+ ) : exportSuccess ? (
1322
+ <>
1323
+ <Check className="w-4 h-4" />
1324
+ Exported to HuggingFace!
1325
+ </>
1326
+ ) : (
1327
+ <>
1328
+ <HuggingFaceLogo className="w-4 h-4" />
1329
+ Export to HuggingFace
1330
+ </>
1331
+ )}
1332
+ </button>
1333
+ ) : (
1334
+ <button
1335
+ onClick={() => setShowSettings(true)}
1336
+ className="w-full py-2.5 px-3 rounded-lg text-sm font-medium flex items-center justify-center gap-2 bg-white/5 text-white/50 border border-white/10 hover:bg-white/10 hover:text-white/70 transition-all"
1337
+ >
1338
+ <HuggingFaceLogo className="w-4 h-4" />
1339
+ Export to HuggingFace
1340
+ </button>
1341
+ )}
1342
+
1343
+ {exportError && (
1344
+ <p className="text-xs text-red-400 mt-2 text-center">{exportError}</p>
1345
+ )}
1346
  </div>
1347
 
1348
+ {/* Warning Banner */}
1349
+ {!hfConnected && (
1350
+ <div className="px-4 py-3 bg-amber-500/10 border-y border-amber-500/20">
1351
+ <div className="flex items-start gap-2">
1352
+ <AlertTriangle className="w-4 h-4 text-amber-400 flex-shrink-0 mt-0.5" />
1353
+ <div>
1354
+ <p className="text-xs text-amber-300 font-medium">Assets are temporary</p>
1355
+ <p className="text-[10px] text-amber-300/60 mt-0.5">
1356
+ Connect HuggingFace in Settings to permanently save your visualizations, models, and datasets.
1357
+ </p>
1358
+ </div>
1359
+ </div>
1360
+ </div>
1361
+ )}
1362
+
1363
  <div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
1364
  {(() => {
1365
  const allPlots: Array<{title: string, url: string, type?: string}> = [];
 
1601
  background: rgba(255, 255, 255, 0.1);
1602
  }
1603
  `}</style>
1604
+
1605
+ {/* Settings Modal */}
1606
+ <SettingsModal
1607
+ isOpen={showSettings}
1608
+ onClose={() => {
1609
+ setShowSettings(false);
1610
+ // Refresh HF connection status when settings modal closes
1611
+ if (user?.id) {
1612
+ getHuggingFaceStatus(user.id).then(status => setHfConnected(status.connected));
1613
+ }
1614
+ }}
1615
+ />
1616
  </div>
1617
  );
1618
  };
FRRONTEEEND/components/SettingsModal.tsx ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, Settings, Eye, EyeOff, Check, Loader2, ExternalLink, AlertTriangle } from 'lucide-react';
4
+ import { useAuth } from '../lib/AuthContext';
5
+ import { updateHuggingFaceToken, getHuggingFaceStatus } from '../lib/supabase';
6
+
7
+ interface SettingsModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ }
11
+
12
+ // HuggingFace logo SVG component
13
+ const HuggingFaceLogo = ({ className = "w-5 h-5" }: { className?: string }) => (
14
+ <svg className={className} viewBox="0 0 120 120" fill="currentColor">
15
+ <path d="M60 0C26.863 0 0 26.863 0 60s26.863 60 60 60 60-26.863 60-60S93.137 0 60 0zm0 10c27.614 0 50 22.386 50 50s-22.386 50-50 50S10 87.614 10 60 32.386 10 60 10z"/>
16
+ <circle cx="40" cy="50" r="8"/>
17
+ <circle cx="80" cy="50" r="8"/>
18
+ <path d="M40 75c0 11.046 8.954 20 20 20s20-8.954 20-20H40z"/>
19
+ </svg>
20
+ );
21
+
22
+ export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose }) => {
23
+ const { user } = useAuth();
24
+ const [activeTab, setActiveTab] = useState<'huggingface' | 'account'>('huggingface');
25
+
26
+ // HuggingFace settings
27
+ const [hfToken, setHfToken] = useState('');
28
+ const [hfUsername, setHfUsername] = useState('');
29
+ const [showToken, setShowToken] = useState(false);
30
+ const [isConnected, setIsConnected] = useState(false);
31
+ const [isSaving, setIsSaving] = useState(false);
32
+ const [isValidating, setIsValidating] = useState(false);
33
+ const [saveSuccess, setSaveSuccess] = useState(false);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [tokenMasked, setTokenMasked] = useState<string | null>(null);
36
+
37
+ // Load current HuggingFace status on mount
38
+ useEffect(() => {
39
+ const loadHfStatus = async () => {
40
+ if (user?.id) {
41
+ const status = await getHuggingFaceStatus(user.id);
42
+ setIsConnected(status.connected);
43
+ setHfUsername(status.username || '');
44
+ setTokenMasked(status.tokenMasked || null);
45
+ }
46
+ };
47
+
48
+ if (isOpen) {
49
+ loadHfStatus();
50
+ }
51
+ }, [user, isOpen]);
52
+
53
+ // Validate HuggingFace token
54
+ const validateToken = async (token: string): Promise<{ valid: boolean; username?: string; error?: string }> => {
55
+ try {
56
+ const response = await fetch('https://huggingface.co/api/whoami-v2', {
57
+ headers: {
58
+ 'Authorization': `Bearer ${token}`
59
+ }
60
+ });
61
+
62
+ if (response.ok) {
63
+ const data = await response.json();
64
+ return { valid: true, username: data.name };
65
+ } else if (response.status === 401) {
66
+ return { valid: false, error: 'Invalid token. Please check your token and try again.' };
67
+ } else {
68
+ return { valid: false, error: 'Could not validate token. Please try again.' };
69
+ }
70
+ } catch (err) {
71
+ return { valid: false, error: 'Network error. Please check your connection.' };
72
+ }
73
+ };
74
+
75
+ // Save HuggingFace settings
76
+ const handleSaveHuggingFace = async () => {
77
+ if (!user?.id) {
78
+ setError('You must be logged in to save settings');
79
+ return;
80
+ }
81
+
82
+ if (!hfToken.trim()) {
83
+ setError('Please enter a HuggingFace token');
84
+ return;
85
+ }
86
+
87
+ setIsSaving(true);
88
+ setError(null);
89
+ setSaveSuccess(false);
90
+
91
+ try {
92
+ // Validate the token first
93
+ setIsValidating(true);
94
+ const validation = await validateToken(hfToken);
95
+ setIsValidating(false);
96
+
97
+ if (!validation.valid) {
98
+ setError(validation.error || 'Invalid token');
99
+ setIsSaving(false);
100
+ return;
101
+ }
102
+
103
+ // Save to Supabase
104
+ const result = await updateHuggingFaceToken(user.id, hfToken, validation.username);
105
+
106
+ if (result) {
107
+ setIsConnected(true);
108
+ setHfUsername(validation.username || '');
109
+ setTokenMasked(`hf_****${hfToken.slice(-4)}`);
110
+ setSaveSuccess(true);
111
+ setHfToken(''); // Clear the input after saving
112
+
113
+ // Hide success message after 3 seconds
114
+ setTimeout(() => setSaveSuccess(false), 3000);
115
+ } else {
116
+ setError('Failed to save token. Please try again.');
117
+ }
118
+ } catch (err) {
119
+ setError('An error occurred. Please try again.');
120
+ } finally {
121
+ setIsSaving(false);
122
+ }
123
+ };
124
+
125
+ // Disconnect HuggingFace
126
+ const handleDisconnect = async () => {
127
+ if (!user?.id) return;
128
+
129
+ setIsSaving(true);
130
+ try {
131
+ const result = await updateHuggingFaceToken(user.id, '', '');
132
+ if (result) {
133
+ setIsConnected(false);
134
+ setHfUsername('');
135
+ setTokenMasked(null);
136
+ setHfToken('');
137
+ }
138
+ } catch (err) {
139
+ setError('Failed to disconnect. Please try again.');
140
+ } finally {
141
+ setIsSaving(false);
142
+ }
143
+ };
144
+
145
+ if (!isOpen) return null;
146
+
147
+ return (
148
+ <AnimatePresence>
149
+ <motion.div
150
+ initial={{ opacity: 0 }}
151
+ animate={{ opacity: 1 }}
152
+ exit={{ opacity: 0 }}
153
+ className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
154
+ onClick={onClose}
155
+ >
156
+ <motion.div
157
+ initial={{ scale: 0.95, opacity: 0 }}
158
+ animate={{ scale: 1, opacity: 1 }}
159
+ exit={{ scale: 0.95, opacity: 0 }}
160
+ className="bg-[#0a0a0a] border border-white/10 rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl"
161
+ onClick={(e) => e.stopPropagation()}
162
+ >
163
+ {/* Header */}
164
+ <div className="flex items-center justify-between p-4 border-b border-white/5">
165
+ <div className="flex items-center gap-3">
166
+ <div className="w-10 h-10 bg-white/5 rounded-xl flex items-center justify-center">
167
+ <Settings className="w-5 h-5 text-white/60" />
168
+ </div>
169
+ <div>
170
+ <h2 className="text-lg font-semibold text-white">Settings</h2>
171
+ <p className="text-xs text-white/40">Configure your integrations</p>
172
+ </div>
173
+ </div>
174
+ <button
175
+ onClick={onClose}
176
+ className="p-2 rounded-lg hover:bg-white/5 transition-colors"
177
+ >
178
+ <X className="w-5 h-5 text-white/60" />
179
+ </button>
180
+ </div>
181
+
182
+ {/* Tabs */}
183
+ <div className="flex border-b border-white/5">
184
+ <button
185
+ onClick={() => setActiveTab('huggingface')}
186
+ className={`flex-1 px-4 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
187
+ activeTab === 'huggingface'
188
+ ? 'text-yellow-400 border-b-2 border-yellow-400 bg-yellow-400/5'
189
+ : 'text-white/50 hover:text-white/70'
190
+ }`}
191
+ >
192
+ <HuggingFaceLogo className="w-4 h-4" />
193
+ HuggingFace
194
+ </button>
195
+ <button
196
+ onClick={() => setActiveTab('account')}
197
+ className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
198
+ activeTab === 'account'
199
+ ? 'text-indigo-400 border-b-2 border-indigo-400 bg-indigo-400/5'
200
+ : 'text-white/50 hover:text-white/70'
201
+ }`}
202
+ >
203
+ Account
204
+ </button>
205
+ </div>
206
+
207
+ {/* Content */}
208
+ <div className="p-6">
209
+ {activeTab === 'huggingface' && (
210
+ <div className="space-y-5">
211
+ {/* Status Card */}
212
+ <div className={`p-4 rounded-xl border ${
213
+ isConnected
214
+ ? 'bg-green-500/10 border-green-500/30'
215
+ : 'bg-yellow-500/10 border-yellow-500/30'
216
+ }`}>
217
+ <div className="flex items-start gap-3">
218
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
219
+ isConnected ? 'bg-green-500/20' : 'bg-yellow-500/20'
220
+ }`}>
221
+ {isConnected ? (
222
+ <Check className="w-4 h-4 text-green-400" />
223
+ ) : (
224
+ <AlertTriangle className="w-4 h-4 text-yellow-400" />
225
+ )}
226
+ </div>
227
+ <div className="flex-1">
228
+ <h4 className={`text-sm font-semibold ${
229
+ isConnected ? 'text-green-300' : 'text-yellow-300'
230
+ }`}>
231
+ {isConnected ? 'Connected to HuggingFace' : 'Not Connected'}
232
+ </h4>
233
+ {isConnected ? (
234
+ <p className="text-xs text-white/50 mt-1">
235
+ Connected as <span className="text-white/80 font-medium">{hfUsername}</span>
236
+ <br />
237
+ Token: <span className="font-mono text-white/60">{tokenMasked}</span>
238
+ </p>
239
+ ) : (
240
+ <p className="text-xs text-white/50 mt-1">
241
+ Connect your HuggingFace account to save and export your assets
242
+ </p>
243
+ )}
244
+ </div>
245
+ {isConnected && (
246
+ <button
247
+ onClick={handleDisconnect}
248
+ disabled={isSaving}
249
+ className="text-xs text-red-400 hover:text-red-300 transition-colors"
250
+ >
251
+ Disconnect
252
+ </button>
253
+ )}
254
+ </div>
255
+ </div>
256
+
257
+ {/* Why Connect Section */}
258
+ {!isConnected && (
259
+ <div className="p-4 bg-white/5 rounded-xl border border-white/10">
260
+ <h4 className="text-sm font-semibold text-white mb-2">πŸš€ Why connect?</h4>
261
+ <ul className="text-xs text-white/60 space-y-1.5">
262
+ <li className="flex items-start gap-2">
263
+ <span className="text-green-400">βœ“</span>
264
+ <span><strong className="text-white/80">Permanent storage</strong> - Your datasets, models & plots saved forever</span>
265
+ </li>
266
+ <li className="flex items-start gap-2">
267
+ <span className="text-green-400">βœ“</span>
268
+ <span><strong className="text-white/80">One-click deploy</strong> - Turn models into APIs instantly</span>
269
+ </li>
270
+ <li className="flex items-start gap-2">
271
+ <span className="text-green-400">βœ“</span>
272
+ <span><strong className="text-white/80">Version control</strong> - Git-based versioning for free</span>
273
+ </li>
274
+ <li className="flex items-start gap-2">
275
+ <span className="text-green-400">βœ“</span>
276
+ <span><strong className="text-white/80">Your data</strong> - Everything stored in YOUR HuggingFace account</span>
277
+ </li>
278
+ </ul>
279
+ </div>
280
+ )}
281
+
282
+ {/* Token Input */}
283
+ <div className="space-y-2">
284
+ <label className="text-sm text-white/70 flex items-center gap-2">
285
+ {isConnected ? 'Update Token' : 'Access Token'}
286
+ <span className="text-xs text-white/40">(Write permission required)</span>
287
+ </label>
288
+ <div className="relative">
289
+ <input
290
+ type={showToken ? 'text' : 'password'}
291
+ value={hfToken}
292
+ onChange={(e) => setHfToken(e.target.value)}
293
+ placeholder="hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
294
+ className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 pr-10 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-yellow-500/50 focus:ring-1 focus:ring-yellow-500/20 font-mono"
295
+ />
296
+ <button
297
+ type="button"
298
+ onClick={() => setShowToken(!showToken)}
299
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-white/60"
300
+ >
301
+ {showToken ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
302
+ </button>
303
+ </div>
304
+ <p className="text-xs text-white/40">
305
+ Get your token from{' '}
306
+ <a
307
+ href="https://huggingface.co/settings/tokens"
308
+ target="_blank"
309
+ rel="noopener noreferrer"
310
+ className="text-yellow-400 hover:text-yellow-300 inline-flex items-center gap-1"
311
+ >
312
+ huggingface.co/settings/tokens
313
+ <ExternalLink className="w-3 h-3" />
314
+ </a>
315
+ </p>
316
+ </div>
317
+
318
+ {/* Error/Success Messages */}
319
+ {error && (
320
+ <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
321
+ {error}
322
+ </div>
323
+ )}
324
+ {saveSuccess && (
325
+ <div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
326
+ <Check className="w-4 h-4" />
327
+ Successfully connected to HuggingFace!
328
+ </div>
329
+ )}
330
+
331
+ {/* Save Button */}
332
+ <button
333
+ onClick={handleSaveHuggingFace}
334
+ disabled={isSaving || !hfToken.trim()}
335
+ className={`w-full py-3 rounded-xl text-sm font-semibold transition-all flex items-center justify-center gap-2 ${
336
+ isSaving || !hfToken.trim()
337
+ ? 'bg-white/5 text-white/30 cursor-not-allowed'
338
+ : 'bg-yellow-500 text-black hover:bg-yellow-400'
339
+ }`}
340
+ >
341
+ {isSaving ? (
342
+ <>
343
+ <Loader2 className="w-4 h-4 animate-spin" />
344
+ {isValidating ? 'Validating...' : 'Saving...'}
345
+ </>
346
+ ) : (
347
+ <>
348
+ <HuggingFaceLogo className="w-4 h-4" />
349
+ {isConnected ? 'Update Connection' : 'Connect HuggingFace'}
350
+ </>
351
+ )}
352
+ </button>
353
+
354
+ {/* Security Note */}
355
+ <p className="text-xs text-white/30 text-center">
356
+ πŸ”’ Your token is encrypted and stored securely. We only use it to save files to your account.
357
+ </p>
358
+ </div>
359
+ )}
360
+
361
+ {activeTab === 'account' && (
362
+ <div className="space-y-4">
363
+ <div className="p-4 bg-white/5 rounded-xl border border-white/10">
364
+ <h4 className="text-sm font-semibold text-white mb-2">Account Information</h4>
365
+ {user ? (
366
+ <div className="space-y-2 text-sm">
367
+ <div className="flex justify-between">
368
+ <span className="text-white/50">Email</span>
369
+ <span className="text-white/80">{user.email}</span>
370
+ </div>
371
+ <div className="flex justify-between">
372
+ <span className="text-white/50">User ID</span>
373
+ <span className="text-white/60 font-mono text-xs">{user.id.slice(0, 8)}...</span>
374
+ </div>
375
+ </div>
376
+ ) : (
377
+ <p className="text-sm text-white/50">Not signed in</p>
378
+ )}
379
+ </div>
380
+
381
+ <div className="p-4 bg-white/5 rounded-xl border border-white/10">
382
+ <h4 className="text-sm font-semibold text-white mb-2">Data & Privacy</h4>
383
+ <p className="text-xs text-white/50">
384
+ Your chat history is stored locally in your browser.
385
+ Connect HuggingFace to permanently save your generated assets.
386
+ </p>
387
+ </div>
388
+ </div>
389
+ )}
390
+ </div>
391
+ </motion.div>
392
+ </motion.div>
393
+ </AnimatePresence>
394
+ );
395
+ };
396
+
397
+ export default SettingsModal;
src/api/app.py CHANGED
@@ -1373,6 +1373,158 @@ async def serve_output_files(file_path: str):
1373
  return FileResponse(output_path, media_type=media_type)
1374
 
1375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1376
  @app.get("/{full_path:path}")
1377
  async def serve_frontend(full_path: str):
1378
  """
 
1373
  return FileResponse(output_path, media_type=media_type)
1374
 
1375
 
1376
+ # ============== HUGGINGFACE EXPORT ENDPOINT ==============
1377
+
1378
+ class HuggingFaceExportRequest(BaseModel):
1379
+ """Request model for HuggingFace export."""
1380
+ user_id: str
1381
+ session_id: str
1382
+
1383
+ @app.post("/api/export/huggingface")
1384
+ async def export_to_huggingface(request: HuggingFaceExportRequest):
1385
+ """
1386
+ Export session assets (datasets, models, plots) to user's HuggingFace account.
1387
+
1388
+ Requires user to have connected their HuggingFace token in settings.
1389
+ """
1390
+ from supabase import create_client, Client
1391
+ import glob
1392
+
1393
+ try:
1394
+ # Get user's HuggingFace credentials from Supabase
1395
+ supabase_url = os.getenv("VITE_SUPABASE_URL") or os.getenv("SUPABASE_URL")
1396
+ supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("VITE_SUPABASE_ANON_KEY")
1397
+
1398
+ if not supabase_url or not supabase_key:
1399
+ raise HTTPException(status_code=500, detail="Supabase configuration missing")
1400
+
1401
+ supabase: Client = create_client(supabase_url, supabase_key)
1402
+
1403
+ # Fetch user's HuggingFace token from profiles
1404
+ result = supabase.table("user_profiles").select(
1405
+ "huggingface_token, huggingface_username"
1406
+ ).eq("user_id", request.user_id).single().execute()
1407
+
1408
+ if not result.data:
1409
+ raise HTTPException(status_code=404, detail="User profile not found")
1410
+
1411
+ hf_token = result.data.get("huggingface_token")
1412
+ hf_username = result.data.get("huggingface_username")
1413
+
1414
+ if not hf_token or not hf_username:
1415
+ raise HTTPException(
1416
+ status_code=400,
1417
+ detail="HuggingFace not connected. Please connect in Settings."
1418
+ )
1419
+
1420
+ # Import HuggingFace storage service
1421
+ from src.storage.huggingface_storage import HuggingFaceStorageService
1422
+
1423
+ hf_service = HuggingFaceStorageService(token=hf_token, username=hf_username)
1424
+
1425
+ # Collect all session assets
1426
+ uploaded_files = []
1427
+ errors = []
1428
+
1429
+ # Session-specific output directory
1430
+ session_outputs_dir = Path(f"./outputs/{request.session_id}")
1431
+ global_outputs_dir = Path("./outputs")
1432
+
1433
+ # Upload datasets (CSVs)
1434
+ csv_patterns = [
1435
+ session_outputs_dir / "*.csv",
1436
+ global_outputs_dir / "*.csv"
1437
+ ]
1438
+ for pattern in csv_patterns:
1439
+ for csv_file in glob.glob(str(pattern)):
1440
+ try:
1441
+ result = hf_service.upload_dataset(
1442
+ file_path=csv_file,
1443
+ dataset_name=Path(csv_file).stem,
1444
+ description=f"Dataset from DS Agent session {request.session_id[:8]}"
1445
+ )
1446
+ uploaded_files.append({"type": "dataset", "name": Path(csv_file).name, "url": result.get("url")})
1447
+ except Exception as e:
1448
+ errors.append(f"Dataset {Path(csv_file).name}: {str(e)}")
1449
+
1450
+ # Upload models (PKL files)
1451
+ model_patterns = [
1452
+ session_outputs_dir / "models" / "*.pkl",
1453
+ global_outputs_dir / "models" / "*.pkl"
1454
+ ]
1455
+ for pattern in model_patterns:
1456
+ for model_file in glob.glob(str(pattern)):
1457
+ try:
1458
+ result = hf_service.upload_model(
1459
+ model_path=model_file,
1460
+ model_name=Path(model_file).stem,
1461
+ description=f"Model from DS Agent session {request.session_id[:8]}"
1462
+ )
1463
+ uploaded_files.append({"type": "model", "name": Path(model_file).name, "url": result.get("url")})
1464
+ except Exception as e:
1465
+ errors.append(f"Model {Path(model_file).name}: {str(e)}")
1466
+
1467
+ # Upload visualizations (HTML plots)
1468
+ plot_patterns = [
1469
+ session_outputs_dir / "*.html",
1470
+ global_outputs_dir / "*.html",
1471
+ session_outputs_dir / "plots" / "*.html",
1472
+ global_outputs_dir / "plots" / "*.html"
1473
+ ]
1474
+ for pattern in plot_patterns:
1475
+ for plot_file in glob.glob(str(pattern)):
1476
+ # Skip index.html or other non-plot files
1477
+ if "index" in Path(plot_file).name.lower():
1478
+ continue
1479
+ try:
1480
+ result = hf_service.upload_plot(
1481
+ plot_path=plot_file,
1482
+ plot_name=Path(plot_file).stem,
1483
+ plot_type="interactive"
1484
+ )
1485
+ uploaded_files.append({"type": "plot", "name": Path(plot_file).name, "url": result.get("url")})
1486
+ except Exception as e:
1487
+ errors.append(f"Plot {Path(plot_file).name}: {str(e)}")
1488
+
1489
+ # Upload PNG images
1490
+ image_patterns = [
1491
+ session_outputs_dir / "*.png",
1492
+ global_outputs_dir / "*.png",
1493
+ session_outputs_dir / "plots" / "*.png",
1494
+ global_outputs_dir / "plots" / "*.png"
1495
+ ]
1496
+ for pattern in image_patterns:
1497
+ for image_file in glob.glob(str(pattern)):
1498
+ try:
1499
+ result = hf_service.upload_plot(
1500
+ plot_path=image_file,
1501
+ plot_name=Path(image_file).stem,
1502
+ plot_type="static"
1503
+ )
1504
+ uploaded_files.append({"type": "image", "name": Path(image_file).name, "url": result.get("url")})
1505
+ except Exception as e:
1506
+ errors.append(f"Image {Path(image_file).name}: {str(e)}")
1507
+
1508
+ if not uploaded_files and errors:
1509
+ raise HTTPException(
1510
+ status_code=500,
1511
+ detail=f"Export failed: {'; '.join(errors)}"
1512
+ )
1513
+
1514
+ return JSONResponse({
1515
+ "success": True,
1516
+ "uploaded_files": uploaded_files,
1517
+ "errors": errors if errors else None,
1518
+ "message": f"Successfully exported {len(uploaded_files)} files to HuggingFace"
1519
+ })
1520
+
1521
+ except HTTPException:
1522
+ raise
1523
+ except Exception as e:
1524
+ logger.error(f"HuggingFace export failed: {str(e)}")
1525
+ raise HTTPException(status_code=500, detail=f"Export failed: {str(e)}")
1526
+
1527
+
1528
  @app.get("/{full_path:path}")
1529
  async def serve_frontend(full_path: str):
1530
  """