| | from datetime import datetime |
| | import json |
| | import os |
| | import html |
| |
|
| | |
| | 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 = "" |
| | modals_html = "" |
| |
|
| | for idx, sub in enumerate(subscriptions): |
| | |
| | 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: |
| | |
| | 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', '') |
| | modal_id = f"modal-{idx}" |
| |
|
| | |
| | connection_btn_html = "" |
| | if is_active and access_token: |
| | connection_btn_html = f''' |
| | <button class="btn-connect" onclick="document.getElementById('{modal_id}').classList.add('show')"> |
| | Connect |
| | </button> |
| | ''' |
| |
|
| | |
| | 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) |
| | mcp_config_for_js = json.dumps(mcp_config_json) |
| |
|
| | |
| | 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) |
| | example_prompt_for_js = json.dumps(example_prompt) |
| |
|
| | |
| | modals_html += f''' |
| | <div id="{modal_id}" class="modal-overlay" onclick="if(event.target===this) this.classList.remove('show')"> |
| | <div class="modal-content"> |
| | <div class="modal-header"> |
| | <div class="modal-header-info"> |
| | <h3 class="modal-title">{html.escape(dataset_id)}</h3> |
| | <span class="modal-status {status_class}">{status_text}</span> |
| | </div> |
| | <button class="modal-close" onclick="document.getElementById('{modal_id}').classList.remove('show')">×</button> |
| | </div> |
| | <div class="modal-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="copyToClipboard({mcp_config_for_js}, this)">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="copyToClipboard({example_prompt_for_js}, this)">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="copyToClipboard('{access_token}', this)">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_btn_html = "" |
| | if is_active: |
| | view_btn_html = f''' |
| | <a href="https://huggingface.co/datasets/{dataset_id}" target="_blank" class="btn-link"> |
| | View on HF |
| | </a> |
| | ''' |
| |
|
| | |
| | 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> |
| | """ |
| |
|
| | result_html = f""" |
| | <div class="subscriptions-container"> |
| | <div class="subscriptions-list"> |
| | {cards_html} |
| | </div> |
| | {modals_html} |
| | </div> |
| | |
| | <script> |
| | function copyToClipboard(text, btn) {{ |
| | navigator.clipboard.writeText(text).then(function() {{ |
| | const originalText = btn.textContent; |
| | btn.textContent = 'Copied!'; |
| | btn.classList.add('copied'); |
| | setTimeout(function() {{ |
| | btn.textContent = originalText; |
| | btn.classList.remove('copied'); |
| | }}, 2000); |
| | }}); |
| | }} |
| | |
| | document.addEventListener('keydown', function(e) {{ |
| | if (e.key === 'Escape') {{ |
| | document.querySelectorAll('.modal-overlay.show').forEach(function(modal) {{ |
| | modal.classList.remove('show'); |
| | }}); |
| | }} |
| | }}); |
| | </script> |
| | |
| | <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; |
| | }} |
| | .btn-link {{ |
| | padding: 0.375rem 0.75rem; |
| | border-radius: 6px; |
| | font-size: 0.75rem; |
| | font-weight: 500; |
| | text-decoration: none; |
| | color: var(--text-secondary, #a1a1a6); |
| | background: var(--bg-tertiary, #2c2c2e); |
| | transition: all 0.15s ease; |
| | }} |
| | .btn-link:hover {{ |
| | background: var(--bg-hover, #3a3a3c); |
| | color: var(--text-primary, #f5f5f7); |
| | }} |
| | |
| | /* Modal - use !important to override Gradio styles */ |
| | .modal-overlay {{ |
| | display: none !important; |
| | position: fixed !important; |
| | top: 0 !important; |
| | left: 0 !important; |
| | right: 0 !important; |
| | bottom: 0 !important; |
| | width: 100vw !important; |
| | height: 100vh !important; |
| | background: rgba(0, 0, 0, 0.8) !important; |
| | backdrop-filter: blur(8px) !important; |
| | z-index: 99999 !important; |
| | justify-content: center !important; |
| | align-items: center !important; |
| | padding: 1rem !important; |
| | margin: 0 !important; |
| | }} |
| | .modal-overlay.show {{ |
| | display: flex !important; |
| | }} |
| | .modal-content {{ |
| | background: var(--bg-primary, #000) !important; |
| | border: 1px solid var(--border-color, rgba(255,255,255,0.08)) !important; |
| | border-radius: 14px !important; |
| | max-width: 480px !important; |
| | width: 100% !important; |
| | max-height: 85vh !important; |
| | overflow-y: auto !important; |
| | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important; |
| | margin: auto !important; |
| | }} |
| | .modal-header {{ |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | padding: 1rem 1.25rem; |
| | border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08)); |
| | gap: 1rem; |
| | }} |
| | .modal-header-info {{ |
| | display: flex; |
| | align-items: center; |
| | gap: 0.625rem; |
| | min-width: 0; |
| | flex: 1; |
| | }} |
| | .modal-title {{ |
| | margin: 0; |
| | font-size: 0.875rem; |
| | font-weight: 600; |
| | color: var(--text-primary, #f5f5f7); |
| | font-family: 'SF Mono', Monaco, monospace; |
| | white-space: nowrap; |
| | overflow: hidden; |
| | text-overflow: ellipsis; |
| | }} |
| | .modal-status {{ |
| | padding: 0.125rem 0.375rem; |
| | border-radius: 4px; |
| | font-size: 0.5625rem; |
| | font-weight: 600; |
| | text-transform: uppercase; |
| | flex-shrink: 0; |
| | }} |
| | .modal-status.active {{ |
| | background: rgba(52, 199, 89, 0.15); |
| | color: #34c759; |
| | }} |
| | .modal-close {{ |
| | background: none; |
| | border: none; |
| | font-size: 1.25rem; |
| | color: var(--text-tertiary, #6e6e73); |
| | cursor: pointer; |
| | padding: 0; |
| | line-height: 1; |
| | transition: color 0.15s; |
| | flex-shrink: 0; |
| | }} |
| | .modal-close:hover {{ |
| | color: var(--text-primary, #f5f5f7); |
| | }} |
| | .modal-body {{ |
| | padding: 1rem 1.25rem; |
| | }} |
| | |
| | /* Accordion sections */ |
| | .config-accordion {{ |
| | border: 1px solid var(--border-color, rgba(255,255,255,0.08)); |
| | border-radius: 8px; |
| | margin-bottom: 0.625rem; |
| | overflow: hidden; |
| | }} |
| | .config-summary {{ |
| | display: flex; |
| | align-items: center; |
| | gap: 0.5rem; |
| | padding: 0.75rem 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: 0.875rem; |
| | }} |
| | .config-label {{ |
| | font-size: 0.8125rem; |
| | 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.75rem 1rem; |
| | border-top: 1px solid var(--border-color, rgba(255,255,255,0.08)); |
| | background: var(--bg-primary, #000); |
| | }} |
| | .config-desc {{ |
| | font-size: 0.75rem; |
| | color: var(--text-tertiary, #6e6e73); |
| | margin: 0 0 0.5rem 0; |
| | }} |
| | .code-wrapper {{ |
| | position: relative; |
| | }} |
| | .code-block {{ |
| | padding: 0.75rem; |
| | 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.6875rem; |
| | 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.5rem 0.75rem; |
| | 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.6875rem; |
| | color: var(--text-primary, #f5f5f7); |
| | word-break: break-all; |
| | }} |
| | .btn-copy {{ |
| | position: absolute; |
| | top: 0.5rem; |
| | right: 0.5rem; |
| | padding: 0.25rem 0.5rem; |
| | 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.625rem; |
| | 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.625rem 0.75rem; |
| | background: rgba(0, 122, 255, 0.08); |
| | border-radius: 6px; |
| | font-size: 0.6875rem; |
| | color: var(--text-secondary, #a1a1a6); |
| | margin-top: 0.25rem; |
| | }} |
| | .config-tip code {{ |
| | background: var(--bg-secondary, #1c1c1e); |
| | padding: 0.0625rem 0.25rem; |
| | border-radius: 3px; |
| | font-size: 0.625rem; |
| | color: var(--text-primary, #f5f5f7); |
| | }} |
| | |
| | @media (max-width: 640px) {{ |
| | .subscription-card {{ |
| | flex-direction: column; |
| | align-items: stretch; |
| | }} |
| | .subscription-actions {{ |
| | justify-content: flex-start; |
| | }} |
| | .modal-content {{ |
| | margin: 0.5rem; |
| | max-width: none; |
| | }} |
| | }} |
| | </style> |
| | """ |
| |
|
| | return result_html |
| |
|