waroca commited on
Commit
20651a6
·
verified ·
1 Parent(s): 3dd587b

Upload folder using huggingface_hub

Browse files
Files changed (8) hide show
  1. README.md +22 -5
  2. __init__.py +0 -0
  3. app.py +528 -0
  4. components/catalog.py +103 -0
  5. components/subscriptions.py +514 -0
  6. requirements.txt +6 -0
  7. theme.py +716 -0
  8. utils.py +87 -0
README.md CHANGED
@@ -1,12 +1,29 @@
1
  ---
2
- title: Datapass
3
- emoji: 🐨
4
- colorFrom: gray
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.0.1
8
  app_file: app.py
9
  pinned: false
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Monetization Frontend
3
+ emoji: 💸
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.0.1
8
  app_file: app.py
9
  pinned: false
10
+ hf_oauth: true
11
+ hf_oauth_scopes:
12
+ - email
13
  ---
14
 
15
+ # Hugging Face Data Monetization Platform - Frontend
16
+
17
+ This is the user-facing frontend for the Data Monetization Platform.
18
+
19
+ ## Configuration
20
+
21
+ This Space requires the following **Secrets** to be set in the Space Settings:
22
+
23
+ - `MCP_SERVER_URL`: The Direct URL of your MCP Server Space (e.g., `https://your-username-monetization-mcp-server.hf.space`).
24
+ - `ADMIN_PASSWORD`: The password for accessing the Creator Console.
25
+ - `HF_TOKEN`: (Optional) A Hugging Face token for specific API calls.
26
+
27
+ ## OAuth
28
+
29
+ OAuth is enabled via `hf_oauth: true` in the metadata above. This allows users to sign in with their Hugging Face account.
__init__.py ADDED
File without changes
app.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gradio as gr
4
+ from dotenv import load_dotenv
5
+ from components import subscriptions
6
+ import utils
7
+ import theme
8
+ import os
9
+
10
+ load_dotenv()
11
+
12
+ # Version marker for cache busting
13
+ __version__ = "3.2.0"
14
+
15
+ # Debug: Check OAuth environment variables
16
+ print("=== OAuth Debug Info (Gradio 6.0.1) ===")
17
+ print(f"OAUTH_CLIENT_ID set: {bool(os.getenv('OAUTH_CLIENT_ID'))}")
18
+ print(f"OAUTH_CLIENT_SECRET set: {bool(os.getenv('OAUTH_CLIENT_SECRET'))}")
19
+ print(f"OAUTH_SCOPES: {os.getenv('OAUTH_SCOPES')}")
20
+ print(f"SPACE_HOST: {os.getenv('SPACE_HOST', 'not set')}")
21
+ print("========================================")
22
+
23
+
24
+ def main():
25
+ # Custom CSS for dataset cards
26
+ custom_css = theme.css + """
27
+ /* Dataset card styling */
28
+ .dataset-card {
29
+ background: var(--bg-card);
30
+ border: 1px solid var(--border-color);
31
+ border-radius: 16px;
32
+ padding: 1.5rem;
33
+ margin-bottom: 1rem;
34
+ transition: all 0.2s ease;
35
+ }
36
+ .dataset-card:hover {
37
+ border-color: var(--border-color-strong);
38
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
39
+ }
40
+ .dataset-card .dataset-title {
41
+ font-size: 1.25rem;
42
+ font-weight: 600;
43
+ margin: 0 0 0.25rem 0;
44
+ color: var(--text-primary);
45
+ }
46
+ .dataset-card .dataset-id {
47
+ font-size: 0.8rem;
48
+ color: var(--text-tertiary);
49
+ margin: 0 0 0.75rem 0;
50
+ font-family: monospace;
51
+ }
52
+ .dataset-card .dataset-desc {
53
+ font-size: 0.9375rem;
54
+ color: var(--text-secondary);
55
+ line-height: 1.5;
56
+ margin: 0 0 1rem 0;
57
+ }
58
+ .dataset-card .card-footer {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ padding-top: 1rem;
63
+ border-top: 1px solid var(--border-color);
64
+ }
65
+ .dataset-card .dataset-price {
66
+ font-size: 1.25rem;
67
+ font-weight: 700;
68
+ color: var(--text-primary);
69
+ margin: 0;
70
+ }
71
+ .dataset-card .dataset-price.free {
72
+ color: #30d158;
73
+ }
74
+ .dataset-card .login-hint {
75
+ font-size: 0.875rem;
76
+ color: var(--text-tertiary);
77
+ font-style: italic;
78
+ }
79
+ /* Card wrapper for gr.Group */
80
+ .card-wrapper {
81
+ background: var(--bg-card) !important;
82
+ border: 1px solid var(--border-color) !important;
83
+ border-radius: 16px !important;
84
+ padding: 1.5rem !important;
85
+ margin-bottom: 1rem !important;
86
+ transition: all 0.2s ease !important;
87
+ }
88
+ .card-wrapper:hover {
89
+ border-color: var(--border-color-strong) !important;
90
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important;
91
+ }
92
+ """
93
+
94
+ with gr.Blocks(title="DataPass") as demo:
95
+ # Hero Section
96
+ gr.HTML("""
97
+ <div class="hero-section">
98
+ <h1 class="hero-title">DataPass</h1>
99
+ <p class="hero-subtitle">Query private data. Keep it private.</p>
100
+ </div>
101
+ """)
102
+
103
+ # Simple auth row with Gradio 6 LoginButton
104
+ with gr.Row():
105
+ user_status = gr.Markdown()
106
+ gr.LoginButton(size="sm")
107
+
108
+ # Status message for subscription actions
109
+ subscribe_status = gr.Markdown()
110
+
111
+ # Main Tabs
112
+ with gr.Tabs() as tabs:
113
+ # How It Works Tab
114
+ with gr.Tab("How It Works", id="about"):
115
+ gr.HTML("""
116
+ <div class="about-section">
117
+ <!-- Traditional vs DataPass Comparison -->
118
+ <div class="comparison-container">
119
+ <h2 class="section-heading">Traditional Sharing vs DataPass</h2>
120
+ <div class="comparison-grid">
121
+ <div class="comparison-card traditional">
122
+ <div class="comparison-icon">📥</div>
123
+ <h3>Traditional</h3>
124
+ <div class="comparison-flow">
125
+ <span class="flow-step">Share dataset</span>
126
+ <span class="flow-arrow">→</span>
127
+ <span class="flow-step">User downloads everything</span>
128
+ </div>
129
+ <p class="comparison-result bad">❌ You lose control of your data</p>
130
+ </div>
131
+ <div class="comparison-card datapass">
132
+ <div class="comparison-icon">🔐</div>
133
+ <h3>DataPass</h3>
134
+ <div class="comparison-flow">
135
+ <span class="flow-step">User asks question</span>
136
+ <span class="flow-arrow">→</span>
137
+ <span class="flow-step">Gets only the answer</span>
138
+ </div>
139
+ <p class="comparison-result good">✅ Data stays private on HF</p>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- How MCP Works -->
145
+ <div class="mcp-section">
146
+ <h2 class="section-heading">MCP Does the Heavy Lifting</h2>
147
+ <p class="section-desc">When a subscriber asks a question, here's what happens behind the scenes:</p>
148
+ <div class="mcp-flow">
149
+ <div class="mcp-step">
150
+ <span class="step-num">1</span>
151
+ <span class="step-text">User asks: <em>"What are the top 10 categories?"</em></span>
152
+ </div>
153
+ <div class="mcp-step">
154
+ <span class="step-num">2</span>
155
+ <span class="step-text">DataPass validates the access token</span>
156
+ </div>
157
+ <div class="mcp-step">
158
+ <span class="step-num">3</span>
159
+ <span class="step-text">LLM converts question to SQL</span>
160
+ </div>
161
+ <div class="mcp-step">
162
+ <span class="step-num">4</span>
163
+ <span class="step-text">DuckDB queries parquet files on HF</span>
164
+ </div>
165
+ <div class="mcp-step">
166
+ <span class="step-num">5</span>
167
+ <span class="step-text">Only the result is returned to user</span>
168
+ </div>
169
+ </div>
170
+ <p class="mcp-note">The user never sees the SQL, never touches DuckDB, never accesses HF directly.</p>
171
+ </div>
172
+
173
+ <!-- Value Props Table -->
174
+ <div class="value-section">
175
+ <h2 class="section-heading">Why DataPass?</h2>
176
+ <div class="value-grid">
177
+ <div class="value-column">
178
+ <h3>For Dataset Owners</h3>
179
+ <ul class="value-list">
180
+ <li>🔒 Keep your data private</li>
181
+ <li>⏱️ Grant time-limited access</li>
182
+ <li>🚫 Revoke anytime</li>
183
+ <li>💰 Monetize with Stripe</li>
184
+ </ul>
185
+ </div>
186
+ <div class="value-column">
187
+ <h3>For Subscribers</h3>
188
+ <ul class="value-list">
189
+ <li>💬 Query with SQL or plain English</li>
190
+ <li>⚡ No setup - just use MCP tools</li>
191
+ <li>📊 Get answers, not giant files</li>
192
+ <li>🔌 Works with Claude, Cursor, etc.</li>
193
+ </ul>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Security Model -->
199
+ <div class="security-section">
200
+ <h2 class="section-heading">Security Model</h2>
201
+ <div class="security-grid">
202
+ <div class="security-item">
203
+ <span class="security-icon">🗄️</span>
204
+ <span>Dataset files never leave Hugging Face</span>
205
+ </div>
206
+ <div class="security-item">
207
+ <span class="security-icon">🎫</span>
208
+ <span>DataPass validated on every request</span>
209
+ </div>
210
+ <div class="security-item">
211
+ <span class="security-icon">📉</span>
212
+ <span>Results capped - no SELECT * dumps</span>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <style>
219
+ .about-section {
220
+ max-width: 900px;
221
+ margin: 0 auto;
222
+ padding: 1rem 0;
223
+ }
224
+ .section-heading {
225
+ font-size: 1.5rem;
226
+ font-weight: 700;
227
+ margin: 0 0 1rem 0;
228
+ color: var(--text-primary);
229
+ }
230
+ .section-desc {
231
+ color: var(--text-secondary);
232
+ margin-bottom: 1.5rem;
233
+ }
234
+
235
+ /* Comparison */
236
+ .comparison-container {
237
+ margin-bottom: 3rem;
238
+ }
239
+ .comparison-grid {
240
+ display: grid;
241
+ grid-template-columns: 1fr 1fr;
242
+ gap: 1.5rem;
243
+ }
244
+ @media (max-width: 640px) {
245
+ .comparison-grid { grid-template-columns: 1fr; }
246
+ }
247
+ .comparison-card {
248
+ padding: 1.5rem;
249
+ border-radius: 12px;
250
+ border: 1px solid var(--border-color);
251
+ background: var(--bg-secondary);
252
+ }
253
+ .comparison-card h3 {
254
+ margin: 0.5rem 0 1rem 0;
255
+ font-size: 1.125rem;
256
+ }
257
+ .comparison-icon {
258
+ font-size: 2rem;
259
+ }
260
+ .comparison-flow {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.5rem;
264
+ flex-wrap: wrap;
265
+ margin-bottom: 1rem;
266
+ font-size: 0.9rem;
267
+ }
268
+ .flow-step {
269
+ background: var(--bg-tertiary);
270
+ padding: 0.375rem 0.75rem;
271
+ border-radius: 6px;
272
+ }
273
+ .flow-arrow {
274
+ color: var(--text-tertiary);
275
+ }
276
+ .comparison-result {
277
+ margin: 0;
278
+ font-weight: 500;
279
+ }
280
+ .comparison-result.bad { color: #ff6b6b; }
281
+ .comparison-result.good { color: #51cf66; }
282
+ .comparison-card.datapass {
283
+ border-color: #51cf66;
284
+ background: rgba(81, 207, 102, 0.05);
285
+ }
286
+
287
+ /* MCP Flow */
288
+ .mcp-section {
289
+ margin-bottom: 3rem;
290
+ }
291
+ .mcp-flow {
292
+ display: flex;
293
+ flex-direction: column;
294
+ gap: 0.75rem;
295
+ }
296
+ .mcp-step {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 1rem;
300
+ padding: 0.875rem 1rem;
301
+ background: var(--bg-secondary);
302
+ border-radius: 8px;
303
+ border-left: 3px solid #007AFF;
304
+ }
305
+ .step-num {
306
+ width: 28px;
307
+ height: 28px;
308
+ background: #007AFF;
309
+ color: white;
310
+ border-radius: 50%;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ font-weight: 600;
315
+ font-size: 0.875rem;
316
+ flex-shrink: 0;
317
+ }
318
+ .step-text {
319
+ color: var(--text-secondary);
320
+ }
321
+ .step-text em {
322
+ color: var(--text-primary);
323
+ font-style: normal;
324
+ background: var(--bg-tertiary);
325
+ padding: 0.125rem 0.5rem;
326
+ border-radius: 4px;
327
+ font-family: monospace;
328
+ }
329
+ .mcp-note {
330
+ margin-top: 1rem;
331
+ padding: 1rem;
332
+ background: rgba(0, 122, 255, 0.1);
333
+ border-radius: 8px;
334
+ color: var(--text-secondary);
335
+ font-size: 0.9rem;
336
+ }
337
+
338
+ /* Value Props */
339
+ .value-section {
340
+ margin-bottom: 3rem;
341
+ }
342
+ .value-grid {
343
+ display: grid;
344
+ grid-template-columns: 1fr 1fr;
345
+ gap: 1.5rem;
346
+ }
347
+ @media (max-width: 640px) {
348
+ .value-grid { grid-template-columns: 1fr; }
349
+ }
350
+ .value-column {
351
+ padding: 1.5rem;
352
+ background: var(--bg-secondary);
353
+ border-radius: 12px;
354
+ border: 1px solid var(--border-color);
355
+ }
356
+ .value-column h3 {
357
+ margin: 0 0 1rem 0;
358
+ font-size: 1rem;
359
+ color: var(--text-primary);
360
+ }
361
+ .value-list {
362
+ list-style: none;
363
+ padding: 0;
364
+ margin: 0;
365
+ }
366
+ .value-list li {
367
+ padding: 0.5rem 0;
368
+ color: var(--text-secondary);
369
+ border-bottom: 1px solid var(--border-color);
370
+ }
371
+ .value-list li:last-child {
372
+ border-bottom: none;
373
+ }
374
+
375
+ /* Security */
376
+ .security-section {
377
+ margin-bottom: 2rem;
378
+ }
379
+ .security-grid {
380
+ display: grid;
381
+ grid-template-columns: repeat(3, 1fr);
382
+ gap: 1rem;
383
+ }
384
+ @media (max-width: 768px) {
385
+ .security-grid { grid-template-columns: 1fr; }
386
+ }
387
+ .security-item {
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 0.75rem;
391
+ padding: 1rem;
392
+ background: var(--bg-secondary);
393
+ border-radius: 8px;
394
+ font-size: 0.9rem;
395
+ color: var(--text-secondary);
396
+ }
397
+ .security-icon {
398
+ font-size: 1.25rem;
399
+ }
400
+ </style>
401
+ """)
402
+
403
+ # Catalog Tab
404
+ with gr.Tab("Catalog", id="catalog"):
405
+ # Use gr.render to dynamically create cards with buttons
406
+ @gr.render(triggers=[demo.load])
407
+ def render_catalog(profile: gr.OAuthProfile | None = None, token: gr.OAuthToken | None = None):
408
+ datasets = utils.get_catalog() or []
409
+
410
+ if not datasets:
411
+ gr.HTML("""
412
+ <div class="empty-state">
413
+ <div class="empty-state-icon">📦</div>
414
+ <div class="empty-state-title">No datasets available</div>
415
+ <div class="empty-state-text">Check back soon for new data products.</div>
416
+ </div>
417
+ """)
418
+ return
419
+
420
+ for dataset in datasets:
421
+ dataset_id = dataset.get('dataset_id', '')
422
+ display_name = dataset.get('display_name', dataset_id)
423
+ description = dataset.get('description', 'No description available.')
424
+
425
+ plans = dataset.get("plans", [])
426
+ is_free = False
427
+ price = "10"
428
+ if plans:
429
+ plan = plans[0]
430
+ price_id = plan.get("stripe_price_id", "")
431
+ if price_id in ["free", "0", 0]:
432
+ is_free = True
433
+ else:
434
+ price = plan.get("price", "10")
435
+
436
+ price_class = "free" if is_free else ""
437
+ price_text = "Free Trial" if is_free else f"${price}/mo"
438
+ button_text = "Start 24h Trial" if is_free else f"Subscribe - ${price}/mo"
439
+
440
+ with gr.Group(elem_classes="card-wrapper"):
441
+ gr.HTML(f"""
442
+ <h3 class="dataset-title">{display_name}</h3>
443
+ <p class="dataset-id">{dataset_id}</p>
444
+ <p class="dataset-desc">{description}</p>
445
+ <p class="dataset-price {price_class}">{price_text}</p>
446
+ """)
447
+
448
+ if profile:
449
+ # Create button with closure to capture dataset info
450
+ btn = gr.Button(button_text, variant="primary", size="sm")
451
+
452
+ # Capture variables in closure
453
+ ds_id = dataset_id
454
+ ds_name = display_name
455
+ ds_is_free = is_free
456
+
457
+ def make_handler(did, dname, dfree):
458
+ def handler(p: gr.OAuthProfile | None, t: gr.OAuthToken | None):
459
+ if not p:
460
+ return "⚠️ Please sign in first to subscribe."
461
+
462
+ hf_token = t.token if t else None
463
+
464
+ if dfree:
465
+ result = utils.subscribe_free(did, p.username, hf_token)
466
+ if "error" in result:
467
+ return f"❌ Error: {result['error']}"
468
+ return f"✅ Your 24-hour DataPass for **{dname}** is active! Go to My Subscriptions to get your access token."
469
+ else:
470
+ result = utils.create_checkout_session(did, p.username, hf_token)
471
+ if "error" in result:
472
+ return f"❌ Error: {result['error']}"
473
+ if "checkout_url" in result:
474
+ return f"🔗 [Click here to complete payment]({result['checkout_url']})"
475
+ return "Error creating checkout session."
476
+ return handler
477
+
478
+ btn.click(
479
+ fn=make_handler(ds_id, ds_name, ds_is_free),
480
+ outputs=[subscribe_status]
481
+ )
482
+ else:
483
+ gr.HTML('<p class="login-hint">Sign in to subscribe</p>')
484
+
485
+ # Subscriptions Tab
486
+ with gr.Tab("My Subscriptions", id="subscriptions"):
487
+ subscriptions_container = gr.HTML()
488
+
489
+ # Footer
490
+ gr.HTML("""
491
+ <div style="text-align: center; padding: 2rem 1rem; margin-top: 2rem;">
492
+ <p style="font-size: 0.8125rem; color: var(--text-tertiary);">
493
+ DataPass — Powered by Hugging Face
494
+ </p>
495
+ </div>
496
+ """)
497
+
498
+ # Load user status
499
+ def load_user_status(profile: gr.OAuthProfile | None):
500
+ if profile:
501
+ return f"👤 Signed in as **{profile.username}**"
502
+ return "Sign in to subscribe to datasets"
503
+
504
+ # Load subscriptions
505
+ def load_subscriptions(profile: gr.OAuthProfile | None, token: gr.OAuthToken | None):
506
+ if not profile:
507
+ return """
508
+ <div class="empty-state">
509
+ <div class="empty-state-icon">🔐</div>
510
+ <div class="empty-state-title">Sign in required</div>
511
+ <div class="empty-state-text">Please sign in with your Hugging Face account to view your subscriptions.</div>
512
+ </div>
513
+ """
514
+ # Pass HF token for authentication
515
+ hf_token = token.token if token else None
516
+ user_subs = utils.get_user_subscriptions(profile.username, hf_token)
517
+ return subscriptions.create_subscriptions_html(user_subs)
518
+
519
+ # Load on page load
520
+ demo.load(fn=load_user_status, outputs=[user_status])
521
+ demo.load(fn=load_subscriptions, outputs=[subscriptions_container])
522
+
523
+ return demo, custom_css
524
+
525
+
526
+ if __name__ == "__main__":
527
+ demo, custom_css = main()
528
+ demo.launch(theme=theme.get_theme(), css=custom_css)
components/catalog.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def create_catalog_html(datasets, username=None):
2
+ """Creates HTML string for the dataset catalog (display only, no buttons)."""
3
+ if not datasets:
4
+ return """
5
+ <div class="empty-state">
6
+ <div class="empty-state-icon">📦</div>
7
+ <div class="empty-state-title">No datasets available</div>
8
+ <div class="empty-state-text">Check back soon for new data products.</div>
9
+ </div>
10
+ """
11
+
12
+ cards_html = ""
13
+ for dataset in datasets:
14
+ # Determine pricing display
15
+ plans = dataset.get("plans", [])
16
+ price_display = ""
17
+ action_text = ""
18
+
19
+ if plans:
20
+ plan = plans[0]
21
+ price_id = plan.get("stripe_price_id", "")
22
+ if price_id in ["free", "0", 0]:
23
+ trial_days = plan.get("access_duration_days", 1)
24
+ trial_text = "24h" if trial_days == 1 else f"{trial_days}-day"
25
+ price_display = f'<span class="price-tag price-free">{trial_text} Free Trial</span>'
26
+ if not username:
27
+ action_text = '<span class="login-hint">Sign in to start trial</span>'
28
+ else:
29
+ price = plan.get("price", "10")
30
+ price_display = f'''
31
+ <span class="price-tag">
32
+ <span class="currency">$</span>{price}
33
+ <span class="period">/mo</span>
34
+ </span>
35
+ '''
36
+ if not username:
37
+ action_text = '<span class="login-hint">Sign in to subscribe</span>'
38
+
39
+ cards_html += f"""
40
+ <div class="dataset-card animate-in">
41
+ <div class="card-header">
42
+ <h3 class="card-title">{dataset.get('display_name', dataset.get('dataset_id', 'Untitled'))}</h3>
43
+ <span class="card-badge">{dataset.get('dataset_id', '')}</span>
44
+ </div>
45
+ <p class="card-description">{dataset.get('description', 'No description available.')}</p>
46
+ <div class="card-footer">
47
+ {price_display}
48
+ <div class="card-actions">
49
+ {action_text}
50
+ </div>
51
+ </div>
52
+ </div>
53
+ """
54
+
55
+ # Wrap in container with grid layout
56
+ html = f"""
57
+ <div class="catalog-grid">
58
+ {cards_html}
59
+ </div>
60
+ <style>
61
+ .catalog-grid {{
62
+ display: grid;
63
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
64
+ gap: 1.5rem;
65
+ padding: 0.5rem 0;
66
+ }}
67
+ .login-hint {{
68
+ font-size: 0.875rem;
69
+ color: var(--text-tertiary);
70
+ font-style: italic;
71
+ }}
72
+ .card-actions {{
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.75rem;
76
+ }}
77
+ </style>
78
+ """
79
+
80
+ return html
81
+
82
+
83
+ def get_dataset_choices(datasets, username=None):
84
+ """Returns list of (label, dataset_id) tuples for dropdown."""
85
+ if not datasets or not username:
86
+ return []
87
+
88
+ choices = []
89
+ for dataset in datasets:
90
+ plans = dataset.get("plans", [])
91
+ if plans:
92
+ plan = plans[0]
93
+ price_id = plan.get("stripe_price_id", "")
94
+ if price_id in ["free", "0", 0]:
95
+ trial_days = plan.get("access_duration_days", 1)
96
+ trial_text = "24h" if trial_days == 1 else f"{trial_days}-day"
97
+ label = f"{dataset.get('display_name', dataset.get('dataset_id'))} ({trial_text} Trial)"
98
+ else:
99
+ price = plan.get("price", "10")
100
+ label = f"{dataset.get('display_name', dataset.get('dataset_id'))} (${price}/mo)"
101
+ choices.append((label, dataset.get('dataset_id')))
102
+
103
+ return choices
components/subscriptions.py ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ import json
3
+ import os
4
+ import html
5
+
6
+ # Get MCP server URL from environment
7
+ MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000")
8
+
9
+
10
+ def create_subscriptions_html(subscriptions):
11
+ """Creates HTML string for user's subscriptions list."""
12
+ if not subscriptions:
13
+ return """
14
+ <div class="empty-state">
15
+ <div class="empty-state-icon">🎫</div>
16
+ <div class="empty-state-title">No DataPass yet</div>
17
+ <div class="empty-state-text">Browse the catalog and start a 24-hour free trial to get your first DataPass.</div>
18
+ </div>
19
+ """
20
+
21
+ cards_html = ""
22
+ modals_html = ""
23
+
24
+ for idx, sub in enumerate(subscriptions):
25
+ # Determine subscription status
26
+ end_date_str = sub.get('subscription_end', '')
27
+ is_active = sub.get('is_active', False)
28
+ status_class = "active" if is_active else "expired"
29
+ status_text = "Active" if is_active else "Expired"
30
+ expires_text = "Unknown expiry"
31
+
32
+ try:
33
+ if end_date_str:
34
+ # Handle Z suffix
35
+ if end_date_str.endswith("Z"):
36
+ end_date_str = end_date_str[:-1]
37
+ end_date = datetime.fromisoformat(end_date_str)
38
+
39
+ if is_active:
40
+ days_left = (end_date - datetime.utcnow()).days
41
+ hours_left = int((end_date - datetime.utcnow()).total_seconds() / 3600)
42
+ if days_left > 0:
43
+ expires_text = f"Expires in {days_left} day{'s' if days_left != 1 else ''}"
44
+ elif hours_left > 0:
45
+ expires_text = f"Expires in {hours_left} hour{'s' if hours_left != 1 else ''}"
46
+ else:
47
+ expires_text = "Expires soon"
48
+ else:
49
+ expires_text = "DataPass expired — Upgrade for continued access"
50
+ except Exception:
51
+ pass
52
+
53
+ dataset_id = sub.get('dataset_id', '')
54
+ access_token = sub.get('access_token', '')
55
+ modal_id = f"modal-{idx}"
56
+ safe_dataset_id = dataset_id.replace('/', '-')
57
+
58
+ # Connection details button for active subscriptions
59
+ connection_btn_html = ""
60
+ if is_active and access_token:
61
+ connection_btn_html = f'''
62
+ <button class="btn btn-primary" onclick="document.getElementById('{modal_id}').classList.add('show')">
63
+ Connection Details
64
+ </button>
65
+ '''
66
+
67
+ # Create MCP config JSON
68
+ # Use dataset name (last part of dataset_id) as server name
69
+ # FastMCP uses /sse endpoint for SSE transport
70
+ dataset_name = dataset_id.split('/')[-1] if '/' in dataset_id else dataset_id
71
+ server_name = f"{dataset_name}-dataset"
72
+ mcp_config = {
73
+ "mcpServers": {
74
+ server_name: {
75
+ "url": f"{MCP_SERVER_URL}/sse",
76
+ "headers": {
77
+ "Authorization": f"Bearer {access_token}"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ mcp_config_json = json.dumps(mcp_config, indent=2)
83
+ mcp_config_escaped = html.escape(mcp_config_json)
84
+
85
+ # For JavaScript - need to escape quotes properly
86
+ mcp_config_for_js = json.dumps(mcp_config_json)
87
+
88
+ # Example prompt
89
+ example_prompt = f"Query the dataset {dataset_id} and show me a summary of the data. What columns are available and how many rows are there?"
90
+ example_prompt_escaped = html.escape(example_prompt)
91
+ example_prompt_for_js = json.dumps(example_prompt)
92
+
93
+ # Modal HTML
94
+ modals_html += f'''
95
+ <div id="{modal_id}" class="modal-overlay" onclick="if(event.target===this) this.classList.remove('show')">
96
+ <div class="modal-content">
97
+ <div class="modal-header">
98
+ <h3 class="modal-title">Connection Details</h3>
99
+ <button class="modal-close" onclick="document.getElementById('{modal_id}').classList.remove('show')">&times;</button>
100
+ </div>
101
+ <div class="modal-body">
102
+ <div class="modal-dataset-info">
103
+ <span class="modal-dataset-name">{html.escape(dataset_id)}</span>
104
+ <span class="modal-status {status_class}">{status_text}</span>
105
+ </div>
106
+
107
+ <div class="config-section">
108
+ <div class="config-header">
109
+ <span class="config-icon">🔌</span>
110
+ <span class="config-label">MCP Configuration</span>
111
+ </div>
112
+ <p class="config-description">Add this to your Claude Desktop or MCP client configuration:</p>
113
+ <div class="code-wrapper">
114
+ <pre class="code-block">{mcp_config_escaped}</pre>
115
+ <button class="btn-copy" onclick="copyToClipboard({mcp_config_for_js}, this)">Copy</button>
116
+ </div>
117
+ </div>
118
+
119
+ <div class="config-section">
120
+ <div class="config-header">
121
+ <span class="config-icon">💬</span>
122
+ <span class="config-label">Example Prompt</span>
123
+ </div>
124
+ <p class="config-description">Try this prompt to get started:</p>
125
+ <div class="code-wrapper">
126
+ <pre class="code-block prompt-block">{example_prompt_escaped}</pre>
127
+ <button class="btn-copy" onclick="copyToClipboard({example_prompt_for_js}, this)">Copy</button>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="config-section">
132
+ <div class="config-header">
133
+ <span class="config-icon">🔑</span>
134
+ <span class="config-label">Access Token</span>
135
+ </div>
136
+ <div class="code-wrapper">
137
+ <code class="token-block">{html.escape(access_token)}</code>
138
+ <button class="btn-copy" onclick="copyToClipboard('{access_token}', this)">Copy</button>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="config-tip">
143
+ <span class="tip-icon">💡</span>
144
+ <span>Use tools like <code>query_dataset</code> or <code>query_dataset_natural_language</code> to analyze your data with SQL or natural language.</span>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ '''
150
+
151
+ # View on HF button for active subscriptions
152
+ view_btn_html = ""
153
+ if is_active:
154
+ view_btn_html = f'''
155
+ <a href="https://huggingface.co/datasets/{dataset_id}" target="_blank" class="btn btn-secondary">
156
+ View on HF
157
+ </a>
158
+ '''
159
+
160
+ # Determine plan display name
161
+ plan_id = sub.get('plan_id', 'trial')
162
+ if plan_id.lower() in ['free', 'trial', 'free_tier']:
163
+ # Check for access_duration_days to determine trial length
164
+ # Note: This info might not be in subscription record, default to showing "Trial"
165
+ plan_display = "Free Trial"
166
+ else:
167
+ plan_display = f"{plan_id} plan"
168
+
169
+ cards_html += f"""
170
+ <div class="subscription-card">
171
+ <div class="subscription-info">
172
+ <h4 class="subscription-dataset">{html.escape(dataset_id)}</h4>
173
+ <div class="subscription-meta">
174
+ <span class="plan-badge">{html.escape(plan_display)}</span>
175
+ <span class="expiry-text">{expires_text}</span>
176
+ </div>
177
+ </div>
178
+ <div class="subscription-actions">
179
+ <span class="status-badge {status_class}">{status_text}</span>
180
+ {connection_btn_html}
181
+ {view_btn_html}
182
+ </div>
183
+ </div>
184
+ """
185
+
186
+ result_html = f"""
187
+ <div class="subscriptions-container">
188
+ <div class="subscriptions-list">
189
+ {cards_html}
190
+ </div>
191
+ {modals_html}
192
+ </div>
193
+
194
+ <script>
195
+ function copyToClipboard(text, btn) {{
196
+ navigator.clipboard.writeText(text).then(function() {{
197
+ const originalText = btn.textContent;
198
+ btn.textContent = 'Copied!';
199
+ btn.classList.add('copied');
200
+ setTimeout(function() {{
201
+ btn.textContent = originalText;
202
+ btn.classList.remove('copied');
203
+ }}, 2000);
204
+ }});
205
+ }}
206
+
207
+ // Close modal on Escape key
208
+ document.addEventListener('keydown', function(e) {{
209
+ if (e.key === 'Escape') {{
210
+ document.querySelectorAll('.modal-overlay.show').forEach(function(modal) {{
211
+ modal.classList.remove('show');
212
+ }});
213
+ }}
214
+ }});
215
+ </script>
216
+
217
+ <style>
218
+ .subscriptions-container {{
219
+ position: relative;
220
+ }}
221
+ .subscriptions-list {{
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 1rem;
225
+ padding: 0.5rem 0;
226
+ }}
227
+ .subscription-card {{
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ padding: 1.25rem 1.5rem;
232
+ background: var(--bg-secondary, #1a1a1a);
233
+ border: 1px solid var(--border-color, #333);
234
+ border-radius: 12px;
235
+ transition: all 0.2s ease;
236
+ flex-wrap: wrap;
237
+ gap: 1rem;
238
+ }}
239
+ .subscription-card:hover {{
240
+ border-color: var(--border-hover, #444);
241
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
242
+ }}
243
+ .subscription-info {{
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 0.375rem;
247
+ min-width: 200px;
248
+ }}
249
+ .subscription-dataset {{
250
+ margin: 0;
251
+ font-size: 1.0625rem;
252
+ font-weight: 600;
253
+ color: var(--text-primary, #fff);
254
+ }}
255
+ .subscription-meta {{
256
+ display: flex;
257
+ gap: 0.75rem;
258
+ font-size: 0.8125rem;
259
+ color: var(--text-secondary, #888);
260
+ }}
261
+ .plan-badge {{
262
+ text-transform: capitalize;
263
+ }}
264
+ .subscription-actions {{
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 0.75rem;
268
+ flex-wrap: wrap;
269
+ }}
270
+ .status-badge {{
271
+ padding: 0.25rem 0.75rem;
272
+ border-radius: 9999px;
273
+ font-size: 0.6875rem;
274
+ font-weight: 600;
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.05em;
277
+ }}
278
+ .status-badge.active {{
279
+ background: rgba(52, 199, 89, 0.15);
280
+ color: #34c759;
281
+ }}
282
+ .status-badge.expired {{
283
+ background: rgba(142, 142, 147, 0.15);
284
+ color: #8e8e93;
285
+ }}
286
+
287
+ /* Buttons */
288
+ .btn {{
289
+ padding: 0.5rem 1rem;
290
+ border-radius: 8px;
291
+ font-size: 0.8125rem;
292
+ font-weight: 500;
293
+ text-decoration: none;
294
+ cursor: pointer;
295
+ border: none;
296
+ transition: all 0.15s ease;
297
+ white-space: nowrap;
298
+ }}
299
+ .btn-primary {{
300
+ background: #007AFF;
301
+ color: white;
302
+ }}
303
+ .btn-primary:hover {{
304
+ background: #0056b3;
305
+ }}
306
+ .btn-secondary {{
307
+ background: var(--bg-tertiary, #2a2a2a);
308
+ color: var(--text-primary, #fff);
309
+ border: 1px solid var(--border-color, #333);
310
+ }}
311
+ .btn-secondary:hover {{
312
+ background: var(--bg-hover, #333);
313
+ }}
314
+
315
+ /* Modal */
316
+ .modal-overlay {{
317
+ display: none;
318
+ position: fixed;
319
+ top: 0;
320
+ left: 0;
321
+ right: 0;
322
+ bottom: 0;
323
+ background: rgba(0, 0, 0, 0.7);
324
+ backdrop-filter: blur(4px);
325
+ z-index: 1000;
326
+ justify-content: center;
327
+ align-items: center;
328
+ padding: 1rem;
329
+ }}
330
+ .modal-overlay.show {{
331
+ display: flex;
332
+ }}
333
+ .modal-content {{
334
+ background: var(--bg-primary, #0d0d0d);
335
+ border: 1px solid var(--border-color, #333);
336
+ border-radius: 16px;
337
+ max-width: 600px;
338
+ width: 100%;
339
+ max-height: 90vh;
340
+ overflow-y: auto;
341
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
342
+ }}
343
+ .modal-header {{
344
+ display: flex;
345
+ justify-content: space-between;
346
+ align-items: center;
347
+ padding: 1.25rem 1.5rem;
348
+ border-bottom: 1px solid var(--border-color, #333);
349
+ }}
350
+ .modal-title {{
351
+ margin: 0;
352
+ font-size: 1.125rem;
353
+ font-weight: 600;
354
+ color: var(--text-primary, #fff);
355
+ }}
356
+ .modal-close {{
357
+ background: none;
358
+ border: none;
359
+ font-size: 1.5rem;
360
+ color: var(--text-secondary, #888);
361
+ cursor: pointer;
362
+ padding: 0;
363
+ line-height: 1;
364
+ transition: color 0.15s;
365
+ }}
366
+ .modal-close:hover {{
367
+ color: var(--text-primary, #fff);
368
+ }}
369
+ .modal-body {{
370
+ padding: 1.5rem;
371
+ }}
372
+ .modal-dataset-info {{
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 0.75rem;
376
+ margin-bottom: 1.5rem;
377
+ padding-bottom: 1rem;
378
+ border-bottom: 1px solid var(--border-color, #333);
379
+ }}
380
+ .modal-dataset-name {{
381
+ font-size: 1rem;
382
+ font-weight: 600;
383
+ color: var(--text-primary, #fff);
384
+ font-family: 'SF Mono', Monaco, monospace;
385
+ }}
386
+ .modal-status {{
387
+ padding: 0.2rem 0.5rem;
388
+ border-radius: 4px;
389
+ font-size: 0.625rem;
390
+ font-weight: 600;
391
+ text-transform: uppercase;
392
+ }}
393
+ .modal-status.active {{
394
+ background: rgba(52, 199, 89, 0.15);
395
+ color: #34c759;
396
+ }}
397
+
398
+ /* Config sections */
399
+ .config-section {{
400
+ margin-bottom: 1.5rem;
401
+ }}
402
+ .config-header {{
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 0.5rem;
406
+ margin-bottom: 0.5rem;
407
+ }}
408
+ .config-icon {{
409
+ font-size: 1rem;
410
+ }}
411
+ .config-label {{
412
+ font-size: 0.875rem;
413
+ font-weight: 600;
414
+ color: var(--text-primary, #fff);
415
+ }}
416
+ .config-description {{
417
+ font-size: 0.8125rem;
418
+ color: var(--text-secondary, #888);
419
+ margin: 0 0 0.75rem 0;
420
+ }}
421
+ .code-wrapper {{
422
+ position: relative;
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 0.5rem;
426
+ }}
427
+ .code-block {{
428
+ padding: 1rem;
429
+ background: var(--bg-tertiary, #1a1a1a);
430
+ border: 1px solid var(--border-color, #333);
431
+ border-radius: 8px;
432
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
433
+ font-size: 0.75rem;
434
+ color: var(--text-primary, #fff);
435
+ overflow-x: auto;
436
+ white-space: pre;
437
+ margin: 0;
438
+ line-height: 1.6;
439
+ }}
440
+ .prompt-block {{
441
+ white-space: pre-wrap;
442
+ word-break: break-word;
443
+ }}
444
+ .token-block {{
445
+ display: block;
446
+ padding: 0.75rem 1rem;
447
+ background: var(--bg-tertiary, #1a1a1a);
448
+ border: 1px solid var(--border-color, #333);
449
+ border-radius: 8px;
450
+ font-family: 'SF Mono', Monaco, monospace;
451
+ font-size: 0.75rem;
452
+ color: var(--text-primary, #fff);
453
+ word-break: break-all;
454
+ }}
455
+ .btn-copy {{
456
+ align-self: flex-end;
457
+ padding: 0.375rem 0.75rem;
458
+ background: var(--bg-tertiary, #2a2a2a);
459
+ color: var(--text-primary, #fff);
460
+ border: 1px solid var(--border-color, #333);
461
+ border-radius: 6px;
462
+ font-size: 0.75rem;
463
+ font-weight: 500;
464
+ cursor: pointer;
465
+ transition: all 0.15s;
466
+ }}
467
+ .btn-copy:hover {{
468
+ background: var(--bg-hover, #333);
469
+ }}
470
+ .btn-copy.copied {{
471
+ background: #34c759;
472
+ border-color: #34c759;
473
+ color: white;
474
+ }}
475
+
476
+ .config-tip {{
477
+ display: flex;
478
+ align-items: flex-start;
479
+ gap: 0.5rem;
480
+ padding: 0.875rem 1rem;
481
+ background: rgba(0, 122, 255, 0.1);
482
+ border-radius: 8px;
483
+ font-size: 0.8125rem;
484
+ color: var(--text-secondary, #888);
485
+ line-height: 1.5;
486
+ }}
487
+ .config-tip code {{
488
+ background: var(--bg-tertiary, #1a1a1a);
489
+ padding: 0.125rem 0.375rem;
490
+ border-radius: 4px;
491
+ font-size: 0.75rem;
492
+ color: var(--text-primary, #fff);
493
+ }}
494
+ .tip-icon {{
495
+ flex-shrink: 0;
496
+ }}
497
+
498
+ @media (max-width: 640px) {{
499
+ .subscription-card {{
500
+ flex-direction: column;
501
+ align-items: flex-start;
502
+ }}
503
+ .subscription-actions {{
504
+ width: 100%;
505
+ justify-content: flex-start;
506
+ }}
507
+ .modal-content {{
508
+ margin: 0.5rem;
509
+ }}
510
+ }}
511
+ </style>
512
+ """
513
+
514
+ return result_html
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio==5.6.0
2
+ huggingface_hub<1.0.0
3
+ stripe
4
+ python-dotenv
5
+ mcp
6
+ typer
theme.py ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ def get_theme():
4
+ """Returns a custom Gradio theme with Apple-inspired design."""
5
+ theme = gr.themes.Base(
6
+ primary_hue="neutral",
7
+ secondary_hue="neutral",
8
+ neutral_hue="neutral",
9
+ spacing_size="lg",
10
+ radius_size="lg",
11
+ text_size="md",
12
+ font=[gr.themes.GoogleFont("SF Pro Display"), gr.themes.GoogleFont("Inter"), "system-ui", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
13
+ font_mono=[gr.themes.GoogleFont("SF Mono"), gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
14
+ ).set(
15
+ # Light mode base
16
+ body_background_fill="*neutral_50",
17
+ body_background_fill_dark="*neutral_950",
18
+ body_text_color="*neutral_900",
19
+ body_text_color_dark="*neutral_100",
20
+
21
+ # Block styling
22
+ block_background_fill="white",
23
+ block_background_fill_dark="*neutral_900",
24
+ block_border_width="0px",
25
+ block_shadow="0 1px 3px 0 rgb(0 0 0 / 0.05)",
26
+ block_shadow_dark="0 1px 3px 0 rgb(0 0 0 / 0.3)",
27
+ block_label_background_fill="transparent",
28
+ block_label_background_fill_dark="transparent",
29
+ block_label_text_color="*neutral_600",
30
+ block_label_text_color_dark="*neutral_400",
31
+ block_title_text_color="*neutral_900",
32
+ block_title_text_color_dark="*neutral_100",
33
+
34
+ # Input styling
35
+ input_background_fill="*neutral_100",
36
+ input_background_fill_dark="*neutral_800",
37
+ input_border_color="*neutral_200",
38
+ input_border_color_dark="*neutral_700",
39
+ input_border_color_focus="*neutral_400",
40
+ input_border_color_focus_dark="*neutral_500",
41
+ input_placeholder_color="*neutral_400",
42
+ input_placeholder_color_dark="*neutral_500",
43
+
44
+ # Primary button (filled, dark)
45
+ button_primary_background_fill="*neutral_900",
46
+ button_primary_background_fill_dark="white",
47
+ button_primary_background_fill_hover="*neutral_800",
48
+ button_primary_background_fill_hover_dark="*neutral_200",
49
+ button_primary_text_color="white",
50
+ button_primary_text_color_dark="*neutral_900",
51
+ button_primary_border_color="transparent",
52
+ button_primary_border_color_dark="transparent",
53
+
54
+ # Secondary button (outlined)
55
+ button_secondary_background_fill="transparent",
56
+ button_secondary_background_fill_dark="transparent",
57
+ button_secondary_background_fill_hover="*neutral_100",
58
+ button_secondary_background_fill_hover_dark="*neutral_800",
59
+ button_secondary_text_color="*neutral_700",
60
+ button_secondary_text_color_dark="*neutral_300",
61
+ button_secondary_border_color="*neutral_300",
62
+ button_secondary_border_color_dark="*neutral_700",
63
+
64
+ # Border and shadow
65
+ border_color_primary="*neutral_200",
66
+ border_color_primary_dark="*neutral_800",
67
+
68
+ # Tab styling
69
+ background_fill_secondary="*neutral_100",
70
+ background_fill_secondary_dark="*neutral_800",
71
+ )
72
+ return theme
73
+
74
+ css = """
75
+ /* Apple-inspired CSS with full dark mode support */
76
+ /* Note: Inter font is loaded via Gradio theme settings */
77
+
78
+ /* CSS Variables for theming */
79
+ :root {
80
+ --bg-primary: #ffffff;
81
+ --bg-secondary: #f5f5f7;
82
+ --bg-tertiary: #fafafa;
83
+ --bg-card: #ffffff;
84
+ --bg-card-hover: #fafafa;
85
+ --bg-glass: rgba(255, 255, 255, 0.72);
86
+
87
+ --text-primary: #1d1d1f;
88
+ --text-secondary: #6e6e73;
89
+ --text-tertiary: #86868b;
90
+
91
+ --border-color: rgba(0, 0, 0, 0.08);
92
+ --border-color-strong: rgba(0, 0, 0, 0.12);
93
+
94
+ --accent-blue: #0071e3;
95
+ --accent-blue-hover: #0077ed;
96
+ --accent-green: #30d158;
97
+ --accent-orange: #ff9f0a;
98
+
99
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
100
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
101
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
102
+ --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
103
+ --shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.12);
104
+
105
+ --radius-sm: 8px;
106
+ --radius-md: 12px;
107
+ --radius-lg: 16px;
108
+ --radius-xl: 20px;
109
+ --radius-full: 9999px;
110
+
111
+ --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
112
+ --transition-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
113
+ --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
114
+ }
115
+
116
+ /* Dark mode variables */
117
+ .dark, [data-theme="dark"] {
118
+ --bg-primary: #000000;
119
+ --bg-secondary: #1c1c1e;
120
+ --bg-tertiary: #2c2c2e;
121
+ --bg-card: #1c1c1e;
122
+ --bg-card-hover: #2c2c2e;
123
+ --bg-glass: rgba(28, 28, 30, 0.72);
124
+
125
+ --text-primary: #f5f5f7;
126
+ --text-secondary: #a1a1a6;
127
+ --text-tertiary: #6e6e73;
128
+
129
+ --border-color: rgba(255, 255, 255, 0.08);
130
+ --border-color-strong: rgba(255, 255, 255, 0.12);
131
+
132
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
133
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3);
134
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.4);
135
+ --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.2), 0 0 1px rgba(255, 255, 255, 0.05);
136
+ --shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.4);
137
+ }
138
+
139
+ /* Also support system preference */
140
+ @media (prefers-color-scheme: dark) {
141
+ :root:not(.light) {
142
+ --bg-primary: #000000;
143
+ --bg-secondary: #1c1c1e;
144
+ --bg-tertiary: #2c2c2e;
145
+ --bg-card: #1c1c1e;
146
+ --bg-card-hover: #2c2c2e;
147
+ --bg-glass: rgba(28, 28, 30, 0.72);
148
+
149
+ --text-primary: #f5f5f7;
150
+ --text-secondary: #a1a1a6;
151
+ --text-tertiary: #6e6e73;
152
+
153
+ --border-color: rgba(255, 255, 255, 0.08);
154
+ --border-color-strong: rgba(255, 255, 255, 0.12);
155
+
156
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
157
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3);
158
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.4);
159
+ --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.2), 0 0 1px rgba(255, 255, 255, 0.05);
160
+ --shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.4);
161
+ }
162
+ }
163
+
164
+ /* Base styles */
165
+ * {
166
+ box-sizing: border-box;
167
+ }
168
+
169
+ body, .gradio-container {
170
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif !important;
171
+ background: var(--bg-secondary) !important;
172
+ color: var(--text-primary) !important;
173
+ -webkit-font-smoothing: antialiased;
174
+ -moz-osx-font-smoothing: grayscale;
175
+ }
176
+
177
+ /* Main container */
178
+ .container {
179
+ max-width: 1200px;
180
+ margin: 0 auto;
181
+ padding: 2rem 1.5rem;
182
+ }
183
+
184
+ /* Hero section */
185
+ .hero-section {
186
+ text-align: center;
187
+ padding: 3rem 1rem 2rem;
188
+ margin-bottom: 1rem;
189
+ }
190
+
191
+ .hero-title {
192
+ font-size: 2.75rem !important;
193
+ font-weight: 700 !important;
194
+ color: var(--text-primary) !important;
195
+ letter-spacing: -0.025em !important;
196
+ line-height: 1.1 !important;
197
+ margin: 0 0 0.75rem 0 !important;
198
+ background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
199
+ -webkit-background-clip: text;
200
+ background-clip: text;
201
+ }
202
+
203
+ .hero-subtitle {
204
+ font-size: 1.125rem !important;
205
+ color: var(--text-secondary) !important;
206
+ font-weight: 400 !important;
207
+ margin: 0 !important;
208
+ max-width: 600px;
209
+ margin: 0 auto !important;
210
+ }
211
+
212
+ /* Override Gradio h1 */
213
+ h1 {
214
+ font-size: 2.5rem !important;
215
+ font-weight: 700 !important;
216
+ color: var(--text-primary) !important;
217
+ letter-spacing: -0.025em !important;
218
+ text-align: center !important;
219
+ margin-bottom: 0.5rem !important;
220
+ }
221
+
222
+ /* Tab styling */
223
+ .tabs {
224
+ border: none !important;
225
+ background: transparent !important;
226
+ }
227
+
228
+ .tab-nav {
229
+ background: var(--bg-card) !important;
230
+ border-radius: var(--radius-lg) !important;
231
+ padding: 6px !important;
232
+ border: 1px solid var(--border-color) !important;
233
+ display: inline-flex !important;
234
+ gap: 4px !important;
235
+ margin-bottom: 2rem !important;
236
+ }
237
+
238
+ .tab-nav button {
239
+ background: transparent !important;
240
+ border: none !important;
241
+ border-radius: var(--radius-md) !important;
242
+ padding: 0.625rem 1.25rem !important;
243
+ font-size: 0.9375rem !important;
244
+ font-weight: 500 !important;
245
+ color: var(--text-secondary) !important;
246
+ transition: all var(--transition-fast) !important;
247
+ cursor: pointer !important;
248
+ }
249
+
250
+ .tab-nav button:hover {
251
+ color: var(--text-primary) !important;
252
+ background: var(--bg-secondary) !important;
253
+ }
254
+
255
+ .tab-nav button.selected {
256
+ background: var(--bg-primary) !important;
257
+ color: var(--text-primary) !important;
258
+ box-shadow: var(--shadow-sm) !important;
259
+ }
260
+
261
+ /* Dataset Card */
262
+ .dataset-card {
263
+ background: var(--bg-card);
264
+ border: 1px solid var(--border-color);
265
+ border-radius: var(--radius-xl);
266
+ padding: 1.5rem;
267
+ margin-bottom: 1rem;
268
+ transition: all var(--transition-base);
269
+ position: relative;
270
+ overflow: hidden;
271
+ }
272
+
273
+ .dataset-card::before {
274
+ content: '';
275
+ position: absolute;
276
+ top: 0;
277
+ left: 0;
278
+ right: 0;
279
+ height: 3px;
280
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-green));
281
+ opacity: 0;
282
+ transition: opacity var(--transition-base);
283
+ }
284
+
285
+ .dataset-card:hover {
286
+ border-color: var(--border-color-strong);
287
+ box-shadow: var(--shadow-card-hover);
288
+ transform: translateY(-2px);
289
+ }
290
+
291
+ .dataset-card:hover::before {
292
+ opacity: 1;
293
+ }
294
+
295
+ .card-header {
296
+ display: flex;
297
+ justify-content: space-between;
298
+ align-items: flex-start;
299
+ gap: 1rem;
300
+ margin-bottom: 0.75rem;
301
+ }
302
+
303
+ .card-title {
304
+ font-size: 1.25rem;
305
+ font-weight: 600;
306
+ color: var(--text-primary);
307
+ margin: 0;
308
+ line-height: 1.3;
309
+ letter-spacing: -0.01em;
310
+ }
311
+
312
+ .card-badge {
313
+ background: var(--bg-secondary);
314
+ color: var(--text-secondary);
315
+ padding: 0.375rem 0.875rem;
316
+ border-radius: var(--radius-full);
317
+ font-size: 0.75rem;
318
+ font-weight: 500;
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.04em;
321
+ white-space: nowrap;
322
+ border: 1px solid var(--border-color);
323
+ }
324
+
325
+ .card-description {
326
+ color: var(--text-secondary);
327
+ font-size: 0.9375rem;
328
+ line-height: 1.6;
329
+ margin: 0 0 1.25rem 0;
330
+ }
331
+
332
+ .card-footer {
333
+ display: flex;
334
+ gap: 0.75rem;
335
+ align-items: center;
336
+ flex-wrap: wrap;
337
+ }
338
+
339
+ /* Button styling */
340
+ .btn {
341
+ display: inline-flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ gap: 0.5rem;
345
+ padding: 0.625rem 1.25rem;
346
+ border-radius: var(--radius-full);
347
+ font-size: 0.875rem;
348
+ font-weight: 500;
349
+ text-decoration: none;
350
+ cursor: pointer;
351
+ transition: all var(--transition-fast);
352
+ border: none;
353
+ outline: none;
354
+ }
355
+
356
+ .btn-primary {
357
+ background: var(--text-primary);
358
+ color: var(--bg-primary);
359
+ }
360
+
361
+ .btn-primary:hover {
362
+ opacity: 0.85;
363
+ transform: scale(1.02);
364
+ }
365
+
366
+ .btn-secondary {
367
+ background: transparent;
368
+ color: var(--text-primary);
369
+ border: 1px solid var(--border-color-strong);
370
+ }
371
+
372
+ .btn-secondary:hover {
373
+ background: var(--bg-secondary);
374
+ border-color: var(--text-tertiary);
375
+ }
376
+
377
+ .btn-accent {
378
+ background: var(--accent-blue);
379
+ color: white;
380
+ }
381
+
382
+ .btn-accent:hover {
383
+ background: var(--accent-blue-hover);
384
+ transform: scale(1.02);
385
+ }
386
+
387
+ /* Price tag */
388
+ .price-tag {
389
+ font-size: 1rem;
390
+ font-weight: 600;
391
+ color: var(--text-primary);
392
+ display: flex;
393
+ align-items: baseline;
394
+ gap: 0.25rem;
395
+ }
396
+
397
+ .price-tag .currency {
398
+ font-size: 0.875rem;
399
+ font-weight: 500;
400
+ }
401
+
402
+ .price-tag .period {
403
+ font-size: 0.75rem;
404
+ color: var(--text-tertiary);
405
+ font-weight: 400;
406
+ }
407
+
408
+ .price-free {
409
+ color: var(--accent-green);
410
+ }
411
+
412
+ /* Empty state */
413
+ .empty-state {
414
+ text-align: center;
415
+ padding: 4rem 2rem;
416
+ color: var(--text-secondary);
417
+ }
418
+
419
+ .empty-state-icon {
420
+ font-size: 3rem;
421
+ margin-bottom: 1rem;
422
+ opacity: 0.5;
423
+ }
424
+
425
+ .empty-state-title {
426
+ font-size: 1.25rem;
427
+ font-weight: 600;
428
+ color: var(--text-primary);
429
+ margin-bottom: 0.5rem;
430
+ }
431
+
432
+ .empty-state-text {
433
+ font-size: 0.9375rem;
434
+ color: var(--text-secondary);
435
+ }
436
+
437
+ /* Admin section */
438
+ .admin-section {
439
+ background: var(--bg-card);
440
+ border: 1px solid var(--border-color);
441
+ border-radius: var(--radius-xl);
442
+ padding: 1.5rem;
443
+ margin-bottom: 1.5rem;
444
+ }
445
+
446
+ .admin-section-title {
447
+ font-size: 1.125rem;
448
+ font-weight: 600;
449
+ color: var(--text-primary);
450
+ margin: 0 0 1rem 0;
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 0.5rem;
454
+ }
455
+
456
+ /* Form styling */
457
+ .form-group {
458
+ margin-bottom: 1rem;
459
+ }
460
+
461
+ .form-label {
462
+ display: block;
463
+ font-size: 0.875rem;
464
+ font-weight: 500;
465
+ color: var(--text-primary);
466
+ margin-bottom: 0.5rem;
467
+ }
468
+
469
+ /* Gradio component overrides */
470
+ .gradio-container .gr-button {
471
+ border-radius: var(--radius-full) !important;
472
+ font-weight: 500 !important;
473
+ transition: all var(--transition-fast) !important;
474
+ }
475
+
476
+ .gradio-container .gr-button-primary {
477
+ background: var(--text-primary) !important;
478
+ border: none !important;
479
+ }
480
+
481
+ .gradio-container .gr-button-secondary {
482
+ background: transparent !important;
483
+ border: 1px solid var(--border-color-strong) !important;
484
+ color: var(--text-primary) !important;
485
+ }
486
+
487
+ .gradio-container .gr-input,
488
+ .gradio-container .gr-text-input,
489
+ .gradio-container textarea {
490
+ background: var(--bg-secondary) !important;
491
+ border: 1px solid var(--border-color) !important;
492
+ border-radius: var(--radius-md) !important;
493
+ color: var(--text-primary) !important;
494
+ transition: all var(--transition-fast) !important;
495
+ }
496
+
497
+ .gradio-container .gr-input:focus,
498
+ .gradio-container .gr-text-input:focus,
499
+ .gradio-container textarea:focus {
500
+ border-color: var(--accent-blue) !important;
501
+ box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.15) !important;
502
+ }
503
+
504
+ .gradio-container .gr-box {
505
+ background: var(--bg-card) !important;
506
+ border: 1px solid var(--border-color) !important;
507
+ border-radius: var(--radius-lg) !important;
508
+ }
509
+
510
+ .gradio-container .gr-panel {
511
+ background: var(--bg-card) !important;
512
+ border: 1px solid var(--border-color) !important;
513
+ border-radius: var(--radius-lg) !important;
514
+ }
515
+
516
+ /* Group styling */
517
+ .gradio-container .gr-group {
518
+ background: transparent !important;
519
+ border: none !important;
520
+ padding: 0 !important;
521
+ }
522
+
523
+ /* JSON viewer */
524
+ .gradio-container .gr-json {
525
+ background: var(--bg-secondary) !important;
526
+ border-radius: var(--radius-md) !important;
527
+ }
528
+
529
+ /* Markdown text color */
530
+ .gradio-container .prose {
531
+ color: var(--text-primary) !important;
532
+ }
533
+
534
+ .gradio-container .prose h1,
535
+ .gradio-container .prose h2,
536
+ .gradio-container .prose h3 {
537
+ color: var(--text-primary) !important;
538
+ }
539
+
540
+ .gradio-container .prose p {
541
+ color: var(--text-secondary) !important;
542
+ }
543
+
544
+ /* Row and column spacing */
545
+ .gradio-container .gr-row {
546
+ gap: 0.75rem !important;
547
+ }
548
+
549
+ /* Status message styling */
550
+ .status-success {
551
+ background: rgba(48, 209, 88, 0.1);
552
+ border: 1px solid rgba(48, 209, 88, 0.2);
553
+ color: var(--accent-green);
554
+ padding: 0.75rem 1rem;
555
+ border-radius: var(--radius-md);
556
+ font-size: 0.875rem;
557
+ }
558
+
559
+ .status-error {
560
+ background: rgba(255, 69, 58, 0.1);
561
+ border: 1px solid rgba(255, 69, 58, 0.2);
562
+ color: #ff453a;
563
+ padding: 0.75rem 1rem;
564
+ border-radius: var(--radius-md);
565
+ font-size: 0.875rem;
566
+ }
567
+
568
+ /* Subscription card specific */
569
+ .subscription-card {
570
+ background: var(--bg-card);
571
+ border: 1px solid var(--border-color);
572
+ border-radius: var(--radius-xl);
573
+ padding: 1.25rem 1.5rem;
574
+ margin-bottom: 0.75rem;
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: space-between;
578
+ gap: 1rem;
579
+ transition: all var(--transition-base);
580
+ }
581
+
582
+ .subscription-card:hover {
583
+ border-color: var(--border-color-strong);
584
+ box-shadow: var(--shadow-card);
585
+ }
586
+
587
+ .subscription-info {
588
+ flex: 1;
589
+ }
590
+
591
+ .subscription-dataset {
592
+ font-size: 1rem;
593
+ font-weight: 600;
594
+ color: var(--text-primary);
595
+ margin: 0 0 0.25rem 0;
596
+ }
597
+
598
+ .subscription-meta {
599
+ font-size: 0.8125rem;
600
+ color: var(--text-tertiary);
601
+ display: flex;
602
+ gap: 1rem;
603
+ align-items: center;
604
+ }
605
+
606
+ .subscription-status {
607
+ display: inline-flex;
608
+ align-items: center;
609
+ gap: 0.375rem;
610
+ padding: 0.25rem 0.625rem;
611
+ border-radius: var(--radius-full);
612
+ font-size: 0.75rem;
613
+ font-weight: 500;
614
+ }
615
+
616
+ .subscription-status.active {
617
+ background: rgba(48, 209, 88, 0.15);
618
+ color: var(--accent-green);
619
+ }
620
+
621
+ .subscription-status.expired {
622
+ background: rgba(255, 69, 58, 0.15);
623
+ color: #ff453a;
624
+ }
625
+
626
+ /* Login card */
627
+ .login-card {
628
+ background: var(--bg-card);
629
+ border: 1px solid var(--border-color);
630
+ border-radius: var(--radius-xl);
631
+ padding: 2rem;
632
+ max-width: 400px;
633
+ margin: 2rem auto;
634
+ text-align: center;
635
+ }
636
+
637
+ .login-card-title {
638
+ font-size: 1.25rem;
639
+ font-weight: 600;
640
+ color: var(--text-primary);
641
+ margin: 0 0 0.5rem 0;
642
+ }
643
+
644
+ .login-card-subtitle {
645
+ font-size: 0.9375rem;
646
+ color: var(--text-secondary);
647
+ margin: 0 0 1.5rem 0;
648
+ }
649
+
650
+ /* Stats grid */
651
+ .stats-grid {
652
+ display: grid;
653
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
654
+ gap: 1rem;
655
+ margin-bottom: 1.5rem;
656
+ }
657
+
658
+ .stat-card {
659
+ background: var(--bg-card);
660
+ border: 1px solid var(--border-color);
661
+ border-radius: var(--radius-lg);
662
+ padding: 1.25rem;
663
+ text-align: center;
664
+ }
665
+
666
+ .stat-value {
667
+ font-size: 2rem;
668
+ font-weight: 700;
669
+ color: var(--text-primary);
670
+ line-height: 1;
671
+ margin-bottom: 0.25rem;
672
+ }
673
+
674
+ .stat-label {
675
+ font-size: 0.8125rem;
676
+ color: var(--text-tertiary);
677
+ text-transform: uppercase;
678
+ letter-spacing: 0.04em;
679
+ }
680
+
681
+ /* Animation */
682
+ @keyframes fadeIn {
683
+ from { opacity: 0; transform: translateY(10px); }
684
+ to { opacity: 1; transform: translateY(0); }
685
+ }
686
+
687
+ .animate-in {
688
+ animation: fadeIn 0.3s ease-out forwards;
689
+ }
690
+
691
+ /* Responsive adjustments */
692
+ @media (max-width: 768px) {
693
+ .hero-title {
694
+ font-size: 2rem !important;
695
+ }
696
+
697
+ .hero-subtitle {
698
+ font-size: 1rem !important;
699
+ }
700
+
701
+ .card-header {
702
+ flex-direction: column;
703
+ gap: 0.5rem;
704
+ }
705
+
706
+ .card-footer {
707
+ flex-direction: column;
708
+ align-items: stretch;
709
+ }
710
+
711
+ .subscription-card {
712
+ flex-direction: column;
713
+ align-items: stretch;
714
+ }
715
+ }
716
+ """
utils.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+
4
+ # Configuration for MCP server
5
+ MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000")
6
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
7
+
8
+
9
+ def call_api_endpoint(endpoint: str, data: dict = None, method: str = "POST"):
10
+ """Call an API endpoint on the MCP server."""
11
+ url = f"{MCP_SERVER_URL}/api/{endpoint}"
12
+
13
+ # Include HF token for authentication to private Spaces
14
+ headers = {}
15
+ if HF_TOKEN:
16
+ headers["Authorization"] = f"Bearer {HF_TOKEN}"
17
+
18
+ try:
19
+ if method == "GET":
20
+ response = requests.get(url, headers=headers)
21
+ else:
22
+ response = requests.post(url, json=data or {}, headers=headers)
23
+ response.raise_for_status()
24
+ return response.json()
25
+ except Exception as e:
26
+ print(f"Error calling API {endpoint}: {e}")
27
+ return {"error": str(e)}
28
+
29
+
30
+ def get_catalog():
31
+ """Fetch the dataset catalog from MCP server."""
32
+ result = call_api_endpoint("catalog", method="GET")
33
+ if isinstance(result, list):
34
+ return result
35
+ if "error" in result:
36
+ print(f"Error fetching catalog: {result['error']}")
37
+ return []
38
+
39
+
40
+ def get_user_subscriptions(hf_user: str, hf_token: str = None):
41
+ """Fetch subscriptions for a specific user. Requires HF token for authentication."""
42
+ if not hf_user:
43
+ return []
44
+ if not hf_token:
45
+ print("Warning: hf_token required for user_subscriptions")
46
+ return []
47
+ result = call_api_endpoint("user_subscriptions", {
48
+ "hf_user": hf_user,
49
+ "hf_token": hf_token
50
+ })
51
+ if isinstance(result, list):
52
+ return result
53
+ if "error" in result:
54
+ print(f"Error fetching subscriptions: {result['error']}")
55
+ return []
56
+
57
+
58
+ def subscribe_free(dataset_id: str, hf_user: str, hf_token: str = None):
59
+ """Subscribe to a free dataset."""
60
+ return call_api_endpoint("subscribe_free", {
61
+ "dataset_id": dataset_id,
62
+ "hf_token": hf_token or "",
63
+ "hf_user": hf_user
64
+ })
65
+
66
+
67
+ def create_checkout_session(dataset_id: str, hf_user: str, hf_token: str = None):
68
+ """Create a Stripe checkout session for a paid dataset."""
69
+ return call_api_endpoint("create_checkout_session", {
70
+ "dataset_id": dataset_id,
71
+ "hf_token": hf_token or "",
72
+ "hf_user": hf_user
73
+ })
74
+
75
+
76
+ # Legacy wrapper for backwards compatibility
77
+ def call_mcp_tool(tool_name: str, arguments: dict):
78
+ """Legacy wrapper. Use specific functions above instead."""
79
+ if tool_name == "subscribe_free":
80
+ return call_api_endpoint("subscribe_free", arguments)
81
+ elif tool_name == "create_checkout_session":
82
+ return call_api_endpoint("create_checkout_session", arguments)
83
+ elif tool_name == "get_dataset_catalog":
84
+ return get_catalog()
85
+
86
+ print(f"Tool {tool_name} not supported via API.")
87
+ return None