Spaces:
Running
Running
Commit
·
cf1ba50
1
Parent(s):
a9bbc87
Dashboard
Browse files- old_ui/__init__.py +15 -0
- old_ui/app.py +61 -0
- old_ui/components.py +481 -0
- old_ui/config.py +583 -0
- old_ui/pages/__init__.py +5 -0
- old_ui/pages/__pycache__/__init__.cpython-312.pyc +0 -0
- old_ui/pages/__pycache__/data.cpython-312.pyc +0 -0
- old_ui/pages/__pycache__/discovery.cpython-312.pyc +0 -0
- old_ui/pages/__pycache__/explorer.cpython-312.pyc +0 -0
- old_ui/pages/__pycache__/home.cpython-312.pyc +0 -0
- old_ui/pages/__pycache__/settings.cpython-312.pyc +0 -0
- old_ui/pages/data.py +163 -0
- old_ui/pages/discovery.py +165 -0
- old_ui/pages/explorer.py +127 -0
- old_ui/pages/home.py +213 -0
- old_ui/pages/settings.py +192 -0
- old_ui/requirements.txt +31 -0
- ui/.vscode/mcp.json +11 -0
- ui/.vscode/tasks.json +44 -0
- ui/README.md +41 -13
- ui/app/api/discovery/route.ts +19 -0
- ui/app/data/page.tsx +116 -0
- ui/app/discovery/page.tsx +182 -0
- ui/app/explorer/page.tsx +213 -0
- ui/app/globals.css +48 -48
- ui/app/layout.tsx +10 -4
- ui/app/page.tsx +161 -57
- ui/app/settings/page.tsx +192 -0
- ui/components/page-header.tsx +37 -0
- ui/components/sidebar.tsx +95 -0
- ui/components/ui/badge.tsx +37 -0
- ui/components/ui/button.tsx +58 -0
- ui/components/ui/card.tsx +77 -0
- ui/components/ui/input.tsx +21 -0
- ui/components/ui/label.tsx +24 -0
- ui/components/ui/select.tsx +190 -0
- ui/components/ui/separator.tsx +28 -0
- ui/components/ui/slider.tsx +63 -0
- ui/components/ui/switch.tsx +35 -0
- ui/components/ui/table.tsx +116 -0
- ui/components/ui/tabs.tsx +91 -0
- ui/components/ui/textarea.tsx +25 -0
- ui/package.json +23 -12
- ui/pnpm-lock.yaml +0 -0
- ui/pnpm-workspace.yaml +0 -3
old_ui/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow UI Package
|
| 3 |
+
===================
|
| 4 |
+
|
| 5 |
+
Modern Streamlit-based interface for the BioFlow platform.
|
| 6 |
+
|
| 7 |
+
Pages:
|
| 8 |
+
- Home: Dashboard with key metrics and quick actions
|
| 9 |
+
- Discovery: Drug discovery pipeline interface
|
| 10 |
+
- Explorer: Vector space visualization
|
| 11 |
+
- Data: Data ingestion and management
|
| 12 |
+
- Settings: Configuration and preferences
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
__version__ = "2.0.0"
|
old_ui/app.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - AI-Powered Drug Discovery Platform
|
| 3 |
+
==============================================
|
| 4 |
+
Main application entry point.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# Setup path for imports
|
| 12 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 13 |
+
|
| 14 |
+
from bioflow.ui.config import get_css
|
| 15 |
+
from bioflow.ui.components import side_nav
|
| 16 |
+
from bioflow.ui.pages import home, discovery, explorer, data, settings
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
"""Main application."""
|
| 21 |
+
|
| 22 |
+
# Page config
|
| 23 |
+
st.set_page_config(
|
| 24 |
+
page_title="BioFlow",
|
| 25 |
+
page_icon="🧬",
|
| 26 |
+
layout="wide",
|
| 27 |
+
initial_sidebar_state="collapsed",
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Inject custom CSS
|
| 31 |
+
st.markdown(get_css(), unsafe_allow_html=True)
|
| 32 |
+
|
| 33 |
+
# Initialize session state
|
| 34 |
+
if "current_page" not in st.session_state:
|
| 35 |
+
st.session_state.current_page = "home"
|
| 36 |
+
|
| 37 |
+
# Layout with left navigation
|
| 38 |
+
nav_col, content_col = st.columns([1, 3.6], gap="large")
|
| 39 |
+
|
| 40 |
+
with nav_col:
|
| 41 |
+
selected = side_nav(active_page=st.session_state.current_page)
|
| 42 |
+
|
| 43 |
+
if selected != st.session_state.current_page:
|
| 44 |
+
st.session_state.current_page = selected
|
| 45 |
+
st.rerun()
|
| 46 |
+
|
| 47 |
+
with content_col:
|
| 48 |
+
page_map = {
|
| 49 |
+
"home": home.render,
|
| 50 |
+
"discovery": discovery.render,
|
| 51 |
+
"explorer": explorer.render,
|
| 52 |
+
"data": data.render,
|
| 53 |
+
"settings": settings.render,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
render_fn = page_map.get(st.session_state.current_page, home.render)
|
| 57 |
+
render_fn()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
main()
|
old_ui/components.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow UI - Components Library
|
| 3 |
+
================================
|
| 4 |
+
Reusable, modern UI components for Streamlit.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from typing import List, Dict, Any, Optional, Callable
|
| 9 |
+
import plotly.express as px
|
| 10 |
+
import plotly.graph_objects as go
|
| 11 |
+
|
| 12 |
+
# Import colors
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 16 |
+
from bioflow.ui.config import COLORS
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# === Navigation ===
|
| 20 |
+
|
| 21 |
+
def side_nav(active_page: str = "home") -> str:
|
| 22 |
+
"""Left vertical navigation list. Returns the selected page key."""
|
| 23 |
+
|
| 24 |
+
nav_items = [
|
| 25 |
+
("home", "🏠", "Home"),
|
| 26 |
+
("discovery", "🔬", "Discovery"),
|
| 27 |
+
("explorer", "🧬", "Explorer"),
|
| 28 |
+
("data", "📊", "Data"),
|
| 29 |
+
("settings", "⚙️", "Settings"),
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
st.markdown(
|
| 33 |
+
f"""
|
| 34 |
+
<div class="nav-rail">
|
| 35 |
+
<div class="nav-brand">
|
| 36 |
+
<div class="nav-logo">🧬</div>
|
| 37 |
+
<div class="nav-title">Bio<span>Flow</span></div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="nav-section">Navigation</div>
|
| 40 |
+
</div>
|
| 41 |
+
""",
|
| 42 |
+
unsafe_allow_html=True,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
label_map = {key: f"{icon} {label}" for key, icon, label in nav_items}
|
| 46 |
+
options = [item[0] for item in nav_items]
|
| 47 |
+
|
| 48 |
+
selected = st.radio(
|
| 49 |
+
"Navigation",
|
| 50 |
+
options=options,
|
| 51 |
+
index=options.index(active_page),
|
| 52 |
+
format_func=lambda x: label_map.get(x, x),
|
| 53 |
+
key="nav_radio",
|
| 54 |
+
label_visibility="collapsed",
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
return selected
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# === Page Structure ===
|
| 61 |
+
|
| 62 |
+
def page_header(title: str, subtitle: str = "", icon: str = ""):
|
| 63 |
+
"""Page header with title and optional subtitle."""
|
| 64 |
+
header_html = f"""
|
| 65 |
+
<div style="margin-bottom: 2rem;">
|
| 66 |
+
<h1 style="display: flex; align-items: center; gap: 0.75rem; margin: 0;">
|
| 67 |
+
{f'<span style="font-size: 2rem;">{icon}</span>' if icon else ''}
|
| 68 |
+
{title}
|
| 69 |
+
</h1>
|
| 70 |
+
{f'<p style="margin-top: 0.5rem; font-size: 1rem; color: {COLORS.text_muted};">{subtitle}</p>' if subtitle else ''}
|
| 71 |
+
</div>
|
| 72 |
+
"""
|
| 73 |
+
st.markdown(header_html, unsafe_allow_html=True)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def section_header(title: str, icon: str = "", link_text: str = "", link_action: Optional[Callable] = None):
|
| 77 |
+
"""Section header with optional action link."""
|
| 78 |
+
col1, col2 = st.columns([4, 1])
|
| 79 |
+
|
| 80 |
+
with col1:
|
| 81 |
+
st.markdown(f"""
|
| 82 |
+
<div class="section-title">
|
| 83 |
+
{f'<span>{icon}</span>' if icon else ''}
|
| 84 |
+
{title}
|
| 85 |
+
</div>
|
| 86 |
+
""", unsafe_allow_html=True)
|
| 87 |
+
|
| 88 |
+
with col2:
|
| 89 |
+
if link_text:
|
| 90 |
+
if st.button(link_text, key=f"section_{title}", use_container_width=True):
|
| 91 |
+
if link_action:
|
| 92 |
+
link_action()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def divider():
|
| 96 |
+
"""Visual divider."""
|
| 97 |
+
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def spacer(height: str = "1rem"):
|
| 101 |
+
"""Vertical spacer."""
|
| 102 |
+
st.markdown(f'<div style="height: {height};"></div>', unsafe_allow_html=True)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# === Metrics ===
|
| 106 |
+
|
| 107 |
+
def metric_card(
|
| 108 |
+
value: str,
|
| 109 |
+
label: str,
|
| 110 |
+
icon: str = "📊",
|
| 111 |
+
change: Optional[str] = None,
|
| 112 |
+
change_type: str = "up",
|
| 113 |
+
color: str = COLORS.primary
|
| 114 |
+
):
|
| 115 |
+
"""Single metric card with icon and optional trend."""
|
| 116 |
+
bg_color = color.replace(")", ", 0.15)").replace("rgb", "rgba") if "rgb" in color else f"{color}22"
|
| 117 |
+
change_html = ""
|
| 118 |
+
if change:
|
| 119 |
+
arrow = "↑" if change_type == "up" else "↓"
|
| 120 |
+
change_html = f'<div class="metric-change {change_type}">{arrow} {change}</div>'
|
| 121 |
+
|
| 122 |
+
st.markdown(f"""
|
| 123 |
+
<div class="metric">
|
| 124 |
+
<div class="metric-icon" style="background: {bg_color}; color: {color};">
|
| 125 |
+
{icon}
|
| 126 |
+
</div>
|
| 127 |
+
<div class="metric-value">{value}</div>
|
| 128 |
+
<div class="metric-label">{label}</div>
|
| 129 |
+
{change_html}
|
| 130 |
+
</div>
|
| 131 |
+
""", unsafe_allow_html=True)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def metric_row(metrics: List[Dict[str, Any]]):
|
| 135 |
+
"""Row of metric cards."""
|
| 136 |
+
cols = st.columns(len(metrics))
|
| 137 |
+
for col, metric in zip(cols, metrics):
|
| 138 |
+
with col:
|
| 139 |
+
metric_card(**metric)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# === Quick Actions ===
|
| 143 |
+
|
| 144 |
+
def quick_action(icon: str, title: str, description: str, key: str) -> bool:
|
| 145 |
+
"""Single quick action card. Returns True if clicked."""
|
| 146 |
+
clicked = st.button(
|
| 147 |
+
f"{icon} {title}",
|
| 148 |
+
key=key,
|
| 149 |
+
use_container_width=True,
|
| 150 |
+
help=description
|
| 151 |
+
)
|
| 152 |
+
return clicked
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def quick_actions_grid(actions: List[Dict[str, Any]], columns: int = 4) -> Optional[str]:
|
| 156 |
+
"""Grid of quick action cards. Returns clicked action key or None."""
|
| 157 |
+
cols = st.columns(columns)
|
| 158 |
+
clicked_key = None
|
| 159 |
+
|
| 160 |
+
for i, action in enumerate(actions):
|
| 161 |
+
with cols[i % columns]:
|
| 162 |
+
st.markdown(f"""
|
| 163 |
+
<div class="quick-action">
|
| 164 |
+
<span class="quick-action-icon">{action['icon']}</span>
|
| 165 |
+
<div class="quick-action-title">{action['title']}</div>
|
| 166 |
+
<div class="quick-action-desc">{action.get('description', '')}</div>
|
| 167 |
+
</div>
|
| 168 |
+
""", unsafe_allow_html=True)
|
| 169 |
+
|
| 170 |
+
if st.button("Select", key=action['key'], use_container_width=True):
|
| 171 |
+
clicked_key = action['key']
|
| 172 |
+
|
| 173 |
+
return clicked_key
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# === Pipeline Progress ===
|
| 177 |
+
|
| 178 |
+
def pipeline_progress(steps: List[Dict[str, Any]]):
|
| 179 |
+
"""Visual pipeline with steps showing progress."""
|
| 180 |
+
html = '<div class="pipeline">'
|
| 181 |
+
|
| 182 |
+
for i, step in enumerate(steps):
|
| 183 |
+
status = step.get('status', 'pending')
|
| 184 |
+
icon = step.get('icon', str(i + 1))
|
| 185 |
+
name = step.get('name', f'Step {i + 1}')
|
| 186 |
+
|
| 187 |
+
# Display icon for completed steps
|
| 188 |
+
if status == 'done':
|
| 189 |
+
display = '✓'
|
| 190 |
+
elif status == 'active':
|
| 191 |
+
display = icon
|
| 192 |
+
else:
|
| 193 |
+
display = str(i + 1)
|
| 194 |
+
|
| 195 |
+
html += f'''
|
| 196 |
+
<div class="step">
|
| 197 |
+
<div class="step-dot {status}">{display}</div>
|
| 198 |
+
<span class="step-name">{name}</span>
|
| 199 |
+
</div>
|
| 200 |
+
'''
|
| 201 |
+
|
| 202 |
+
# Add connecting line (except after last step)
|
| 203 |
+
if i < len(steps) - 1:
|
| 204 |
+
line_status = 'done' if status == 'done' else ''
|
| 205 |
+
html += f'<div class="step-line {line_status}"></div>'
|
| 206 |
+
|
| 207 |
+
html += '</div>'
|
| 208 |
+
st.markdown(html, unsafe_allow_html=True)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# === Results ===
|
| 212 |
+
|
| 213 |
+
def result_card(
|
| 214 |
+
title: str,
|
| 215 |
+
score: float,
|
| 216 |
+
properties: Dict[str, str] = None,
|
| 217 |
+
badges: List[str] = None,
|
| 218 |
+
key: str = ""
|
| 219 |
+
) -> bool:
|
| 220 |
+
"""Result card with score and properties. Returns True if clicked."""
|
| 221 |
+
|
| 222 |
+
# Score color
|
| 223 |
+
if score >= 0.8:
|
| 224 |
+
score_class = "score-high"
|
| 225 |
+
elif score >= 0.5:
|
| 226 |
+
score_class = "score-med"
|
| 227 |
+
else:
|
| 228 |
+
score_class = "score-low"
|
| 229 |
+
|
| 230 |
+
# Properties HTML
|
| 231 |
+
props_html = ""
|
| 232 |
+
if properties:
|
| 233 |
+
props_html = '<div style="display: flex; gap: 1rem; margin-top: 0.75rem; flex-wrap: wrap;">'
|
| 234 |
+
for k, v in properties.items():
|
| 235 |
+
props_html += f'''
|
| 236 |
+
<div style="font-size: 0.8125rem;">
|
| 237 |
+
<span style="color: {COLORS.text_muted};">{k}:</span>
|
| 238 |
+
<span style="color: {COLORS.text_secondary}; margin-left: 0.25rem;">{v}</span>
|
| 239 |
+
</div>
|
| 240 |
+
'''
|
| 241 |
+
props_html += '</div>'
|
| 242 |
+
|
| 243 |
+
# Badges HTML
|
| 244 |
+
badges_html = ""
|
| 245 |
+
if badges:
|
| 246 |
+
badges_html = '<div style="display: flex; gap: 0.5rem; margin-top: 0.75rem;">'
|
| 247 |
+
for b in badges:
|
| 248 |
+
badges_html += f'<span class="badge badge-primary">{b}</span>'
|
| 249 |
+
badges_html += '</div>'
|
| 250 |
+
|
| 251 |
+
st.markdown(f"""
|
| 252 |
+
<div class="result">
|
| 253 |
+
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
| 254 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">{title}</div>
|
| 255 |
+
<div class="{score_class}" style="font-size: 1.25rem; font-weight: 700;">{score:.1%}</div>
|
| 256 |
+
</div>
|
| 257 |
+
{props_html}
|
| 258 |
+
{badges_html}
|
| 259 |
+
</div>
|
| 260 |
+
""", unsafe_allow_html=True)
|
| 261 |
+
|
| 262 |
+
return st.button("View Details", key=key, use_container_width=True) if key else False
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def results_list(results: List[Dict[str, Any]], empty_message: str = "No results found"):
|
| 266 |
+
"""List of result cards."""
|
| 267 |
+
if not results:
|
| 268 |
+
empty_state(icon="🔍", title="No Results", description=empty_message)
|
| 269 |
+
return
|
| 270 |
+
|
| 271 |
+
for i, result in enumerate(results):
|
| 272 |
+
result_card(
|
| 273 |
+
title=result.get('title', f'Result {i + 1}'),
|
| 274 |
+
score=result.get('score', 0),
|
| 275 |
+
properties=result.get('properties'),
|
| 276 |
+
badges=result.get('badges'),
|
| 277 |
+
key=f"result_{i}"
|
| 278 |
+
)
|
| 279 |
+
spacer("0.75rem")
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# === Charts ===
|
| 283 |
+
|
| 284 |
+
def bar_chart(data: Dict[str, float], title: str = "", height: int = 300):
|
| 285 |
+
"""Styled bar chart."""
|
| 286 |
+
fig = go.Figure(data=[
|
| 287 |
+
go.Bar(
|
| 288 |
+
x=list(data.keys()),
|
| 289 |
+
y=list(data.values()),
|
| 290 |
+
marker_color=COLORS.primary,
|
| 291 |
+
marker_line_width=0,
|
| 292 |
+
)
|
| 293 |
+
])
|
| 294 |
+
|
| 295 |
+
fig.update_layout(
|
| 296 |
+
title=title,
|
| 297 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 298 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 299 |
+
font=dict(family="Inter", color=COLORS.text_secondary),
|
| 300 |
+
height=height,
|
| 301 |
+
margin=dict(l=40, r=20, t=40, b=40),
|
| 302 |
+
xaxis=dict(
|
| 303 |
+
showgrid=False,
|
| 304 |
+
showline=True,
|
| 305 |
+
linecolor=COLORS.border,
|
| 306 |
+
),
|
| 307 |
+
yaxis=dict(
|
| 308 |
+
showgrid=True,
|
| 309 |
+
gridcolor=COLORS.border,
|
| 310 |
+
showline=False,
|
| 311 |
+
),
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def scatter_chart(x: List, y: List, labels: List = None, title: str = "", height: int = 400):
|
| 318 |
+
"""Styled scatter plot."""
|
| 319 |
+
fig = go.Figure(data=[
|
| 320 |
+
go.Scatter(
|
| 321 |
+
x=x,
|
| 322 |
+
y=y,
|
| 323 |
+
mode='markers',
|
| 324 |
+
marker=dict(
|
| 325 |
+
size=10,
|
| 326 |
+
color=COLORS.primary,
|
| 327 |
+
opacity=0.7,
|
| 328 |
+
),
|
| 329 |
+
text=labels,
|
| 330 |
+
hovertemplate='<b>%{text}</b><br>X: %{x}<br>Y: %{y}<extra></extra>' if labels else None,
|
| 331 |
+
)
|
| 332 |
+
])
|
| 333 |
+
|
| 334 |
+
fig.update_layout(
|
| 335 |
+
title=title,
|
| 336 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 337 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 338 |
+
font=dict(family="Inter", color=COLORS.text_secondary),
|
| 339 |
+
height=height,
|
| 340 |
+
margin=dict(l=40, r=20, t=40, b=40),
|
| 341 |
+
xaxis=dict(
|
| 342 |
+
showgrid=True,
|
| 343 |
+
gridcolor=COLORS.border,
|
| 344 |
+
showline=True,
|
| 345 |
+
linecolor=COLORS.border,
|
| 346 |
+
),
|
| 347 |
+
yaxis=dict(
|
| 348 |
+
showgrid=True,
|
| 349 |
+
gridcolor=COLORS.border,
|
| 350 |
+
showline=True,
|
| 351 |
+
linecolor=COLORS.border,
|
| 352 |
+
),
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def heatmap(data: List[List[float]], x_labels: List[str], y_labels: List[str], title: str = "", height: int = 400):
|
| 359 |
+
"""Styled heatmap."""
|
| 360 |
+
fig = go.Figure(data=[
|
| 361 |
+
go.Heatmap(
|
| 362 |
+
z=data,
|
| 363 |
+
x=x_labels,
|
| 364 |
+
y=y_labels,
|
| 365 |
+
colorscale=[
|
| 366 |
+
[0, COLORS.bg_hover],
|
| 367 |
+
[0.5, COLORS.primary],
|
| 368 |
+
[1, COLORS.cyan],
|
| 369 |
+
],
|
| 370 |
+
)
|
| 371 |
+
])
|
| 372 |
+
|
| 373 |
+
fig.update_layout(
|
| 374 |
+
title=title,
|
| 375 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 376 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 377 |
+
font=dict(family="Inter", color=COLORS.text_secondary),
|
| 378 |
+
height=height,
|
| 379 |
+
margin=dict(l=80, r=20, t=40, b=60),
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# === Data Display ===
|
| 386 |
+
|
| 387 |
+
def data_table(data: List[Dict], columns: List[str] = None):
|
| 388 |
+
"""Styled data table."""
|
| 389 |
+
import pandas as pd
|
| 390 |
+
df = pd.DataFrame(data)
|
| 391 |
+
if columns:
|
| 392 |
+
df = df[columns]
|
| 393 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# === States ===
|
| 397 |
+
|
| 398 |
+
def empty_state(icon: str = "📭", title: str = "No Data", description: str = ""):
|
| 399 |
+
"""Empty state placeholder."""
|
| 400 |
+
st.markdown(f"""
|
| 401 |
+
<div class="empty">
|
| 402 |
+
<div class="empty-icon">{icon}</div>
|
| 403 |
+
<div class="empty-title">{title}</div>
|
| 404 |
+
<div class="empty-desc">{description}</div>
|
| 405 |
+
</div>
|
| 406 |
+
""", unsafe_allow_html=True)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def loading_state(message: str = "Loading..."):
|
| 410 |
+
"""Loading state with spinner."""
|
| 411 |
+
st.markdown(f"""
|
| 412 |
+
<div class="loading">
|
| 413 |
+
<div class="spinner"></div>
|
| 414 |
+
<div class="loading-text">{message}</div>
|
| 415 |
+
</div>
|
| 416 |
+
""", unsafe_allow_html=True)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# === Molecule Display ===
|
| 420 |
+
|
| 421 |
+
def molecule_2d(smiles: str, size: int = 200):
|
| 422 |
+
"""Display 2D molecule structure from SMILES."""
|
| 423 |
+
try:
|
| 424 |
+
from rdkit import Chem
|
| 425 |
+
from rdkit.Chem import Draw
|
| 426 |
+
import base64
|
| 427 |
+
from io import BytesIO
|
| 428 |
+
|
| 429 |
+
mol = Chem.MolFromSmiles(smiles)
|
| 430 |
+
if mol:
|
| 431 |
+
img = Draw.MolToImage(mol, size=(size, size))
|
| 432 |
+
buffered = BytesIO()
|
| 433 |
+
img.save(buffered, format="PNG")
|
| 434 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
| 435 |
+
|
| 436 |
+
st.markdown(f"""
|
| 437 |
+
<div class="mol-container">
|
| 438 |
+
<img src="data:image/png;base64,{img_str}" alt="Molecule" style="max-width: 100%; height: auto;">
|
| 439 |
+
</div>
|
| 440 |
+
""", unsafe_allow_html=True)
|
| 441 |
+
else:
|
| 442 |
+
st.warning("Invalid SMILES")
|
| 443 |
+
except ImportError:
|
| 444 |
+
st.info(f"SMILES: `{smiles}`")
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
# === Evidence & Links ===
|
| 448 |
+
|
| 449 |
+
def evidence_row(items: List[Dict[str, str]]):
|
| 450 |
+
"""Row of evidence/source links."""
|
| 451 |
+
html = '<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem;">'
|
| 452 |
+
for item in items:
|
| 453 |
+
icon = item.get('icon', '📄')
|
| 454 |
+
label = item.get('label', 'Source')
|
| 455 |
+
url = item.get('url', '#')
|
| 456 |
+
html += f'''
|
| 457 |
+
<a href="{url}" target="_blank" class="evidence">
|
| 458 |
+
<span>{icon}</span>
|
| 459 |
+
<span>{label}</span>
|
| 460 |
+
</a>
|
| 461 |
+
'''
|
| 462 |
+
html += '</div>'
|
| 463 |
+
st.markdown(html, unsafe_allow_html=True)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# === Badges ===
|
| 467 |
+
|
| 468 |
+
def badge(text: str, variant: str = "primary"):
|
| 469 |
+
"""Inline badge component."""
|
| 470 |
+
st.markdown(f'<span class="badge badge-{variant}">{text}</span>', unsafe_allow_html=True)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def badge_row(badges: List[Dict[str, str]]):
|
| 474 |
+
"""Row of badges."""
|
| 475 |
+
html = '<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">'
|
| 476 |
+
for b in badges:
|
| 477 |
+
text = b.get('text', '')
|
| 478 |
+
variant = b.get('variant', 'primary')
|
| 479 |
+
html += f'<span class="badge badge-{variant}">{text}</span>'
|
| 480 |
+
html += '</div>'
|
| 481 |
+
st.markdown(html, unsafe_allow_html=True)
|
old_ui/config.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow UI - Modern Design System
|
| 3 |
+
==================================
|
| 4 |
+
Clean, minimal, and highly usable interface.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class Colors:
|
| 12 |
+
"""Color palette - Modern dark theme."""
|
| 13 |
+
# Primary
|
| 14 |
+
primary: str = "#8B5CF6"
|
| 15 |
+
primary_hover: str = "#A78BFA"
|
| 16 |
+
primary_muted: str = "rgba(139, 92, 246, 0.15)"
|
| 17 |
+
|
| 18 |
+
# Accents
|
| 19 |
+
cyan: str = "#22D3EE"
|
| 20 |
+
emerald: str = "#34D399"
|
| 21 |
+
amber: str = "#FBBF24"
|
| 22 |
+
rose: str = "#FB7185"
|
| 23 |
+
|
| 24 |
+
# Backgrounds
|
| 25 |
+
bg_app: str = "#0C0E14"
|
| 26 |
+
bg_surface: str = "#14161E"
|
| 27 |
+
bg_elevated: str = "#1C1F2B"
|
| 28 |
+
bg_hover: str = "#252836"
|
| 29 |
+
|
| 30 |
+
# Text
|
| 31 |
+
text_primary: str = "#F8FAFC"
|
| 32 |
+
text_secondary: str = "#A1A7BB"
|
| 33 |
+
text_muted: str = "#6B7280"
|
| 34 |
+
|
| 35 |
+
# Borders
|
| 36 |
+
border: str = "#2A2D3A"
|
| 37 |
+
border_hover: str = "#3F4354"
|
| 38 |
+
|
| 39 |
+
# Status
|
| 40 |
+
success: str = "#10B981"
|
| 41 |
+
warning: str = "#F59E0B"
|
| 42 |
+
error: str = "#EF4444"
|
| 43 |
+
info: str = "#3B82F6"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
COLORS = Colors()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_css() -> str:
|
| 50 |
+
"""Minimalist, professional CSS using string concatenation to avoid f-string issues."""
|
| 51 |
+
|
| 52 |
+
css = """
|
| 53 |
+
<style>
|
| 54 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
| 55 |
+
|
| 56 |
+
:root {
|
| 57 |
+
--primary: """ + COLORS.primary + """;
|
| 58 |
+
--bg-app: """ + COLORS.bg_app + """;
|
| 59 |
+
--bg-surface: """ + COLORS.bg_surface + """;
|
| 60 |
+
--text: """ + COLORS.text_primary + """;
|
| 61 |
+
--text-muted: """ + COLORS.text_muted + """;
|
| 62 |
+
--border: """ + COLORS.border + """;
|
| 63 |
+
--radius: 12px;
|
| 64 |
+
--transition: 150ms ease;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.stApp {
|
| 68 |
+
background: """ + COLORS.bg_app + """;
|
| 69 |
+
font-family: 'Inter', sans-serif;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
#MainMenu, footer, header { visibility: hidden; }
|
| 73 |
+
.stDeployButton { display: none; }
|
| 74 |
+
|
| 75 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 76 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 77 |
+
::-webkit-scrollbar-thumb { background: """ + COLORS.border + """; border-radius: 3px; }
|
| 78 |
+
::-webkit-scrollbar-thumb:hover { background: """ + COLORS.border_hover + """; }
|
| 79 |
+
|
| 80 |
+
section[data-testid="stSidebar"] { display: none !important; }
|
| 81 |
+
|
| 82 |
+
h1, h2, h3 {
|
| 83 |
+
font-weight: 600;
|
| 84 |
+
color: """ + COLORS.text_primary + """;
|
| 85 |
+
letter-spacing: -0.025em;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
h1 { font-size: 1.875rem; margin-bottom: 0.5rem; }
|
| 89 |
+
h2 { font-size: 1.5rem; }
|
| 90 |
+
h3 { font-size: 1.125rem; }
|
| 91 |
+
|
| 92 |
+
p { color: """ + COLORS.text_secondary + """; line-height: 1.6; }
|
| 93 |
+
|
| 94 |
+
.card {
|
| 95 |
+
background: """ + COLORS.bg_surface + """;
|
| 96 |
+
border: 1px solid """ + COLORS.border + """;
|
| 97 |
+
border-radius: var(--radius);
|
| 98 |
+
padding: 1.25rem;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.metric {
|
| 102 |
+
background: """ + COLORS.bg_surface + """;
|
| 103 |
+
border: 1px solid """ + COLORS.border + """;
|
| 104 |
+
border-radius: var(--radius);
|
| 105 |
+
padding: 1.25rem;
|
| 106 |
+
transition: border-color var(--transition);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.metric:hover { border-color: """ + COLORS.primary + """; }
|
| 110 |
+
|
| 111 |
+
.metric-icon {
|
| 112 |
+
width: 44px;
|
| 113 |
+
height: 44px;
|
| 114 |
+
border-radius: 10px;
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: center;
|
| 118 |
+
font-size: 1.375rem;
|
| 119 |
+
margin-bottom: 1rem;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.metric-value {
|
| 123 |
+
font-size: 2rem;
|
| 124 |
+
font-weight: 700;
|
| 125 |
+
color: """ + COLORS.text_primary + """;
|
| 126 |
+
line-height: 1;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.metric-label {
|
| 130 |
+
font-size: 0.875rem;
|
| 131 |
+
color: """ + COLORS.text_muted + """;
|
| 132 |
+
margin-top: 0.375rem;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.metric-change {
|
| 136 |
+
display: inline-flex;
|
| 137 |
+
align-items: center;
|
| 138 |
+
font-size: 0.75rem;
|
| 139 |
+
font-weight: 500;
|
| 140 |
+
padding: 0.25rem 0.5rem;
|
| 141 |
+
border-radius: 6px;
|
| 142 |
+
margin-top: 0.5rem;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.metric-change.up { background: rgba(16, 185, 129, 0.15); color: """ + COLORS.success + """; }
|
| 146 |
+
.metric-change.down { background: rgba(239, 68, 68, 0.15); color: """ + COLORS.error + """; }
|
| 147 |
+
|
| 148 |
+
.stButton > button {
|
| 149 |
+
font-family: 'Inter', sans-serif;
|
| 150 |
+
font-weight: 500;
|
| 151 |
+
font-size: 0.875rem;
|
| 152 |
+
border-radius: 8px;
|
| 153 |
+
padding: 0.625rem 1.25rem;
|
| 154 |
+
transition: all var(--transition);
|
| 155 |
+
border: none;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.stTextInput input,
|
| 159 |
+
.stTextArea textarea,
|
| 160 |
+
.stSelectbox > div > div {
|
| 161 |
+
background: """ + COLORS.bg_app + """ !important;
|
| 162 |
+
border: 1px solid """ + COLORS.border + """ !important;
|
| 163 |
+
border-radius: 10px !important;
|
| 164 |
+
color: """ + COLORS.text_primary + """ !important;
|
| 165 |
+
font-family: 'Inter', sans-serif !important;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.stTextInput input:focus,
|
| 169 |
+
.stTextArea textarea:focus {
|
| 170 |
+
border-color: """ + COLORS.primary + """ !important;
|
| 171 |
+
box-shadow: 0 0 0 3px """ + COLORS.primary_muted + """ !important;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 175 |
+
gap: 0;
|
| 176 |
+
background: """ + COLORS.bg_surface + """;
|
| 177 |
+
border-radius: 10px;
|
| 178 |
+
padding: 4px;
|
| 179 |
+
border: 1px solid """ + COLORS.border + """;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.stTabs [data-baseweb="tab"] {
|
| 183 |
+
height: auto;
|
| 184 |
+
padding: 0.625rem 1.25rem;
|
| 185 |
+
border-radius: 8px;
|
| 186 |
+
font-weight: 500;
|
| 187 |
+
font-size: 0.875rem;
|
| 188 |
+
color: """ + COLORS.text_muted + """;
|
| 189 |
+
background: transparent;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.stTabs [aria-selected="true"] {
|
| 193 |
+
background: """ + COLORS.primary + """ !important;
|
| 194 |
+
color: white !important;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.stTabs [data-baseweb="tab-highlight"],
|
| 198 |
+
.stTabs [data-baseweb="tab-border"] { display: none; }
|
| 199 |
+
|
| 200 |
+
.pipeline {
|
| 201 |
+
display: flex;
|
| 202 |
+
align-items: center;
|
| 203 |
+
background: """ + COLORS.bg_surface + """;
|
| 204 |
+
border: 1px solid """ + COLORS.border + """;
|
| 205 |
+
border-radius: var(--radius);
|
| 206 |
+
padding: 1.5rem;
|
| 207 |
+
gap: 0;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.step {
|
| 211 |
+
display: flex;
|
| 212 |
+
flex-direction: column;
|
| 213 |
+
align-items: center;
|
| 214 |
+
gap: 0.5rem;
|
| 215 |
+
flex: 1;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.step-dot {
|
| 219 |
+
width: 44px;
|
| 220 |
+
height: 44px;
|
| 221 |
+
border-radius: 50%;
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
justify-content: center;
|
| 225 |
+
font-size: 1.125rem;
|
| 226 |
+
font-weight: 600;
|
| 227 |
+
transition: all var(--transition);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.step-dot.pending {
|
| 231 |
+
background: """ + COLORS.bg_hover + """;
|
| 232 |
+
color: """ + COLORS.text_muted + """;
|
| 233 |
+
border: 2px dashed """ + COLORS.border_hover + """;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.step-dot.active {
|
| 237 |
+
background: """ + COLORS.primary + """;
|
| 238 |
+
color: white;
|
| 239 |
+
box-shadow: 0 0 24px rgba(139, 92, 246, 0.5);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.step-dot.done {
|
| 243 |
+
background: """ + COLORS.emerald + """;
|
| 244 |
+
color: white;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.step-name {
|
| 248 |
+
font-size: 0.75rem;
|
| 249 |
+
font-weight: 500;
|
| 250 |
+
color: """ + COLORS.text_muted + """;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.step-line {
|
| 254 |
+
flex: 0.6;
|
| 255 |
+
height: 2px;
|
| 256 |
+
background: """ + COLORS.border + """;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.step-line.done { background: """ + COLORS.emerald + """; }
|
| 260 |
+
|
| 261 |
+
.result {
|
| 262 |
+
background: """ + COLORS.bg_surface + """;
|
| 263 |
+
border: 1px solid """ + COLORS.border + """;
|
| 264 |
+
border-radius: var(--radius);
|
| 265 |
+
padding: 1.25rem;
|
| 266 |
+
transition: all var(--transition);
|
| 267 |
+
cursor: pointer;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.result:hover {
|
| 271 |
+
border-color: """ + COLORS.primary + """;
|
| 272 |
+
transform: translateY(-2px);
|
| 273 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.score-high { color: """ + COLORS.emerald + """; }
|
| 277 |
+
.score-med { color: """ + COLORS.amber + """; }
|
| 278 |
+
.score-low { color: """ + COLORS.rose + """; }
|
| 279 |
+
|
| 280 |
+
.badge {
|
| 281 |
+
display: inline-flex;
|
| 282 |
+
align-items: center;
|
| 283 |
+
padding: 0.25rem 0.625rem;
|
| 284 |
+
border-radius: 6px;
|
| 285 |
+
font-size: 0.6875rem;
|
| 286 |
+
font-weight: 600;
|
| 287 |
+
text-transform: uppercase;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.badge-primary { background: """ + COLORS.primary_muted + """; color: """ + COLORS.primary + """; }
|
| 291 |
+
.badge-success { background: rgba(16, 185, 129, 0.15); color: """ + COLORS.success + """; }
|
| 292 |
+
.badge-warning { background: rgba(245, 158, 11, 0.15); color: """ + COLORS.warning + """; }
|
| 293 |
+
.badge-error { background: rgba(239, 68, 68, 0.15); color: """ + COLORS.error + """; }
|
| 294 |
+
|
| 295 |
+
.quick-action {
|
| 296 |
+
background: """ + COLORS.bg_surface + """;
|
| 297 |
+
border: 1px solid """ + COLORS.border + """;
|
| 298 |
+
border-radius: var(--radius);
|
| 299 |
+
padding: 1.5rem;
|
| 300 |
+
text-align: center;
|
| 301 |
+
cursor: pointer;
|
| 302 |
+
transition: all var(--transition);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.quick-action:hover {
|
| 306 |
+
border-color: """ + COLORS.primary + """;
|
| 307 |
+
transform: translateY(-4px);
|
| 308 |
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.quick-action-icon {
|
| 312 |
+
font-size: 2.5rem;
|
| 313 |
+
margin-bottom: 0.75rem;
|
| 314 |
+
display: block;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.quick-action-title {
|
| 318 |
+
font-size: 0.9375rem;
|
| 319 |
+
font-weight: 600;
|
| 320 |
+
color: """ + COLORS.text_primary + """;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.quick-action-desc {
|
| 324 |
+
font-size: 0.8125rem;
|
| 325 |
+
color: """ + COLORS.text_muted + """;
|
| 326 |
+
margin-top: 0.25rem;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.section-header {
|
| 330 |
+
display: flex;
|
| 331 |
+
align-items: center;
|
| 332 |
+
justify-content: space-between;
|
| 333 |
+
margin-bottom: 1rem;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.section-title {
|
| 337 |
+
font-size: 1rem;
|
| 338 |
+
font-weight: 600;
|
| 339 |
+
color: """ + COLORS.text_primary + """;
|
| 340 |
+
display: flex;
|
| 341 |
+
align-items: center;
|
| 342 |
+
gap: 0.5rem;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.section-link {
|
| 346 |
+
font-size: 0.8125rem;
|
| 347 |
+
color: """ + COLORS.primary + """;
|
| 348 |
+
cursor: pointer;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.section-link:hover { text-decoration: underline; }
|
| 352 |
+
|
| 353 |
+
.empty {
|
| 354 |
+
display: flex;
|
| 355 |
+
flex-direction: column;
|
| 356 |
+
align-items: center;
|
| 357 |
+
justify-content: center;
|
| 358 |
+
padding: 4rem 2rem;
|
| 359 |
+
text-align: center;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.empty-icon { font-size: 3.5rem; margin-bottom: 1rem; opacity: 0.4; }
|
| 363 |
+
.empty-title { font-size: 1.125rem; font-weight: 600; color: """ + COLORS.text_primary + """; }
|
| 364 |
+
.empty-desc { font-size: 0.9375rem; color: """ + COLORS.text_muted + """; max-width: 320px; margin-top: 0.5rem; }
|
| 365 |
+
|
| 366 |
+
.loading {
|
| 367 |
+
display: flex;
|
| 368 |
+
flex-direction: column;
|
| 369 |
+
align-items: center;
|
| 370 |
+
padding: 3rem;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.spinner {
|
| 374 |
+
width: 40px;
|
| 375 |
+
height: 40px;
|
| 376 |
+
border: 3px solid """ + COLORS.border + """;
|
| 377 |
+
border-top-color: """ + COLORS.primary + """;
|
| 378 |
+
border-radius: 50%;
|
| 379 |
+
animation: spin 0.8s linear infinite;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 383 |
+
|
| 384 |
+
.loading-text {
|
| 385 |
+
margin-top: 1rem;
|
| 386 |
+
color: """ + COLORS.text_muted + """;
|
| 387 |
+
font-size: 0.875rem;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.stProgress > div > div > div {
|
| 391 |
+
background: linear-gradient(90deg, """ + COLORS.primary + """ 0%, """ + COLORS.cyan + """ 100%);
|
| 392 |
+
border-radius: 4px;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.stProgress > div > div {
|
| 396 |
+
background: """ + COLORS.bg_hover + """;
|
| 397 |
+
border-radius: 4px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.divider {
|
| 401 |
+
height: 1px;
|
| 402 |
+
background: """ + COLORS.border + """;
|
| 403 |
+
margin: 1.5rem 0;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.mol-container {
|
| 407 |
+
background: white;
|
| 408 |
+
border-radius: 10px;
|
| 409 |
+
padding: 0.75rem;
|
| 410 |
+
display: flex;
|
| 411 |
+
align-items: center;
|
| 412 |
+
justify-content: center;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.evidence {
|
| 416 |
+
display: inline-flex;
|
| 417 |
+
align-items: center;
|
| 418 |
+
gap: 0.375rem;
|
| 419 |
+
padding: 0.5rem 0.75rem;
|
| 420 |
+
background: """ + COLORS.bg_app + """;
|
| 421 |
+
border: 1px solid """ + COLORS.border + """;
|
| 422 |
+
border-radius: 8px;
|
| 423 |
+
font-size: 0.8125rem;
|
| 424 |
+
color: """ + COLORS.text_secondary + """;
|
| 425 |
+
transition: all var(--transition);
|
| 426 |
+
text-decoration: none;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.evidence:hover {
|
| 430 |
+
border-color: """ + COLORS.primary + """;
|
| 431 |
+
color: """ + COLORS.primary + """;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.stAlert { border-radius: 10px; border: none; }
|
| 435 |
+
|
| 436 |
+
.stDataFrame {
|
| 437 |
+
border-radius: var(--radius);
|
| 438 |
+
overflow: hidden;
|
| 439 |
+
border: 1px solid """ + COLORS.border + """;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.block-container {
|
| 443 |
+
padding-top: 1.25rem;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.nav-rail {
|
| 447 |
+
position: sticky;
|
| 448 |
+
top: 1rem;
|
| 449 |
+
display: flex;
|
| 450 |
+
flex-direction: column;
|
| 451 |
+
gap: 0.75rem;
|
| 452 |
+
padding: 1rem;
|
| 453 |
+
background: """ + COLORS.bg_surface + """;
|
| 454 |
+
border: 1px solid """ + COLORS.border + """;
|
| 455 |
+
border-radius: 16px;
|
| 456 |
+
margin-bottom: 1rem;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.nav-brand {
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
gap: 0.75rem;
|
| 463 |
+
padding-bottom: 0.5rem;
|
| 464 |
+
border-bottom: 1px solid """ + COLORS.border + """;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.nav-logo { font-size: 1.5rem; }
|
| 468 |
+
|
| 469 |
+
.nav-title {
|
| 470 |
+
font-size: 1.1rem;
|
| 471 |
+
font-weight: 700;
|
| 472 |
+
color: """ + COLORS.text_primary + """;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.nav-title span {
|
| 476 |
+
background: linear-gradient(135deg, """ + COLORS.primary + """ 0%, """ + COLORS.cyan + """ 100%);
|
| 477 |
+
-webkit-background-clip: text;
|
| 478 |
+
-webkit-text-fill-color: transparent;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.nav-section {
|
| 482 |
+
font-size: 0.75rem;
|
| 483 |
+
text-transform: uppercase;
|
| 484 |
+
letter-spacing: 0.08em;
|
| 485 |
+
color: """ + COLORS.text_muted + """;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
div[data-testid="stRadio"] {
|
| 489 |
+
background: """ + COLORS.bg_surface + """;
|
| 490 |
+
border: 1px solid """ + COLORS.border + """;
|
| 491 |
+
border-radius: 16px;
|
| 492 |
+
padding: 0.75rem;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
div[data-testid="stRadio"] div[role="radiogroup"] {
|
| 496 |
+
display: flex;
|
| 497 |
+
flex-direction: column;
|
| 498 |
+
gap: 0.5rem;
|
| 499 |
+
margin-top: 0.25rem;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
div[data-testid="stRadio"] input {
|
| 503 |
+
display: none !important;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
div[data-testid="stRadio"] label {
|
| 507 |
+
background: """ + COLORS.bg_app + """;
|
| 508 |
+
border: 1px solid """ + COLORS.border + """;
|
| 509 |
+
border-radius: 12px;
|
| 510 |
+
padding: 0.65rem 0.9rem;
|
| 511 |
+
font-weight: 500;
|
| 512 |
+
color: """ + COLORS.text_secondary + """;
|
| 513 |
+
transition: all var(--transition);
|
| 514 |
+
margin: 0 !important;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
div[data-testid="stRadio"] label:hover {
|
| 518 |
+
border-color: """ + COLORS.primary + """;
|
| 519 |
+
color: """ + COLORS.text_primary + """;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
div[data-testid="stRadio"] label:has(input:checked) {
|
| 523 |
+
background: """ + COLORS.primary + """;
|
| 524 |
+
border-color: """ + COLORS.primary + """;
|
| 525 |
+
color: white;
|
| 526 |
+
box-shadow: 0 8px 20px rgba(139, 92, 246, 0.25);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.hero {
|
| 530 |
+
position: relative;
|
| 531 |
+
background: linear-gradient(135deg, rgba(139, 92, 246, 0.12) 0%, rgba(34, 211, 238, 0.08) 100%);
|
| 532 |
+
border: 1px solid """ + COLORS.border + """;
|
| 533 |
+
border-radius: 20px;
|
| 534 |
+
padding: 2.75rem;
|
| 535 |
+
overflow: hidden;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.hero-badge {
|
| 539 |
+
display: inline-flex;
|
| 540 |
+
align-items: center;
|
| 541 |
+
gap: 0.5rem;
|
| 542 |
+
padding: 0.35rem 0.75rem;
|
| 543 |
+
border-radius: 999px;
|
| 544 |
+
background: """ + COLORS.primary_muted + """;
|
| 545 |
+
color: """ + COLORS.primary + """;
|
| 546 |
+
font-size: 0.75rem;
|
| 547 |
+
font-weight: 600;
|
| 548 |
+
text-transform: uppercase;
|
| 549 |
+
letter-spacing: 0.08em;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.hero-title {
|
| 553 |
+
font-size: 2.25rem;
|
| 554 |
+
font-weight: 700;
|
| 555 |
+
color: """ + COLORS.text_primary + """;
|
| 556 |
+
margin-top: 1rem;
|
| 557 |
+
line-height: 1.1;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.hero-subtitle {
|
| 561 |
+
font-size: 1rem;
|
| 562 |
+
color: """ + COLORS.text_muted + """;
|
| 563 |
+
margin-top: 0.75rem;
|
| 564 |
+
max-width: 560px;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.hero-actions {
|
| 568 |
+
display: flex;
|
| 569 |
+
gap: 0.75rem;
|
| 570 |
+
margin-top: 1.5rem;
|
| 571 |
+
flex-wrap: wrap;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.hero-card {
|
| 575 |
+
background: """ + COLORS.bg_surface + """;
|
| 576 |
+
border: 1px solid """ + COLORS.border + """;
|
| 577 |
+
border-radius: 16px;
|
| 578 |
+
padding: 1.5rem;
|
| 579 |
+
}
|
| 580 |
+
</style>
|
| 581 |
+
"""
|
| 582 |
+
|
| 583 |
+
return css
|
old_ui/pages/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Page exports."""
|
| 2 |
+
|
| 3 |
+
from bioflow.ui.pages import home, discovery, explorer, data, settings
|
| 4 |
+
|
| 5 |
+
__all__ = ["home", "discovery", "explorer", "data", "settings"]
|
old_ui/pages/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (351 Bytes). View file
|
|
|
old_ui/pages/__pycache__/data.cpython-312.pyc
ADDED
|
Binary file (9.69 kB). View file
|
|
|
old_ui/pages/__pycache__/discovery.cpython-312.pyc
ADDED
|
Binary file (8.66 kB). View file
|
|
|
old_ui/pages/__pycache__/explorer.cpython-312.pyc
ADDED
|
Binary file (7.02 kB). View file
|
|
|
old_ui/pages/__pycache__/home.cpython-312.pyc
ADDED
|
Binary file (10.7 kB). View file
|
|
|
old_ui/pages/__pycache__/settings.cpython-312.pyc
ADDED
|
Binary file (9.34 kB). View file
|
|
|
old_ui/pages/data.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - Data Page
|
| 3 |
+
===================
|
| 4 |
+
Data management and upload.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
| 14 |
+
|
| 15 |
+
from bioflow.ui.components import (
|
| 16 |
+
page_header, section_header, divider, spacer,
|
| 17 |
+
metric_card, data_table, empty_state
|
| 18 |
+
)
|
| 19 |
+
from bioflow.ui.config import COLORS
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def render():
|
| 23 |
+
"""Render data page."""
|
| 24 |
+
|
| 25 |
+
page_header("Data Management", "Upload, manage, and organize your datasets", "📊")
|
| 26 |
+
|
| 27 |
+
# Stats Row
|
| 28 |
+
cols = st.columns(4)
|
| 29 |
+
|
| 30 |
+
with cols[0]:
|
| 31 |
+
metric_card("5", "Datasets", "📁", color=COLORS.primary)
|
| 32 |
+
with cols[1]:
|
| 33 |
+
metric_card("24.5K", "Molecules", "🧪", color=COLORS.cyan)
|
| 34 |
+
with cols[2]:
|
| 35 |
+
metric_card("1.2K", "Proteins", "🧬", color=COLORS.emerald)
|
| 36 |
+
with cols[3]:
|
| 37 |
+
metric_card("156 MB", "Storage Used", "💾", color=COLORS.amber)
|
| 38 |
+
|
| 39 |
+
spacer("2rem")
|
| 40 |
+
|
| 41 |
+
# Tabs
|
| 42 |
+
tabs = st.tabs(["📁 Datasets", "📤 Upload", "🔧 Processing"])
|
| 43 |
+
|
| 44 |
+
with tabs[0]:
|
| 45 |
+
section_header("Your Datasets", "📁")
|
| 46 |
+
|
| 47 |
+
# Dataset list
|
| 48 |
+
datasets = [
|
| 49 |
+
{"name": "DrugBank Compounds", "type": "Molecules", "count": "12,450", "size": "45.2 MB", "updated": "2024-01-15"},
|
| 50 |
+
{"name": "ChEMBL Kinase Inhibitors", "type": "Molecules", "count": "8,234", "size": "32.1 MB", "updated": "2024-01-10"},
|
| 51 |
+
{"name": "Custom Protein Targets", "type": "Proteins", "count": "1,245", "size": "78.5 MB", "updated": "2024-01-08"},
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
for ds in datasets:
|
| 55 |
+
st.markdown(f"""
|
| 56 |
+
<div class="card" style="margin-bottom: 0.75rem;">
|
| 57 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 58 |
+
<div>
|
| 59 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">{ds["name"]}</div>
|
| 60 |
+
<div style="display: flex; gap: 1.5rem; margin-top: 0.5rem;">
|
| 61 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 62 |
+
<span style="color: {COLORS.primary};">●</span> {ds["type"]}
|
| 63 |
+
</span>
|
| 64 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">{ds["count"]} items</span>
|
| 65 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">{ds["size"]}</span>
|
| 66 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">Updated: {ds["updated"]}</span>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
<div style="display: flex; gap: 0.5rem;">
|
| 70 |
+
<span class="badge badge-primary">{ds["type"]}</span>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
""", unsafe_allow_html=True)
|
| 75 |
+
|
| 76 |
+
# Action buttons
|
| 77 |
+
btn_cols = st.columns([1, 1, 1, 4])
|
| 78 |
+
with btn_cols[0]:
|
| 79 |
+
st.button("View", key=f"view_{ds['name']}", use_container_width=True)
|
| 80 |
+
with btn_cols[1]:
|
| 81 |
+
st.button("Export", key=f"export_{ds['name']}", use_container_width=True)
|
| 82 |
+
with btn_cols[2]:
|
| 83 |
+
st.button("Delete", key=f"delete_{ds['name']}", use_container_width=True)
|
| 84 |
+
|
| 85 |
+
spacer("0.5rem")
|
| 86 |
+
|
| 87 |
+
with tabs[1]:
|
| 88 |
+
section_header("Upload New Data", "📤")
|
| 89 |
+
|
| 90 |
+
# Upload area
|
| 91 |
+
st.markdown(f"""
|
| 92 |
+
<div style="
|
| 93 |
+
border: 2px dashed {COLORS.border};
|
| 94 |
+
border-radius: 16px;
|
| 95 |
+
padding: 3rem;
|
| 96 |
+
text-align: center;
|
| 97 |
+
background: {COLORS.bg_surface};
|
| 98 |
+
">
|
| 99 |
+
<div style="font-size: 3rem; margin-bottom: 1rem;">📁</div>
|
| 100 |
+
<div style="font-size: 1.125rem; font-weight: 600; color: {COLORS.text_primary};">
|
| 101 |
+
Drag & drop files here
|
| 102 |
+
</div>
|
| 103 |
+
<div style="font-size: 0.875rem; color: {COLORS.text_muted}; margin-top: 0.5rem;">
|
| 104 |
+
or click to browse
|
| 105 |
+
</div>
|
| 106 |
+
<div style="font-size: 0.75rem; color: {COLORS.text_muted}; margin-top: 1rem;">
|
| 107 |
+
Supports: CSV, SDF, FASTA, PDB, JSON
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
""", unsafe_allow_html=True)
|
| 111 |
+
|
| 112 |
+
uploaded_file = st.file_uploader(
|
| 113 |
+
"Upload file",
|
| 114 |
+
type=["csv", "sdf", "fasta", "pdb", "json"],
|
| 115 |
+
label_visibility="collapsed"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
if uploaded_file:
|
| 119 |
+
st.success(f"✓ File uploaded: {uploaded_file.name}")
|
| 120 |
+
|
| 121 |
+
col1, col2 = st.columns(2)
|
| 122 |
+
with col1:
|
| 123 |
+
dataset_name = st.text_input("Dataset Name", value=uploaded_file.name.split('.')[0])
|
| 124 |
+
with col2:
|
| 125 |
+
data_type = st.selectbox("Data Type", ["Molecules", "Proteins", "Text"])
|
| 126 |
+
|
| 127 |
+
if st.button("Process & Import", type="primary", use_container_width=True):
|
| 128 |
+
with st.spinner("Processing..."):
|
| 129 |
+
import time
|
| 130 |
+
time.sleep(2)
|
| 131 |
+
st.success("✓ Dataset imported successfully!")
|
| 132 |
+
|
| 133 |
+
with tabs[2]:
|
| 134 |
+
section_header("Data Processing", "🔧")
|
| 135 |
+
|
| 136 |
+
st.markdown(f"""
|
| 137 |
+
<div class="card">
|
| 138 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.75rem;">
|
| 139 |
+
Available Operations
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
""", unsafe_allow_html=True)
|
| 143 |
+
|
| 144 |
+
operations = [
|
| 145 |
+
{"icon": "🧹", "name": "Clean & Validate", "desc": "Remove duplicates, fix invalid structures"},
|
| 146 |
+
{"icon": "🔢", "name": "Compute Descriptors", "desc": "Calculate molecular properties and fingerprints"},
|
| 147 |
+
{"icon": "🧠", "name": "Generate Embeddings", "desc": "Create vector representations using AI models"},
|
| 148 |
+
{"icon": "🔗", "name": "Merge Datasets", "desc": "Combine multiple datasets with deduplication"},
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
for op in operations:
|
| 152 |
+
st.markdown(f"""
|
| 153 |
+
<div class="quick-action" style="margin-bottom: 0.75rem; text-align: left;">
|
| 154 |
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
| 155 |
+
<span style="font-size: 1.5rem;">{op["icon"]}</span>
|
| 156 |
+
<div>
|
| 157 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">{op["name"]}</div>
|
| 158 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">{op["desc"]}</div>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
""", unsafe_allow_html=True)
|
| 163 |
+
st.button(f"Run {op['name']}", key=f"op_{op['name']}", use_container_width=True)
|
old_ui/pages/discovery.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - Discovery Page
|
| 3 |
+
========================
|
| 4 |
+
Drug discovery pipeline interface.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
| 12 |
+
|
| 13 |
+
from bioflow.ui.components import (
|
| 14 |
+
page_header, section_header, divider, spacer,
|
| 15 |
+
pipeline_progress, bar_chart, empty_state, loading_state
|
| 16 |
+
)
|
| 17 |
+
from bioflow.ui.config import COLORS
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def render():
|
| 21 |
+
"""Render discovery page."""
|
| 22 |
+
|
| 23 |
+
page_header("Drug Discovery", "Search for drug candidates with AI-powered analysis", "🔬")
|
| 24 |
+
|
| 25 |
+
# Query Input Section
|
| 26 |
+
st.markdown(f"""
|
| 27 |
+
<div class="card" style="margin-bottom: 1.5rem;">
|
| 28 |
+
<div style="font-size: 0.875rem; font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.75rem;">
|
| 29 |
+
Search Query
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
""", unsafe_allow_html=True)
|
| 33 |
+
|
| 34 |
+
col1, col2 = st.columns([3, 1])
|
| 35 |
+
|
| 36 |
+
with col1:
|
| 37 |
+
query = st.text_area(
|
| 38 |
+
"Query",
|
| 39 |
+
placeholder="Enter a natural language query, SMILES string, or FASTA sequence...",
|
| 40 |
+
height=100,
|
| 41 |
+
label_visibility="collapsed"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
with col2:
|
| 45 |
+
st.selectbox("Search Type", ["Similarity", "Binding Affinity", "Properties"], label_visibility="collapsed")
|
| 46 |
+
st.selectbox("Database", ["All", "DrugBank", "ChEMBL", "ZINC"], label_visibility="collapsed")
|
| 47 |
+
search_clicked = st.button("🔍 Search", type="primary", use_container_width=True)
|
| 48 |
+
|
| 49 |
+
spacer("1.5rem")
|
| 50 |
+
|
| 51 |
+
# Pipeline Progress
|
| 52 |
+
section_header("Pipeline Status", "🔄")
|
| 53 |
+
|
| 54 |
+
if "discovery_step" not in st.session_state:
|
| 55 |
+
st.session_state.discovery_step = 0
|
| 56 |
+
|
| 57 |
+
steps = [
|
| 58 |
+
{"name": "Input", "status": "done" if st.session_state.discovery_step > 0 else "active"},
|
| 59 |
+
{"name": "Encode", "status": "done" if st.session_state.discovery_step > 1 else ("active" if st.session_state.discovery_step == 1 else "pending")},
|
| 60 |
+
{"name": "Search", "status": "done" if st.session_state.discovery_step > 2 else ("active" if st.session_state.discovery_step == 2 else "pending")},
|
| 61 |
+
{"name": "Predict", "status": "done" if st.session_state.discovery_step > 3 else ("active" if st.session_state.discovery_step == 3 else "pending")},
|
| 62 |
+
{"name": "Results", "status": "active" if st.session_state.discovery_step == 4 else "pending"},
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
pipeline_progress(steps)
|
| 66 |
+
|
| 67 |
+
spacer("2rem")
|
| 68 |
+
divider()
|
| 69 |
+
spacer("2rem")
|
| 70 |
+
|
| 71 |
+
# Results Section
|
| 72 |
+
section_header("Results", "🎯")
|
| 73 |
+
|
| 74 |
+
if search_clicked and query:
|
| 75 |
+
st.session_state.discovery_step = 4
|
| 76 |
+
st.session_state.discovery_query = query
|
| 77 |
+
|
| 78 |
+
if st.session_state.discovery_step >= 4:
|
| 79 |
+
# Show results
|
| 80 |
+
tabs = st.tabs(["Top Candidates", "Property Analysis", "Evidence"])
|
| 81 |
+
|
| 82 |
+
with tabs[0]:
|
| 83 |
+
# Results list
|
| 84 |
+
results = [
|
| 85 |
+
{"name": "Candidate A", "score": 0.95, "mw": "342.4", "logp": "2.1", "hbd": "2"},
|
| 86 |
+
{"name": "Candidate B", "score": 0.89, "mw": "298.3", "logp": "1.8", "hbd": "3"},
|
| 87 |
+
{"name": "Candidate C", "score": 0.82, "mw": "415.5", "logp": "3.2", "hbd": "1"},
|
| 88 |
+
{"name": "Candidate D", "score": 0.76, "mw": "267.3", "logp": "1.5", "hbd": "2"},
|
| 89 |
+
{"name": "Candidate E", "score": 0.71, "mw": "389.4", "logp": "2.8", "hbd": "2"},
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
for r in results:
|
| 93 |
+
score_color = COLORS.emerald if r["score"] >= 0.8 else (COLORS.amber if r["score"] >= 0.5 else COLORS.rose)
|
| 94 |
+
st.markdown(f"""
|
| 95 |
+
<div class="result">
|
| 96 |
+
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
| 97 |
+
<div>
|
| 98 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">{r["name"]}</div>
|
| 99 |
+
<div style="display: flex; gap: 1rem; margin-top: 0.5rem;">
|
| 100 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">MW: {r["mw"]}</span>
|
| 101 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">LogP: {r["logp"]}</span>
|
| 102 |
+
<span style="font-size: 0.8125rem; color: {COLORS.text_muted};">HBD: {r["hbd"]}</span>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
<div style="font-size: 1.5rem; font-weight: 700; color: {score_color};">{r["score"]:.0%}</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
""", unsafe_allow_html=True)
|
| 109 |
+
spacer("0.75rem")
|
| 110 |
+
|
| 111 |
+
with tabs[1]:
|
| 112 |
+
# Property distribution
|
| 113 |
+
col1, col2 = st.columns(2)
|
| 114 |
+
|
| 115 |
+
with col1:
|
| 116 |
+
bar_chart(
|
| 117 |
+
{"<200": 5, "200-300": 12, "300-400": 8, "400-500": 3, ">500": 2},
|
| 118 |
+
title="Molecular Weight Distribution",
|
| 119 |
+
height=250
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
with col2:
|
| 123 |
+
bar_chart(
|
| 124 |
+
{"<1": 4, "1-2": 10, "2-3": 8, "3-4": 5, ">4": 3},
|
| 125 |
+
title="LogP Distribution",
|
| 126 |
+
height=250
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
with tabs[2]:
|
| 130 |
+
# Evidence
|
| 131 |
+
st.markdown(f"""
|
| 132 |
+
<div class="card">
|
| 133 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 1rem;">
|
| 134 |
+
Related Literature
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
""", unsafe_allow_html=True)
|
| 138 |
+
|
| 139 |
+
papers = [
|
| 140 |
+
{"title": "Novel therapeutic targets for cancer treatment", "year": "2024", "journal": "Nature Medicine"},
|
| 141 |
+
{"title": "Molecular docking studies of kinase inhibitors", "year": "2023", "journal": "J. Med. Chem."},
|
| 142 |
+
{"title": "AI-driven drug discovery approaches", "year": "2024", "journal": "Drug Discovery Today"},
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
for p in papers:
|
| 146 |
+
st.markdown(f"""
|
| 147 |
+
<div style="
|
| 148 |
+
padding: 1rem;
|
| 149 |
+
border: 1px solid {COLORS.border};
|
| 150 |
+
border-radius: 8px;
|
| 151 |
+
margin-bottom: 0.75rem;
|
| 152 |
+
">
|
| 153 |
+
<div style="font-weight: 500; color: {COLORS.text_primary};">{p["title"]}</div>
|
| 154 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted}; margin-top: 0.25rem;">
|
| 155 |
+
{p["journal"]} • {p["year"]}
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
""", unsafe_allow_html=True)
|
| 159 |
+
|
| 160 |
+
else:
|
| 161 |
+
empty_state(
|
| 162 |
+
"🔍",
|
| 163 |
+
"No Results Yet",
|
| 164 |
+
"Enter a query and click Search to find drug candidates"
|
| 165 |
+
)
|
old_ui/pages/explorer.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - Explorer Page
|
| 3 |
+
=======================
|
| 4 |
+
Data exploration and visualization.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
| 13 |
+
|
| 14 |
+
from bioflow.ui.components import (
|
| 15 |
+
page_header, section_header, divider, spacer,
|
| 16 |
+
scatter_chart, heatmap, metric_card, empty_state
|
| 17 |
+
)
|
| 18 |
+
from bioflow.ui.config import COLORS
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def render():
|
| 22 |
+
"""Render explorer page."""
|
| 23 |
+
|
| 24 |
+
page_header("Data Explorer", "Visualize molecular embeddings and relationships", "🧬")
|
| 25 |
+
|
| 26 |
+
# Controls
|
| 27 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 28 |
+
|
| 29 |
+
with col1:
|
| 30 |
+
dataset = st.selectbox("Dataset", ["DrugBank", "ChEMBL", "ZINC", "Custom"])
|
| 31 |
+
with col2:
|
| 32 |
+
viz_type = st.selectbox("Visualization", ["UMAP", "t-SNE", "PCA"])
|
| 33 |
+
with col3:
|
| 34 |
+
color_by = st.selectbox("Color by", ["Activity", "MW", "LogP", "Cluster"])
|
| 35 |
+
with col4:
|
| 36 |
+
st.write("") # Spacing
|
| 37 |
+
st.write("")
|
| 38 |
+
if st.button("🔄 Refresh", use_container_width=True):
|
| 39 |
+
st.rerun()
|
| 40 |
+
|
| 41 |
+
spacer("1.5rem")
|
| 42 |
+
|
| 43 |
+
# Main visualization area
|
| 44 |
+
col_viz, col_details = st.columns([2, 1])
|
| 45 |
+
|
| 46 |
+
with col_viz:
|
| 47 |
+
section_header("Embedding Space", "🗺️")
|
| 48 |
+
|
| 49 |
+
# Generate sample data
|
| 50 |
+
np.random.seed(42)
|
| 51 |
+
n_points = 200
|
| 52 |
+
|
| 53 |
+
# Create clusters
|
| 54 |
+
cluster1_x = np.random.normal(2, 0.8, n_points // 4)
|
| 55 |
+
cluster1_y = np.random.normal(3, 0.8, n_points // 4)
|
| 56 |
+
|
| 57 |
+
cluster2_x = np.random.normal(-2, 1, n_points // 4)
|
| 58 |
+
cluster2_y = np.random.normal(-1, 1, n_points // 4)
|
| 59 |
+
|
| 60 |
+
cluster3_x = np.random.normal(4, 0.6, n_points // 4)
|
| 61 |
+
cluster3_y = np.random.normal(-2, 0.6, n_points // 4)
|
| 62 |
+
|
| 63 |
+
cluster4_x = np.random.normal(-1, 0.9, n_points // 4)
|
| 64 |
+
cluster4_y = np.random.normal(4, 0.9, n_points // 4)
|
| 65 |
+
|
| 66 |
+
x = list(cluster1_x) + list(cluster2_x) + list(cluster3_x) + list(cluster4_x)
|
| 67 |
+
y = list(cluster1_y) + list(cluster2_y) + list(cluster3_y) + list(cluster4_y)
|
| 68 |
+
labels = [f"Mol_{i}" for i in range(n_points)]
|
| 69 |
+
|
| 70 |
+
scatter_chart(x, y, labels, title=f"{viz_type} Projection - {dataset}", height=450)
|
| 71 |
+
|
| 72 |
+
with col_details:
|
| 73 |
+
section_header("Statistics", "📊")
|
| 74 |
+
|
| 75 |
+
metric_card("12,450", "Total Molecules", "🧪", color=COLORS.primary)
|
| 76 |
+
spacer("0.75rem")
|
| 77 |
+
metric_card("4", "Clusters Found", "🎯", color=COLORS.cyan)
|
| 78 |
+
spacer("0.75rem")
|
| 79 |
+
metric_card("0.89", "Silhouette Score", "📈", color=COLORS.emerald)
|
| 80 |
+
spacer("0.75rem")
|
| 81 |
+
metric_card("85%", "Coverage", "✓", color=COLORS.amber)
|
| 82 |
+
|
| 83 |
+
spacer("2rem")
|
| 84 |
+
divider()
|
| 85 |
+
spacer("2rem")
|
| 86 |
+
|
| 87 |
+
# Similarity Heatmap
|
| 88 |
+
section_header("Similarity Matrix", "🔥")
|
| 89 |
+
|
| 90 |
+
# Sample similarity matrix
|
| 91 |
+
np.random.seed(123)
|
| 92 |
+
labels_short = ["Cluster A", "Cluster B", "Cluster C", "Cluster D", "Cluster E"]
|
| 93 |
+
similarity = np.random.uniform(0.3, 1.0, (5, 5))
|
| 94 |
+
similarity = (similarity + similarity.T) / 2 # Make symmetric
|
| 95 |
+
np.fill_diagonal(similarity, 1.0)
|
| 96 |
+
|
| 97 |
+
heatmap(
|
| 98 |
+
similarity.tolist(),
|
| 99 |
+
labels_short,
|
| 100 |
+
labels_short,
|
| 101 |
+
title="Inter-cluster Similarity",
|
| 102 |
+
height=350
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
spacer("2rem")
|
| 106 |
+
|
| 107 |
+
# Export options
|
| 108 |
+
st.markdown(f"""
|
| 109 |
+
<div class="card">
|
| 110 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 111 |
+
<div>
|
| 112 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">Export Data</div>
|
| 113 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted}; margin-top: 0.25rem;">
|
| 114 |
+
Download embeddings, clusters, or full dataset
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
""", unsafe_allow_html=True)
|
| 120 |
+
|
| 121 |
+
exp_cols = st.columns(3)
|
| 122 |
+
with exp_cols[0]:
|
| 123 |
+
st.button("📥 Embeddings (CSV)", use_container_width=True)
|
| 124 |
+
with exp_cols[1]:
|
| 125 |
+
st.button("📥 Clusters (JSON)", use_container_width=True)
|
| 126 |
+
with exp_cols[2]:
|
| 127 |
+
st.button("📥 Full Dataset", use_container_width=True)
|
old_ui/pages/home.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - Home Page
|
| 3 |
+
====================
|
| 4 |
+
Clean dashboard with key metrics and quick actions.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
| 12 |
+
|
| 13 |
+
from bioflow.ui.components import (
|
| 14 |
+
section_header, divider, spacer,
|
| 15 |
+
metric_card, pipeline_progress, bar_chart
|
| 16 |
+
)
|
| 17 |
+
from bioflow.ui.config import COLORS
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def render():
|
| 21 |
+
"""Render home page."""
|
| 22 |
+
|
| 23 |
+
# Hero Section (Tailark-inspired)
|
| 24 |
+
hero_col, hero_side = st.columns([3, 1.4])
|
| 25 |
+
|
| 26 |
+
with hero_col:
|
| 27 |
+
st.markdown(f"""
|
| 28 |
+
<div class="hero">
|
| 29 |
+
<div class="hero-badge">New • BioFlow 2.0</div>
|
| 30 |
+
<div class="hero-title">AI-Powered Drug Discovery</div>
|
| 31 |
+
<div class="hero-subtitle">
|
| 32 |
+
Run discovery pipelines, predict binding, and surface evidence in one streamlined workspace.
|
| 33 |
+
</div>
|
| 34 |
+
<div class="hero-actions">
|
| 35 |
+
<span class="badge badge-primary">Model-aware search</span>
|
| 36 |
+
<span class="badge badge-success">Evidence-linked</span>
|
| 37 |
+
<span class="badge badge-warning">Fast iteration</span>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
""", unsafe_allow_html=True)
|
| 41 |
+
|
| 42 |
+
spacer("0.75rem")
|
| 43 |
+
btn1, btn2 = st.columns(2)
|
| 44 |
+
with btn1:
|
| 45 |
+
if st.button("Start Discovery", type="primary", use_container_width=True):
|
| 46 |
+
st.session_state.current_page = "discovery"
|
| 47 |
+
st.rerun()
|
| 48 |
+
with btn2:
|
| 49 |
+
if st.button("Explore Data", use_container_width=True):
|
| 50 |
+
st.session_state.current_page = "explorer"
|
| 51 |
+
st.rerun()
|
| 52 |
+
|
| 53 |
+
with hero_side:
|
| 54 |
+
st.markdown(f"""
|
| 55 |
+
<div class="hero-card">
|
| 56 |
+
<div style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: {COLORS.text_muted};">
|
| 57 |
+
Today
|
| 58 |
+
</div>
|
| 59 |
+
<div style="font-size: 1.75rem; font-weight: 700; color: {COLORS.text_primary}; margin-top: 0.5rem;">
|
| 60 |
+
156 Discoveries
|
| 61 |
+
</div>
|
| 62 |
+
<div style="font-size: 0.875rem; color: {COLORS.text_muted}; margin-top: 0.5rem;">
|
| 63 |
+
+12% vs last week
|
| 64 |
+
</div>
|
| 65 |
+
<div class="divider" style="margin: 1rem 0;"></div>
|
| 66 |
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
| 67 |
+
<span class="badge badge-primary">Discovery</span>
|
| 68 |
+
<span class="badge badge-success">Prediction</span>
|
| 69 |
+
<span class="badge badge-warning">Evidence</span>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
""", unsafe_allow_html=True)
|
| 73 |
+
|
| 74 |
+
# Metrics Row
|
| 75 |
+
cols = st.columns(4)
|
| 76 |
+
|
| 77 |
+
with cols[0]:
|
| 78 |
+
metric_card("12.5M", "Molecules", "🧪", "+2.3%", "up", COLORS.primary)
|
| 79 |
+
with cols[1]:
|
| 80 |
+
metric_card("847K", "Proteins", "🧬", "+1.8%", "up", COLORS.cyan)
|
| 81 |
+
with cols[2]:
|
| 82 |
+
metric_card("1.2M", "Papers", "📚", "+5.2%", "up", COLORS.emerald)
|
| 83 |
+
with cols[3]:
|
| 84 |
+
metric_card("156", "Discoveries", "✨", "+12%", "up", COLORS.amber)
|
| 85 |
+
|
| 86 |
+
spacer("2rem")
|
| 87 |
+
|
| 88 |
+
# Quick Actions
|
| 89 |
+
section_header("Quick Actions", "⚡")
|
| 90 |
+
|
| 91 |
+
action_cols = st.columns(4)
|
| 92 |
+
|
| 93 |
+
with action_cols[0]:
|
| 94 |
+
st.markdown(f"""
|
| 95 |
+
<div class="quick-action">
|
| 96 |
+
<span class="quick-action-icon">🔍</span>
|
| 97 |
+
<div class="quick-action-title">New Discovery</div>
|
| 98 |
+
<div class="quick-action-desc">Start a pipeline</div>
|
| 99 |
+
</div>
|
| 100 |
+
""", unsafe_allow_html=True)
|
| 101 |
+
if st.button("Start", key="qa_discovery", use_container_width=True):
|
| 102 |
+
st.session_state.current_page = "discovery"
|
| 103 |
+
st.rerun()
|
| 104 |
+
|
| 105 |
+
with action_cols[1]:
|
| 106 |
+
st.markdown(f"""
|
| 107 |
+
<div class="quick-action">
|
| 108 |
+
<span class="quick-action-icon">📊</span>
|
| 109 |
+
<div class="quick-action-title">Explore Data</div>
|
| 110 |
+
<div class="quick-action-desc">Visualize embeddings</div>
|
| 111 |
+
</div>
|
| 112 |
+
""", unsafe_allow_html=True)
|
| 113 |
+
if st.button("Explore", key="qa_explorer", use_container_width=True):
|
| 114 |
+
st.session_state.current_page = "explorer"
|
| 115 |
+
st.rerun()
|
| 116 |
+
|
| 117 |
+
with action_cols[2]:
|
| 118 |
+
st.markdown(f"""
|
| 119 |
+
<div class="quick-action">
|
| 120 |
+
<span class="quick-action-icon">📁</span>
|
| 121 |
+
<div class="quick-action-title">Upload Data</div>
|
| 122 |
+
<div class="quick-action-desc">Add molecules</div>
|
| 123 |
+
</div>
|
| 124 |
+
""", unsafe_allow_html=True)
|
| 125 |
+
if st.button("Upload", key="qa_data", use_container_width=True):
|
| 126 |
+
st.session_state.current_page = "data"
|
| 127 |
+
st.rerun()
|
| 128 |
+
|
| 129 |
+
with action_cols[3]:
|
| 130 |
+
st.markdown(f"""
|
| 131 |
+
<div class="quick-action">
|
| 132 |
+
<span class="quick-action-icon">⚙️</span>
|
| 133 |
+
<div class="quick-action-title">Settings</div>
|
| 134 |
+
<div class="quick-action-desc">Configure models</div>
|
| 135 |
+
</div>
|
| 136 |
+
""", unsafe_allow_html=True)
|
| 137 |
+
if st.button("Configure", key="qa_settings", use_container_width=True):
|
| 138 |
+
st.session_state.current_page = "settings"
|
| 139 |
+
st.rerun()
|
| 140 |
+
|
| 141 |
+
spacer("2rem")
|
| 142 |
+
divider()
|
| 143 |
+
spacer("2rem")
|
| 144 |
+
|
| 145 |
+
# Two Column Layout
|
| 146 |
+
col1, col2 = st.columns([3, 2])
|
| 147 |
+
|
| 148 |
+
with col1:
|
| 149 |
+
section_header("Recent Discoveries", "🎯")
|
| 150 |
+
|
| 151 |
+
# Sample results
|
| 152 |
+
results = [
|
| 153 |
+
{"name": "Aspirin analog", "score": 0.94, "mw": "180.16"},
|
| 154 |
+
{"name": "Novel kinase inhibitor", "score": 0.87, "mw": "331.39"},
|
| 155 |
+
{"name": "EGFR binder candidate", "score": 0.72, "mw": "311.38"},
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
for r in results:
|
| 159 |
+
score_color = COLORS.emerald if r["score"] >= 0.8 else (COLORS.amber if r["score"] >= 0.5 else COLORS.rose)
|
| 160 |
+
st.markdown(f"""
|
| 161 |
+
<div class="result">
|
| 162 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 163 |
+
<div style="font-weight: 600; color: {COLORS.text_primary};">{r["name"]}</div>
|
| 164 |
+
<div style="font-size: 1.25rem; font-weight: 700; color: {score_color};">{r["score"]:.0%}</div>
|
| 165 |
+
</div>
|
| 166 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted}; margin-top: 0.5rem;">
|
| 167 |
+
MW: {r["mw"]}
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
""", unsafe_allow_html=True)
|
| 171 |
+
spacer("0.75rem")
|
| 172 |
+
|
| 173 |
+
with col2:
|
| 174 |
+
section_header("Pipeline Activity", "📈")
|
| 175 |
+
|
| 176 |
+
bar_chart(
|
| 177 |
+
{"Mon": 23, "Tue": 31, "Wed": 28, "Thu": 45, "Fri": 38, "Sat": 12, "Sun": 8},
|
| 178 |
+
title="",
|
| 179 |
+
height=250
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
spacer("1rem")
|
| 183 |
+
section_header("Active Pipeline", "🔄")
|
| 184 |
+
|
| 185 |
+
pipeline_progress([
|
| 186 |
+
{"name": "Encode", "status": "done"},
|
| 187 |
+
{"name": "Search", "status": "active"},
|
| 188 |
+
{"name": "Predict", "status": "pending"},
|
| 189 |
+
{"name": "Verify", "status": "pending"},
|
| 190 |
+
])
|
| 191 |
+
|
| 192 |
+
spacer("2rem")
|
| 193 |
+
|
| 194 |
+
# Tip
|
| 195 |
+
st.markdown(f"""
|
| 196 |
+
<div style="
|
| 197 |
+
background: {COLORS.bg_surface};
|
| 198 |
+
border: 1px solid {COLORS.border};
|
| 199 |
+
border-radius: 12px;
|
| 200 |
+
padding: 1.25rem;
|
| 201 |
+
display: flex;
|
| 202 |
+
align-items: center;
|
| 203 |
+
gap: 1rem;
|
| 204 |
+
">
|
| 205 |
+
<span style="font-size: 1.5rem;">💡</span>
|
| 206 |
+
<div>
|
| 207 |
+
<div style="font-size: 0.9375rem; color: {COLORS.text_primary}; font-weight: 500;">Pro Tip</div>
|
| 208 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 209 |
+
Use natural language like "Find molecules similar to aspirin that can cross the blood-brain barrier"
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
""", unsafe_allow_html=True)
|
old_ui/pages/settings.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BioFlow - Settings Page
|
| 3 |
+
========================
|
| 4 |
+
Configuration and preferences.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
| 12 |
+
|
| 13 |
+
from bioflow.ui.components import page_header, section_header, divider, spacer
|
| 14 |
+
from bioflow.ui.config import COLORS
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def render():
|
| 18 |
+
"""Render settings page."""
|
| 19 |
+
|
| 20 |
+
page_header("Settings", "Configure models, databases, and preferences", "⚙️")
|
| 21 |
+
|
| 22 |
+
# Tabs for different settings sections
|
| 23 |
+
tabs = st.tabs(["🧠 Models", "🗄️ Database", "🔌 API Keys", "🎨 Appearance"])
|
| 24 |
+
|
| 25 |
+
with tabs[0]:
|
| 26 |
+
section_header("Model Configuration", "🧠")
|
| 27 |
+
|
| 28 |
+
st.markdown(f"""
|
| 29 |
+
<div class="card" style="margin-bottom: 1rem;">
|
| 30 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.5rem;">
|
| 31 |
+
Embedding Models
|
| 32 |
+
</div>
|
| 33 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 34 |
+
Configure models used for molecular and protein embeddings
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
""", unsafe_allow_html=True)
|
| 38 |
+
|
| 39 |
+
col1, col2 = st.columns(2)
|
| 40 |
+
|
| 41 |
+
with col1:
|
| 42 |
+
st.selectbox(
|
| 43 |
+
"Molecule Encoder",
|
| 44 |
+
["MolCLR (Recommended)", "ChemBERTa", "GraphMVP", "MolBERT"],
|
| 45 |
+
help="Model for generating molecular embeddings"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
st.selectbox(
|
| 49 |
+
"Protein Encoder",
|
| 50 |
+
["ESM-2 (Recommended)", "ProtTrans", "UniRep", "SeqVec"],
|
| 51 |
+
help="Model for generating protein embeddings"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
with col2:
|
| 55 |
+
st.selectbox(
|
| 56 |
+
"Binding Predictor",
|
| 57 |
+
["DrugBAN (Recommended)", "DeepDTA", "GraphDTA", "Custom"],
|
| 58 |
+
help="Model for predicting drug-target binding"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
st.selectbox(
|
| 62 |
+
"Property Predictor",
|
| 63 |
+
["ADMET-AI (Recommended)", "ChemProp", "Custom"],
|
| 64 |
+
help="Model for ADMET property prediction"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
spacer("1rem")
|
| 68 |
+
|
| 69 |
+
st.markdown(f"""
|
| 70 |
+
<div class="card" style="margin-bottom: 1rem;">
|
| 71 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.5rem;">
|
| 72 |
+
LLM Settings
|
| 73 |
+
</div>
|
| 74 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 75 |
+
Configure language models for evidence retrieval and reasoning
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
""", unsafe_allow_html=True)
|
| 79 |
+
|
| 80 |
+
col1, col2 = st.columns(2)
|
| 81 |
+
|
| 82 |
+
with col1:
|
| 83 |
+
st.selectbox(
|
| 84 |
+
"LLM Provider",
|
| 85 |
+
["OpenAI", "Anthropic", "Local (Ollama)", "Azure OpenAI"]
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
with col2:
|
| 89 |
+
st.selectbox(
|
| 90 |
+
"Model",
|
| 91 |
+
["GPT-4o", "GPT-4-turbo", "Claude 3.5 Sonnet", "Llama 3.1 70B"]
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
st.slider("Temperature", 0.0, 1.0, 0.7, 0.1)
|
| 95 |
+
st.number_input("Max Tokens", 100, 4096, 2048, 100)
|
| 96 |
+
|
| 97 |
+
with tabs[1]:
|
| 98 |
+
section_header("Database Configuration", "🗄️")
|
| 99 |
+
|
| 100 |
+
st.markdown(f"""
|
| 101 |
+
<div class="card" style="margin-bottom: 1rem;">
|
| 102 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.5rem;">
|
| 103 |
+
Vector Database
|
| 104 |
+
</div>
|
| 105 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 106 |
+
Configure the vector store for similarity search
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
""", unsafe_allow_html=True)
|
| 110 |
+
|
| 111 |
+
col1, col2 = st.columns(2)
|
| 112 |
+
|
| 113 |
+
with col1:
|
| 114 |
+
st.selectbox("Vector Store", ["Qdrant (Recommended)", "Milvus", "Pinecone", "Weaviate", "ChromaDB"])
|
| 115 |
+
st.text_input("Host", value="localhost")
|
| 116 |
+
|
| 117 |
+
with col2:
|
| 118 |
+
st.number_input("Port", 1, 65535, 6333)
|
| 119 |
+
st.text_input("Collection Name", value="bioflow_embeddings")
|
| 120 |
+
|
| 121 |
+
spacer("1rem")
|
| 122 |
+
|
| 123 |
+
st.markdown(f"""
|
| 124 |
+
<div class="card" style="margin-bottom: 1rem;">
|
| 125 |
+
<div style="font-weight: 600; color: {COLORS.text_primary}; margin-bottom: 0.5rem;">
|
| 126 |
+
Knowledge Sources
|
| 127 |
+
</div>
|
| 128 |
+
<div style="font-size: 0.8125rem; color: {COLORS.text_muted};">
|
| 129 |
+
External databases for evidence retrieval
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
""", unsafe_allow_html=True)
|
| 133 |
+
|
| 134 |
+
col1, col2 = st.columns(2)
|
| 135 |
+
|
| 136 |
+
with col1:
|
| 137 |
+
st.checkbox("PubMed", value=True)
|
| 138 |
+
st.checkbox("DrugBank", value=True)
|
| 139 |
+
st.checkbox("ChEMBL", value=True)
|
| 140 |
+
|
| 141 |
+
with col2:
|
| 142 |
+
st.checkbox("UniProt", value=True)
|
| 143 |
+
st.checkbox("KEGG", value=False)
|
| 144 |
+
st.checkbox("Reactome", value=False)
|
| 145 |
+
|
| 146 |
+
with tabs[2]:
|
| 147 |
+
section_header("API Keys", "🔌")
|
| 148 |
+
|
| 149 |
+
st.warning("⚠️ API keys are stored locally and never sent to external servers.")
|
| 150 |
+
|
| 151 |
+
st.text_input("OpenAI API Key", type="password", placeholder="sk-...")
|
| 152 |
+
st.text_input("Anthropic API Key", type="password", placeholder="sk-ant-...")
|
| 153 |
+
st.text_input("PubMed API Key", type="password", placeholder="Optional - for higher rate limits")
|
| 154 |
+
st.text_input("ChEMBL API Key", type="password", placeholder="Optional")
|
| 155 |
+
|
| 156 |
+
spacer("1rem")
|
| 157 |
+
|
| 158 |
+
if st.button("💾 Save API Keys", type="primary"):
|
| 159 |
+
st.success("✓ API keys saved securely")
|
| 160 |
+
|
| 161 |
+
with tabs[3]:
|
| 162 |
+
section_header("Appearance", "🎨")
|
| 163 |
+
|
| 164 |
+
st.selectbox("Theme", ["Dark (Default)", "Light", "System"])
|
| 165 |
+
st.selectbox("Accent Color", ["Purple", "Blue", "Green", "Cyan", "Pink"])
|
| 166 |
+
st.checkbox("Enable animations", value=True)
|
| 167 |
+
st.checkbox("Compact mode", value=False)
|
| 168 |
+
st.slider("Font size", 12, 18, 14)
|
| 169 |
+
|
| 170 |
+
spacer("2rem")
|
| 171 |
+
divider()
|
| 172 |
+
spacer("1rem")
|
| 173 |
+
|
| 174 |
+
# Save buttons
|
| 175 |
+
col1, col2, col3 = st.columns([1, 1, 2])
|
| 176 |
+
|
| 177 |
+
with col1:
|
| 178 |
+
if st.button("💾 Save Settings", type="primary", use_container_width=True):
|
| 179 |
+
st.success("✓ Settings saved successfully!")
|
| 180 |
+
|
| 181 |
+
with col2:
|
| 182 |
+
if st.button("🔄 Reset to Defaults", use_container_width=True):
|
| 183 |
+
st.info("Settings reset to defaults")
|
| 184 |
+
|
| 185 |
+
spacer("2rem")
|
| 186 |
+
|
| 187 |
+
# Version info
|
| 188 |
+
st.markdown(f"""
|
| 189 |
+
<div style="text-align: center; padding: 1rem; color: {COLORS.text_muted}; font-size: 0.75rem;">
|
| 190 |
+
BioFlow v0.1.0 • Built with OpenBioMed
|
| 191 |
+
</div>
|
| 192 |
+
""", unsafe_allow_html=True)
|
old_ui/requirements.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BioFlow UI Dependencies
|
| 2 |
+
# =======================
|
| 3 |
+
|
| 4 |
+
# Core Streamlit
|
| 5 |
+
streamlit>=1.29.0
|
| 6 |
+
|
| 7 |
+
# Visualization
|
| 8 |
+
plotly>=5.18.0
|
| 9 |
+
altair>=5.2.0
|
| 10 |
+
|
| 11 |
+
# Data handling
|
| 12 |
+
pandas>=2.0.0
|
| 13 |
+
numpy>=1.24.0
|
| 14 |
+
|
| 15 |
+
# Molecular visualization (optional)
|
| 16 |
+
rdkit>=2023.9.1
|
| 17 |
+
py3Dmol>=2.0.0
|
| 18 |
+
|
| 19 |
+
# Machine Learning (optional, for real encoders)
|
| 20 |
+
# torch>=2.0.0
|
| 21 |
+
# transformers>=4.35.0
|
| 22 |
+
|
| 23 |
+
# Vector database
|
| 24 |
+
qdrant-client>=1.7.0
|
| 25 |
+
|
| 26 |
+
# Image processing
|
| 27 |
+
Pillow>=10.0.0
|
| 28 |
+
|
| 29 |
+
# Utilities
|
| 30 |
+
python-dotenv>=1.0.0
|
| 31 |
+
pyyaml>=6.0.1
|
ui/.vscode/mcp.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"servers": {
|
| 3 |
+
"shadcn": {
|
| 4 |
+
"command": "npx",
|
| 5 |
+
"args": [
|
| 6 |
+
"shadcn@latest",
|
| 7 |
+
"mcp"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
ui/.vscode/tasks.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "2.0.0",
|
| 3 |
+
"tasks": [
|
| 4 |
+
{
|
| 5 |
+
"label": "Type Check",
|
| 6 |
+
"type": "shell",
|
| 7 |
+
"command": "pnpm",
|
| 8 |
+
"args": [
|
| 9 |
+
"run",
|
| 10 |
+
"type-check"
|
| 11 |
+
],
|
| 12 |
+
"problemMatcher": [
|
| 13 |
+
"$tsc"
|
| 14 |
+
],
|
| 15 |
+
"group": "test"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"label": "Lint",
|
| 19 |
+
"type": "shell",
|
| 20 |
+
"command": "pnpm",
|
| 21 |
+
"args": [
|
| 22 |
+
"run",
|
| 23 |
+
"lint"
|
| 24 |
+
],
|
| 25 |
+
"problemMatcher": [
|
| 26 |
+
"$eslint-stylish"
|
| 27 |
+
],
|
| 28 |
+
"group": "test"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"label": "Lint",
|
| 32 |
+
"type": "shell",
|
| 33 |
+
"command": "npm",
|
| 34 |
+
"args": [
|
| 35 |
+
"run",
|
| 36 |
+
"lint"
|
| 37 |
+
],
|
| 38 |
+
"problemMatcher": [
|
| 39 |
+
"$eslint-stylish"
|
| 40 |
+
],
|
| 41 |
+
"group": "test"
|
| 42 |
+
}
|
| 43 |
+
]
|
| 44 |
+
}
|
ui/README.md
CHANGED
|
@@ -1,22 +1,50 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
|
| 3 |
## Getting Started
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
|
|
|
|
| 1 |
+
# BioFlow UI (Next.js)
|
| 2 |
+
|
| 3 |
+
This project is a migration of the Streamlit "old_ui" to Next.js 16, Shadcn UI, Framer Motion, and Tailwind CSS v4.
|
| 4 |
|
| 5 |
## Getting Started
|
| 6 |
|
| 7 |
+
1. **Install dependencies:**
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
cd ui
|
| 11 |
+
pnpm install
|
| 12 |
+
# or
|
| 13 |
+
npm install
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
2. **Run the development server:**
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
pnpm dev
|
| 20 |
+
# or
|
| 21 |
+
npm run dev
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 25 |
+
|
| 26 |
+
## Structure
|
| 27 |
|
| 28 |
+
- `app/`: App Router pages and layout.
|
| 29 |
+
- `page.tsx`: Home Dashboard.
|
| 30 |
+
- `discovery/`: Drug Discovery Pipeline interface.
|
| 31 |
+
- `explorer/`: Data Explorer with Charts.
|
| 32 |
+
- `data/`: Data Management.
|
| 33 |
+
- `settings/`: Configuration.
|
| 34 |
+
- `api/`: API Route handlers.
|
| 35 |
+
- `components/`: Reusable UI components.
|
| 36 |
+
- `ui/`: Shadcn-like primitive components (Button, Card, etc.).
|
| 37 |
+
- `sidebar.tsx`: Main Navigation.
|
| 38 |
+
- `page-header.tsx`: Standard page headers.
|
| 39 |
|
| 40 |
+
## Tech Stack
|
| 41 |
|
| 42 |
+
- **Framework:** Next.js 16 (App Router)
|
| 43 |
+
- **Styling:** Tailwind CSS v4
|
| 44 |
+
- **UI Library:** Custom components inspired by Shadcn UI
|
| 45 |
+
- **Icons:** Lucide React
|
| 46 |
+
- **Charts:** Recharts
|
| 47 |
+
- **Animations:** Framer Motion (basic animations included, Framer Motion ready)
|
| 48 |
|
| 49 |
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 50 |
|
ui/app/api/discovery/route.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
export async function POST(request: Request) {
|
| 4 |
+
const body = await request.json();
|
| 5 |
+
const { query } = body;
|
| 6 |
+
|
| 7 |
+
console.log("Starting discovery for:", query);
|
| 8 |
+
|
| 9 |
+
// Here you would typically call your Python backend
|
| 10 |
+
// e.g., await fetch('http://localhost:8000/api/discovery', { ... })
|
| 11 |
+
|
| 12 |
+
// Mock response
|
| 13 |
+
return NextResponse.json({
|
| 14 |
+
success: true,
|
| 15 |
+
jobId: "job_" + Date.now(),
|
| 16 |
+
status: "processing",
|
| 17 |
+
message: "Pipeline started successfully"
|
| 18 |
+
});
|
| 19 |
+
}
|
ui/app/data/page.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { PageHeader, SectionHeader } from "@/components/page-header"
|
| 4 |
+
import { Card, CardContent } from "@/components/ui/card"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
+
import { Badge } from "@/components/ui/badge"
|
| 7 |
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
| 8 |
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
| 9 |
+
import { Folder, FileText, Database, HardDrive, Upload, CloudUpload, Eye, Download, Trash2 } from "lucide-react"
|
| 10 |
+
|
| 11 |
+
export default function DataPage() {
|
| 12 |
+
const datasets = [
|
| 13 |
+
{ name: "DrugBank Compounds", type: "Molecules", count: "12,450", size: "45.2 MB", updated: "2024-01-15" },
|
| 14 |
+
{ name: "ChEMBL Kinase Inhibitors", type: "Molecules", count: "8,234", size: "32.1 MB", updated: "2024-01-10" },
|
| 15 |
+
{ name: "Custom Protein Targets", type: "Proteins", count: "1,245", size: "78.5 MB", updated: "2024-01-08" },
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 20 |
+
<PageHeader
|
| 21 |
+
title="Data Management"
|
| 22 |
+
subtitle="Upload, manage, and organize your datasets"
|
| 23 |
+
icon={<Database className="h-8 w-8" />}
|
| 24 |
+
/>
|
| 25 |
+
|
| 26 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 27 |
+
{[
|
| 28 |
+
{ label: "Datasets", value: "5", icon: Folder, color: "text-blue-500" },
|
| 29 |
+
{ label: "Molecules", value: "24.5K", icon: FileText, color: "text-cyan-500" },
|
| 30 |
+
{ label: "Proteins", value: "1.2K", icon: Database, color: "text-emerald-500" },
|
| 31 |
+
{ label: "Storage Used", value: "156 MB", icon: HardDrive, color: "text-amber-500" },
|
| 32 |
+
].map((stat, i) => (
|
| 33 |
+
<Card key={i}>
|
| 34 |
+
<CardContent className="p-6">
|
| 35 |
+
<div className="flex justify-between items-start mb-2">
|
| 36 |
+
<div className="text-sm font-medium text-muted-foreground">{stat.label}</div>
|
| 37 |
+
<div className={`text-lg ${stat.color}`}><stat.icon className="h-5 w-5" /></div>
|
| 38 |
+
</div>
|
| 39 |
+
<div className="text-2xl font-bold">{stat.value}</div>
|
| 40 |
+
</CardContent>
|
| 41 |
+
</Card>
|
| 42 |
+
))}
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<Tabs defaultValue="datasets">
|
| 46 |
+
<TabsList>
|
| 47 |
+
<TabsTrigger value="datasets">Datasets</TabsTrigger>
|
| 48 |
+
<TabsTrigger value="upload">Upload</TabsTrigger>
|
| 49 |
+
<TabsTrigger value="processing">Processing</TabsTrigger>
|
| 50 |
+
</TabsList>
|
| 51 |
+
<TabsContent value="datasets" className="space-y-4">
|
| 52 |
+
<SectionHeader title="Your Datasets" icon={<Folder className="h-5 w-5 text-primary" />} />
|
| 53 |
+
<Card>
|
| 54 |
+
<Table>
|
| 55 |
+
<TableHeader>
|
| 56 |
+
<TableRow>
|
| 57 |
+
<TableHead>Name</TableHead>
|
| 58 |
+
<TableHead>Type</TableHead>
|
| 59 |
+
<TableHead>Items</TableHead>
|
| 60 |
+
<TableHead>Size</TableHead>
|
| 61 |
+
<TableHead>Updated</TableHead>
|
| 62 |
+
<TableHead className="text-right">Actions</TableHead>
|
| 63 |
+
</TableRow>
|
| 64 |
+
</TableHeader>
|
| 65 |
+
<TableBody>
|
| 66 |
+
{datasets.map((ds, i) => (
|
| 67 |
+
<TableRow key={i}>
|
| 68 |
+
<TableCell className="font-medium">{ds.name}</TableCell>
|
| 69 |
+
<TableCell>
|
| 70 |
+
<div className="flex items-center gap-2">
|
| 71 |
+
<Badge variant={ds.type === 'Molecules' ? 'default' : 'secondary'}>{ds.type}</Badge>
|
| 72 |
+
</div>
|
| 73 |
+
</TableCell>
|
| 74 |
+
<TableCell>{ds.count}</TableCell>
|
| 75 |
+
<TableCell>{ds.size}</TableCell>
|
| 76 |
+
<TableCell>{ds.updated}</TableCell>
|
| 77 |
+
<TableCell className="text-right">
|
| 78 |
+
<div className="flex justify-end gap-2">
|
| 79 |
+
<Button size="icon" variant="ghost" className="h-8 w-8"><Eye className="h-4 w-4" /></Button>
|
| 80 |
+
<Button size="icon" variant="ghost" className="h-8 w-8"><Download className="h-4 w-4" /></Button>
|
| 81 |
+
<Button size="icon" variant="ghost" className="h-8 w-8 text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
|
| 82 |
+
</div>
|
| 83 |
+
</TableCell>
|
| 84 |
+
</TableRow>
|
| 85 |
+
))}
|
| 86 |
+
</TableBody>
|
| 87 |
+
</Table>
|
| 88 |
+
</Card>
|
| 89 |
+
</TabsContent>
|
| 90 |
+
<TabsContent value="upload">
|
| 91 |
+
<SectionHeader title="Upload New Data" icon={<Upload className="h-5 w-5 text-primary" />} />
|
| 92 |
+
<Card>
|
| 93 |
+
<CardContent className="p-12">
|
| 94 |
+
<div className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center text-center space-y-4 hover:bg-accent/50 transition-colors cursor-pointer">
|
| 95 |
+
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
| 96 |
+
<CloudUpload className="h-8 w-8" />
|
| 97 |
+
</div>
|
| 98 |
+
<div>
|
| 99 |
+
<div className="text-lg font-semibold">Click to upload or drag and drop</div>
|
| 100 |
+
<div className="text-sm text-muted-foreground">CSV, SDF, FASTA, or JSON (max 50MB)</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</CardContent>
|
| 104 |
+
</Card>
|
| 105 |
+
</TabsContent>
|
| 106 |
+
<TabsContent value="processing">
|
| 107 |
+
<Card>
|
| 108 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 109 |
+
No active processing tasks.
|
| 110 |
+
</CardContent>
|
| 111 |
+
</Card>
|
| 112 |
+
</TabsContent>
|
| 113 |
+
</Tabs>
|
| 114 |
+
</div>
|
| 115 |
+
)
|
| 116 |
+
}
|
ui/app/discovery/page.tsx
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { PageHeader, SectionHeader } from "@/components/page-header"
|
| 5 |
+
import { Card, CardContent } from "@/components/ui/card"
|
| 6 |
+
import { Textarea } from "@/components/ui/textarea"
|
| 7 |
+
import { Button } from "@/components/ui/button"
|
| 8 |
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
| 9 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 10 |
+
import { Label } from "@/components/ui/label"
|
| 11 |
+
import { Microscope, Search, CheckCircle2, Circle, Loader2, ArrowRight } from "lucide-react"
|
| 12 |
+
|
| 13 |
+
export default function DiscoveryPage() {
|
| 14 |
+
const [query, setQuery] = React.useState("")
|
| 15 |
+
const [isSearching, setIsSearching] = React.useState(false)
|
| 16 |
+
const [step, setStep] = React.useState(0)
|
| 17 |
+
|
| 18 |
+
const handleSearch = () => {
|
| 19 |
+
setIsSearching(true)
|
| 20 |
+
setStep(1)
|
| 21 |
+
|
| 22 |
+
// Simulate pipeline
|
| 23 |
+
setTimeout(() => setStep(2), 1500)
|
| 24 |
+
setTimeout(() => setStep(3), 3000)
|
| 25 |
+
setTimeout(() => {
|
| 26 |
+
setStep(4)
|
| 27 |
+
setIsSearching(false)
|
| 28 |
+
}, 4500)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const steps = [
|
| 32 |
+
{ name: "Input", status: step > 0 ? "done" : "active" },
|
| 33 |
+
{ name: "Encode", status: step > 1 ? "done" : (step === 1 ? "active" : "pending") },
|
| 34 |
+
{ name: "Search", status: step > 2 ? "done" : (step === 2 ? "active" : "pending") },
|
| 35 |
+
{ name: "Predict", status: step > 3 ? "done" : (step === 3 ? "active" : "pending") },
|
| 36 |
+
{ name: "Results", status: step === 4 ? "active" : "pending" },
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 41 |
+
<PageHeader
|
| 42 |
+
title="Drug Discovery"
|
| 43 |
+
subtitle="Search for drug candidates with AI-powered analysis"
|
| 44 |
+
icon={<Microscope className="h-8 w-8" />}
|
| 45 |
+
/>
|
| 46 |
+
|
| 47 |
+
<Card>
|
| 48 |
+
<div className="p-4 border-b font-semibold">Search Query</div>
|
| 49 |
+
<CardContent className="p-6">
|
| 50 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
| 51 |
+
<div className="md:col-span-3">
|
| 52 |
+
<Textarea
|
| 53 |
+
placeholder="Enter a natural language query, SMILES string, or FASTA sequence..."
|
| 54 |
+
className="min-h-[120px] font-mono"
|
| 55 |
+
value={query}
|
| 56 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 57 |
+
/>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="space-y-4">
|
| 60 |
+
<div className="space-y-2">
|
| 61 |
+
<Label>Search Type</Label>
|
| 62 |
+
<Select defaultValue="Similarity">
|
| 63 |
+
<SelectTrigger>
|
| 64 |
+
<SelectValue placeholder="Select type" />
|
| 65 |
+
</SelectTrigger>
|
| 66 |
+
<SelectContent>
|
| 67 |
+
<SelectItem value="Similarity">Similarity</SelectItem>
|
| 68 |
+
<SelectItem value="Binding Affinity">Binding Affinity</SelectItem>
|
| 69 |
+
<SelectItem value="Properties">Properties</SelectItem>
|
| 70 |
+
</SelectContent>
|
| 71 |
+
</Select>
|
| 72 |
+
</div>
|
| 73 |
+
<div className="space-y-2">
|
| 74 |
+
<Label>Database</Label>
|
| 75 |
+
<Select defaultValue="All">
|
| 76 |
+
<SelectTrigger>
|
| 77 |
+
<SelectValue placeholder="Select database" />
|
| 78 |
+
</SelectTrigger>
|
| 79 |
+
<SelectContent>
|
| 80 |
+
<SelectItem value="All">All</SelectItem>
|
| 81 |
+
<SelectItem value="DrugBank">DrugBank</SelectItem>
|
| 82 |
+
<SelectItem value="ChEMBL">ChEMBL</SelectItem>
|
| 83 |
+
<SelectItem value="ZINC">ZINC</SelectItem>
|
| 84 |
+
</SelectContent>
|
| 85 |
+
</Select>
|
| 86 |
+
</div>
|
| 87 |
+
<Button
|
| 88 |
+
className="w-full"
|
| 89 |
+
onClick={handleSearch}
|
| 90 |
+
disabled={isSearching || !query}
|
| 91 |
+
>
|
| 92 |
+
{isSearching ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
|
| 93 |
+
{isSearching ? "Running..." : "Search"}
|
| 94 |
+
</Button>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</CardContent>
|
| 98 |
+
</Card>
|
| 99 |
+
|
| 100 |
+
<div className="space-y-4">
|
| 101 |
+
<SectionHeader title="Pipeline Status" icon={<ArrowRight className="h-5 w-5 text-muted-foreground" />} />
|
| 102 |
+
|
| 103 |
+
<div className="relative">
|
| 104 |
+
<div className="absolute left-0 top-1/2 w-full h-0.5 bg-muted -z-10 transform -translate-y-1/2"></div>
|
| 105 |
+
<div className="flex justify-between items-center w-full px-4">
|
| 106 |
+
{steps.map((s, i) => (
|
| 107 |
+
<div key={i} className="flex flex-col items-center gap-2 bg-background px-2">
|
| 108 |
+
<div className={`h-8 w-8 rounded-full flex items-center justify-center border-2 transition-colors ${
|
| 109 |
+
s.status === 'done' ? 'bg-primary border-primary text-primary-foreground' :
|
| 110 |
+
s.status === 'active' ? 'border-primary text-primary animate-pulse' : 'border-muted text-muted-foreground bg-background'
|
| 111 |
+
}`}>
|
| 112 |
+
{s.status === 'done' ? <CheckCircle2 className="h-5 w-5" /> : <Circle className="h-5 w-5" />}
|
| 113 |
+
</div>
|
| 114 |
+
<span className={`text-sm font-medium ${s.status === 'pending' ? 'text-muted-foreground' : 'text-foreground'}`}>
|
| 115 |
+
{s.name}
|
| 116 |
+
</span>
|
| 117 |
+
</div>
|
| 118 |
+
))}
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
{step === 4 && (
|
| 124 |
+
<div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
|
| 125 |
+
<SectionHeader title="Results" icon={<CheckCircle2 className="h-5 w-5 text-green-500" />} />
|
| 126 |
+
|
| 127 |
+
<Tabs defaultValue="candidates">
|
| 128 |
+
<TabsList>
|
| 129 |
+
<TabsTrigger value="candidates">Top Candidates</TabsTrigger>
|
| 130 |
+
<TabsTrigger value="analysis">Property Analysis</TabsTrigger>
|
| 131 |
+
<TabsTrigger value="evidence">Evidence</TabsTrigger>
|
| 132 |
+
</TabsList>
|
| 133 |
+
<TabsContent value="candidates" className="space-y-4">
|
| 134 |
+
{[
|
| 135 |
+
{ name: "Candidate A", score: 0.95, mw: "342.4", logp: "2.1" },
|
| 136 |
+
{ name: "Candidate B", score: 0.89, mw: "298.3", logp: "1.8" },
|
| 137 |
+
{ name: "Candidate C", score: 0.82, mw: "415.5", logp: "3.2" },
|
| 138 |
+
{ name: "Candidate D", score: 0.76, mw: "267.3", logp: "1.5" },
|
| 139 |
+
{ name: "Candidate E", score: 0.71, mw: "389.4", logp: "2.8" },
|
| 140 |
+
].map((candidate, i) => (
|
| 141 |
+
<Card key={i}>
|
| 142 |
+
<CardContent className="p-4 flex items-center justify-between">
|
| 143 |
+
<div>
|
| 144 |
+
<div className="font-bold text-lg">{candidate.name}</div>
|
| 145 |
+
<div className="flex gap-4 text-sm text-muted-foreground">
|
| 146 |
+
<span>MW: {candidate.mw}</span>
|
| 147 |
+
<span>LogP: {candidate.logp}</span>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
<div className="text-right">
|
| 151 |
+
<div className="text-sm text-muted-foreground">Score</div>
|
| 152 |
+
<div className={`text-xl font-bold ${
|
| 153 |
+
candidate.score >= 0.9 ? 'text-green-600' :
|
| 154 |
+
candidate.score >= 0.8 ? 'text-green-500' : 'text-amber-500'
|
| 155 |
+
}`}>
|
| 156 |
+
{candidate.score}
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</CardContent>
|
| 160 |
+
</Card>
|
| 161 |
+
))}
|
| 162 |
+
</TabsContent>
|
| 163 |
+
<TabsContent value="analysis">
|
| 164 |
+
<Card>
|
| 165 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 166 |
+
Chart visualization would go here (using Recharts).
|
| 167 |
+
</CardContent>
|
| 168 |
+
</Card>
|
| 169 |
+
</TabsContent>
|
| 170 |
+
<TabsContent value="evidence">
|
| 171 |
+
<Card>
|
| 172 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 173 |
+
Evidence graph visualization would go here.
|
| 174 |
+
</CardContent>
|
| 175 |
+
</Card>
|
| 176 |
+
</TabsContent>
|
| 177 |
+
</Tabs>
|
| 178 |
+
</div>
|
| 179 |
+
)}
|
| 180 |
+
</div>
|
| 181 |
+
)
|
| 182 |
+
}
|
ui/app/explorer/page.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { PageHeader, SectionHeader } from "@/components/page-header"
|
| 5 |
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
| 6 |
+
import { Button } from "@/components/ui/button"
|
| 7 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 8 |
+
import { Label } from "@/components/ui/label"
|
| 9 |
+
import { Dna, RefreshCw, Map as MapIcon, BarChart2, Activity, Zap, Grid3X3 } from "lucide-react"
|
| 10 |
+
import { ResponsiveContainer, ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, Cell } from "recharts"
|
| 11 |
+
|
| 12 |
+
interface DataPoint {
|
| 13 |
+
x: number;
|
| 14 |
+
y: number;
|
| 15 |
+
z: number;
|
| 16 |
+
color: string;
|
| 17 |
+
name: string;
|
| 18 |
+
affinity: number;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export default function ExplorerPage() {
|
| 22 |
+
const [dataset, setDataset] = React.useState("DrugBank")
|
| 23 |
+
const [visualization, setVisualization] = React.useState("UMAP")
|
| 24 |
+
const [colorBy, setColorBy] = React.useState("Activity")
|
| 25 |
+
const [data, setData] = React.useState<DataPoint[]>([])
|
| 26 |
+
|
| 27 |
+
// Sample Data Generation
|
| 28 |
+
React.useEffect(() => {
|
| 29 |
+
const points: DataPoint[] = []
|
| 30 |
+
const clusters = [
|
| 31 |
+
{ cx: 2, cy: 3, color: "var(--color-chart-1)" },
|
| 32 |
+
{ cx: -2, cy: -1, color: "var(--color-chart-2)" },
|
| 33 |
+
{ cx: 4, cy: -2, color: "var(--color-chart-3)" },
|
| 34 |
+
{ cx: -1, cy: 4, color: "var(--color-chart-4)" }
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
for (let i = 0; i < 200; i++) {
|
| 38 |
+
const cluster = clusters[Math.floor(i / 50)]
|
| 39 |
+
points.push({
|
| 40 |
+
x: cluster.cx + (Math.random() - 0.5) * 2,
|
| 41 |
+
y: cluster.cy + (Math.random() - 0.5) * 2,
|
| 42 |
+
z: Math.random() * 100,
|
| 43 |
+
color: cluster.color,
|
| 44 |
+
name: `Mol_${i}`,
|
| 45 |
+
affinity: Math.random() * 100
|
| 46 |
+
})
|
| 47 |
+
}
|
| 48 |
+
setData(points)
|
| 49 |
+
}, [dataset])
|
| 50 |
+
|
| 51 |
+
return (
|
| 52 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 53 |
+
<PageHeader
|
| 54 |
+
title="Data Explorer"
|
| 55 |
+
subtitle="Visualize molecular embeddings and relationships"
|
| 56 |
+
icon={<Dna className="h-8 w-8 text-primary" />}
|
| 57 |
+
/>
|
| 58 |
+
|
| 59 |
+
<Card className="border-l-4 border-l-primary/50">
|
| 60 |
+
<CardHeader>
|
| 61 |
+
<CardTitle className="flex items-center gap-2">
|
| 62 |
+
<Activity className="h-5 w-5" />
|
| 63 |
+
Visualization Controls
|
| 64 |
+
</CardTitle>
|
| 65 |
+
<CardDescription>Adjust projection parameters and visual styles</CardDescription>
|
| 66 |
+
</CardHeader>
|
| 67 |
+
<CardContent className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
| 68 |
+
<div className="space-y-2">
|
| 69 |
+
<Label htmlFor="dataset">Dataset</Label>
|
| 70 |
+
<Select value={dataset} onValueChange={setDataset}>
|
| 71 |
+
<SelectTrigger id="dataset">
|
| 72 |
+
<SelectValue placeholder="Select dataset" />
|
| 73 |
+
</SelectTrigger>
|
| 74 |
+
<SelectContent>
|
| 75 |
+
<SelectItem value="DrugBank">DrugBank</SelectItem>
|
| 76 |
+
<SelectItem value="ChEMBL">ChEMBL</SelectItem>
|
| 77 |
+
<SelectItem value="ZINC">ZINC</SelectItem>
|
| 78 |
+
</SelectContent>
|
| 79 |
+
</Select>
|
| 80 |
+
</div>
|
| 81 |
+
<div className="space-y-2">
|
| 82 |
+
<Label htmlFor="visualization">Algorithm</Label>
|
| 83 |
+
<Select value={visualization} onValueChange={setVisualization}>
|
| 84 |
+
<SelectTrigger id="visualization">
|
| 85 |
+
<SelectValue placeholder="Select algorithm" />
|
| 86 |
+
</SelectTrigger>
|
| 87 |
+
<SelectContent>
|
| 88 |
+
<SelectItem value="UMAP">UMAP</SelectItem>
|
| 89 |
+
<SelectItem value="t-SNE">t-SNE</SelectItem>
|
| 90 |
+
<SelectItem value="PCA">PCA</SelectItem>
|
| 91 |
+
</SelectContent>
|
| 92 |
+
</Select>
|
| 93 |
+
</div>
|
| 94 |
+
<div className="space-y-2">
|
| 95 |
+
<Label htmlFor="colorBy">Color Mapping</Label>
|
| 96 |
+
<Select value={colorBy} onValueChange={setColorBy}>
|
| 97 |
+
<SelectTrigger id="colorBy">
|
| 98 |
+
<SelectValue placeholder="Select color metric" />
|
| 99 |
+
</SelectTrigger>
|
| 100 |
+
<SelectContent>
|
| 101 |
+
<SelectItem value="Activity">Binding Affinity</SelectItem>
|
| 102 |
+
<SelectItem value="MW">Molecular Weight</SelectItem>
|
| 103 |
+
<SelectItem value="LogP">LogP</SelectItem>
|
| 104 |
+
<SelectItem value="Cluster">Cluster ID</SelectItem>
|
| 105 |
+
</SelectContent>
|
| 106 |
+
</Select>
|
| 107 |
+
</div>
|
| 108 |
+
<div className="flex items-end">
|
| 109 |
+
<Button variant="secondary" className="w-full" onClick={() => setDataset(d => d === "DrugBank" ? "ChEMBL" : "DrugBank")}>
|
| 110 |
+
<RefreshCw className="mr-2 h-4 w-4" />
|
| 111 |
+
Regenerate View
|
| 112 |
+
</Button>
|
| 113 |
+
</div>
|
| 114 |
+
</CardContent>
|
| 115 |
+
</Card>
|
| 116 |
+
|
| 117 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 118 |
+
<div className="lg:col-span-2 space-y-4">
|
| 119 |
+
<SectionHeader title="Embedding Space" icon={<MapIcon className="h-5 w-5 text-primary" />} />
|
| 120 |
+
<Card className="h-[500px] overflow-hidden bg-gradient-to-br from-card to-secondary/30">
|
| 121 |
+
<CardContent className="p-4 h-full">
|
| 122 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 123 |
+
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
| 124 |
+
<XAxis type="number" dataKey="x" name="PC1" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
|
| 125 |
+
<YAxis type="number" dataKey="y" name="PC2" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
|
| 126 |
+
<ZAxis type="number" dataKey="z" range={[50, 400]} />
|
| 127 |
+
<Tooltip
|
| 128 |
+
cursor={{ strokeDasharray: '3 3' }}
|
| 129 |
+
content={({ active, payload }) => {
|
| 130 |
+
if (active && payload && payload.length) {
|
| 131 |
+
const data = payload[0].payload;
|
| 132 |
+
return (
|
| 133 |
+
<div className="bg-popover border border-border p-3 rounded-lg shadow-xl text-sm">
|
| 134 |
+
<p className="font-bold mb-1 text-primary">{data.name}</p>
|
| 135 |
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
| 136 |
+
<span>X:</span> <span className="text-foreground">{Number(data.x).toFixed(2)}</span>
|
| 137 |
+
<span>Y:</span> <span className="text-foreground">{Number(data.y).toFixed(2)}</span>
|
| 138 |
+
<span>Affinity:</span> <span className="text-foreground">{Number(data.affinity).toFixed(2)}</span>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
)
|
| 142 |
+
}
|
| 143 |
+
return null;
|
| 144 |
+
}}
|
| 145 |
+
/>
|
| 146 |
+
<Scatter name="Molecules" data={data} fill="#8884d8">
|
| 147 |
+
{data.map((entry, index) => (
|
| 148 |
+
<Cell key={`cell-${index}`} fill={entry.color} fillOpacity={0.7} className="hover:opacity-100 transition-opacity duration-200" />
|
| 149 |
+
))}
|
| 150 |
+
</Scatter>
|
| 151 |
+
</ScatterChart>
|
| 152 |
+
</ResponsiveContainer>
|
| 153 |
+
</CardContent>
|
| 154 |
+
</Card>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div className="space-y-6">
|
| 158 |
+
<SectionHeader title="Data Intelligence" icon={<Grid3X3 className="h-5 w-5 text-primary" />} />
|
| 159 |
+
|
| 160 |
+
<div className="grid grid-cols-1 gap-4">
|
| 161 |
+
<Card>
|
| 162 |
+
<CardHeader className="pb-2">
|
| 163 |
+
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center justify-between">
|
| 164 |
+
Active Molecules
|
| 165 |
+
<Zap className="h-4 w-4 text-yellow-500" />
|
| 166 |
+
</CardTitle>
|
| 167 |
+
</CardHeader>
|
| 168 |
+
<CardContent>
|
| 169 |
+
<div className="text-2xl font-bold">12,450</div>
|
| 170 |
+
<p className="text-xs text-muted-foreground mt-1">+2.5% from last month</p>
|
| 171 |
+
</CardContent>
|
| 172 |
+
</Card>
|
| 173 |
+
<Card>
|
| 174 |
+
<CardHeader className="pb-2">
|
| 175 |
+
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center justify-between">
|
| 176 |
+
Clusters Identified
|
| 177 |
+
<Grid3X3 className="h-4 w-4 text-blue-500" />
|
| 178 |
+
</CardTitle>
|
| 179 |
+
</CardHeader>
|
| 180 |
+
<CardContent>
|
| 181 |
+
<div className="text-2xl font-bold text-chart-2">4</div>
|
| 182 |
+
<p className="text-xs text-muted-foreground mt-1">Distinct chemical series</p>
|
| 183 |
+
</CardContent>
|
| 184 |
+
</Card>
|
| 185 |
+
<Card>
|
| 186 |
+
<CardHeader className="pb-2">
|
| 187 |
+
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center justify-between">
|
| 188 |
+
Avg Confidence
|
| 189 |
+
<BarChart2 className="h-4 w-4 text-green-500" />
|
| 190 |
+
</CardTitle>
|
| 191 |
+
</CardHeader>
|
| 192 |
+
<CardContent>
|
| 193 |
+
<div className="text-2xl font-bold text-chart-3">0.89</div>
|
| 194 |
+
<p className="text-xs text-muted-foreground mt-1">Across all predictions</p>
|
| 195 |
+
</CardContent>
|
| 196 |
+
</Card>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
<div className="p-4 rounded-lg bg-secondary/50 border border-secondary">
|
| 200 |
+
<h4 className="font-semibold mb-2 text-sm flex items-center gap-2">
|
| 201 |
+
<Zap className="h-3 w-3 text-primary" />
|
| 202 |
+
Quick Actions
|
| 203 |
+
</h4>
|
| 204 |
+
<div className="space-y-2">
|
| 205 |
+
<Button variant="outline" size="sm" className="w-full justify-start text-xs">Exort Selection as CSV</Button>
|
| 206 |
+
<Button variant="outline" size="sm" className="w-full justify-start text-xs">Run Clustering Analysis</Button>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
)
|
| 213 |
+
}
|
ui/app/globals.css
CHANGED
|
@@ -48,71 +48,71 @@
|
|
| 48 |
|
| 49 |
:root {
|
| 50 |
--radius: 0.625rem;
|
| 51 |
-
--background: oklch(
|
| 52 |
-
--foreground: oklch(0.
|
| 53 |
--card: oklch(1 0 0);
|
| 54 |
-
--card-foreground: oklch(0.
|
| 55 |
--popover: oklch(1 0 0);
|
| 56 |
-
--popover-foreground: oklch(0.
|
| 57 |
-
--primary: oklch(0.
|
| 58 |
-
--primary-foreground: oklch(0.
|
| 59 |
-
--secondary: oklch(0.
|
| 60 |
-
--secondary-foreground: oklch(0.
|
| 61 |
-
--muted: oklch(0.
|
| 62 |
-
--muted-foreground: oklch(0.
|
| 63 |
-
--accent: oklch(0.
|
| 64 |
-
--accent-foreground: oklch(0.
|
| 65 |
--destructive: oklch(0.577 0.245 27.325);
|
| 66 |
-
--border: oklch(0.
|
| 67 |
-
--input: oklch(0.
|
| 68 |
-
--ring: oklch(0.
|
| 69 |
--chart-1: oklch(0.646 0.222 41.116);
|
| 70 |
--chart-2: oklch(0.6 0.118 184.704);
|
| 71 |
--chart-3: oklch(0.398 0.07 227.392);
|
| 72 |
--chart-4: oklch(0.828 0.189 84.429);
|
| 73 |
--chart-5: oklch(0.769 0.188 70.08);
|
| 74 |
-
--sidebar: oklch(0.
|
| 75 |
-
--sidebar-foreground: oklch(0.
|
| 76 |
-
--sidebar-primary: oklch(0.
|
| 77 |
-
--sidebar-primary-foreground: oklch(0.
|
| 78 |
-
--sidebar-accent: oklch(0.
|
| 79 |
-
--sidebar-accent-foreground: oklch(0.
|
| 80 |
-
--sidebar-border: oklch(0.
|
| 81 |
-
--sidebar-ring: oklch(0.
|
| 82 |
}
|
| 83 |
|
| 84 |
.dark {
|
| 85 |
-
--background: oklch(0.
|
| 86 |
-
--foreground: oklch(0.
|
| 87 |
-
--card: oklch(0.
|
| 88 |
-
--card-foreground: oklch(0.
|
| 89 |
-
--popover: oklch(0.
|
| 90 |
-
--popover-foreground: oklch(0.
|
| 91 |
-
--primary: oklch(0.
|
| 92 |
-
--primary-foreground: oklch(0.
|
| 93 |
-
--secondary: oklch(0.
|
| 94 |
-
--secondary-foreground: oklch(0.
|
| 95 |
-
--muted: oklch(0.
|
| 96 |
-
--muted-foreground: oklch(0.
|
| 97 |
-
--accent: oklch(0.
|
| 98 |
-
--accent-foreground: oklch(0.
|
| 99 |
--destructive: oklch(0.704 0.191 22.216);
|
| 100 |
-
--border: oklch(
|
| 101 |
-
--input: oklch(
|
| 102 |
-
--ring: oklch(0.
|
| 103 |
--chart-1: oklch(0.488 0.243 264.376);
|
| 104 |
--chart-2: oklch(0.696 0.17 162.48);
|
| 105 |
--chart-3: oklch(0.769 0.188 70.08);
|
| 106 |
--chart-4: oklch(0.627 0.265 303.9);
|
| 107 |
--chart-5: oklch(0.645 0.246 16.439);
|
| 108 |
-
--sidebar: oklch(0.
|
| 109 |
-
--sidebar-foreground: oklch(0.
|
| 110 |
-
--sidebar-primary: oklch(0.
|
| 111 |
-
--sidebar-primary-foreground: oklch(0.
|
| 112 |
-
--sidebar-accent: oklch(0.
|
| 113 |
-
--sidebar-accent-foreground: oklch(0.
|
| 114 |
-
--sidebar-border: oklch(
|
| 115 |
-
--sidebar-ring: oklch(0.
|
| 116 |
}
|
| 117 |
|
| 118 |
@layer base {
|
|
|
|
| 48 |
|
| 49 |
:root {
|
| 50 |
--radius: 0.625rem;
|
| 51 |
+
--background: oklch(0.985 0 0);
|
| 52 |
+
--foreground: oklch(0.145 0.02 260);
|
| 53 |
--card: oklch(1 0 0);
|
| 54 |
+
--card-foreground: oklch(0.145 0.02 260);
|
| 55 |
--popover: oklch(1 0 0);
|
| 56 |
+
--popover-foreground: oklch(0.145 0.02 260);
|
| 57 |
+
--primary: oklch(0.55 0.2 280);
|
| 58 |
+
--primary-foreground: oklch(0.985 0 0);
|
| 59 |
+
--secondary: oklch(0.97 0.01 260);
|
| 60 |
+
--secondary-foreground: oklch(0.55 0.2 280);
|
| 61 |
+
--muted: oklch(0.97 0.01 260);
|
| 62 |
+
--muted-foreground: oklch(0.556 0.03 260);
|
| 63 |
+
--accent: oklch(0.97 0.01 260);
|
| 64 |
+
--accent-foreground: oklch(0.55 0.2 280);
|
| 65 |
--destructive: oklch(0.577 0.245 27.325);
|
| 66 |
+
--border: oklch(0.922 0.01 260);
|
| 67 |
+
--input: oklch(0.922 0.01 260);
|
| 68 |
+
--ring: oklch(0.55 0.2 280);
|
| 69 |
--chart-1: oklch(0.646 0.222 41.116);
|
| 70 |
--chart-2: oklch(0.6 0.118 184.704);
|
| 71 |
--chart-3: oklch(0.398 0.07 227.392);
|
| 72 |
--chart-4: oklch(0.828 0.189 84.429);
|
| 73 |
--chart-5: oklch(0.769 0.188 70.08);
|
| 74 |
+
--sidebar: oklch(0.985 0 0);
|
| 75 |
+
--sidebar-foreground: oklch(0.145 0 0);
|
| 76 |
+
--sidebar-primary: oklch(0.205 0 0);
|
| 77 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 78 |
+
--sidebar-accent: oklch(0.97 0 0);
|
| 79 |
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 80 |
+
--sidebar-border: oklch(0.922 0 0);
|
| 81 |
+
--sidebar-ring: oklch(0.705 0 0);
|
| 82 |
}
|
| 83 |
|
| 84 |
.dark {
|
| 85 |
+
--background: oklch(0.12 0.03 260);
|
| 86 |
+
--foreground: oklch(0.95 0.01 260);
|
| 87 |
+
--card: oklch(0.15 0.03 260);
|
| 88 |
+
--card-foreground: oklch(0.95 0.01 260);
|
| 89 |
+
--popover: oklch(0.15 0.03 260);
|
| 90 |
+
--popover-foreground: oklch(0.95 0.01 260);
|
| 91 |
+
--primary: oklch(0.65 0.2 280);
|
| 92 |
+
--primary-foreground: oklch(0.12 0.03 260);
|
| 93 |
+
--secondary: oklch(0.2 0.04 260);
|
| 94 |
+
--secondary-foreground: oklch(0.95 0.01 260);
|
| 95 |
+
--muted: oklch(0.2 0.04 260);
|
| 96 |
+
--muted-foreground: oklch(0.7 0.04 260);
|
| 97 |
+
--accent: oklch(0.2 0.04 260);
|
| 98 |
+
--accent-foreground: oklch(0.95 0.01 260);
|
| 99 |
--destructive: oklch(0.704 0.191 22.216);
|
| 100 |
+
--border: oklch(0.25 0.04 260);
|
| 101 |
+
--input: oklch(0.25 0.04 260);
|
| 102 |
+
--ring: oklch(0.65 0.2 280);
|
| 103 |
--chart-1: oklch(0.488 0.243 264.376);
|
| 104 |
--chart-2: oklch(0.696 0.17 162.48);
|
| 105 |
--chart-3: oklch(0.769 0.188 70.08);
|
| 106 |
--chart-4: oklch(0.627 0.265 303.9);
|
| 107 |
--chart-5: oklch(0.645 0.246 16.439);
|
| 108 |
+
--sidebar: oklch(0.15 0.03 260);
|
| 109 |
+
--sidebar-foreground: oklch(0.95 0.01 260);
|
| 110 |
+
--sidebar-primary: oklch(0.65 0.2 280);
|
| 111 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 112 |
+
--sidebar-accent: oklch(0.2 0.04 260);
|
| 113 |
+
--sidebar-accent-foreground: oklch(0.95 0.01 260);
|
| 114 |
+
--sidebar-border: oklch(0.25 0.04 260);
|
| 115 |
+
--sidebar-ring: oklch(0.65 0.2 280);
|
| 116 |
}
|
| 117 |
|
| 118 |
@layer base {
|
ui/app/layout.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
import "./globals.css";
|
|
|
|
| 4 |
|
| 5 |
const geistSans = Geist({
|
| 6 |
variable: "--font-geist-sans",
|
|
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
|
| 13 |
});
|
| 14 |
|
| 15 |
export const metadata: Metadata = {
|
| 16 |
-
title: "
|
| 17 |
-
description: "
|
| 18 |
};
|
| 19 |
|
| 20 |
export default function RootLayout({
|
|
@@ -25,9 +26,14 @@ export default function RootLayout({
|
|
| 25 |
return (
|
| 26 |
<html lang="en">
|
| 27 |
<body
|
| 28 |
-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 29 |
>
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</body>
|
| 32 |
</html>
|
| 33 |
);
|
|
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
+
import { Sidebar } from "@/components/sidebar";
|
| 5 |
|
| 6 |
const geistSans = Geist({
|
| 7 |
variable: "--font-geist-sans",
|
|
|
|
| 14 |
});
|
| 15 |
|
| 16 |
export const metadata: Metadata = {
|
| 17 |
+
title: "BioFlow",
|
| 18 |
+
description: "AI-Powered Drug Discovery Platform",
|
| 19 |
};
|
| 20 |
|
| 21 |
export default function RootLayout({
|
|
|
|
| 26 |
return (
|
| 27 |
<html lang="en">
|
| 28 |
<body
|
| 29 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased flex h-screen overflow-hidden`}
|
| 30 |
>
|
| 31 |
+
<Sidebar />
|
| 32 |
+
<main className="flex-1 overflow-y-auto bg-background p-8">
|
| 33 |
+
<div className="mx-auto max-w-7xl">
|
| 34 |
+
{children}
|
| 35 |
+
</div>
|
| 36 |
+
</main>
|
| 37 |
</body>
|
| 38 |
</html>
|
| 39 |
);
|
ui/app/page.tsx
CHANGED
|
@@ -1,65 +1,169 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
| 5 |
-
<div className="
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
priority
|
| 14 |
-
/>
|
| 15 |
-
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
| 16 |
-
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
| 17 |
-
To get started, edit the page.tsx file.
|
| 18 |
-
</h1>
|
| 19 |
-
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
| 20 |
-
Looking for a starting point or more instructions? Head over to{" "}
|
| 21 |
-
<a
|
| 22 |
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 23 |
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 24 |
-
>
|
| 25 |
-
Templates
|
| 26 |
-
</a>{" "}
|
| 27 |
-
or the{" "}
|
| 28 |
-
<a
|
| 29 |
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 30 |
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 31 |
-
>
|
| 32 |
-
Learning
|
| 33 |
-
</a>{" "}
|
| 34 |
-
center.
|
| 35 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
</div>
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</div>
|
| 62 |
-
</
|
| 63 |
</div>
|
| 64 |
-
)
|
| 65 |
}
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button"
|
| 4 |
+
import { Badge } from "@/components/ui/badge"
|
| 5 |
+
import { Card, CardContent } from "@/components/ui/card"
|
| 6 |
+
import { Separator } from "@/components/ui/separator"
|
| 7 |
+
import { SectionHeader } from "@/components/page-header"
|
| 8 |
+
import { ArrowUp, Zap, Search, Database, FileText, Sparkles } from "lucide-react"
|
| 9 |
+
import Link from "next/link"
|
| 10 |
|
| 11 |
export default function Home() {
|
| 12 |
return (
|
| 13 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 14 |
+
{/* Hero Section */}
|
| 15 |
+
<div className="flex flex-col lg:flex-row gap-6">
|
| 16 |
+
<div className="flex-1 rounded-2xl bg-gradient-to-br from-primary/10 via-background to-background p-8 border">
|
| 17 |
+
<Badge variant="secondary" className="mb-4">New • BioFlow 2.0</Badge>
|
| 18 |
+
<h1 className="text-4xl font-bold tracking-tight mb-4">AI-Powered Drug Discovery</h1>
|
| 19 |
+
<p className="text-lg text-muted-foreground mb-6 max-w-xl">
|
| 20 |
+
Run discovery pipelines, predict binding, and surface evidence in one streamlined workspace.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</p>
|
| 22 |
+
<div className="flex gap-2 mb-6">
|
| 23 |
+
<Badge variant="outline" className="bg-primary/5 border-primary/20 text-primary">Model-aware search</Badge>
|
| 24 |
+
<Badge variant="outline" className="bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-400">Evidence-linked</Badge>
|
| 25 |
+
<Badge variant="outline" className="bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-400">Fast iteration</Badge>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div className="flex gap-4">
|
| 29 |
+
<Link href="/discovery">
|
| 30 |
+
<Button size="lg" className="font-semibold">
|
| 31 |
+
Start Discovery
|
| 32 |
+
</Button>
|
| 33 |
+
</Link>
|
| 34 |
+
<Link href="/explorer">
|
| 35 |
+
<Button size="lg" variant="outline">
|
| 36 |
+
Explore Data
|
| 37 |
+
</Button>
|
| 38 |
+
</Link>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div className="lg:w-[350px]">
|
| 43 |
+
<Card className="h-full">
|
| 44 |
+
<CardContent className="p-6 flex flex-col justify-between h-full">
|
| 45 |
+
<div>
|
| 46 |
+
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">Today</div>
|
| 47 |
+
<div className="text-4xl font-bold mb-2">156 Discoveries</div>
|
| 48 |
+
<div className="text-sm text-green-600 font-medium flex items-center gap-1">
|
| 49 |
+
<ArrowUp className="h-4 w-4" />
|
| 50 |
+
+12% vs last week
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<Separator className="my-4" />
|
| 55 |
+
|
| 56 |
+
<div className="space-y-2">
|
| 57 |
+
<div className="flex items-center justify-between text-sm">
|
| 58 |
+
<span className="flex items-center gap-2">
|
| 59 |
+
<span className="h-2 w-2 rounded-full bg-primary"></span>
|
| 60 |
+
Discovery
|
| 61 |
+
</span>
|
| 62 |
+
<span className="font-mono font-medium">64</span>
|
| 63 |
+
</div>
|
| 64 |
+
<div className="flex items-center justify-between text-sm">
|
| 65 |
+
<span className="flex items-center gap-2">
|
| 66 |
+
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
| 67 |
+
Prediction
|
| 68 |
+
</span>
|
| 69 |
+
<span className="font-mono font-medium">42</span>
|
| 70 |
+
</div>
|
| 71 |
+
<div className="flex items-center justify-between text-sm">
|
| 72 |
+
<span className="flex items-center gap-2">
|
| 73 |
+
<span className="h-2 w-2 rounded-full bg-amber-500"></span>
|
| 74 |
+
Evidence
|
| 75 |
+
</span>
|
| 76 |
+
<span className="font-mono font-medium">50</span>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</CardContent>
|
| 80 |
+
</Card>
|
| 81 |
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
{/* Metrics Row */}
|
| 85 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 86 |
+
{[
|
| 87 |
+
{ label: "Molecules", value: "12.5M", icon: "🧪", change: "+2.3%", color: "text-blue-500" },
|
| 88 |
+
{ label: "Proteins", value: "847K", icon: "🧬", change: "+1.8%", color: "text-cyan-500" },
|
| 89 |
+
{ label: "Papers", value: "1.2M", icon: "📚", change: "+5.2%", color: "text-emerald-500" },
|
| 90 |
+
{ label: "Discoveries", value: "156", icon: "✨", change: "+12%", color: "text-amber-500" }
|
| 91 |
+
].map((metric, i) => (
|
| 92 |
+
<Card key={i}>
|
| 93 |
+
<CardContent className="p-6">
|
| 94 |
+
<div className="flex justify-between items-start mb-2">
|
| 95 |
+
<div className="text-sm font-medium text-muted-foreground">{metric.label}</div>
|
| 96 |
+
<div className="text-lg">{metric.icon}</div>
|
| 97 |
+
</div>
|
| 98 |
+
<div className="text-2xl font-bold mb-1">{metric.value}</div>
|
| 99 |
+
<div className="text-xs font-medium flex items-center gap-1 text-green-500">
|
| 100 |
+
<ArrowUp className="h-3 w-3" />
|
| 101 |
+
{metric.change}
|
| 102 |
+
</div>
|
| 103 |
+
</CardContent>
|
| 104 |
+
</Card>
|
| 105 |
+
))}
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
{/* Quick Actions */}
|
| 109 |
+
<div className="pt-4">
|
| 110 |
+
<SectionHeader title="Quick Actions" icon={<Zap className="h-5 w-5 text-amber-500" />} />
|
| 111 |
+
|
| 112 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 113 |
+
<Link href="/discovery" className="block">
|
| 114 |
+
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 115 |
+
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 116 |
+
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
| 117 |
+
<Search className="h-5 w-5" />
|
| 118 |
+
</div>
|
| 119 |
+
<div>
|
| 120 |
+
<div className="font-semibold">New Discovery</div>
|
| 121 |
+
<div className="text-sm text-muted-foreground">Start a pipeline</div>
|
| 122 |
+
</div>
|
| 123 |
+
</CardContent>
|
| 124 |
+
</Card>
|
| 125 |
+
</Link>
|
| 126 |
+
<Link href="/explorer" className="block">
|
| 127 |
+
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 128 |
+
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 129 |
+
<div className="h-10 w-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
|
| 130 |
+
<Database className="h-5 w-5" />
|
| 131 |
+
</div>
|
| 132 |
+
<div>
|
| 133 |
+
<div className="font-semibold">Browse Data</div>
|
| 134 |
+
<div className="text-sm text-muted-foreground">Explore datasets</div>
|
| 135 |
+
</div>
|
| 136 |
+
</CardContent>
|
| 137 |
+
</Card>
|
| 138 |
+
</Link>
|
| 139 |
+
<Link href="/data" className="block">
|
| 140 |
+
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 141 |
+
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 142 |
+
<div className="h-10 w-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
|
| 143 |
+
<FileText className="h-5 w-5" />
|
| 144 |
+
</div>
|
| 145 |
+
<div>
|
| 146 |
+
<div className="font-semibold">Training</div>
|
| 147 |
+
<div className="text-sm text-muted-foreground">Train new models</div>
|
| 148 |
+
</div>
|
| 149 |
+
</CardContent>
|
| 150 |
+
</Card>
|
| 151 |
+
</Link>
|
| 152 |
+
<Link href="/settings" className="block">
|
| 153 |
+
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 154 |
+
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 155 |
+
<div className="h-10 w-10 rounded-full bg-slate-500/10 flex items-center justify-center text-slate-500">
|
| 156 |
+
<Sparkles className="h-5 w-5" />
|
| 157 |
+
</div>
|
| 158 |
+
<div>
|
| 159 |
+
<div className="font-semibold">View Insights</div>
|
| 160 |
+
<div className="text-sm text-muted-foreground">Check predictions</div>
|
| 161 |
+
</div>
|
| 162 |
+
</CardContent>
|
| 163 |
+
</Card>
|
| 164 |
+
</Link>
|
| 165 |
</div>
|
| 166 |
+
</div>
|
| 167 |
</div>
|
| 168 |
+
)
|
| 169 |
}
|
ui/app/settings/page.tsx
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { PageHeader, SectionHeader } from "@/components/page-header"
|
| 4 |
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
| 7 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 8 |
+
import { Label } from "@/components/ui/label"
|
| 9 |
+
import { Slider } from "@/components/ui/slider"
|
| 10 |
+
import { Switch } from "@/components/ui/switch"
|
| 11 |
+
import { Settings, Brain, Save } from "lucide-react"
|
| 12 |
+
|
| 13 |
+
export default function SettingsPage() {
|
| 14 |
+
return (
|
| 15 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 16 |
+
<PageHeader
|
| 17 |
+
title="Settings"
|
| 18 |
+
subtitle="Configure models, databases, and preferences"
|
| 19 |
+
icon={<Settings className="h-8 w-8 text-primary" />}
|
| 20 |
+
/>
|
| 21 |
+
|
| 22 |
+
<Tabs defaultValue="models" className="w-full">
|
| 23 |
+
<TabsList className="w-full justify-start overflow-x-auto">
|
| 24 |
+
<TabsTrigger value="models">Models</TabsTrigger>
|
| 25 |
+
<TabsTrigger value="database">Database</TabsTrigger>
|
| 26 |
+
<TabsTrigger value="api">API Keys</TabsTrigger>
|
| 27 |
+
<TabsTrigger value="appearance">Appearance</TabsTrigger>
|
| 28 |
+
<TabsTrigger value="system">System</TabsTrigger>
|
| 29 |
+
</TabsList>
|
| 30 |
+
<TabsContent value="models" className="space-y-6 mt-6">
|
| 31 |
+
<SectionHeader title="Model Configuration" icon={<Brain className="h-5 w-5 text-primary" />} />
|
| 32 |
+
|
| 33 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 34 |
+
<Card>
|
| 35 |
+
<CardHeader>
|
| 36 |
+
<CardTitle>Embedding Models</CardTitle>
|
| 37 |
+
<CardDescription>Configure models used for molecular and protein embeddings</CardDescription>
|
| 38 |
+
</CardHeader>
|
| 39 |
+
<CardContent className="space-y-4">
|
| 40 |
+
<div className="space-y-2">
|
| 41 |
+
<Label htmlFor="mol-encoder">Molecule Encoder</Label>
|
| 42 |
+
<Select defaultValue="MolCLR">
|
| 43 |
+
<SelectTrigger id="mol-encoder">
|
| 44 |
+
<SelectValue placeholder="Select molecule encoder" />
|
| 45 |
+
</SelectTrigger>
|
| 46 |
+
<SelectContent>
|
| 47 |
+
<SelectItem value="MolCLR">MolCLR (Recommended)</SelectItem>
|
| 48 |
+
<SelectItem value="ChemBERTa">ChemBERTa</SelectItem>
|
| 49 |
+
<SelectItem value="GraphMVP">GraphMVP</SelectItem>
|
| 50 |
+
<SelectItem value="MolBERT">MolBERT</SelectItem>
|
| 51 |
+
</SelectContent>
|
| 52 |
+
</Select>
|
| 53 |
+
</div>
|
| 54 |
+
<div className="space-y-2">
|
| 55 |
+
<Label htmlFor="prot-encoder">Protein Encoder</Label>
|
| 56 |
+
<Select defaultValue="ESM-2">
|
| 57 |
+
<SelectTrigger id="prot-encoder">
|
| 58 |
+
<SelectValue placeholder="Select protein encoder" />
|
| 59 |
+
</SelectTrigger>
|
| 60 |
+
<SelectContent>
|
| 61 |
+
<SelectItem value="ESM-2">ESM-2 (Recommended)</SelectItem>
|
| 62 |
+
<SelectItem value="ProtTrans">ProtTrans</SelectItem>
|
| 63 |
+
<SelectItem value="UniRep">UniRep</SelectItem>
|
| 64 |
+
<SelectItem value="SeqVec">SeqVec</SelectItem>
|
| 65 |
+
</SelectContent>
|
| 66 |
+
</Select>
|
| 67 |
+
</div>
|
| 68 |
+
</CardContent>
|
| 69 |
+
</Card>
|
| 70 |
+
|
| 71 |
+
<Card>
|
| 72 |
+
<CardHeader>
|
| 73 |
+
<CardTitle>Prediction Heads</CardTitle>
|
| 74 |
+
<CardDescription>Configure downstream task predictors</CardDescription>
|
| 75 |
+
</CardHeader>
|
| 76 |
+
<CardContent className="space-y-4">
|
| 77 |
+
<div className="space-y-2">
|
| 78 |
+
<Label htmlFor="binding">Binding Predictor</Label>
|
| 79 |
+
<Select defaultValue="DrugBAN">
|
| 80 |
+
<SelectTrigger id="binding">
|
| 81 |
+
<SelectValue placeholder="Select predictor" />
|
| 82 |
+
</SelectTrigger>
|
| 83 |
+
<SelectContent>
|
| 84 |
+
<SelectItem value="DrugBAN">DrugBAN (Recommended)</SelectItem>
|
| 85 |
+
<SelectItem value="DeepDTA">DeepDTA</SelectItem>
|
| 86 |
+
<SelectItem value="GraphDTA">GraphDTA</SelectItem>
|
| 87 |
+
<SelectItem value="Custom">Custom</SelectItem>
|
| 88 |
+
</SelectContent>
|
| 89 |
+
</Select>
|
| 90 |
+
</div>
|
| 91 |
+
<div className="space-y-2">
|
| 92 |
+
<Label htmlFor="property">Property Predictor</Label>
|
| 93 |
+
<Select defaultValue="ADMET-AI">
|
| 94 |
+
<SelectTrigger id="property">
|
| 95 |
+
<SelectValue placeholder="Select predictor" />
|
| 96 |
+
</SelectTrigger>
|
| 97 |
+
<SelectContent>
|
| 98 |
+
<SelectItem value="ADMET-AI">ADMET-AI (Recommended)</SelectItem>
|
| 99 |
+
<SelectItem value="ChemProp">ChemProp</SelectItem>
|
| 100 |
+
<SelectItem value="Custom">Custom</SelectItem>
|
| 101 |
+
</SelectContent>
|
| 102 |
+
</Select>
|
| 103 |
+
</div>
|
| 104 |
+
</CardContent>
|
| 105 |
+
</Card>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<Card>
|
| 109 |
+
<CardHeader>
|
| 110 |
+
<CardTitle>LLM Settings</CardTitle>
|
| 111 |
+
<CardDescription>Configure language models for evidence retrieval and reasoning</CardDescription>
|
| 112 |
+
</CardHeader>
|
| 113 |
+
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 114 |
+
<div className="space-y-2">
|
| 115 |
+
<Label htmlFor="llm-provider">LLM Provider</Label>
|
| 116 |
+
<Select defaultValue="OpenAI">
|
| 117 |
+
<SelectTrigger id="llm-provider">
|
| 118 |
+
<SelectValue placeholder="Select provider" />
|
| 119 |
+
</SelectTrigger>
|
| 120 |
+
<SelectContent>
|
| 121 |
+
<SelectItem value="OpenAI">OpenAI</SelectItem>
|
| 122 |
+
<SelectItem value="Anthropic">Anthropic</SelectItem>
|
| 123 |
+
<SelectItem value="Local">Local (Ollama)</SelectItem>
|
| 124 |
+
<SelectItem value="Azure">Azure OpenAI</SelectItem>
|
| 125 |
+
</SelectContent>
|
| 126 |
+
</Select>
|
| 127 |
+
</div>
|
| 128 |
+
<div className="space-y-2">
|
| 129 |
+
<Label htmlFor="llm-model">Model</Label>
|
| 130 |
+
<Select defaultValue="GPT-4o">
|
| 131 |
+
<SelectTrigger id="llm-model">
|
| 132 |
+
<SelectValue placeholder="Select model" />
|
| 133 |
+
</SelectTrigger>
|
| 134 |
+
<SelectContent>
|
| 135 |
+
<SelectItem value="GPT-4o">GPT-4o</SelectItem>
|
| 136 |
+
<SelectItem value="GPT-4-turbo">GPT-4-turbo</SelectItem>
|
| 137 |
+
<SelectItem value="Claude 3.5">Claude 3.5 Sonnet</SelectItem>
|
| 138 |
+
<SelectItem value="Llama 3">Llama 3.1 70B</SelectItem>
|
| 139 |
+
</SelectContent>
|
| 140 |
+
</Select>
|
| 141 |
+
</div>
|
| 142 |
+
<div className="col-span-1 md:col-span-2 space-y-4">
|
| 143 |
+
<div className="space-y-2">
|
| 144 |
+
<div className="flex items-center justify-between">
|
| 145 |
+
<Label>Temperature: 0.7</Label>
|
| 146 |
+
<span className="text-xs text-muted-foreground">Creativity vs Precision</span>
|
| 147 |
+
</div>
|
| 148 |
+
<Slider defaultValue={[0.7]} max={1} step={0.1} />
|
| 149 |
+
</div>
|
| 150 |
+
<div className="flex items-center space-x-2">
|
| 151 |
+
<Switch id="stream" defaultChecked />
|
| 152 |
+
<Label htmlFor="stream">Stream Responses</Label>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</CardContent>
|
| 156 |
+
</Card>
|
| 157 |
+
</TabsContent>
|
| 158 |
+
|
| 159 |
+
<TabsContent value="appearance">
|
| 160 |
+
<Card>
|
| 161 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 162 |
+
Theme settings coming soon.
|
| 163 |
+
</CardContent>
|
| 164 |
+
</Card>
|
| 165 |
+
</TabsContent>
|
| 166 |
+
|
| 167 |
+
<TabsContent value="database">
|
| 168 |
+
<Card>
|
| 169 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 170 |
+
Database connection settings.
|
| 171 |
+
</CardContent>
|
| 172 |
+
</Card>
|
| 173 |
+
</TabsContent>
|
| 174 |
+
|
| 175 |
+
<TabsContent value="api">
|
| 176 |
+
<Card>
|
| 177 |
+
<CardContent className="p-12 text-center text-muted-foreground">
|
| 178 |
+
API Key configuration.
|
| 179 |
+
</CardContent>
|
| 180 |
+
</Card>
|
| 181 |
+
</TabsContent>
|
| 182 |
+
</Tabs>
|
| 183 |
+
|
| 184 |
+
<div className="fixed bottom-6 right-6">
|
| 185 |
+
<Button size="lg" className="shadow-2xl">
|
| 186 |
+
<Save className="mr-2 h-4 w-4" />
|
| 187 |
+
Save Changes
|
| 188 |
+
</Button>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
)
|
| 192 |
+
}
|
ui/components/page-header.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui/components/page-header.tsx
|
| 2 |
+
import { cn } from "@/lib/utils"
|
| 3 |
+
|
| 4 |
+
interface PageHeaderProps {
|
| 5 |
+
title: string
|
| 6 |
+
subtitle?: string
|
| 7 |
+
icon?: React.ReactNode
|
| 8 |
+
className?: string
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export function PageHeader({ title, subtitle, icon, className }: PageHeaderProps) {
|
| 12 |
+
return (
|
| 13 |
+
<div className={cn("mb-8 space-y-2", className)}>
|
| 14 |
+
<h1 className="flex items-center gap-3 text-3xl font-bold tracking-tight">
|
| 15 |
+
{icon && <span className="text-primary">{icon}</span>}
|
| 16 |
+
{title}
|
| 17 |
+
</h1>
|
| 18 |
+
{subtitle && (
|
| 19 |
+
<p className="text-lg text-muted-foreground">
|
| 20 |
+
{subtitle}
|
| 21 |
+
</p>
|
| 22 |
+
)}
|
| 23 |
+
</div>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function SectionHeader({ title, icon, action }: { title: string, icon?: React.ReactNode, action?: React.ReactNode }) {
|
| 28 |
+
return (
|
| 29 |
+
<div className="flex items-center justify-between mb-6">
|
| 30 |
+
<h2 className="text-xl font-semibold flex items-center gap-2">
|
| 31 |
+
{icon}
|
| 32 |
+
{title}
|
| 33 |
+
</h2>
|
| 34 |
+
{action}
|
| 35 |
+
</div>
|
| 36 |
+
)
|
| 37 |
+
}
|
ui/components/sidebar.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui/components/sidebar.tsx
|
| 2 |
+
"use client"
|
| 3 |
+
|
| 4 |
+
import Link from "next/link"
|
| 5 |
+
import { usePathname } from "next/navigation"
|
| 6 |
+
import { Home, Microscope, Dna, BarChart2, Settings, Terminal } from "lucide-react"
|
| 7 |
+
|
| 8 |
+
import { cn } from "@/lib/utils"
|
| 9 |
+
import { Button } from "@/components/ui/button"
|
| 10 |
+
|
| 11 |
+
const sidebarItems = [
|
| 12 |
+
{
|
| 13 |
+
title: "Home",
|
| 14 |
+
href: "/",
|
| 15 |
+
icon: Home,
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
title: "Discovery",
|
| 19 |
+
href: "/discovery",
|
| 20 |
+
icon: Microscope,
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
title: "Explorer",
|
| 24 |
+
href: "/explorer",
|
| 25 |
+
icon: Dna,
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
title: "Data",
|
| 29 |
+
href: "/data",
|
| 30 |
+
icon: BarChart2,
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
title: "Settings",
|
| 34 |
+
href: "/settings",
|
| 35 |
+
icon: Settings,
|
| 36 |
+
},
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
export function Sidebar() {
|
| 40 |
+
const pathname = usePathname()
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="flex h-screen w-[250px] flex-col border-r bg-card pb-4 pt-6">
|
| 44 |
+
<div className="px-6 mb-8 flex items-center gap-2">
|
| 45 |
+
<div className="h-8 w-8 rounded-lg bg-primary/20 flex items-center justify-center text-primary">
|
| 46 |
+
<Dna className="h-5 w-5" />
|
| 47 |
+
</div>
|
| 48 |
+
<div className="font-bold text-xl tracking-tight">
|
| 49 |
+
Bio<span className="text-primary">Flow</span>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div className="px-4 py-2">
|
| 54 |
+
<div className="text-xs font-semibold text-muted-foreground mb-4 px-2 uppercase tracking-wider">
|
| 55 |
+
Navigation
|
| 56 |
+
</div>
|
| 57 |
+
<nav className="space-y-1">
|
| 58 |
+
{sidebarItems.map((item) => (
|
| 59 |
+
<Link
|
| 60 |
+
key={item.href}
|
| 61 |
+
href={item.href}
|
| 62 |
+
>
|
| 63 |
+
<Button
|
| 64 |
+
variant={pathname === item.href ? "secondary" : "ghost"}
|
| 65 |
+
className={cn(
|
| 66 |
+
"w-full justify-start gap-3",
|
| 67 |
+
pathname === item.href && "bg-secondary font-medium"
|
| 68 |
+
)}
|
| 69 |
+
>
|
| 70 |
+
<item.icon className="h-4 w-4" />
|
| 71 |
+
{item.title}
|
| 72 |
+
</Button>
|
| 73 |
+
</Link>
|
| 74 |
+
))}
|
| 75 |
+
</nav>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div className="mt-auto px-4">
|
| 79 |
+
<div className="rounded-lg border bg-muted/50 p-4">
|
| 80 |
+
<div className="flex items-center gap-2 mb-2">
|
| 81 |
+
<Terminal className="h-4 w-4 text-muted-foreground" />
|
| 82 |
+
<span className="text-xs font-medium">Status</span>
|
| 83 |
+
</div>
|
| 84 |
+
<div className="flex items-center gap-2">
|
| 85 |
+
<span className="relative flex h-2 w-2">
|
| 86 |
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
| 87 |
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
| 88 |
+
</span>
|
| 89 |
+
<span className="text-xs text-muted-foreground">System Online</span>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
)
|
| 95 |
+
}
|
ui/components/ui/badge.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui/components/ui/badge.tsx
|
| 2 |
+
import * as React from "react"
|
| 3 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const badgeVariants = cva(
|
| 8 |
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
| 9 |
+
{
|
| 10 |
+
variants: {
|
| 11 |
+
variant: {
|
| 12 |
+
default:
|
| 13 |
+
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
| 14 |
+
secondary:
|
| 15 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 16 |
+
destructive:
|
| 17 |
+
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
| 18 |
+
outline: "text-foreground",
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
defaultVariants: {
|
| 22 |
+
variant: "default",
|
| 23 |
+
},
|
| 24 |
+
}
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
export interface BadgeProps
|
| 28 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
| 29 |
+
VariantProps<typeof badgeVariants> {}
|
| 30 |
+
|
| 31 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 32 |
+
return (
|
| 33 |
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
| 34 |
+
)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export { Badge, badgeVariants }
|
ui/components/ui/button.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui/components/ui/button.tsx
|
| 2 |
+
import * as React from "react"
|
| 3 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 4 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const buttonVariants = cva(
|
| 9 |
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 10 |
+
{
|
| 11 |
+
variants: {
|
| 12 |
+
variant: {
|
| 13 |
+
default:
|
| 14 |
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
| 15 |
+
destructive:
|
| 16 |
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
| 17 |
+
outline:
|
| 18 |
+
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
| 19 |
+
secondary:
|
| 20 |
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
| 21 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 22 |
+
link: "text-primary underline-offset-4 hover:underline",
|
| 23 |
+
},
|
| 24 |
+
size: {
|
| 25 |
+
default: "h-9 px-4 py-2",
|
| 26 |
+
sm: "h-8 rounded-md px-3 text-xs",
|
| 27 |
+
lg: "h-10 rounded-md px-8",
|
| 28 |
+
icon: "h-9 w-9",
|
| 29 |
+
},
|
| 30 |
+
},
|
| 31 |
+
defaultVariants: {
|
| 32 |
+
variant: "default",
|
| 33 |
+
size: "default",
|
| 34 |
+
},
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
export interface ButtonProps
|
| 39 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 40 |
+
VariantProps<typeof buttonVariants> {
|
| 41 |
+
asChild?: boolean
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 45 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 46 |
+
const Comp = asChild ? Slot : "button"
|
| 47 |
+
return (
|
| 48 |
+
<Comp
|
| 49 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 50 |
+
ref={ref}
|
| 51 |
+
{...props}
|
| 52 |
+
/>
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
)
|
| 56 |
+
Button.displayName = "Button"
|
| 57 |
+
|
| 58 |
+
export { Button, buttonVariants }
|
ui/components/ui/card.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui/components/ui/card.tsx
|
| 2 |
+
import * as React from "react"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Card = React.forwardRef<
|
| 7 |
+
HTMLDivElement,
|
| 8 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 9 |
+
>(({ className, ...props }, ref) => (
|
| 10 |
+
<div
|
| 11 |
+
ref={ref}
|
| 12 |
+
className={cn(
|
| 13 |
+
"rounded-xl border bg-card text-card-foreground shadow",
|
| 14 |
+
className
|
| 15 |
+
)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
))
|
| 19 |
+
Card.displayName = "Card"
|
| 20 |
+
|
| 21 |
+
const CardHeader = React.forwardRef<
|
| 22 |
+
HTMLDivElement,
|
| 23 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 24 |
+
>(({ className, ...props }, ref) => (
|
| 25 |
+
<div
|
| 26 |
+
ref={ref}
|
| 27 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 28 |
+
{...props}
|
| 29 |
+
/>
|
| 30 |
+
))
|
| 31 |
+
CardHeader.displayName = "CardHeader"
|
| 32 |
+
|
| 33 |
+
const CardTitle = React.forwardRef<
|
| 34 |
+
HTMLDivElement,
|
| 35 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 36 |
+
>(({ className, ...props }, ref) => (
|
| 37 |
+
<div
|
| 38 |
+
ref={ref}
|
| 39 |
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
| 40 |
+
{...props}
|
| 41 |
+
/>
|
| 42 |
+
))
|
| 43 |
+
CardTitle.displayName = "CardTitle"
|
| 44 |
+
|
| 45 |
+
const CardDescription = React.forwardRef<
|
| 46 |
+
HTMLDivElement,
|
| 47 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 48 |
+
>(({ className, ...props }, ref) => (
|
| 49 |
+
<div
|
| 50 |
+
ref={ref}
|
| 51 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 52 |
+
{...props}
|
| 53 |
+
/>
|
| 54 |
+
))
|
| 55 |
+
CardDescription.displayName = "CardDescription"
|
| 56 |
+
|
| 57 |
+
const CardContent = React.forwardRef<
|
| 58 |
+
HTMLDivElement,
|
| 59 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 60 |
+
>(({ className, ...props }, ref) => (
|
| 61 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 62 |
+
))
|
| 63 |
+
CardContent.displayName = "CardContent"
|
| 64 |
+
|
| 65 |
+
const CardFooter = React.forwardRef<
|
| 66 |
+
HTMLDivElement,
|
| 67 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 68 |
+
>(({ className, ...props }, ref) => (
|
| 69 |
+
<div
|
| 70 |
+
ref={ref}
|
| 71 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
| 72 |
+
{...props}
|
| 73 |
+
/>
|
| 74 |
+
))
|
| 75 |
+
CardFooter.displayName = "CardFooter"
|
| 76 |
+
|
| 77 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
ui/components/ui/input.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
| 6 |
+
return (
|
| 7 |
+
<input
|
| 8 |
+
type={type}
|
| 9 |
+
data-slot="input"
|
| 10 |
+
className={cn(
|
| 11 |
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 12 |
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
| 13 |
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 14 |
+
className
|
| 15 |
+
)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export { Input }
|
ui/components/ui/label.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Label({
|
| 9 |
+
className,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 12 |
+
return (
|
| 13 |
+
<LabelPrimitive.Root
|
| 14 |
+
data-slot="label"
|
| 15 |
+
className={cn(
|
| 16 |
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export { Label }
|
ui/components/ui/select.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 5 |
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
function Select({
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
| 12 |
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function SelectGroup({
|
| 16 |
+
...props
|
| 17 |
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
| 18 |
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function SelectValue({
|
| 22 |
+
...props
|
| 23 |
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
| 24 |
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function SelectTrigger({
|
| 28 |
+
className,
|
| 29 |
+
size = "default",
|
| 30 |
+
children,
|
| 31 |
+
...props
|
| 32 |
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
| 33 |
+
size?: "sm" | "default"
|
| 34 |
+
}) {
|
| 35 |
+
return (
|
| 36 |
+
<SelectPrimitive.Trigger
|
| 37 |
+
data-slot="select-trigger"
|
| 38 |
+
data-size={size}
|
| 39 |
+
className={cn(
|
| 40 |
+
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 41 |
+
className
|
| 42 |
+
)}
|
| 43 |
+
{...props}
|
| 44 |
+
>
|
| 45 |
+
{children}
|
| 46 |
+
<SelectPrimitive.Icon asChild>
|
| 47 |
+
<ChevronDownIcon className="size-4 opacity-50" />
|
| 48 |
+
</SelectPrimitive.Icon>
|
| 49 |
+
</SelectPrimitive.Trigger>
|
| 50 |
+
)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function SelectContent({
|
| 54 |
+
className,
|
| 55 |
+
children,
|
| 56 |
+
position = "item-aligned",
|
| 57 |
+
align = "center",
|
| 58 |
+
...props
|
| 59 |
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
| 60 |
+
return (
|
| 61 |
+
<SelectPrimitive.Portal>
|
| 62 |
+
<SelectPrimitive.Content
|
| 63 |
+
data-slot="select-content"
|
| 64 |
+
className={cn(
|
| 65 |
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
| 66 |
+
position === "popper" &&
|
| 67 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 68 |
+
className
|
| 69 |
+
)}
|
| 70 |
+
position={position}
|
| 71 |
+
align={align}
|
| 72 |
+
{...props}
|
| 73 |
+
>
|
| 74 |
+
<SelectScrollUpButton />
|
| 75 |
+
<SelectPrimitive.Viewport
|
| 76 |
+
className={cn(
|
| 77 |
+
"p-1",
|
| 78 |
+
position === "popper" &&
|
| 79 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
| 80 |
+
)}
|
| 81 |
+
>
|
| 82 |
+
{children}
|
| 83 |
+
</SelectPrimitive.Viewport>
|
| 84 |
+
<SelectScrollDownButton />
|
| 85 |
+
</SelectPrimitive.Content>
|
| 86 |
+
</SelectPrimitive.Portal>
|
| 87 |
+
)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function SelectLabel({
|
| 91 |
+
className,
|
| 92 |
+
...props
|
| 93 |
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
| 94 |
+
return (
|
| 95 |
+
<SelectPrimitive.Label
|
| 96 |
+
data-slot="select-label"
|
| 97 |
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
| 98 |
+
{...props}
|
| 99 |
+
/>
|
| 100 |
+
)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function SelectItem({
|
| 104 |
+
className,
|
| 105 |
+
children,
|
| 106 |
+
...props
|
| 107 |
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
| 108 |
+
return (
|
| 109 |
+
<SelectPrimitive.Item
|
| 110 |
+
data-slot="select-item"
|
| 111 |
+
className={cn(
|
| 112 |
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
| 113 |
+
className
|
| 114 |
+
)}
|
| 115 |
+
{...props}
|
| 116 |
+
>
|
| 117 |
+
<span
|
| 118 |
+
data-slot="select-item-indicator"
|
| 119 |
+
className="absolute right-2 flex size-3.5 items-center justify-center"
|
| 120 |
+
>
|
| 121 |
+
<SelectPrimitive.ItemIndicator>
|
| 122 |
+
<CheckIcon className="size-4" />
|
| 123 |
+
</SelectPrimitive.ItemIndicator>
|
| 124 |
+
</span>
|
| 125 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 126 |
+
</SelectPrimitive.Item>
|
| 127 |
+
)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function SelectSeparator({
|
| 131 |
+
className,
|
| 132 |
+
...props
|
| 133 |
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
| 134 |
+
return (
|
| 135 |
+
<SelectPrimitive.Separator
|
| 136 |
+
data-slot="select-separator"
|
| 137 |
+
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
| 138 |
+
{...props}
|
| 139 |
+
/>
|
| 140 |
+
)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function SelectScrollUpButton({
|
| 144 |
+
className,
|
| 145 |
+
...props
|
| 146 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
| 147 |
+
return (
|
| 148 |
+
<SelectPrimitive.ScrollUpButton
|
| 149 |
+
data-slot="select-scroll-up-button"
|
| 150 |
+
className={cn(
|
| 151 |
+
"flex cursor-default items-center justify-center py-1",
|
| 152 |
+
className
|
| 153 |
+
)}
|
| 154 |
+
{...props}
|
| 155 |
+
>
|
| 156 |
+
<ChevronUpIcon className="size-4" />
|
| 157 |
+
</SelectPrimitive.ScrollUpButton>
|
| 158 |
+
)
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function SelectScrollDownButton({
|
| 162 |
+
className,
|
| 163 |
+
...props
|
| 164 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
| 165 |
+
return (
|
| 166 |
+
<SelectPrimitive.ScrollDownButton
|
| 167 |
+
data-slot="select-scroll-down-button"
|
| 168 |
+
className={cn(
|
| 169 |
+
"flex cursor-default items-center justify-center py-1",
|
| 170 |
+
className
|
| 171 |
+
)}
|
| 172 |
+
{...props}
|
| 173 |
+
>
|
| 174 |
+
<ChevronDownIcon className="size-4" />
|
| 175 |
+
</SelectPrimitive.ScrollDownButton>
|
| 176 |
+
)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
export {
|
| 180 |
+
Select,
|
| 181 |
+
SelectContent,
|
| 182 |
+
SelectGroup,
|
| 183 |
+
SelectItem,
|
| 184 |
+
SelectLabel,
|
| 185 |
+
SelectScrollDownButton,
|
| 186 |
+
SelectScrollUpButton,
|
| 187 |
+
SelectSeparator,
|
| 188 |
+
SelectTrigger,
|
| 189 |
+
SelectValue,
|
| 190 |
+
}
|
ui/components/ui/separator.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Separator({
|
| 9 |
+
className,
|
| 10 |
+
orientation = "horizontal",
|
| 11 |
+
decorative = true,
|
| 12 |
+
...props
|
| 13 |
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
| 14 |
+
return (
|
| 15 |
+
<SeparatorPrimitive.Root
|
| 16 |
+
data-slot="separator"
|
| 17 |
+
decorative={decorative}
|
| 18 |
+
orientation={orientation}
|
| 19 |
+
className={cn(
|
| 20 |
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
| 21 |
+
className
|
| 22 |
+
)}
|
| 23 |
+
{...props}
|
| 24 |
+
/>
|
| 25 |
+
)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export { Separator }
|
ui/components/ui/slider.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SliderPrimitive from "@radix-ui/react-slider"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Slider({
|
| 9 |
+
className,
|
| 10 |
+
defaultValue,
|
| 11 |
+
value,
|
| 12 |
+
min = 0,
|
| 13 |
+
max = 100,
|
| 14 |
+
...props
|
| 15 |
+
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
| 16 |
+
const _values = React.useMemo(
|
| 17 |
+
() =>
|
| 18 |
+
Array.isArray(value)
|
| 19 |
+
? value
|
| 20 |
+
: Array.isArray(defaultValue)
|
| 21 |
+
? defaultValue
|
| 22 |
+
: [min, max],
|
| 23 |
+
[value, defaultValue, min, max]
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<SliderPrimitive.Root
|
| 28 |
+
data-slot="slider"
|
| 29 |
+
defaultValue={defaultValue}
|
| 30 |
+
value={value}
|
| 31 |
+
min={min}
|
| 32 |
+
max={max}
|
| 33 |
+
className={cn(
|
| 34 |
+
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
| 35 |
+
className
|
| 36 |
+
)}
|
| 37 |
+
{...props}
|
| 38 |
+
>
|
| 39 |
+
<SliderPrimitive.Track
|
| 40 |
+
data-slot="slider-track"
|
| 41 |
+
className={cn(
|
| 42 |
+
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
| 43 |
+
)}
|
| 44 |
+
>
|
| 45 |
+
<SliderPrimitive.Range
|
| 46 |
+
data-slot="slider-range"
|
| 47 |
+
className={cn(
|
| 48 |
+
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
| 49 |
+
)}
|
| 50 |
+
/>
|
| 51 |
+
</SliderPrimitive.Track>
|
| 52 |
+
{Array.from({ length: _values.length }, (_, index) => (
|
| 53 |
+
<SliderPrimitive.Thumb
|
| 54 |
+
data-slot="slider-thumb"
|
| 55 |
+
key={index}
|
| 56 |
+
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
| 57 |
+
/>
|
| 58 |
+
))}
|
| 59 |
+
</SliderPrimitive.Root>
|
| 60 |
+
)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export { Slider }
|
ui/components/ui/switch.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Switch({
|
| 9 |
+
className,
|
| 10 |
+
size = "default",
|
| 11 |
+
...props
|
| 12 |
+
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
| 13 |
+
size?: "sm" | "default"
|
| 14 |
+
}) {
|
| 15 |
+
return (
|
| 16 |
+
<SwitchPrimitive.Root
|
| 17 |
+
data-slot="switch"
|
| 18 |
+
data-size={size}
|
| 19 |
+
className={cn(
|
| 20 |
+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
|
| 21 |
+
className
|
| 22 |
+
)}
|
| 23 |
+
{...props}
|
| 24 |
+
>
|
| 25 |
+
<SwitchPrimitive.Thumb
|
| 26 |
+
data-slot="switch-thumb"
|
| 27 |
+
className={cn(
|
| 28 |
+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
| 29 |
+
)}
|
| 30 |
+
/>
|
| 31 |
+
</SwitchPrimitive.Root>
|
| 32 |
+
)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export { Switch }
|
ui/components/ui/table.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
| 8 |
+
return (
|
| 9 |
+
<div
|
| 10 |
+
data-slot="table-container"
|
| 11 |
+
className="relative w-full overflow-x-auto"
|
| 12 |
+
>
|
| 13 |
+
<table
|
| 14 |
+
data-slot="table"
|
| 15 |
+
className={cn("w-full caption-bottom text-sm", className)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
</div>
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
| 23 |
+
return (
|
| 24 |
+
<thead
|
| 25 |
+
data-slot="table-header"
|
| 26 |
+
className={cn("[&_tr]:border-b", className)}
|
| 27 |
+
{...props}
|
| 28 |
+
/>
|
| 29 |
+
)
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
| 33 |
+
return (
|
| 34 |
+
<tbody
|
| 35 |
+
data-slot="table-body"
|
| 36 |
+
className={cn("[&_tr:last-child]:border-0", className)}
|
| 37 |
+
{...props}
|
| 38 |
+
/>
|
| 39 |
+
)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
| 43 |
+
return (
|
| 44 |
+
<tfoot
|
| 45 |
+
data-slot="table-footer"
|
| 46 |
+
className={cn(
|
| 47 |
+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
| 48 |
+
className
|
| 49 |
+
)}
|
| 50 |
+
{...props}
|
| 51 |
+
/>
|
| 52 |
+
)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
| 56 |
+
return (
|
| 57 |
+
<tr
|
| 58 |
+
data-slot="table-row"
|
| 59 |
+
className={cn(
|
| 60 |
+
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
| 61 |
+
className
|
| 62 |
+
)}
|
| 63 |
+
{...props}
|
| 64 |
+
/>
|
| 65 |
+
)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
| 69 |
+
return (
|
| 70 |
+
<th
|
| 71 |
+
data-slot="table-head"
|
| 72 |
+
className={cn(
|
| 73 |
+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
| 74 |
+
className
|
| 75 |
+
)}
|
| 76 |
+
{...props}
|
| 77 |
+
/>
|
| 78 |
+
)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
| 82 |
+
return (
|
| 83 |
+
<td
|
| 84 |
+
data-slot="table-cell"
|
| 85 |
+
className={cn(
|
| 86 |
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
| 87 |
+
className
|
| 88 |
+
)}
|
| 89 |
+
{...props}
|
| 90 |
+
/>
|
| 91 |
+
)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function TableCaption({
|
| 95 |
+
className,
|
| 96 |
+
...props
|
| 97 |
+
}: React.ComponentProps<"caption">) {
|
| 98 |
+
return (
|
| 99 |
+
<caption
|
| 100 |
+
data-slot="table-caption"
|
| 101 |
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
| 102 |
+
{...props}
|
| 103 |
+
/>
|
| 104 |
+
)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
export {
|
| 108 |
+
Table,
|
| 109 |
+
TableHeader,
|
| 110 |
+
TableBody,
|
| 111 |
+
TableFooter,
|
| 112 |
+
TableHead,
|
| 113 |
+
TableRow,
|
| 114 |
+
TableCell,
|
| 115 |
+
TableCaption,
|
| 116 |
+
}
|
ui/components/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
| 5 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
function Tabs({
|
| 10 |
+
className,
|
| 11 |
+
orientation = "horizontal",
|
| 12 |
+
...props
|
| 13 |
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
| 14 |
+
return (
|
| 15 |
+
<TabsPrimitive.Root
|
| 16 |
+
data-slot="tabs"
|
| 17 |
+
data-orientation={orientation}
|
| 18 |
+
orientation={orientation}
|
| 19 |
+
className={cn(
|
| 20 |
+
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
| 21 |
+
className
|
| 22 |
+
)}
|
| 23 |
+
{...props}
|
| 24 |
+
/>
|
| 25 |
+
)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const tabsListVariants = cva(
|
| 29 |
+
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
| 30 |
+
{
|
| 31 |
+
variants: {
|
| 32 |
+
variant: {
|
| 33 |
+
default: "bg-muted",
|
| 34 |
+
line: "gap-1 bg-transparent",
|
| 35 |
+
},
|
| 36 |
+
},
|
| 37 |
+
defaultVariants: {
|
| 38 |
+
variant: "default",
|
| 39 |
+
},
|
| 40 |
+
}
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
function TabsList({
|
| 44 |
+
className,
|
| 45 |
+
variant = "default",
|
| 46 |
+
...props
|
| 47 |
+
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
| 48 |
+
VariantProps<typeof tabsListVariants>) {
|
| 49 |
+
return (
|
| 50 |
+
<TabsPrimitive.List
|
| 51 |
+
data-slot="tabs-list"
|
| 52 |
+
data-variant={variant}
|
| 53 |
+
className={cn(tabsListVariants({ variant }), className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function TabsTrigger({
|
| 60 |
+
className,
|
| 61 |
+
...props
|
| 62 |
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
| 63 |
+
return (
|
| 64 |
+
<TabsPrimitive.Trigger
|
| 65 |
+
data-slot="tabs-trigger"
|
| 66 |
+
className={cn(
|
| 67 |
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 68 |
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
| 69 |
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
| 70 |
+
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
| 71 |
+
className
|
| 72 |
+
)}
|
| 73 |
+
{...props}
|
| 74 |
+
/>
|
| 75 |
+
)
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function TabsContent({
|
| 79 |
+
className,
|
| 80 |
+
...props
|
| 81 |
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
| 82 |
+
return (
|
| 83 |
+
<TabsPrimitive.Content
|
| 84 |
+
data-slot="tabs-content"
|
| 85 |
+
className={cn("flex-1 outline-none", className)}
|
| 86 |
+
{...props}
|
| 87 |
+
/>
|
| 88 |
+
)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
ui/components/ui/textarea.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
| 6 |
+
export interface TextareaProps
|
| 7 |
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
| 8 |
+
|
| 9 |
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
| 10 |
+
({ className, ...props }, ref) => {
|
| 11 |
+
return (
|
| 12 |
+
<textarea
|
| 13 |
+
className={cn(
|
| 14 |
+
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
| 15 |
+
className
|
| 16 |
+
)}
|
| 17 |
+
ref={ref}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
)
|
| 21 |
+
}
|
| 22 |
+
)
|
| 23 |
+
Textarea.displayName = "Textarea"
|
| 24 |
+
|
| 25 |
+
export { Textarea }
|
ui/package.json
CHANGED
|
@@ -6,26 +6,37 @@
|
|
| 6 |
"dev": "next dev",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
-
"lint": "eslint"
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"class-variance-authority": "^0.7.1",
|
| 13 |
"clsx": "^2.1.1",
|
|
|
|
| 14 |
"lucide-react": "^0.563.0",
|
| 15 |
-
"next": "16.1.4",
|
| 16 |
-
"react": "19.2.3",
|
| 17 |
-
"react-dom": "19.2.3",
|
|
|
|
| 18 |
"tailwind-merge": "^3.4.0"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
-
"@tailwindcss/postcss": "^4",
|
| 22 |
-
"@types/node": "^
|
| 23 |
-
"@types/react": "^19",
|
| 24 |
-
"@types/react-dom": "^19",
|
| 25 |
-
"eslint": "^9",
|
| 26 |
-
"eslint-config-next": "16.1.4",
|
| 27 |
-
"
|
|
|
|
| 28 |
"tw-animate-css": "^1.4.0",
|
| 29 |
-
"typescript": "^5"
|
| 30 |
}
|
| 31 |
}
|
|
|
|
| 6 |
"dev": "next dev",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"type-check": "tsc --noEmit"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
+
"@radix-ui/react-label": "^2.1.8",
|
| 14 |
+
"@radix-ui/react-select": "^2.2.6",
|
| 15 |
+
"@radix-ui/react-separator": "^1.1.8",
|
| 16 |
+
"@radix-ui/react-slider": "^1.3.6",
|
| 17 |
+
"@radix-ui/react-slot": "^1.2.4",
|
| 18 |
+
"@radix-ui/react-switch": "^1.2.6",
|
| 19 |
+
"@radix-ui/react-tabs": "^1.1.13",
|
| 20 |
"class-variance-authority": "^0.7.1",
|
| 21 |
"clsx": "^2.1.1",
|
| 22 |
+
"framer-motion": "^12.29.2",
|
| 23 |
"lucide-react": "^0.563.0",
|
| 24 |
+
"next": "^16.1.4",
|
| 25 |
+
"react": "^19.2.3",
|
| 26 |
+
"react-dom": "^19.2.3",
|
| 27 |
+
"recharts": "^3.7.0",
|
| 28 |
"tailwind-merge": "^3.4.0"
|
| 29 |
},
|
| 30 |
"devDependencies": {
|
| 31 |
+
"@tailwindcss/postcss": "^4.1.18",
|
| 32 |
+
"@types/node": "^25.0.10",
|
| 33 |
+
"@types/react": "^19.2.9",
|
| 34 |
+
"@types/react-dom": "^19.2.3",
|
| 35 |
+
"eslint": "^9.39.2",
|
| 36 |
+
"eslint-config-next": "^16.1.4",
|
| 37 |
+
"shadcn": "^3.7.0",
|
| 38 |
+
"tailwindcss": "^4.1.18",
|
| 39 |
"tw-animate-css": "^1.4.0",
|
| 40 |
+
"typescript": "^5.9.3"
|
| 41 |
}
|
| 42 |
}
|
ui/pnpm-lock.yaml
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ui/pnpm-workspace.yaml
CHANGED
|
@@ -1,5 +1,2 @@
|
|
| 1 |
packages:
|
| 2 |
- .
|
| 3 |
-
ignoredBuiltDependencies:
|
| 4 |
-
- sharp
|
| 5 |
-
- unrs-resolver
|
|
|
|
| 1 |
packages:
|
| 2 |
- .
|
|
|
|
|
|
|
|
|