| from __future__ import annotations |
|
|
| import gradio as gr |
| from dotenv import load_dotenv |
| from components import subscriptions |
| import utils |
| import theme |
| import os |
|
|
| load_dotenv() |
|
|
| |
| __version__ = "3.2.0" |
|
|
| |
| print("=== OAuth Debug Info (Gradio 6.0.1) ===") |
| print(f"OAUTH_CLIENT_ID set: {bool(os.getenv('OAUTH_CLIENT_ID'))}") |
| print(f"OAUTH_CLIENT_SECRET set: {bool(os.getenv('OAUTH_CLIENT_SECRET'))}") |
| print(f"OAUTH_SCOPES: {os.getenv('OAUTH_SCOPES')}") |
| print(f"SPACE_HOST: {os.getenv('SPACE_HOST', 'not set')}") |
| print("========================================") |
|
|
|
|
| def main(): |
| |
| custom_css = theme.css + """ |
| /* Hide any share buttons globally */ |
| .share-button, |
| [class*="share"], |
| button[title="Share"], |
| .icon-buttons, |
| .image-button-group { |
| display: none !important; |
| } |
| |
| /* Hero section styling */ |
| .hero-section { |
| justify-content: center !important; |
| padding: 1.5rem 1rem 0.5rem !important; |
| background: transparent !important; |
| } |
| .hero-logo { |
| max-width: 200px !important; |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| margin: 0 auto !important; |
| } |
| .hero-logo img { |
| object-fit: contain !important; |
| } |
| .hero-subtitle { |
| font-size: 1rem !important; |
| color: var(--text-secondary, #98989d) !important; |
| margin: 0 0 1rem 0 !important; |
| font-weight: 400 !important; |
| text-align: center !important; |
| } |
| /* Force center the subtitle container */ |
| .hero-subtitle-wrapper { |
| width: 100% !important; |
| display: flex !important; |
| justify-content: center !important; |
| } |
| .hero-subtitle-wrapper + div, |
| .hero-subtitle-wrapper ~ div { |
| width: 100%; |
| } |
| |
| /* Dataset card styling */ |
| .dataset-card-group { |
| background: var(--bg-card, #1c1c1e) !important; |
| border: 1px solid rgba(255,255,255,0.06) !important; |
| border-radius: 16px !important; |
| padding: 1.25rem 1.5rem !important; |
| margin-bottom: 0.75rem !important; |
| transition: all 0.2s ease !important; |
| } |
| .dataset-card-group:hover { |
| border-color: rgba(255,255,255,0.12) !important; |
| background: rgba(255,255,255,0.02) !important; |
| } |
| |
| /* Card header */ |
| .card-header-row { |
| margin-bottom: 0.5rem !important; |
| gap: 1rem !important; |
| align-items: center !important; |
| } |
| .card-title-col { |
| min-width: 0 !important; |
| } |
| .card-price-col { |
| flex-shrink: 0 !important; |
| min-width: auto !important; |
| } |
| .dataset-title { |
| font-size: 1.125rem !important; |
| font-weight: 600 !important; |
| margin: 0 !important; |
| color: var(--text-primary, #f5f5f7) !important; |
| line-height: 1.4 !important; |
| } |
| .dataset-id { |
| font-size: 0.6875rem !important; |
| color: var(--text-tertiary, #6e6e73) !important; |
| margin: 0.25rem 0 0 0 !important; |
| font-family: 'JetBrains Mono', monospace !important; |
| background: rgba(255,255,255,0.04) !important; |
| padding: 0.15rem 0.4rem !important; |
| border-radius: 4px !important; |
| display: inline-block !important; |
| } |
| .dataset-desc { |
| font-size: 0.875rem !important; |
| color: var(--text-secondary, #98989d) !important; |
| line-height: 1.5 !important; |
| margin: 0 0 1rem 0 !important; |
| } |
| |
| /* Price badges */ |
| .price-badge { |
| display: inline-flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| padding: 0.4rem 0.9rem !important; |
| border-radius: 20px !important; |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| white-space: nowrap !important; |
| } |
| .price-badge.paid { |
| background: rgba(255,255,255,0.08) !important; |
| color: var(--text-primary, #f5f5f7) !important; |
| } |
| .price-badge.free { |
| background: rgba(52, 199, 89, 0.15) !important; |
| color: #32d74b !important; |
| } |
| |
| /* Card footer */ |
| .card-footer-row { |
| padding-top: 0.875rem !important; |
| margin-top: 0.25rem !important; |
| border-top: 1px solid rgba(255,255,255,0.06) !important; |
| gap: 0 !important; |
| } |
| .card-footer-row > div { |
| flex: 1 !important; |
| } |
| |
| /* Subscribe buttons */ |
| .subscribe-btn, |
| .subscribe-btn button { |
| width: 100% !important; |
| background: linear-gradient(180deg, #f7d052 0%, #e9b93a 100%) !important; |
| color: #1a1a1a !important; |
| border: none !important; |
| border-radius: 10px !important; |
| padding: 0.75rem 1.5rem !important; |
| font-weight: 600 !important; |
| font-size: 0.9375rem !important; |
| transition: all 0.15s ease !important; |
| cursor: pointer !important; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1), inset 0 1px 0 rgba(255,255,255,0.2) !important; |
| } |
| .subscribe-btn:hover, |
| .subscribe-btn:hover button { |
| background: linear-gradient(180deg, #fad965 0%, #f0c243 100%) !important; |
| box-shadow: 0 2px 8px rgba(247, 208, 82, 0.3), inset 0 1px 0 rgba(255,255,255,0.25) !important; |
| } |
| .subscribe-btn:active, |
| .subscribe-btn:active button { |
| background: linear-gradient(180deg, #e5b835 0%, #d4a52e 100%) !important; |
| box-shadow: inset 0 1px 2px rgba(0,0,0,0.1) !important; |
| } |
| |
| /* Login hint */ |
| .login-hint { |
| font-size: 0.8125rem !important; |
| color: var(--text-tertiary, #6e6e73) !important; |
| font-style: italic !important; |
| margin: 0 !important; |
| padding: 0.5rem 0 !important; |
| text-align: center !important; |
| } |
| |
| /* Tighten up Gradio's default spacing */ |
| .gradio-container .gr-group { |
| gap: 0 !important; |
| } |
| .gradio-container .gr-block { |
| padding: 0 !important; |
| } |
| """ |
|
|
| with gr.Blocks(title="DataPass", theme=theme.get_theme(), css=custom_css) as demo: |
| |
| logged_in_user = gr.State(value=None) |
|
|
| |
| with gr.Row(elem_classes="hero-section"): |
| with gr.Column(): |
| gr.Image( |
| value="datapass_logo.png", |
| show_label=False, |
| show_download_button=False, |
| show_fullscreen_button=False, |
| show_share_button=False, |
| container=False, |
| height=120, |
| elem_classes="hero-logo" |
| ) |
| gr.HTML('<p style="text-align:center;color:#98989d;margin:0.5rem 0 1rem 0;font-size:1.25rem;">Your pass to private datasets.</p>') |
|
|
| |
| with gr.Row(): |
| user_status = gr.Markdown() |
| gr.LoginButton(size="sm") |
|
|
| |
| subscribe_status = gr.Markdown() |
|
|
| |
| with gr.Tabs() as tabs: |
| |
| with gr.Tab("How It Works", id="about"): |
| gr.HTML(""" |
| <div class="about-section"> |
| <!-- Traditional vs DataPass Comparison --> |
| <div class="comparison-container"> |
| <h2 class="section-heading">Traditional Sharing vs DataPass</h2> |
| <div class="comparison-grid"> |
| <div class="comparison-card traditional"> |
| <div class="comparison-icon">📥</div> |
| <h3>Traditional</h3> |
| <div class="comparison-flow"> |
| <span class="flow-step">Share dataset</span> |
| <span class="flow-arrow">→</span> |
| <span class="flow-step">User downloads everything</span> |
| </div> |
| <p class="comparison-result bad">❌ You lose control of your data</p> |
| </div> |
| <div class="comparison-card datapass"> |
| <div class="comparison-icon">🔐</div> |
| <h3>DataPass</h3> |
| <div class="comparison-flow"> |
| <span class="flow-step">User asks question</span> |
| <span class="flow-arrow">→</span> |
| <span class="flow-step">Gets only the answer</span> |
| </div> |
| <p class="comparison-result good">✅ Data stays private on HF</p> |
| </div> |
| </div> |
| </div> |
| |
| <!-- How MCP Works --> |
| <div class="mcp-section"> |
| <h2 class="section-heading">MCP Does the Heavy Lifting</h2> |
| <p class="section-desc">When a subscriber asks a question, here's what happens behind the scenes:</p> |
| <div class="mcp-flow"> |
| <div class="mcp-step"> |
| <span class="step-num">1</span> |
| <span class="step-text">User asks: <em>"What are the top 10 categories?"</em></span> |
| </div> |
| <div class="mcp-step"> |
| <span class="step-num">2</span> |
| <span class="step-text">DataPass validates the access token</span> |
| </div> |
| <div class="mcp-step"> |
| <span class="step-num">3</span> |
| <span class="step-text">LLM converts question to SQL</span> |
| </div> |
| <div class="mcp-step"> |
| <span class="step-num">4</span> |
| <span class="step-text">DuckDB queries parquet files on HF</span> |
| </div> |
| <div class="mcp-step"> |
| <span class="step-num">5</span> |
| <span class="step-text">Only the result is returned to user</span> |
| </div> |
| </div> |
| <p class="mcp-note">The user never sees the SQL, never touches DuckDB, never accesses HF directly.</p> |
| </div> |
| |
| <!-- Value Props Table --> |
| <div class="value-section"> |
| <h2 class="section-heading">Why DataPass?</h2> |
| <div class="value-grid"> |
| <div class="value-column"> |
| <h3>For Dataset Owners</h3> |
| <ul class="value-list"> |
| <li>🔒 Keep your data private</li> |
| <li>⏱️ Grant time-limited access</li> |
| <li>🚫 Revoke anytime</li> |
| <li>💰 Monetize with Stripe</li> |
| </ul> |
| </div> |
| <div class="value-column"> |
| <h3>For Subscribers</h3> |
| <ul class="value-list"> |
| <li>💬 Query with SQL or plain English</li> |
| <li>⚡ No setup - just use MCP tools</li> |
| <li>📊 Get answers, not giant files</li> |
| <li>🔌 Works with Claude, Cursor, etc.</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Security Model --> |
| <div class="security-section"> |
| <h2 class="section-heading">Security Model</h2> |
| <div class="security-grid"> |
| <div class="security-item"> |
| <span class="security-icon">🗄️</span> |
| <span>Dataset files never leave Hugging Face</span> |
| </div> |
| <div class="security-item"> |
| <span class="security-icon">🎫</span> |
| <span>DataPass validated on every request</span> |
| </div> |
| <div class="security-item"> |
| <span class="security-icon">📉</span> |
| <span>Results capped - no SELECT * dumps</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <style> |
| .about-section { |
| max-width: 900px; |
| margin: 0 auto; |
| padding: 1rem 0; |
| } |
| .section-heading { |
| font-size: 1.5rem; |
| font-weight: 700; |
| margin: 0 0 1rem 0; |
| color: var(--text-primary); |
| } |
| .section-desc { |
| color: var(--text-secondary); |
| margin-bottom: 1.5rem; |
| } |
| |
| /* Comparison */ |
| .comparison-container { |
| margin-bottom: 3rem; |
| } |
| .comparison-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 1.5rem; |
| } |
| @media (max-width: 640px) { |
| .comparison-grid { grid-template-columns: 1fr; } |
| } |
| .comparison-card { |
| padding: 1.5rem; |
| border-radius: 12px; |
| border: 1px solid var(--border-color); |
| background: var(--bg-secondary); |
| } |
| .comparison-card h3 { |
| margin: 0.5rem 0 1rem 0; |
| font-size: 1.125rem; |
| } |
| .comparison-icon { |
| font-size: 2rem; |
| } |
| .comparison-flow { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| flex-wrap: wrap; |
| margin-bottom: 1rem; |
| font-size: 0.9rem; |
| } |
| .flow-step { |
| background: var(--bg-tertiary); |
| padding: 0.375rem 0.75rem; |
| border-radius: 6px; |
| } |
| .flow-arrow { |
| color: var(--text-tertiary); |
| } |
| .comparison-result { |
| margin: 0; |
| font-weight: 500; |
| } |
| .comparison-result.bad { color: #ff6b6b; } |
| .comparison-result.good { color: #51cf66; } |
| .comparison-card.datapass { |
| border-color: #51cf66; |
| background: rgba(81, 207, 102, 0.05); |
| } |
| |
| /* MCP Flow */ |
| .mcp-section { |
| margin-bottom: 3rem; |
| } |
| .mcp-flow { |
| display: flex; |
| flex-direction: column; |
| gap: 0.75rem; |
| } |
| .mcp-step { |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| padding: 0.875rem 1rem; |
| background: var(--bg-secondary); |
| border-radius: 8px; |
| border-left: 3px solid #007AFF; |
| } |
| .step-num { |
| width: 28px; |
| height: 28px; |
| background: #007AFF; |
| color: white; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: 600; |
| font-size: 0.875rem; |
| flex-shrink: 0; |
| } |
| .step-text { |
| color: var(--text-secondary); |
| } |
| .step-text em { |
| color: var(--text-primary); |
| font-style: normal; |
| background: var(--bg-tertiary); |
| padding: 0.125rem 0.5rem; |
| border-radius: 4px; |
| font-family: monospace; |
| } |
| .mcp-note { |
| margin-top: 1rem; |
| padding: 1rem; |
| background: rgba(0, 122, 255, 0.1); |
| border-radius: 8px; |
| color: var(--text-secondary); |
| font-size: 0.9rem; |
| } |
| |
| /* Value Props */ |
| .value-section { |
| margin-bottom: 3rem; |
| } |
| .value-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 1.5rem; |
| } |
| @media (max-width: 640px) { |
| .value-grid { grid-template-columns: 1fr; } |
| } |
| .value-column { |
| padding: 1.5rem; |
| background: var(--bg-secondary); |
| border-radius: 12px; |
| border: 1px solid var(--border-color); |
| } |
| .value-column h3 { |
| margin: 0 0 1rem 0; |
| font-size: 1rem; |
| color: var(--text-primary); |
| } |
| .value-list { |
| list-style: none; |
| padding: 0; |
| margin: 0; |
| } |
| .value-list li { |
| padding: 0.5rem 0; |
| color: var(--text-secondary); |
| border-bottom: 1px solid var(--border-color); |
| } |
| .value-list li:last-child { |
| border-bottom: none; |
| } |
| |
| /* Security */ |
| .security-section { |
| margin-bottom: 2rem; |
| } |
| .security-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 1rem; |
| } |
| @media (max-width: 768px) { |
| .security-grid { grid-template-columns: 1fr; } |
| } |
| .security-item { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| padding: 1rem; |
| background: var(--bg-secondary); |
| border-radius: 8px; |
| font-size: 0.9rem; |
| color: var(--text-secondary); |
| } |
| .security-icon { |
| font-size: 1.25rem; |
| } |
| </style> |
| """) |
|
|
| |
| with gr.Tab("Catalog", id="catalog"): |
| |
| @gr.render(inputs=logged_in_user) |
| def render_catalog(username): |
| datasets = utils.get_catalog() or [] |
|
|
| if not datasets: |
| gr.HTML(""" |
| <div class="empty-state"> |
| <div class="empty-state-icon">📦</div> |
| <div class="empty-state-title">No datasets available</div> |
| <div class="empty-state-text">Check back soon for new data products.</div> |
| </div> |
| """) |
| return |
|
|
| for dataset in datasets: |
| dataset_id = dataset.get('dataset_id', '') |
| display_name = dataset.get('display_name', dataset_id) |
| description = dataset.get('description', 'No description available.') |
|
|
| |
| plans = dataset.get("plans", []) |
| is_free = False |
| price = "10" |
| if plans: |
| plan = plans[0] |
| price_id = plan.get("stripe_price_id", "") |
| if price_id in ["free", "0", 0]: |
| is_free = True |
| else: |
| price = plan.get("price", "10") |
|
|
| price_class = "free" if is_free else "paid" |
| price_text = "Free Trial" if is_free else f"${price}/mo" |
| button_text = "Start Free Trial" if is_free else f"Subscribe - ${price}/mo" |
|
|
| |
| with gr.Group(elem_classes="dataset-card-group"): |
| |
| with gr.Row(elem_classes="card-header-row"): |
| with gr.Column(scale=4, elem_classes="card-title-col"): |
| gr.HTML(f''' |
| <h3 class="dataset-title">{display_name}</h3> |
| <p class="dataset-id">{dataset_id}</p> |
| ''') |
| with gr.Column(scale=1, min_width=100, elem_classes="card-price-col"): |
| gr.HTML(f'<span class="price-badge {price_class}">{price_text}</span>') |
|
|
| |
| gr.HTML(f'<p class="dataset-desc">{description}</p>') |
|
|
| |
| with gr.Row(elem_classes="card-footer-row"): |
| if username: |
| |
| btn = gr.Button( |
| button_text, |
| variant="primary", |
| size="lg", |
| key=f"subscribe-btn-{dataset_id}", |
| elem_classes="subscribe-btn" |
| ) |
|
|
| |
| def make_subscribe_handler(ds_id, ds_name, ds_free): |
| def handler(p: gr.OAuthProfile | None, t: gr.OAuthToken | None): |
| if not p: |
| return "Please sign in first to subscribe." |
|
|
| hf_token = t.token if t else None |
|
|
| if ds_free: |
| result = utils.subscribe_free(ds_id, p.username, hf_token) |
| if "error" in result: |
| return f"Error: {result['error']}" |
| return f"Your 24-hour DataPass for **{ds_name}** is active! Go to **My Subscriptions** to get your access token." |
| else: |
| result = utils.create_checkout_session(ds_id, p.username, hf_token) |
| if "error" in result: |
| return f"Error: {result['error']}" |
| if "checkout_url" in result: |
| return f"[Click here to complete payment]({result['checkout_url']})" |
| return "Error creating checkout session." |
| return handler |
|
|
| btn.click( |
| fn=make_subscribe_handler(dataset_id, display_name, is_free), |
| outputs=[subscribe_status] |
| ) |
| else: |
| |
| gr.HTML('<p class="login-hint">Sign in with Hugging Face to subscribe</p>') |
|
|
| |
| with gr.Tab("My Subscriptions", id="subscriptions"): |
| subscriptions_container = gr.HTML() |
|
|
| |
| gr.HTML(""" |
| <div style="text-align: center; padding: 2rem 1rem; margin-top: 2rem;"> |
| <p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.25rem;"> |
| <strong>DataPass</strong> — Your pass to private datasets. |
| </p> |
| <p style="font-size: 0.75rem; color: var(--text-tertiary);"> |
| Powered by Hugging Face & MCP |
| </p> |
| </div> |
| """) |
|
|
| |
| def load_user_status(profile: gr.OAuthProfile | None): |
| if profile: |
| |
| return f"👤 Signed in as **{profile.username}**", profile.username |
| return "Sign in to subscribe to datasets", None |
|
|
| |
| def load_subscriptions(profile: gr.OAuthProfile | None, token: gr.OAuthToken | None): |
| if not profile: |
| return """ |
| <div class="empty-state"> |
| <div class="empty-state-icon">🔐</div> |
| <div class="empty-state-title">Sign in required</div> |
| <div class="empty-state-text">Please sign in with your Hugging Face account to view your subscriptions.</div> |
| </div> |
| """ |
| |
| hf_token = token.token if token else None |
| user_subs = utils.get_user_subscriptions(profile.username, hf_token) |
| return subscriptions.create_subscriptions_html(user_subs) |
|
|
| |
| demo.load(fn=load_user_status, outputs=[user_status, logged_in_user]) |
| demo.load(fn=load_subscriptions, outputs=[subscriptions_container]) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| demo = main() |
| demo.launch() |
|
|