Spaces:
Sleeping
Sleeping
| from datetime import datetime | |
| import json | |
| import os | |
| import html | |
| import base64 | |
| # Get MCP server URL from environment | |
| MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000") | |
| def create_subscriptions_html(subscriptions): | |
| """Creates HTML string for user's subscriptions list.""" | |
| if not subscriptions: | |
| return """ | |
| <div class="empty-state"> | |
| <div class="empty-state-icon">🎫</div> | |
| <div class="empty-state-title">No DataPass yet</div> | |
| <div class="empty-state-text">Browse the catalog and start a 24-hour free trial to get your first DataPass.</div> | |
| </div> | |
| """ | |
| cards_html = "" | |
| drawers_html = "" | |
| # Store data for copy functionality | |
| copy_data = {} | |
| for idx, sub in enumerate(subscriptions): | |
| # Determine subscription status | |
| end_date_str = sub.get('subscription_end', '') | |
| is_active = sub.get('is_active', False) | |
| status_class = "active" if is_active else "expired" | |
| status_text = "Active" if is_active else "Expired" | |
| expires_text = "Unknown expiry" | |
| try: | |
| if end_date_str: | |
| # Handle Z suffix | |
| if end_date_str.endswith("Z"): | |
| end_date_str = end_date_str[:-1] | |
| end_date = datetime.fromisoformat(end_date_str) | |
| if is_active: | |
| days_left = (end_date - datetime.utcnow()).days | |
| hours_left = int((end_date - datetime.utcnow()).total_seconds() / 3600) | |
| if days_left > 0: | |
| expires_text = f"Expires in {days_left} day{'s' if days_left != 1 else ''}" | |
| elif hours_left > 0: | |
| expires_text = f"Expires in {hours_left} hour{'s' if hours_left != 1 else ''}" | |
| else: | |
| expires_text = "Expires soon" | |
| else: | |
| expires_text = "DataPass expired — Upgrade for continued access" | |
| except Exception: | |
| pass | |
| dataset_id = sub.get('dataset_id', '') | |
| access_token = sub.get('access_token', '') | |
| drawer_id = f"drawer-{idx}" | |
| # Connection details button for active subscriptions | |
| connection_btn_html = "" | |
| if is_active and access_token: | |
| connection_btn_html = f''' | |
| <button class="btn-connect" onclick="document.getElementById('{drawer_id}').classList.add('show'); document.body.style.overflow='hidden';"> | |
| Connect | |
| </button> | |
| ''' | |
| # Create MCP config JSON | |
| dataset_name = dataset_id.split('/')[-1] if '/' in dataset_id else dataset_id | |
| server_name = f"{dataset_name}-dataset" | |
| mcp_config = { | |
| "mcpServers": { | |
| server_name: { | |
| "url": f"{MCP_SERVER_URL}/sse", | |
| "headers": { | |
| "Authorization": f"Bearer {access_token}" | |
| } | |
| } | |
| } | |
| } | |
| mcp_config_json = json.dumps(mcp_config, indent=2) | |
| mcp_config_escaped = html.escape(mcp_config_json) | |
| # Example prompt | |
| 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?" | |
| example_prompt_escaped = html.escape(example_prompt) | |
| # Store data for copy buttons using base64 encoding to avoid JS escaping issues | |
| copy_data[f"{drawer_id}-mcp"] = base64.b64encode(mcp_config_json.encode()).decode() | |
| copy_data[f"{drawer_id}-prompt"] = base64.b64encode(example_prompt.encode()).decode() | |
| copy_data[f"{drawer_id}-token"] = base64.b64encode(access_token.encode()).decode() | |
| # Drawer HTML (slides in from right) | |
| drawers_html += f''' | |
| <div id="{drawer_id}" class="drawer-overlay" onclick="if(event.target===this){{this.classList.remove('show'); document.body.style.overflow='';}}"> | |
| <div class="drawer-panel"> | |
| <div class="drawer-header"> | |
| <div class="drawer-header-info"> | |
| <h3 class="drawer-title">{html.escape(dataset_id)}</h3> | |
| <span class="drawer-status {status_class}">{status_text}</span> | |
| </div> | |
| <button class="drawer-close" onclick="document.getElementById('{drawer_id}').classList.remove('show'); document.body.style.overflow='';">×</button> | |
| </div> | |
| <div class="drawer-body"> | |
| <!-- MCP Config - expanded by default --> | |
| <details class="config-accordion" open> | |
| <summary class="config-summary"> | |
| <span class="config-icon">🔌</span> | |
| <span class="config-label">MCP Configuration</span> | |
| <span class="chevron"></span> | |
| </summary> | |
| <div class="config-content"> | |
| <p class="config-desc">Add to your Claude Desktop config:</p> | |
| <div class="code-wrapper"> | |
| <pre class="code-block">{mcp_config_escaped}</pre> | |
| <button class="btn-copy" onclick="(function(b,v){{var t=atob(v);navigator.clipboard.writeText(t).then(function(){{b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}}).catch(function(){{var a=document.createElement('textarea');a.value=t;a.style.cssText='position:fixed;opacity:0';document.body.appendChild(a);a.select();document.execCommand('copy');document.body.removeChild(a);b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}})}})( this,'{copy_data[f"{drawer_id}-mcp"]}')">Copy</button> | |
| </div> | |
| </div> | |
| </details> | |
| <!-- Example Prompt - collapsed --> | |
| <details class="config-accordion"> | |
| <summary class="config-summary"> | |
| <span class="config-icon">💬</span> | |
| <span class="config-label">Example Prompt</span> | |
| <span class="chevron"></span> | |
| </summary> | |
| <div class="config-content"> | |
| <div class="code-wrapper"> | |
| <pre class="code-block prompt-block">{example_prompt_escaped}</pre> | |
| <button class="btn-copy" onclick="(function(b,v){{var t=atob(v);navigator.clipboard.writeText(t).then(function(){{b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}}).catch(function(){{var a=document.createElement('textarea');a.value=t;a.style.cssText='position:fixed;opacity:0';document.body.appendChild(a);a.select();document.execCommand('copy');document.body.removeChild(a);b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}})}})( this,'{copy_data[f"{drawer_id}-prompt"]}')">Copy</button> | |
| </div> | |
| </div> | |
| </details> | |
| <!-- Access Token - collapsed --> | |
| <details class="config-accordion"> | |
| <summary class="config-summary"> | |
| <span class="config-icon">🔑</span> | |
| <span class="config-label">Access Token</span> | |
| <span class="chevron"></span> | |
| </summary> | |
| <div class="config-content"> | |
| <div class="code-wrapper"> | |
| <code class="token-block">{html.escape(access_token)}</code> | |
| <button class="btn-copy" onclick="(function(b,v){{var t=atob(v);navigator.clipboard.writeText(t).then(function(){{b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}}).catch(function(){{var a=document.createElement('textarea');a.value=t;a.style.cssText='position:fixed;opacity:0';document.body.appendChild(a);a.select();document.execCommand('copy');document.body.removeChild(a);b.textContent='Copied!';b.classList.add('copied');setTimeout(function(){{b.textContent='Copy';b.classList.remove('copied')}},2000)}})}})( this,'{copy_data[f"{drawer_id}-token"]}')">Copy</button> | |
| </div> | |
| </div> | |
| </details> | |
| <div class="config-tip"> | |
| <span>Use <code>query_dataset</code> or <code>query_dataset_natural_language</code> to analyze your data.</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ''' | |
| # View on HF button removed - access only through MCP | |
| view_btn_html = "" | |
| # Determine plan display name | |
| plan_id = sub.get('plan_id', 'trial') | |
| if plan_id.lower() in ['free', 'trial', 'free_tier']: | |
| plan_display = "Free Trial" | |
| else: | |
| plan_display = f"{plan_id} plan" | |
| cards_html += f""" | |
| <div class="subscription-card"> | |
| <div class="subscription-info"> | |
| <h4 class="subscription-dataset">{html.escape(dataset_id)}</h4> | |
| <div class="subscription-meta"> | |
| <span class="plan-badge">{html.escape(plan_display)}</span> | |
| <span class="separator">·</span> | |
| <span class="expiry-text">{expires_text}</span> | |
| </div> | |
| </div> | |
| <div class="subscription-actions"> | |
| <span class="status-badge {status_class}">{status_text}</span> | |
| {connection_btn_html} | |
| {view_btn_html} | |
| </div> | |
| </div> | |
| """ | |
| # Convert copy_data to JSON for embedding in script | |
| copy_data_json = json.dumps(copy_data) | |
| # Build inline handlers that don't rely on global functions | |
| result_html = f""" | |
| <div class="subscriptions-container" id="datapass-subs-container" data-copy='{copy_data_json}'> | |
| <div class="subscriptions-list"> | |
| {cards_html} | |
| </div> | |
| {drawers_html} | |
| </div> | |
| <style> | |
| .subscriptions-container {{ | |
| position: relative; | |
| }} | |
| .subscriptions-list {{ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| }} | |
| .subscription-card {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem 1.25rem; | |
| background: var(--bg-secondary, #1c1c1e); | |
| border: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| border-radius: 12px; | |
| transition: all 0.15s ease; | |
| gap: 1rem; | |
| }} | |
| .subscription-card:hover {{ | |
| border-color: var(--border-color-strong, rgba(255,255,255,0.12)); | |
| }} | |
| .subscription-info {{ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| min-width: 0; | |
| flex: 1; | |
| }} | |
| .subscription-dataset {{ | |
| margin: 0; | |
| font-size: 0.9375rem; | |
| font-weight: 600; | |
| color: var(--text-primary, #f5f5f7); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| }} | |
| .subscription-meta {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.75rem; | |
| color: var(--text-tertiary, #6e6e73); | |
| }} | |
| .separator {{ | |
| opacity: 0.5; | |
| }} | |
| .subscription-actions {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| flex-shrink: 0; | |
| }} | |
| .status-badge {{ | |
| padding: 0.1875rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.625rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| }} | |
| .status-badge.active {{ | |
| background: rgba(52, 199, 89, 0.15); | |
| color: #34c759; | |
| }} | |
| .status-badge.expired {{ | |
| background: rgba(142, 142, 147, 0.15); | |
| color: #8e8e93; | |
| }} | |
| /* Compact Buttons */ | |
| .btn-connect {{ | |
| padding: 0.375rem 0.75rem; | |
| border-radius: 6px; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| border: none; | |
| background: #007AFF; | |
| color: white; | |
| transition: all 0.15s ease; | |
| }} | |
| .btn-connect:hover {{ | |
| background: #0056b3; | |
| }} | |
| /* Drawer - slides from right */ | |
| .drawer-overlay {{ | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(4px); | |
| z-index: 99999; | |
| margin: 0; | |
| }} | |
| .drawer-overlay.show {{ | |
| display: block; | |
| }} | |
| .drawer-panel {{ | |
| position: fixed; | |
| top: 0; | |
| right: 0; | |
| width: 100%; | |
| max-width: 420px; | |
| height: 100vh; | |
| background: var(--bg-primary, #000); | |
| border-left: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| box-shadow: -10px 0 40px rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| flex-direction: column; | |
| transform: translateX(100%); | |
| animation: slideIn 0.25s ease forwards; | |
| }} | |
| @keyframes slideIn {{ | |
| to {{ | |
| transform: translateX(0); | |
| }} | |
| }} | |
| .drawer-overlay:not(.show) .drawer-panel {{ | |
| animation: slideOut 0.2s ease forwards; | |
| }} | |
| @keyframes slideOut {{ | |
| from {{ | |
| transform: translateX(0); | |
| }} | |
| to {{ | |
| transform: translateX(100%); | |
| }} | |
| }} | |
| .drawer-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1.25rem 1.5rem; | |
| border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| gap: 1rem; | |
| flex-shrink: 0; | |
| }} | |
| .drawer-header-info {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.625rem; | |
| min-width: 0; | |
| flex: 1; | |
| }} | |
| .drawer-title {{ | |
| margin: 0; | |
| font-size: 0.9375rem; | |
| font-weight: 600; | |
| color: var(--text-primary, #f5f5f7); | |
| font-family: 'SF Mono', Monaco, monospace; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| }} | |
| .drawer-status {{ | |
| padding: 0.125rem 0.375rem; | |
| border-radius: 4px; | |
| font-size: 0.5625rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| flex-shrink: 0; | |
| }} | |
| .drawer-status.active {{ | |
| background: rgba(52, 199, 89, 0.15); | |
| color: #34c759; | |
| }} | |
| .drawer-close {{ | |
| background: none; | |
| border: none; | |
| font-size: 1.5rem; | |
| color: var(--text-tertiary, #6e6e73); | |
| cursor: pointer; | |
| padding: 0; | |
| line-height: 1; | |
| transition: color 0.15s; | |
| flex-shrink: 0; | |
| }} | |
| .drawer-close:hover {{ | |
| color: var(--text-primary, #f5f5f7); | |
| }} | |
| .drawer-body {{ | |
| padding: 1.25rem 1.5rem; | |
| overflow-y: auto; | |
| flex: 1; | |
| }} | |
| /* Accordion sections */ | |
| .config-accordion {{ | |
| border: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| border-radius: 8px; | |
| margin-bottom: 0.75rem; | |
| overflow: hidden; | |
| }} | |
| .config-summary {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.875rem 1rem; | |
| cursor: pointer; | |
| user-select: none; | |
| background: var(--bg-secondary, #1c1c1e); | |
| transition: background 0.15s; | |
| list-style: none; | |
| }} | |
| .config-summary::-webkit-details-marker {{ | |
| display: none; | |
| }} | |
| .config-summary:hover {{ | |
| background: var(--bg-tertiary, #2c2c2e); | |
| }} | |
| .config-icon {{ | |
| font-size: 1rem; | |
| }} | |
| .config-label {{ | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| color: var(--text-primary, #f5f5f7); | |
| flex: 1; | |
| }} | |
| .chevron {{ | |
| width: 0; | |
| height: 0; | |
| border-left: 4px solid transparent; | |
| border-right: 4px solid transparent; | |
| border-top: 5px solid var(--text-tertiary, #6e6e73); | |
| transition: transform 0.2s; | |
| }} | |
| details[open] .chevron {{ | |
| transform: rotate(180deg); | |
| }} | |
| .config-content {{ | |
| padding: 0.875rem 1rem; | |
| border-top: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| background: var(--bg-primary, #000); | |
| }} | |
| .config-desc {{ | |
| font-size: 0.8125rem; | |
| color: var(--text-tertiary, #6e6e73); | |
| margin: 0 0 0.625rem 0; | |
| }} | |
| .code-wrapper {{ | |
| position: relative; | |
| }} | |
| .code-block {{ | |
| padding: 0.875rem; | |
| padding-right: 4rem; | |
| background: var(--bg-secondary, #1c1c1e); | |
| border: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| border-radius: 6px; | |
| font-family: 'SF Mono', Monaco, monospace; | |
| font-size: 0.75rem; | |
| color: var(--text-primary, #f5f5f7); | |
| overflow-x: auto; | |
| white-space: pre; | |
| margin: 0; | |
| line-height: 1.5; | |
| }} | |
| .prompt-block {{ | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| }} | |
| .token-block {{ | |
| display: block; | |
| padding: 0.625rem 0.875rem; | |
| padding-right: 4rem; | |
| background: var(--bg-secondary, #1c1c1e); | |
| border: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| border-radius: 6px; | |
| font-family: 'SF Mono', Monaco, monospace; | |
| font-size: 0.75rem; | |
| color: var(--text-primary, #f5f5f7); | |
| word-break: break-all; | |
| }} | |
| .btn-copy {{ | |
| position: absolute; | |
| top: 0.5rem; | |
| right: 0.5rem; | |
| padding: 0.375rem 0.625rem; | |
| background: var(--bg-tertiary, #2c2c2e); | |
| color: var(--text-secondary, #a1a1a6); | |
| border: 1px solid var(--border-color, rgba(255,255,255,0.08)); | |
| border-radius: 4px; | |
| font-size: 0.6875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| }} | |
| .btn-copy:hover {{ | |
| background: var(--bg-hover, #3a3a3c); | |
| color: var(--text-primary, #f5f5f7); | |
| }} | |
| .btn-copy.copied {{ | |
| background: #34c759; | |
| border-color: #34c759; | |
| color: white; | |
| }} | |
| .config-tip {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.375rem; | |
| padding: 0.75rem 1rem; | |
| background: rgba(0, 122, 255, 0.08); | |
| border-radius: 6px; | |
| font-size: 0.75rem; | |
| color: var(--text-secondary, #a1a1a6); | |
| margin-top: 0.5rem; | |
| }} | |
| .config-tip code {{ | |
| background: var(--bg-secondary, #1c1c1e); | |
| padding: 0.125rem 0.375rem; | |
| border-radius: 3px; | |
| font-size: 0.6875rem; | |
| color: var(--text-primary, #f5f5f7); | |
| }} | |
| @media (max-width: 640px) {{ | |
| .subscription-card {{ | |
| flex-direction: column; | |
| align-items: stretch; | |
| }} | |
| .subscription-actions {{ | |
| justify-content: flex-start; | |
| }} | |
| .drawer-panel {{ | |
| max-width: 100%; | |
| }} | |
| }} | |
| </style> | |
| """ | |
| return result_html | |