Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions dashboard/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import LandingPage from './pages/LandingPage'
import MonitoringPage from './pages/MonitoringPage'
import ConfigPage from './pages/ConfigPage'
import PlaygroundPage from './pages/PlaygroundPage'
import { ConfigSection } from './components/ConfigNav'

const App: React.FC = () => {
const [isInIframe, setIsInIframe] = useState(false)
const [configSection, setConfigSection] = useState<ConfigSection>('models')

useEffect(() => {
// Detect if we're running inside an iframe (potential loop)
Expand Down Expand Up @@ -70,9 +72,39 @@ const App: React.FC = () => {
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/monitoring" element={<Layout><MonitoringPage /></Layout>} />
<Route path="/config" element={<Layout><ConfigPage /></Layout>} />
<Route path="/playground" element={<Layout><PlaygroundPage /></Layout>} />
<Route
path="/monitoring"
element={
<Layout
configSection={configSection}
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
>
<MonitoringPage />
</Layout>
}
/>
<Route
path="/config"
element={
<Layout
configSection={configSection}
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
>
<ConfigPage activeSection={configSection} />
</Layout>
}
/>
<Route
path="/playground"
element={
<Layout
configSection={configSection}
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
>
<PlaygroundPage />
</Layout>
}
/>
</Routes>
</BrowserRouter>
)
Expand Down
60 changes: 55 additions & 5 deletions dashboard/frontend/src/components/EditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ interface EditModalProps {
export interface FieldConfig {
name: string
label: string
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'textarea' | 'json'
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'textarea' | 'json' | 'percentage'
required?: boolean
options?: string[]
placeholder?: string
description?: string
min?: number
max?: number
step?: number
}

const EditModal: React.FC<EditModalProps> = ({
Expand All @@ -36,10 +39,17 @@ const EditModal: React.FC<EditModalProps> = ({

useEffect(() => {
if (isOpen) {
setFormData(data || {})
// Convert percentage fields from 0-1 to 0-100 for display
const convertedData = { ...data }
fields.forEach(field => {
if (field.type === 'percentage' && convertedData[field.name] !== undefined) {
convertedData[field.name] = Math.round(convertedData[field.name] * 100)
}
})
setFormData(convertedData || {})
setError(null)
}
}, [isOpen, data])
}, [isOpen, data, fields])

const handleChange = (fieldName: string, value: any) => {
setFormData((prev: any) => ({
Expand All @@ -54,7 +64,14 @@ const EditModal: React.FC<EditModalProps> = ({
setError(null)

try {
await onSave(formData)
// Convert percentage fields from 0-100 back to 0-1 before saving
const convertedData = { ...formData }
fields.forEach(field => {
if (field.type === 'percentage' && convertedData[field.name] !== undefined) {
convertedData[field.name] = convertedData[field.name] / 100
}
})
await onSave(convertedData)
onClose()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save')
Expand Down Expand Up @@ -106,7 +123,9 @@ const EditModal: React.FC<EditModalProps> = ({
{field.type === 'number' && (
<input
type="number"
step="any"
step={field.step !== undefined ? field.step : "any"}
min={field.min}
max={field.max}
className={styles.input}
value={formData[field.name] || ''}
onChange={(e) => handleChange(field.name, parseFloat(e.target.value))}
Expand All @@ -115,6 +134,37 @@ const EditModal: React.FC<EditModalProps> = ({
/>
)}

{field.type === 'percentage' && (
<div style={{ position: 'relative' }}>
<input
type="number"
step={field.step !== undefined ? field.step : 1}
min={0}
max={100}
className={styles.input}
value={formData[field.name] !== undefined ? formData[field.name] : ''}
onChange={(e) => {
const val = e.target.value
handleChange(field.name, val === '' ? '' : parseFloat(val))
}}
Comment on lines +146 to +149
Copy link

Copilot AI Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseFloat conversion could result in NaN for invalid input, which should be handled. Consider adding validation to ensure the parsed value is a valid number before calling handleChange.

Copilot uses AI. Check for mistakes.

placeholder={field.placeholder}
required={field.required}
style={{ paddingRight: '2.5rem' }}
/>
<span style={{
position: 'absolute',
right: '0.75rem',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--color-text-secondary)',
fontSize: '0.875rem',
pointerEvents: 'none'
}}>
%
</span>
</div>
)}

{field.type === 'boolean' && (
<label className={styles.checkbox}>
<input
Expand Down
88 changes: 88 additions & 0 deletions dashboard/frontend/src/components/Layout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
align-items: center;
gap: 0.5rem;
padding: 0 0.5rem;
text-decoration: none;
border-radius: var(--radius-md);
transition: background-color var(--transition-fast);
cursor: pointer;
}

.brand:hover {
background-color: var(--color-bg-tertiary);
}

.logo {
Expand Down Expand Up @@ -78,22 +86,102 @@
white-space: nowrap;
}

/* Sub Navigation (Configuration sections) - Match parent nav style */
.subNav {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
padding-left: 1.75rem;
}

.subNavLink {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.625rem;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
font-size: 0.9rem;
font-weight: 500;
transition: all var(--transition-fast);
background: transparent;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
}

.subNavLink:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text);
}

.subNavLinkActive {
background-color: var(--color-primary);
color: white;
}

.subNavLinkActive:hover {
background-color: var(--color-primary-dark);
color: white;
}

.subNavIcon {
font-size: 1rem;
line-height: 1;
width: 1.25rem;
text-align: center;
flex-shrink: 0;
}

.subNavText {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.sidebarFooter {
margin-top: auto;
padding: 0 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}

.themeToggle {
padding: 0.5rem;
font-size: 1.25rem;
border-radius: var(--radius-md);
transition: background-color var(--transition-fast);
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text);
}

.themeToggle:hover {
background-color: var(--color-bg-tertiary);
}

.iconButton {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border-radius: var(--radius-md);
transition: background-color var(--transition-fast);
color: var(--color-text-secondary);
text-decoration: none;
cursor: pointer;
}

.iconButton:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text);
}

.main {
flex: 1;
display: flex;
Expand Down
75 changes: 62 additions & 13 deletions dashboard/frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React, { useState, useEffect, ReactNode } from 'react'
import { NavLink } from 'react-router-dom'
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import styles from './Layout.module.css'

interface LayoutProps {
children: ReactNode
configSection?: string
onConfigSectionChange?: (section: string) => void
}

const Layout: React.FC<LayoutProps> = ({ children }) => {
const Layout: React.FC<LayoutProps> = ({ children, configSection, onConfigSectionChange }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('dark')
const location = useLocation()
const navigate = useNavigate()
const isConfigPage = location.pathname === '/config'

useEffect(() => {
// Check system preference or stored preference
Expand All @@ -28,10 +33,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className={styles.container}>
<aside className={styles.sidebar}>
<div className={styles.brand}>
<NavLink to="/" className={styles.brand}>
<img src="/vllm.png" alt="vLLM" className={styles.logo} />
<span className={styles.brandText}>Semantic Router</span>
</div>
</NavLink>
<nav className={styles.nav}>
<NavLink
to="/playground"
Expand All @@ -42,15 +47,34 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
<span className={styles.navIcon}>🎮</span>
<span className={styles.navText}>Playground</span>
</NavLink>
<NavLink
to="/config"
className={({ isActive }) =>
isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink
}
>
<span className={styles.navIcon}>⚙️</span>
<span className={styles.navText}>Configuration</span>
</NavLink>

{/* Configuration sections - Same level as other nav items */}
{onConfigSectionChange && (
<>
{[
{ id: 'models', icon: '🤖', title: 'Models' },
{ id: 'prompt-guard', icon: '🛡️', title: 'Prompt Guard' },
{ id: 'similarity-cache', icon: '⚡', title: 'Similarity Cache' },
{ id: 'intelligent-routing', icon: '🧠', title: 'Intelligent Routing' },
{ id: 'tools-selection', icon: '🔧', title: 'Tools Selection' },
{ id: 'observability', icon: '👁️', title: 'Observability' },
{ id: 'classification-api', icon: '🔌', title: 'Classification API' }
].map((section) => (
<button
key={section.id}
className={`${styles.navLink} ${isConfigPage && configSection === section.id ? styles.navLinkActive : ''}`}
onClick={() => {
onConfigSectionChange(section.id)
navigate('/config')
}}
>
<span className={styles.navIcon}>{section.icon}</span>
<span className={styles.navText}>{section.title}</span>
</button>
))}
</>
)}

<NavLink
to="/monitoring"
className={({ isActive }) =>
Expand All @@ -70,6 +94,31 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
<a
href="https://github.com/vllm-project/vllm"
target="_blank"
rel="noopener noreferrer"
className={styles.iconButton}
aria-label="GitHub"
title="GitHub Repository"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a
href="https://docs.vllm.ai"
target="_blank"
rel="noopener noreferrer"
className={styles.iconButton}
aria-label="Documentation"
title="Documentation"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
</a>
</div>
</aside>
<main className={styles.main}>{children}</main>
Expand Down
Loading
Loading