|
|
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() |
|
|
|