Skip to content

Commit c91def2

Browse files
authored
Merge pull request #66 from amito/feature/ui-sortable-table-1
2 parents 732dab0 + 7dc7da3 commit c91def2

File tree

1 file changed

+167
-37
lines changed

1 file changed

+167
-37
lines changed

ui/app.py

Lines changed: 167 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
import pandas as pd
2121
import requests
22+
2223
import streamlit as st
24+
import streamlit.components.v1 as components
2325

2426
# Configuration
2527
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000")
@@ -151,7 +153,6 @@ def format_display_name(raw_name: str) -> str:
151153

152154
st.markdown("""
153155
<style>
154-
/* Import Google Fonts - Qualifire & HuggingFace Inspired Typography */
155156
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap');
156157
157158
/* Global Styles */
@@ -444,8 +445,7 @@ def format_display_name(raw_name: str) -> str:
444445
outline: none;
445446
box-shadow: 0 0 0 3px rgba(56, 239, 125, 0.25);
446447
}
447-
448-
/* TOP LEADERBOARD TABLE - HuggingFace/Qualifire Inspired */
448+
449449
.leaderboard-container {
450450
background: var(--bg-card);
451451
backdrop-filter: blur(12px);
@@ -1107,8 +1107,7 @@ def format_display_name(raw_name: str) -> str:
11071107
color: var(--text-secondary);
11081108
line-height: 1.6;
11091109
}
1110-
1111-
/* Input Container - Qualifire-inspired clean design */
1110+
11121111
.input-container {
11131112
background: var(--bg-card);
11141113
padding: 2.5rem;
@@ -5115,10 +5114,10 @@ def render_options_list_inline():
51155114

51165115
# Define categories
51175116
categories = [
5118-
("balanced", "Balanced", "#EE0000"),
5119-
("best_accuracy", "Best Accuracy", "#ffffff"),
5120-
("lowest_cost", "Lowest Cost", "#f59e0b"),
5121-
("lowest_latency", "Lowest Latency", "#ffffff"),
5117+
("balanced", "Balanced"),
5118+
("best_accuracy", "Best Accuracy"),
5119+
("lowest_cost", "Lowest Cost"),
5120+
("lowest_latency", "Lowest Latency"),
51225121
]
51235122

51245123
# Helper function to format GPU config
@@ -5131,37 +5130,168 @@ def format_gpu_config(gpu_config: dict) -> str:
51315130
replicas = gpu_config.get("replicas", 1)
51325131
return f"{gpu_count}x {gpu_type} (TP={tp}, R={replicas})"
51335132

5134-
# Build table rows
5135-
all_rows = []
5136-
for cat_key, cat_name, cat_color in categories:
5133+
# Build table data
5134+
table_data = []
5135+
for cat_key, cat_name in categories:
51375136
recs = ranked_response.get(cat_key, [])
5138-
if not recs:
5139-
all_rows.append(f'<tr style="border-bottom: 1px solid rgba(255,255,255,0.1);"><td style="padding: 0.75rem 0.5rem;"><span style="color: {cat_color}; font-weight: 600;">{cat_name}</span></td><td colspan="7" style="padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.5); font-style: italic;">No configurations found</td></tr>')
5140-
else:
5141-
for i, rec in enumerate(recs[:5]): # Show top 5 per category
5142-
model_name = format_display_name(rec.get("model_name", "Unknown"))
5143-
gpu_config = rec.get("gpu_config", {})
5144-
gpu_str = format_gpu_config(gpu_config)
5145-
ttft = rec.get("predicted_ttft_p95_ms", 0)
5146-
cost = rec.get("cost_per_month_usd", 0)
5147-
scores = rec.get("scores", {}) or {}
5148-
accuracy = scores.get("accuracy_score", 0)
5149-
balanced = scores.get("balanced_score", 0)
5150-
meets_slo = rec.get("meets_slo", False)
5151-
slo_icon = "Yes" if meets_slo else "No"
5152-
5153-
cat_display = f'<span style="color: {cat_color}; font-weight: 600;">{cat_name}</span> (+{len(recs)-1})' if i == 0 else ""
5154-
5155-
row = f'<tr style="border-bottom: 1px solid rgba(255,255,255,0.1);"><td style="padding: 0.75rem 0.5rem;">{cat_display}</td><td style="padding: 0.75rem 0.5rem; color: white; font-weight: 500;">{model_name}</td><td style="padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem;">{gpu_str}</td><td style="padding: 0.75rem 0.5rem; text-align: right; color: #06b6d4;">{ttft:.0f}ms</td><td style="padding: 0.75rem 0.5rem; text-align: right; color: #f59e0b;">${cost:,.0f}</td><td style="padding: 0.75rem 0.5rem; text-align: center; color: #10b981;">{accuracy:.0f}</td><td style="padding: 0.75rem 0.5rem; text-align: center; color: #8b5cf6;">{balanced:.1f}</td><td style="padding: 0.75rem 0.5rem; text-align: center;">{slo_icon}</td></tr>'
5156-
all_rows.append(row)
5157-
5158-
# Table header
5159-
header = '<thead><tr style="border-bottom: 2px solid rgba(255,255,255,0.2);"><th style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Category</th><th style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Model</th><th style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">GPU Config</th><th style="text-align: right; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">TTFT</th><th style="text-align: right; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Cost/mo</th><th style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Acc</th><th style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Score</th><th style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">SLO</th></tr></thead>'
5137+
for rec in recs[:5]: # Show top 5 per category
5138+
model_name = format_display_name(rec.get("model_name", "Unknown"))
5139+
gpu_config = rec.get("gpu_config", {})
5140+
gpu_str = format_gpu_config(gpu_config)
5141+
ttft = rec.get("predicted_ttft_p95_ms", 0)
5142+
cost = rec.get("cost_per_month_usd", 0)
5143+
scores = rec.get("scores", {}) or {}
5144+
accuracy = scores.get("accuracy_score", 0)
5145+
balanced = scores.get("balanced_score", 0)
5146+
meets_slo = rec.get("meets_slo", False)
5147+
slo_value = 1 if meets_slo else 0 # Numeric value for sorting
5148+
5149+
table_data.append({
5150+
"category": cat_name,
5151+
"model": model_name,
5152+
"gpu_config": gpu_str,
5153+
"ttft": ttft,
5154+
"cost": cost,
5155+
"accuracy": accuracy,
5156+
"balanced": balanced,
5157+
"slo": "Yes" if meets_slo else "No",
5158+
"slo_value": slo_value,
5159+
})
51605160

5161-
# Render table
5162-
table_html = f'<table style="width: 100%; border-collapse: collapse; background: rgba(13, 17, 23, 0.95); border-radius: 8px;">{header}<tbody>{"".join(all_rows)}</tbody></table>'
5161+
if table_data:
5162+
# Build table rows HTML
5163+
rows_html = []
5164+
for row in table_data:
5165+
rows_html.append(f'''
5166+
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1);"
5167+
data-category="{row['category']}"
5168+
data-model="{row['model']}"
5169+
data-gpu="{row['gpu_config']}"
5170+
data-ttft="{row['ttft']}"
5171+
data-cost="{row['cost']}"
5172+
data-accuracy="{row['accuracy']}"
5173+
data-balanced="{row['balanced']}"
5174+
data-slo="{row['slo_value']}">
5175+
<td style="padding: 0.75rem 0.5rem; color: white;">{row['category']}</td>
5176+
<td style="padding: 0.75rem 0.5rem; color: white; font-weight: 500;">{row['model']}</td>
5177+
<td style="padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem;">{row['gpu_config']}</td>
5178+
<td style="padding: 0.75rem 0.5rem; text-align: right; color: #06b6d4;">{row['ttft']:.0f}ms</td>
5179+
<td style="padding: 0.75rem 0.5rem; text-align: right; color: #f59e0b;">${row['cost']:,.0f}</td>
5180+
<td style="padding: 0.75rem 0.5rem; text-align: center; color: #10b981;">{row['accuracy']:.0f}</td>
5181+
<td style="padding: 0.75rem 0.5rem; text-align: center; color: #8b5cf6;">{row['balanced']:.1f}</td>
5182+
<td style="padding: 0.75rem 0.5rem; text-align: center;">{row['slo']}</td>
5183+
</tr>
5184+
''')
5185+
5186+
# Table HTML with JavaScript sorting
5187+
table_html = f'''
5188+
<!DOCTYPE html>
5189+
<html>
5190+
<head>
5191+
<style>
5192+
body {{
5193+
margin: 0;
5194+
padding: 0;
5195+
background: transparent;
5196+
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
5197+
}}
5198+
.sortable-table {{
5199+
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
5200+
}}
5201+
.sortable-table th {{
5202+
cursor: pointer;
5203+
user-select: none;
5204+
position: relative;
5205+
}}
5206+
.sortable-table th:hover {{
5207+
background: rgba(50, 50, 50, 0.8) !important;
5208+
}}
5209+
.sortable-table th.sort-asc::after {{
5210+
content: " ▲";
5211+
font-size: 0.7em;
5212+
color: #06b6d4;
5213+
}}
5214+
.sortable-table th.sort-desc::after {{
5215+
content: " ▼";
5216+
font-size: 0.7em;
5217+
color: #06b6d4;
5218+
}}
5219+
.sortable-table tbody tr:hover {{
5220+
background: rgba(30, 30, 30, 0.6) !important;
5221+
}}
5222+
</style>
5223+
</head>
5224+
<body>
5225+
<table class="sortable-table" id="recsTableInline" style="width: 100%; border-collapse: collapse; background: rgba(13, 17, 23, 0.95); border-radius: 8px;">
5226+
<thead>
5227+
<tr style="border-bottom: 2px solid rgba(255,255,255,0.2);">
5228+
<th onclick="sortTableInline(0, 'string')" style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Category</th>
5229+
<th onclick="sortTableInline(1, 'string')" style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Model</th>
5230+
<th onclick="sortTableInline(2, 'string')" style="text-align: left; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">GPU Config</th>
5231+
<th onclick="sortTableInline(3, 'number')" style="text-align: right; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">TTFT</th>
5232+
<th onclick="sortTableInline(4, 'number')" style="text-align: right; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Cost/mo</th>
5233+
<th onclick="sortTableInline(5, 'number')" style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Acc</th>
5234+
<th onclick="sortTableInline(6, 'number')" style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">Score</th>
5235+
<th onclick="sortTableInline(7, 'number')" style="text-align: center; padding: 0.75rem 0.5rem; color: rgba(255,255,255,0.7); font-size: 0.85rem; font-weight: 600;">SLO</th>
5236+
</tr>
5237+
</thead>
5238+
<tbody>
5239+
{''.join(rows_html)}
5240+
</tbody>
5241+
</table>
5242+
<script>
5243+
let sortDirectionInline = {{}};
5244+
5245+
function sortTableInline(columnIndex, type) {{
5246+
const table = document.getElementById('recsTableInline');
5247+
const tbody = table.querySelector('tbody');
5248+
const rows = Array.from(tbody.querySelectorAll('tr'));
5249+
const headers = table.querySelectorAll('th');
5250+
5251+
// Toggle sort direction
5252+
const key = columnIndex;
5253+
sortDirectionInline[key] = sortDirectionInline[key] === 'asc' ? 'desc' : 'asc';
5254+
const isAsc = sortDirectionInline[key] === 'asc';
5255+
5256+
// Clear all header classes
5257+
headers.forEach(h => {{
5258+
h.classList.remove('sort-asc', 'sort-desc');
5259+
}});
5260+
5261+
// Add class to current header
5262+
headers[columnIndex].classList.add(isAsc ? 'sort-asc' : 'sort-desc');
5263+
5264+
// Sort rows
5265+
rows.sort((a, b) => {{
5266+
let aVal, bVal;
5267+
5268+
if (type === 'number') {{
5269+
const attrs = ['category', 'model', 'gpu', 'ttft', 'cost', 'accuracy', 'balanced', 'slo'];
5270+
const attr = 'data-' + attrs[columnIndex];
5271+
aVal = parseFloat(a.getAttribute(attr)) || 0;
5272+
bVal = parseFloat(b.getAttribute(attr)) || 0;
5273+
}} else {{
5274+
aVal = a.cells[columnIndex].textContent.trim();
5275+
bVal = b.cells[columnIndex].textContent.trim();
5276+
}}
5277+
5278+
if (aVal < bVal) return isAsc ? -1 : 1;
5279+
if (aVal > bVal) return isAsc ? 1 : -1;
5280+
return 0;
5281+
}});
5282+
5283+
// Re-append sorted rows
5284+
rows.forEach(row => tbody.appendChild(row));
5285+
}}
5286+
</script>
5287+
</body>
5288+
</html>
5289+
'''
51635290

5164-
st.markdown(table_html, unsafe_allow_html=True)
5291+
components.html(table_html, height=450, scrolling=True)
5292+
st.markdown('<p style="color: rgba(255,255,255,0.6); font-size: 0.85rem; margin-top: 0.5rem;">Click on column headers to sort the table</p>', unsafe_allow_html=True)
5293+
else:
5294+
st.warning("No configurations to display")
51655295

51665296

51675297
# =============================================================================

0 commit comments

Comments
 (0)