Skip to content

Commit a6f4ea6

Browse files
authored
feat(frontend): Midnight Cyberpunk UI redesign (#132)
1 parent 4da540e commit a6f4ea6

38 files changed

Lines changed: 1051 additions & 948 deletions

frontend/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
66
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400;500;700;900&display=swap" rel="stylesheet" />
9+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
810
<title>KubeAIRunway - Model Deployment Platform</title>
911
</head>
1012
<body>

frontend/src/components/deployments/DeploymentForm.tsx

Lines changed: 139 additions & 155 deletions
Large diffs are not rendered by default.

frontend/src/components/deployments/DeploymentList.tsx

Lines changed: 74 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useState } from 'react'
22
import { Link, useNavigate } from 'react-router-dom'
33
import { Button } from '@/components/ui/button'
44
import { Badge } from '@/components/ui/badge'
5-
import { EmptyState } from '@/components/ui/empty-state'
65
import { SkeletonTable } from '@/components/ui/skeleton'
76
import {
87
Dialog,
@@ -12,11 +11,10 @@ import {
1211
DialogHeader,
1312
DialogTitle,
1413
} from '@/components/ui/dialog'
15-
import { DeploymentStatusBadge } from './DeploymentStatusBadge'
1614
import { useDeleteDeployment, type DeploymentStatus } from '@/hooks/useDeployments'
1715
import { useToast } from '@/hooks/useToast'
1816
import { formatRelativeTime, generateAynaUrl } from '@/lib/utils'
19-
import { Eye, Trash2, MessageSquare } from 'lucide-react'
17+
import { Eye, Trash2, MessageSquare, Rocket } from 'lucide-react'
2018

2119
interface DeploymentListProps {
2220
deployments: DeploymentStatus[]
@@ -25,13 +23,34 @@ interface DeploymentListProps {
2523

2624
function getProviderBadgeClass(provider: string): string {
2725
switch (provider) {
28-
case 'kuberay': return 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300'
29-
case 'kaito': return 'bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300'
30-
case 'llmd': return 'bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300'
31-
default: return 'bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300'
26+
case 'kuberay': return 'bg-blue-500/10 text-blue-400 border-blue-500/20'
27+
case 'kaito': return 'bg-purple-500/10 text-purple-400 border-purple-500/20'
28+
case 'llmd': return 'bg-orange-500/10 text-orange-400 border-orange-500/20'
29+
case 'dynamo': return 'bg-teal-500/10 text-teal-400 border-teal-500/20'
30+
default: return 'bg-green-500/10 text-green-400 border-green-500/20'
3231
}
3332
}
3433

34+
function getStatusDotColor(phase: DeploymentStatus['phase']): string {
35+
switch (phase) {
36+
case 'Running': return 'bg-green-500'
37+
case 'Pending': return 'bg-amber-400 animate-pulse'
38+
case 'Deploying': return 'bg-blue-500 animate-pulse'
39+
case 'Failed': return 'bg-red-400'
40+
case 'Terminating': return 'bg-slate-400 animate-pulse'
41+
default: return 'bg-slate-500'
42+
}
43+
}
44+
45+
function getReplicaColorClass(deployment: DeploymentStatus): string {
46+
if (deployment.mode === 'disaggregated' && deployment.prefillReplicas && deployment.decodeReplicas) {
47+
const allReady = deployment.prefillReplicas.ready === deployment.prefillReplicas.desired &&
48+
deployment.decodeReplicas.ready === deployment.decodeReplicas.desired
49+
return allReady ? 'text-green-400' : 'text-amber-400'
50+
}
51+
return deployment.replicas.ready === deployment.replicas.desired ? 'text-green-400' : 'text-amber-400'
52+
}
53+
3554
function getProviderDisplayName(provider: string): string {
3655
switch (provider) {
3756
case 'kuberay': return 'KubeRay'
@@ -95,72 +114,77 @@ export function DeploymentList({ deployments, isLoading }: DeploymentListProps)
95114
// Empty state
96115
if (deployments.length === 0) {
97116
return (
98-
<EmptyState
99-
preset="no-deployments"
100-
title="No deployments yet"
101-
description="Deploy your first model to start serving inference requests. Choose from our curated model library or search HuggingFace."
102-
actionLabel="Browse Models"
103-
onAction={() => navigate('/')}
104-
/>
117+
<div className="glass-panel flex flex-col items-center justify-center py-16 text-center">
118+
<Rocket className="h-12 w-12 text-muted-foreground/50 mb-4" />
119+
<h3 className="text-lg font-medium text-foreground mb-1">No deployments yet</h3>
120+
<p className="text-sm text-muted-foreground mb-6 max-w-md">
121+
Deploy your first model to start serving inference requests.
122+
</p>
123+
<Button onClick={() => navigate('/')} className="bg-cyan-600 hover:bg-cyan-700 text-white">
124+
Deploy your first model
125+
</Button>
126+
</div>
105127
)
106128
}
107129

108130
return (
109131
<>
110-
{/* Mobile Card View */}
111-
<div className="md:hidden space-y-3">
132+
{/* Card-based rows */}
133+
<div className="space-y-3">
112134
{deployments.map((deployment, index) => (
113135
<div
114136
key={deployment.name}
115-
className="rounded-lg border shadow-soft-sm p-4 space-y-3 bg-card"
137+
className="glass-panel !p-4 flex items-center gap-4 group hover:bg-white/5 hover:border-white/10 transition-all duration-200"
116138
style={{ animationDelay: `${index * 50}ms` }}
117139
>
118-
{/* Header: Name and Status */}
119-
<div className="flex items-start justify-between gap-2">
140+
{/* Status dot */}
141+
<div className="shrink-0">
142+
<span className={`h-3 w-3 rounded-full inline-block ${getStatusDotColor(deployment.phase)}`} />
143+
</div>
144+
145+
{/* Name & Model */}
146+
<div className="flex-1 min-w-0">
120147
<Link
121148
to={`/deployments/${deployment.name}?namespace=${deployment.namespace}`}
122-
className="font-medium hover:text-primary transition-colors text-base break-all"
149+
className="font-medium text-foreground hover:text-primary transition-colors"
123150
>
124151
{deployment.name}
125152
</Link>
126-
<DeploymentStatusBadge phase={deployment.phase} />
153+
<p className="text-sm text-muted-foreground truncate">
154+
{deployment.modelId}
155+
</p>
127156
</div>
128157

129-
{/* Model */}
130-
<p className="text-sm text-muted-foreground break-all">
131-
{deployment.modelId}
132-
</p>
133-
134-
{/* Badges Row */}
135-
<div className="flex flex-wrap items-center gap-2">
136-
<Badge variant="outline">
137-
{deployment.engine ? (deployment.engine === 'llamacpp' ? 'Llama.cpp' : deployment.engine.toUpperCase()) : 'Pending'}
138-
</Badge>
158+
{/* Badges (hidden on small screens) */}
159+
<div className="hidden md:flex items-center gap-2">
139160
<Badge
140161
variant="secondary"
141162
className={getProviderBadgeClass(deployment.provider)}
142163
>
143164
{getProviderDisplayName(deployment.provider)}
144165
</Badge>
166+
<Badge variant="outline">
167+
{deployment.engine ? (deployment.engine === 'llamacpp' ? 'Llama.cpp' : deployment.engine.toUpperCase()) : 'Pending'}
168+
</Badge>
145169
{deployment.mode === 'disaggregated' && (
146170
<Badge variant="secondary" className="text-xs">P/D</Badge>
147171
)}
148-
</div>
149-
150-
{/* Meta Row */}
151-
<div className="flex items-center justify-between text-sm text-muted-foreground pt-1 border-t">
152-
<span title={deployment.mode === 'disaggregated' ? 'Prefill / Decode replicas' : 'Worker replicas'}>
153-
Replicas: {formatReplicaStatus(deployment)}
172+
<span
173+
className={`text-sm tabular-nums ${getReplicaColorClass(deployment)}`}
174+
title={deployment.mode === 'disaggregated' ? 'Prefill / Decode replicas' : 'Worker replicas'}
175+
>
176+
{formatReplicaStatus(deployment)} ready
154177
</span>
155-
<span>{formatRelativeTime(deployment.createdAt)}</span>
156178
</div>
157179

158-
{/* Actions */}
159-
<div className="flex items-center gap-2 pt-2 border-t">
160-
<Link to={`/deployments/${deployment.name}?namespace=${deployment.namespace}`} className="flex-1">
161-
<Button size="sm" variant="outline" className="w-full">
162-
<Eye className="h-4 w-4 mr-2" />
163-
View
180+
{/* Age & Actions */}
181+
<div className="flex items-center gap-1">
182+
<span className="text-sm text-muted-foreground hidden lg:inline mr-2">
183+
{formatRelativeTime(deployment.createdAt)}
184+
</span>
185+
<Link to={`/deployments/${deployment.name}?namespace=${deployment.namespace}`}>
186+
<Button size="sm" variant="ghost" title="View details">
187+
<Eye className="h-4 w-4" />
164188
</Button>
165189
</Link>
166190
<a
@@ -170,18 +194,18 @@ export function DeploymentList({ deployments, isLoading }: DeploymentListProps)
170194
endpoint: 'http://localhost:8000',
171195
type: 'chat',
172196
})}
173-
className="flex-1"
197+
title="Open in Ayna"
174198
>
175-
<Button size="sm" variant="outline" className="w-full">
176-
<MessageSquare className="h-4 w-4 mr-2" />
177-
Chat
199+
<Button size="sm" variant="ghost">
200+
<MessageSquare className="h-4 w-4" />
178201
</Button>
179202
</a>
180203
<Button
181204
size="sm"
182-
variant="outline"
205+
variant="ghost"
183206
onClick={() => setDeleteTarget(deployment)}
184-
className="text-destructive hover:text-destructive"
207+
title="Delete deployment"
208+
className="text-red-400 hover:bg-red-500/10 hover:text-red-400"
185209
>
186210
<Trash2 className="h-4 w-4" />
187211
</Button>
@@ -190,106 +214,6 @@ export function DeploymentList({ deployments, isLoading }: DeploymentListProps)
190214
))}
191215
</div>
192216

193-
{/* Desktop Table View */}
194-
<div className="hidden md:block rounded-lg border shadow-soft-sm overflow-x-auto">
195-
<table className="w-full min-w-[600px]">
196-
<thead>
197-
<tr className="border-b bg-muted/50">
198-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap">Name</th>
199-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap">Model</th>
200-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap hidden lg:table-cell">Engine</th>
201-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap hidden lg:table-cell">Runtime</th>
202-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap">Status</th>
203-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap hidden xl:table-cell">Replicas</th>
204-
<th className="px-4 py-3 text-left text-sm font-medium whitespace-nowrap">Age</th>
205-
<th className="px-4 py-3 text-right text-sm font-medium whitespace-nowrap">Actions</th>
206-
</tr>
207-
</thead>
208-
<tbody>
209-
{deployments.map((deployment, index) => (
210-
<tr
211-
key={deployment.name}
212-
className="border-b last:border-0 hover:bg-muted/30 transition-colors duration-150"
213-
style={{ animationDelay: `${index * 50}ms` }}
214-
>
215-
<td className="px-4 py-3">
216-
<Link
217-
to={`/deployments/${deployment.name}?namespace=${deployment.namespace}`}
218-
className="font-medium hover:text-primary transition-colors whitespace-nowrap"
219-
>
220-
{deployment.name}
221-
</Link>
222-
</td>
223-
<td className="px-4 py-3">
224-
<span className="text-sm text-muted-foreground truncate max-w-[200px] block">
225-
{deployment.modelId}
226-
</span>
227-
</td>
228-
<td className="px-4 py-3 hidden lg:table-cell">
229-
<Badge variant="outline">
230-
{deployment.engine ? (deployment.engine === 'llamacpp' ? 'Llama.cpp' : deployment.engine.toUpperCase()) : 'Pending'}
231-
</Badge>
232-
</td>
233-
<td className="px-4 py-3 hidden lg:table-cell">
234-
<Badge
235-
variant="secondary"
236-
className={getProviderBadgeClass(deployment.provider)}
237-
>
238-
{getProviderDisplayName(deployment.provider)}
239-
</Badge>
240-
</td>
241-
<td className="px-4 py-3">
242-
<DeploymentStatusBadge phase={deployment.phase} />
243-
</td>
244-
<td className="px-4 py-3 hidden xl:table-cell">
245-
<span className="text-sm whitespace-nowrap" title={deployment.mode === 'disaggregated' ? 'Prefill / Decode replicas' : 'Worker replicas'}>
246-
{formatReplicaStatus(deployment)}
247-
</span>
248-
{deployment.mode === 'disaggregated' && (
249-
<Badge variant="secondary" className="ml-2 text-xs">P/D</Badge>
250-
)}
251-
</td>
252-
<td className="px-4 py-3">
253-
<span className="text-sm text-muted-foreground whitespace-nowrap">
254-
{formatRelativeTime(deployment.createdAt)}
255-
</span>
256-
</td>
257-
<td className="px-4 py-3">
258-
<div className="flex items-center justify-end gap-1">
259-
<Link to={`/deployments/${deployment.name}?namespace=${deployment.namespace}`}>
260-
<Button size="sm" variant="ghost" title="View details">
261-
<Eye className="h-4 w-4" />
262-
</Button>
263-
</Link>
264-
<a
265-
href={generateAynaUrl({
266-
model: deployment.modelId,
267-
provider: 'openai',
268-
endpoint: 'http://localhost:8000',
269-
type: 'chat',
270-
})}
271-
title="Open in Ayna"
272-
>
273-
<Button size="sm" variant="ghost">
274-
<MessageSquare className="h-4 w-4" />
275-
</Button>
276-
</a>
277-
<Button
278-
size="sm"
279-
variant="ghost"
280-
onClick={() => setDeleteTarget(deployment)}
281-
title="Delete deployment"
282-
>
283-
<Trash2 className="h-4 w-4 text-destructive" />
284-
</Button>
285-
</div>
286-
</td>
287-
</tr>
288-
))}
289-
</tbody>
290-
</table>
291-
</div>
292-
293217
{/* Delete Confirmation Dialog */}
294218
<Dialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
295219
<DialogContent>

0 commit comments

Comments
 (0)