|
|
from datetime import datetime |
|
|
import json |
|
|
import os |
|
|
import html |
|
|
import base64 |
|
|
|
|
|
|
|
|
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 = "" |
|
|
|
|
|
copy_data = {} |
|
|
|
|
|
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', '') |
|
|
drawer_id = f"drawer-{idx}" |
|
|
|
|
|
|
|
|
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> |
|
|
''' |
|
|
|
|
|
|
|
|
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 = 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) |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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_btn_html = "" |
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
|
|
|
copy_data_json = json.dumps(copy_data) |
|
|
|
|
|
|
|
|
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 |
|
|
|